Compare commits

...
102 Commits
Author SHA1 Message Date
AmineB 64dec53a7a fix: this may fix the workflow not stopping on error.
Check PR code / Lint (pull_request) Failing after 7m7s
Check PR code / build (pull_request) Has been skipped
2025-01-11 20:59:25 +01:00
AmineB 55a6b83837 chore: code style fixes for detekt
Check PR code / Lint (pull_request) Successful in 3m18s
Check PR code / build (pull_request) Successful in 11m53s
2025-01-11 20:52:53 +01:00
AmineB 4fbebf2954 chore: code style fixes for ktlint
Check PR code / Lint (pull_request) Failing after 2m19s
Check PR code / build (pull_request) Has been skipped
2025-01-11 15:23:02 +01:00
AmineB 5035392aff chore: more debug to fix a context issue. 2025-01-11 12:52:55 +01:00
giteadrone 54dbda76ab Changelog for v125010031
Check master code / build (push) Successful in 15m56s
2025-01-03 09:09:18 +00:00
AmineB 11c39ae87c Merge pull request 'Bump dependencies' (#173) from upgarde into master
Check master code / build (push) Successful in 19m42s
Create tag / build (push) Successful in 27m4s
Create tag / createTagAndChangelog (push) Successful in 52s
Create tag / release (push) Successful in 7m41s
Reviewed-on: #173
2025-01-03 08:41:15 +00:00
AmineB 6645902ec8 chore: "faster" action.
Check PR code / Lint (pull_request) Successful in 5m59s
Check PR code / build (pull_request) Successful in 14m16s
2025-01-03 09:19:12 +01:00
AmineB 0a07a5dfad fastlane: icon change.
Check PR code / Lint (pull_request) Has been cancelled
Check PR code / build (pull_request) Has been cancelled
2025-01-03 09:18:06 +01:00
AmineB d88d38fd3b chore: ignoring a pixel issue.
Check PR code / Lint (pull_request) Successful in 9m15s
Check PR code / build (pull_request) Successful in 23m30s
2025-01-02 20:53:00 +01:00
AmineB 28fe38aa17 test: fixed an ui test issue.
Check PR code / Lint (pull_request) Successful in 11m23s
Check PR code / build (pull_request) Successful in 19m41s
2025-01-02 20:32:53 +01:00
AmineB d524c30732 fix: center the loading thing.
Check PR code / Lint (pull_request) Successful in 5m35s
Check PR code / build (pull_request) Successful in 18m15s
2024-12-31 15:31:47 +01:00
AmineB 8c00aa65da test: items displaying.
Check PR code / Lint (pull_request) Successful in 9m8s
Check PR code / build (pull_request) Successful in 36m8s
2024-12-31 15:23:26 +01:00
AmineB ae81261cb1 bump: sqldelight.
Check PR code / Lint (pull_request) Successful in 1m47s
Check PR code / build (pull_request) Successful in 15m9s
2024-12-31 12:37:36 +01:00
AmineB 03c567ee33 bump: material, desugar jdk, jsoup, kodein, settings, napier, mock.
Check PR code / Lint (pull_request) Successful in 1m37s
Check PR code / build (pull_request) Successful in 13m29s
2024-12-31 11:45:11 +01:00
AmineB d23dd82fc2 bump: androix and coroutines.
Check PR code / Lint (pull_request) Successful in 1m50s
Check PR code / build (pull_request) Successful in 13m29s
2024-12-31 11:06:36 +01:00
AmineB 2e7a168424 bump: ktor. Closes #67.
Check PR code / Lint (pull_request) Successful in 47s
Check PR code / build (pull_request) Successful in 15m22s
2024-12-31 10:03:21 +01:00
giteadrone 5bc2f614af Changelog for v124123651
Check master code / build (push) Successful in 12m31s
2024-12-30 22:54:15 +00:00
AmineB 934c112db5 Merge pull request 'Bugfixes' (#171) from bugfixes into master
Check master code / build (push) Successful in 19m51s
Create tag / build (push) Successful in 21m4s
Create tag / createTagAndChangelog (push) Successful in 50s
Create tag / release (push) Successful in 3m39s
Reviewed-on: #171
2024-12-30 22:32:11 +00:00
AmineB ad7549a89f config: crowdin
Check PR code / Lint (pull_request) Successful in 54s
Check PR code / build (pull_request) Successful in 10m35s
2024-12-30 23:20:03 +01:00
AmineB fb9ceecabd chore: can links be empty ?
Check PR code / Lint (pull_request) Successful in 1m3s
Check PR code / build (pull_request) Successful in 11m14s
2024-12-30 22:45:18 +01:00
AmineB 61b9fd30e0 fix: Context issues in article fragment. 2024-12-30 22:37:36 +01:00
AmineB 806e56e20b fix: Context issues in fragment sheet.
Check PR code / Lint (pull_request) Successful in 1m7s
Check PR code / build (pull_request) Successful in 12m15s
2024-12-30 22:12:38 +01:00
AmineB cd8b7aaf9d fix: build. 2024-12-30 22:12:21 +01:00
AmineB c25ad7621e chore: compile issue fix.
Check PR code / Lint (pull_request) Successful in 1m10s
Check PR code / build (pull_request) Successful in 9m57s
2024-12-30 15:07:35 +01:00
AmineB 63da3b9fe7 chore: filter some bugs. 2024-12-30 15:07:18 +01:00
AmineB 1d99eeb633 bugfix: catch users using something other than selfoss. 2024-12-30 13:51:45 +01:00
AmineB 162a350a8f bugfix: No browser, no link. 2024-12-30 13:51:14 +01:00
AmineB 27c1bba146 translations
Check PR code / Lint (pull_request) Successful in 54s
Check PR code / build (pull_request) Successful in 9m26s
2024-12-30 13:49:42 +01:00
AmineB b7f3a9877a chore: remove log. 2024-12-30 12:58:34 +01:00
AmineB 47f78754dc translation
Check PR code / Lint (pull_request) Successful in 46s
Check PR code / build (pull_request) Failing after 3m20s
2024-12-30 12:48:42 +01:00
giteadrone 1bdfb143ac Changelog for v124123641
Check master code / coverage (push) Successful in 11m32s
Check master code / build (push) Successful in 15m2s
2024-12-29 22:02:12 +00:00
AmineB d81ced3964 Chore: no tests on build.
Check master code / coverage (push) Successful in 9m42s
Check master code / build (push) Successful in 12m57s
Create tag / build (push) Successful in 9m8s
Create tag / createTagAndChangelog (push) Successful in 46s
Create tag / release (push) Successful in 6m34s
2024-12-29 22:37:59 +01:00
AmineB fbafece1fa Merge pull request 'testing' (#170) from testing into master
Check master code / coverage (push) Successful in 10m16s
Check master code / build (push) Successful in 12m51s
Reviewed-on: #170
2024-12-29 21:23:52 +00:00
AmineB cbed8f07cb fix: Displaying fixes. Fixes #155
Check PR code / Lint (pull_request) Successful in 1m24s
Check PR code / build (pull_request) Successful in 9m20s
2024-12-29 22:13:45 +01:00
AmineB f54fcc3ba1 test: coverage
Check PR code / Lint (pull_request) Successful in 1m13s
Check PR code / build (pull_request) Successful in 9m43s
2024-12-29 21:35:12 +01:00
aminecmi aad93ef722 chore: update and use multiplatform datetime 2024-12-21 20:32:58 +01:00
giteadrone 9e83af0302 Changelog for v124123421
Check master code / build (push) Successful in 11m23s
2024-12-07 17:41:21 +00:00
aminecmi 24b86e66b4 fix: Trying to fix the serialization issue.
Check master code / build (push) Successful in 9m40s
Create tag / build (push) Successful in 8m10s
Create tag / createTagAndChangelog (push) Successful in 32s
Create tag / release (push) Successful in 6m48s
2024-12-07 18:20:49 +01:00
giteadrone 641c444061 Changelog for v124113311 2024-11-26 20:40:47 +00:00
0902c61544 chore: update versions. (#165)
Check master code / build (push) Successful in 8m8s
Create tag / build (push) Successful in 8m15s
Create tag / createTagAndChangelog (push) Successful in 1m0s
Create tag / release (push) Successful in 6m52s
## Types of changes

- [ ] I have read the **CONTRIBUTING** document.
- [ ] My code follows the code style of this project.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] This is **NOT** translation related.

This closes issue #XXX

This is implements feature #YYY

This finishes chore #ZZZ

Co-authored-by: aminecmi <aminecmi@gmail.com>
Reviewed-on: #165
Co-authored-by: administrateur <administrateur@hidden.hidden>
Co-committed-by: administrateur <administrateur@hidden.hidden>
2024-11-26 20:19:43 +00:00
aminecmi 6790152a0b chore: fastlane changelog.
Check master code / build (push) Successful in 8m3s
2024-11-26 21:06:57 +01:00
aminecmi 46d1ba418e chore: fastlane fixes.
Check master code / build (push) Has been cancelled
2024-11-26 21:03:31 +01:00
giteadroneandaminecmi 436373d0ad Changelog for v124113301
Check master code / build (push) Successful in 7m42s
2024-11-25 22:23:23 +01:00
aminecmi 5b9b51c02d chore: Gitea Action
Check master code / build (push) Successful in 7m51s
Create tag / build (push) Successful in 8m15s
Create tag / createTagAndChangelog (push) Successful in 46s
Create tag / release (push) Successful in 6m27s
2024-11-25 21:55:48 +01:00
administrateur b81abe384a Merge pull request 'chore: Gitea Action' (#164) from runner into master
Push/PR Steps / BuildAndTest (push) Has been cancelled
Reviewed-on: #164
2024-11-23 14:14:45 +00:00
aminecmi 851f862dbe chore: Gitea Action
Push/PR Steps / Lint (pull_request) Successful in 1m0s
Push/PR Steps / BuildAndTest (pull_request) Successful in 7m49s
2024-11-23 14:54:45 +01:00
aminecmi 8d7e302af8 chore: Readme update. 2024-11-20 20:45:06 +01:00
AmineBandaminecmi 236e1cca90 Fix recycleview article positions (#163)
Reviewed-on: #163
Reviewed-by: Amine Bouabdallaoui <amineb@hidden.hidden>
2024-11-20 09:32:29 +01:00
davidoskky 3a33cb4510 Provide method to update items in the home
Removed in the previous commit, the item adapter accepts a method to update the articles list in the home page.
Renamed the method to make its objective more clear.
Removed a debugging log.
Reverted change to function name.
2024-11-20 01:28:47 +01:00
davidoskky 0bf9ca9a49 Fix recycleview article positions
The articles were being opened by setting a click listener on the binding.
In card view this was being done through a function and as such it used the overall viewbind of the view rather than the binding of the item viewholder.
Now the functions are more explicit to avoid future errors.
Pulled up a few members from ItemCardAdapter and ItemListAdapter to ItemAdapter.
2024-11-19 01:50:58 +01:00
aminecmi 61e0087894 Changelog for v124041081 [CI SKIP] 2024-04-17 10:57:12 +00:00
aminecmi 1ec05d9913 chore: comment.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
continuous-integration/drone/tag Build is passing
2024-04-17 12:39:45 +02:00
aminecmi 859bd91bbb fix: Last time fixing the parsing date hack before moving it to os version. 2024-04-17 12:22:33 +02:00
aminecmi 204b736c53 Changelog for v124030731 [CI SKIP] 2024-03-13 19:51:14 +00:00
aminecmi f24609c143 fix: Basic auth and password can have non whitspace characters. Fixes 142.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-03-13 20:31:24 +01:00
aminecmi b94d7dc537 Changelog for v124020451 [CI SKIP] 2024-02-14 19:54:05 +00:00
aminecmi 41910cc4cd fix: Fixed handling of position in card adapter.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-02-13 21:31:02 +01:00
aminecmi db166ca9d4 Changelog for v124010301 [CI SKIP] 2024-01-30 19:36:37 +00:00
aminecmi db0d5a4a85 fix: This may fix the oom errors.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-29 20:55:29 +01:00
aminecmi 3bc0d7cf95 Changelog for v124010191 [CI SKIP] 2024-01-19 21:16:23 +00:00
aminecmi 8f464d95fd fix: moving listeners.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-19 21:32:43 +01:00
aminecmi 5ccd6a3368 chore: removed a useless log. 2024-01-19 21:32:43 +01:00
aminecmi cdbded246e Changelog for v124010032 [CI SKIP] 2024-01-03 22:16:35 +00:00
aminecmi 750c7758bd fix: Another date format thing.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-03 23:07:57 +01:00
aminecmi 22f8b14ecd Changelog for v124010031 [CI SKIP] 2024-01-03 21:46:48 +00:00
aminecmi 6e27d6d4e6 fix: Checking if selfoss instance.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2024-01-03 22:28:57 +01:00
aminecmi 14ff4dbd05 fix: handle three characters lenght hexcode colors. 2024-01-03 21:35:58 +01:00
aminecmi 390c2d0cf3 Changelog for v123113311 [CI SKIP] 2023-11-27 20:48:32 +00:00
aminecmi e58914ef58 chore: Source tracker url in the menu.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2023-11-27 21:31:46 +01:00
aminecmi a03f08fca1 fix: Handle kodein proguard rules. 2023-11-27 21:31:26 +01:00
aminecmi 8e9b87f00c Changelog for v123102961 [CI SKIP] 2023-10-23 21:11:15 +00:00
aminecmi f765224a86 chore: domain changes.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2023-10-21 23:41:17 +02:00
aminecmi 14d2219eb8 Changelog for v123102852 [CI SKIP] 2023-10-12 18:48:06 +00:00
aminecmi 137580ccf9 chore: lint cleaning.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
2023-10-12 20:38:01 +02:00
aminecmi f101d22f54 Changelog for v123102841 [CI SKIP] 2023-10-12 20:13:17 +02:00
aminecmi 68aedb7641 chore: verbose.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is failing
2023-10-11 21:13:10 +02:00
754d526b49 chore: cleaning ci steps and upgrading dependencies.
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is failing
## Types of changes

- [ ] I have read the **CONTRIBUTING** document.
- [ ] My code follows the code style of this project.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] This is **NOT** translation related.

This closes issue #XXX

This is implements feature #YYY

This finishes chore #ZZZ

Co-authored-by: davidoskky <davidoskky@yahoo.it>
Co-authored-by: aminecmi <aminecmi@gmail.com>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/150
2023-10-10 20:52:26 +00:00
Amine Louveau c458871569 feat: Self signed ssl support.
continuous-integration/drone/push Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/141
2023-09-17 18:28:47 +00:00
davidoskky 056825aa0c Revert xmlns changes
continuous-integration/drone/pr Build is passing
2023-09-17 12:01:32 +02:00
davidoskky 16b19fc5ce Revert version upgrades 2023-09-17 11:59:08 +02:00
davidoskky 4ad4a23ed8 Revert to private functions
continuous-integration/drone/pr Build is failing
2023-09-12 00:38:37 +02:00
davidoskky d8c215eacc Reintroduce removed parameter 2023-09-12 00:36:56 +02:00
davidoskky 2b446ab22b Revert dependency version changes 2023-09-12 00:36:04 +02:00
davidoskky a029d8a7dc Move api client creation function within api class
continuous-integration/drone/pr Build is passing
2023-09-10 21:33:28 +02:00
davidoskky 4482234e1a Remove unused strings file
continuous-integration/drone/pr Build is passing
2023-09-10 21:15:04 +02:00
davidoskky b5de30f561 Add translations 2023-09-10 21:14:43 +02:00
davidoskky 70ad5f322c Handle most HTTP client creation in common code 2023-09-10 21:14:43 +02:00
davidoskky d167092c83 Add a login switch to disable SSL verification 2023-09-10 20:24:22 +02:00
davidoskky c4f4bafe85 Add a switch in the login screen to disable SSL 2023-07-13 14:55:48 +02:00
davidoskky ed06b22a77 Tentative self signed ssl support 2023-07-13 14:55:48 +02:00
aminecmi 172362b533 Changelog for v123061811 [CI SKIP] 2023-06-30 19:06:44 +00:00
aminecmi ad72cb6f56 feat: Added confirmation dialog for disconnect item menu.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-06-30 20:37:43 +02:00
aminecmi 9057ee0052 Changelog for v123061651 [CI SKIP] 2023-06-14 18:23:59 +00:00
aminecmi 50d0b44315 i18n: Translation update.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-06-13 20:52:42 +02:00
aminecmi 21b08ed384 i18n: Translation update.
continuous-integration/drone/push Build is running
2023-06-13 20:26:36 +02:00
aminecmi 993c4d2ee9 i18n: Translation update.
continuous-integration/drone/push Build is passing
2023-06-12 20:50:26 +02:00
aminecmi 57a9d51027 fix: avoid trying to open invalid image urls.
continuous-integration/drone/push Build is running
2023-06-12 20:34:35 +02:00
aminecmi 673f0edb8b Changelog for v123051471 [CI SKIP] 2023-05-27 19:25:35 +00:00
aminecmi 7f96798f13 fix: images could be null.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-05-27 21:06:56 +02:00
aminecmi 6e5704a45b fix: Check if color is not empty before parsing it. 2023-05-27 21:02:25 +02:00
aminecmi 495591159f chore: Removed unused log. 2023-05-27 21:01:54 +02:00
aminecmi 718fe7c5ee Changelog for v123051331 [CI SKIP] 2023-05-13 20:24:57 +00:00
135 changed files with 6084 additions and 2796 deletions
-163
View File
@@ -1,163 +0,0 @@
kind: pipeline
type: docker
name: test
steps:
- name: BuildAndTest
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------"
- echo "Building..."
- ./gradlew build -x test
- echo "---------------------------------------------------------"
- echo "Testing..."
- echo "---------------------------------------------------------"
- ./gradlew koverMergedXmlReport
environment:
TZ: Europe/Paris
SONAR_HOST_URL:
from_secret: sonarScannerHostUrl
SONAR_LOGIN:
from_secret: sonarScannerLogin
- name: Analyse
image: kytay/sonar-node-plugin
settings:
sonar_host:
from_secret: sonarScannerHostUrl
sonar_token:
from_secret: sonarScannerLogin
use_node_version: 16.18.1
sonar_debug: true
sonar_project_settings: ./sonar-project.properties
trigger:
event:
- push
- pull_request
---
kind: pipeline
type: docker
name: Publish
steps:
- name: createTagAndChangelog
image: ubuntu:latest
commands:
- apt-get update && apt-get install -y git
- git fetch --tags -p
- PREV=$(git describe --tags --abbrev=0)
- ./build.sh --publish --from-ci
- git remote add pushing https://$GITEA_USR:$GITEA_PASS@gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform.git
- 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 [CI SKIP]"
- git push pushing master
- git push pushing --tags
environment:
TZ: Europe/Paris
GITEA_USR:
from_secret: giteaUsr
GITEA_PASS:
from_secret: giteaPass
- name: scpFiles
image: appleboy/drone-scp
settings:
host: amine-louveau.fr
username: ubuntu
key:
from_secret: privateKey
port: 22
target: /home/ubuntu/
source: version.txt
- name: deploy
image: appleboy/drone-ssh
settings:
host: amine-louveau.fr
user: ubuntu
key:
from_secret: privateKey
command_timeout: 2m
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/
trigger:
event:
- promote
target:
- production
---
kind: pipeline
type: docker
name: Release
steps:
- name: build
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- echo "Fetch tags..."
- git fetch --tags
- echo "---------------------------------------------------------"
- echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=false\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------"
- echo "Generate APK"
- ./gradlew :androidApp:assembleGithubConfigRelease -P pushCache=false
- echo "---------------------------------------------------------"
- echo "Get Key"
- wget https://amine-louveau.fr/key
- echo "---------------------------------------------------------"
- echo "Zipalign"
- $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
- echo "---------------------------------------------------------"
- echo "Sign"
- $ANDROID_HOME/build-tools/31.0.0/apksigner sign -v --out signed.apk --ks ./key --ks-key-alias $YOUR_KEY_ALIAS --ks-pass pass:$YOUR_KEYSTORE_PASSWORD --v1-signing-enabled true --v2-signing-enabled true androidApp/build/outputs/apk/githubConfig/release/android-prod-released-ziped.apk
- echo "---------------------------------------------------------"
- echo "Verify"
- $ANDROID_HOME/build-tools/31.0.0/apksigner verify signed.apk
environment:
TZ: Europe/Paris
YOUR_KEYSTORE_PASSWORD:
from_secret: keyPass
YOUR_KEY_ALIAS:
from_secret: keyAlias
- name: gitea_release
image: plugins/gitea-release
settings:
api_key:
from_secret: giteaAPI
base_url: https://gitea.amine-louveau.fr
files: signed.apk
- name: notify
image: drillster/drone-email
failure: ignore
settings:
host:
from_secret: smtpHOST
port:
from_secret: smtpPORT
username:
from_secret: smtpUSERNAME
password:
from_secret: smtpPASSWORD
from:
from_secret: smtpFROM
subject: Mapping file
recipients:
from_secret: smtpTO
recipients_only: true
skip_verify: true
attachment: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
trigger:
event:
- tag
+36
View File
@@ -0,0 +1,36 @@
root = true
[*]
insert_final_newline = true
[.editorconfig]
insert_final_newline = false
ij_kotlin_line_break_after_multiline_when_entry = false
[*.{kt,kts}]
# Disable wildcard imports entirely
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
end_of_line = lf
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
ij_kotlin_indent_before_arrow_on_new_line = false
ij_kotlin_line_break_after_multiline_when_entry = true
ij_kotlin_packages_to_use_import_on_demand = unset
indent_size = 4
indent_style = space
ktlint_argument_list_wrapping_ignore_when_parameter_count_greater_or_equal_than = unset
ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = 4
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
ktlint_code_style = ktlint_official
ktlint_enum_entry_name_casing = upper_or_camel_cases
ktlint_function_naming_ignore_when_annotated_with = unset
ktlint_function_signature_body_expression_wrapping = multiline
ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2
ktlint_ignore_back_ticked_identifier = false
ktlint_property_naming_constant_naming = screaming_snake_case
max_line_length = 140
[**/build]
ktlint = disabled
@@ -0,0 +1,10 @@
version: '3'
services:
selfoss:
container_name: selfoss
image: rsprta/selfoss
network_mode: "host"
ports:
- "8888:8888"
+45
View File
@@ -0,0 +1,45 @@
name: Build
on:
workflow_call:
jobs:
BuildAndTestAndCoverage:
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'
cache: gradle
- uses: gradle/actions/setup-gradle@v3
- uses: android-actions/setup-android@v3
- name: Configure gradle...
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties
- name: Build and test
run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest # These tests will be done
- uses: KengoTODA/actions-setup-docker-compose@v1
with:
version: "2.23.3"
- name: run selfoss
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
- name: coverage
run: |
./gradlew :koverHtmlReport
- uses: actions/upload-artifact@v3
with:
name: coverage
path: build/reports/kover/html
retention-days: 1
overwrite: true
include-hidden-files: true
- name: Clean
if: always()
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml stop
+127
View File
@@ -0,0 +1,127 @@
name: Create tag
on:
push:
branches:
- release
workflow_dispatch:
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
- name: Config git
run: |
git config --global user.email aminecmi+giteadrone@pm.me
git config --global user.name giteadrone
- name: Creating the tag and generate changelog
run: |
git fetch --tags -p
PREV=$(git describe --tags --abbrev=0)
./build.sh --publish --from-ci
VER=$(git describe --tags --abbrev=0)
CHANGELOG=$(git log $PREV..HEAD --pretty="- %s")
echo "**$VER
$CHANGELOG
--------------------------------------------------------------------
$(cat CHANGELOG.md)" > CHANGELOG.md
git add CHANGELOG.md
touch ./fastlane/metadata/android/en\-US/changelogs/$VER.txt
echo "**$VER**
$CHANGELOG" > ./fastlane/metadata/android/en\-US/changelogs/$VER.txt
git add ./fastlane/metadata/android/en\-US/changelogs/$VER.txt
git commit -m "Changelog for $VER"
- name: Push changes
uses: appleboy/git-push-action@v1.0.0
with:
author_name: giteadrone
author_email: aminecmi+giteadrone@pm.me
remote: ${{ secrets.REMOTE_URL }}
followtags: true
ssh_key: ${{ secrets.PRIVATE_KEY }}
tags: true
branch: release
- name: copy file via ssh password
uses: appleboy/scp-action@v0.1.7
with:
host: amine-bouabdallaoui.fr
username: ubuntu
key: ${{ secrets.PRIVATE_KEY }}
source: "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/
release:
runs-on: ubuntu-latest
needs: createTagAndChangelog
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch tags
id: version
run: |
git fetch --tags -p
PREV=$(git describe --tags --abbrev=0)
echo $PREV
echo "VERSION=$PREV" >> $GITHUB_OUTPUT
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- 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: setup go
uses: https://github.com/actions/setup-go@v4
with:
go-version: '>=1.20.1'
- name: Generate APK
run: ./gradlew :androidApp:assembleGithubConfigRelease
- name: Get Key
run: wget ${{ secrets.KEY_URL }}
- name: Zippalign
run: |
sdkmanager "build-tools;31.0.0"
ls $ANDROID_HOME/build-tools
$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/gitea-release-action@main
with:
files: signed.apk
token: ${{ secrets.API_KEY }}
tag_name: ${{ steps.version.outputs.VERSION }}
name: ${{ steps.version.outputs.VERSION }}
- 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: Nouveau fichier de mapping pour la version ${{ steps.version.outputs.VERSION }}
attachments: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
+29
View File
@@ -0,0 +1,29 @@
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'
java-version: '17'
cache: gradle
- name: Install klint
run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.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.7/detekt-cli-1.23.7.zip unzip detekt-cli-1.23.7.zip
- name: Linting...
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
- name: Detecting...
run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt'
build:
needs: Lint
steps:
- uses: ./.gitea/workflows/common_build.yml
+9
View File
@@ -0,0 +1,9 @@
name: Check master code
on:
push:
branches:
- master
jobs:
build:
uses: ./.gitea/workflows/common_build.yml
+1 -1
View File
@@ -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-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)
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)
### What I can't help you with.
+4 -1
View File
@@ -320,4 +320,7 @@ fabric.properties
# End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin,androidstudio,android,xcode,swift
crowdin.properties
crowdin.properties
.kotlin/
build-cache/
+187
View File
@@ -1,3 +1,190 @@
**v125010031
- Merge pull request 'Bump dependencies' (#173) from upgarde into master
- chore: "faster" action.
- fastlane: icon change.
- chore: ignoring a pixel issue.
- test: fixed an ui test issue.
- fix: center the loading thing.
- test: items displaying.
- bump: sqldelight.
- bump: material, desugar jdk, jsoup, kodein, settings, napier, mock.
- bump: androix and coroutines.
- bump: ktor. Closes #67.
- Changelog for v124123651
--------------------------------------------------------------------
**v124123651
- Merge pull request 'Bugfixes' (#171) from bugfixes into master
- config: crowdin
- chore: can links be empty ?
- fix: Context issues in article fragment.
- fix: Context issues in fragment sheet.
- fix: build.
- chore: compile issue fix.
- chore: filter some bugs.
- bugfix: catch users using something other than selfoss.
- bugfix: No browser, no link.
- translations
- chore: remove log.
- translation
- Changelog for v124123641
--------------------------------------------------------------------
**v124123641
- Chore: no tests on build.
- Merge pull request 'testing' (#170) from testing into master
- fix: Displaying fixes. Fixes #155
- test: coverage
- chore: update and use multiplatform datetime
- Changelog for v124123421
--------------------------------------------------------------------
**v124123421
- fix: Trying to fix the serialization issue.
- Changelog for v124113311
--------------------------------------------------------------------
**v124113311
- chore: update versions. (#165)
- chore: fastlane changelog.
- chore: fastlane fixes.
- Changelog for v124113301
--------------------------------------------------------------------
**v124113301**
- chore: Gitea Action
- Merge pull request 'chore: Gitea Action' (#164) from runner into master
- chore: Gitea Action
- chore: Readme update.
--------------------------------------------------------------------
**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.
+6 -10
View File
@@ -1,4 +1,4 @@
# ReaderForSelfoss-multiplatform [![Build Status](https://build.amine-louveau.fr/api/badges/Louvorg/ReaderForSelfoss-multiplatform/status.svg)](https://build.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform)
# 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)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/readerforselfoss/localized.svg)](https://crowdin.com/project/readerforselfoss)
@@ -10,10 +10,6 @@ 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>
@@ -22,15 +18,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-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md).
2. Check the [Contribution guide](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md).
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)
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)
## Useful links
- [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)
- [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)
- [Help translation the app](https://crowdin.com/project/readerforselfoss)
## Contributors (V1) (Alphabetical order) ❤️
+1
View File
@@ -1 +1,2 @@
/build
.kotlin/
+54 -40
View File
@@ -1,22 +1,23 @@
import java.io.ByteArrayOutputStream
val ignoreGitVersion: String by project
val acraVersion = "5.9.7"
val acraVersion = "5.12.0"
plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
id("com.mikepenz.aboutlibraries.plugin")
id("org.jetbrains.kotlinx.kover") version "0.6.1"
id("org.jetbrains.kotlinx.kover")
id("app.cash.sqldelight") version "2.0.2"
}
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
var result: String = ByteArrayOutputStream().use { outputStream ->
val result: String = ByteArrayOutputStream().use { outputStream ->
project.exec {
commandLine = cmd.split(" ")
standardOutput = outputStream
isIgnoreExitValue = ignore ?: false
isIgnoreExitValue = ignore
}
outputStream.toString()
}
@@ -24,9 +25,8 @@ fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
}
fun gitVersion(): String {
var process = ""
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
process = if (maybeTagOfCurrentCommit.isEmpty()) {
val 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")
} else {
@@ -58,23 +58,22 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
// Flag to enable support for the new language APIs
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
// For Kotlin projects
kotlinOptions {
jvmTarget = "11"
jvmTarget = "17"
}
compileSdk = 33
buildToolsVersion = "33.0.0"
compileSdk = 35
buildFeatures {
viewBinding = true
}
defaultConfig {
applicationId = "bou.amine.apps.readerforselfossv2.android"
minSdk = 21
targetSdk = 33
minSdk = 25
targetSdk = 34 // 35 when edge-to-edge is handled
versionCode = versionCodeFromGit()
versionName = versionNameFromGit()
@@ -86,8 +85,9 @@ android {
// tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
packagingOptions {
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
@@ -109,32 +109,37 @@ android {
}
}
namespace = "bou.amine.apps.readerforselfossv2.android"
testOptions {
animationsDisabled = true
execution = "ANDROIDX_TEST_ORCHESTRATOR"
unitTests {
isIncludeAndroidResources = true
}
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
implementation(project(":shared"))
implementation("com.google.android.material:material:1.5.0")
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
// Android Support
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("com.google.android.material:material:1.12.0")
implementation("androidx.recyclerview:recyclerview:1.4.0-rc01")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.vectordrawable:vectordrawable:1.2.0-alpha02")
implementation("androidx.vectordrawable:vectordrawable:1.2.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.annotation:annotation:1.3.0")
implementation("androidx.work:work-runtime-ktx:2.7.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("org.jsoup:jsoup:1.14.3")
implementation("androidx.annotation:annotation:1.9.1")
implementation("androidx.work:work-runtime-ktx:2.10.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.0")
implementation("org.jsoup:jsoup:1.18.3")
//multidex
implementation("androidx.multidex:multidex:2.0.1")
@@ -147,31 +152,31 @@ dependencies {
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
// 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.16.0")
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
// Themes
implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
// Pager
implementation("me.relex:circleindicator:2.1.6")
implementation("androidx.viewpager2:viewpager2:1.1.0-beta01")
implementation("androidx.viewpager2:viewpager2:1.1.0")
//Dependency Injection
implementation("org.kodein.di:kodein-di:7.14.0")
implementation("org.kodein.di:kodein-di-framework-android-x:7.14.0")
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.14.0")
implementation("org.kodein.di:kodein-di:7.23.1")
implementation("org.kodein.di:kodein-di-framework-android-x:7.23.1")
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.23.1")
//Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:0.9")
implementation("com.russhwolf:multiplatform-settings-no-arg:1.3.0")
//Logging
implementation("io.github.aakira:napier:2.6.1")
implementation("io.github.aakira:napier:2.7.1")
//PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("androidx.core:core-ktx:1.8.0")
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
@@ -179,16 +184,25 @@ dependencies {
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
// SQLDELIGHT
implementation("com.squareup.sqldelight:android-driver:1.5.4")
implementation("app.cash.sqldelight:android-driver:2.0.2")
//test
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.12.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
testImplementation("io.mockk:mockk:1.13.14")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation("androidx.test:rules:1.6.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
implementation("androidx.test.espresso:espresso-idling-resource:3.6.1")
androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1")
androidTestUtil("androidx.test:orchestrator:1.5.1")
testImplementation("org.robolectric:robolectric:4.14.1")
testImplementation("androidx.test:core-ktx:1.6.1")
implementation("ch.acra:acra-http:$acraVersion")
implementation("ch.acra:acra-toast:$acraVersion")
implementation("com.google.auto.service:auto-service:1.1.1")
}
tasks.withType<Test> {
+10
View File
@@ -55,6 +55,7 @@
# 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.
@@ -85,3 +86,12 @@
-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
@@ -0,0 +1,121 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import androidx.annotation.ArrayRes
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers.hasToString
fun performLogin(someUrl: String? = null) {
onView(withId(R.id.urlView)).perform(click()).perform(
typeTextIntoFocusedView(
if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888",
),
)
onView(withId(R.id.signInButton)).perform(click())
}
fun loginAndInitHome() {
performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
onView(withText("OK")).perform(click())
}
fun changeAndCancelSetting(
oldValue: String,
newValue: String,
openSettingItem: () -> Unit,
) {
openSettingItem()
onView(
withId(android.R.id.edit),
).perform(replaceText(newValue))
onView(
withId(android.R.id.button2),
).perform(click())
openSettingItem()
onView(
withId(android.R.id.edit),
).check(matches(withText(oldValue)))
onView(
withText(newValue),
).check(doesNotExist())
onView(
withId(android.R.id.button2),
).perform(click())
}
fun changeAndSaveSetting(
oldValue: String,
newValue: String,
openSettingItem: () -> Unit,
) {
openSettingItem()
onView(
withId(android.R.id.edit),
).perform(replaceText(newValue))
onView(
withId(android.R.id.button1),
).perform(click())
openSettingItem()
onView(
withId(android.R.id.edit),
).check(matches(withText(newValue)))
if (oldValue.isNotEmpty()) {
onView(
withText(oldValue),
).check(doesNotExist())
}
onView(
withId(android.R.id.button2),
).perform(click())
}
fun testPreferencesFromArray(
context: Context,
@ArrayRes arrayRes: Int,
openSettingItem: () -> Unit,
) {
openSettingItem()
context.resources.getStringArray(arrayRes).forEach { res ->
onView(withText(res)).check(matches(allOf(isDisplayed(), isNotChecked())))
onView(withText(res)).perform(click())
onView(withText(res)).check(doesNotExist())
openSettingItem()
onView(withText(res)).check(matches(allOf(isDisplayed(), isChecked())))
}
}
fun testAddSourceWithUrl(
url: String,
sourceName: String,
) {
onView(withId(R.id.fab))
.perform(click())
onView(withId(R.id.nameInput))
.perform(click())
.perform(typeTextIntoFocusedView(sourceName))
onView(withId(R.id.sourceUri))
.perform(click())
.perform(typeTextIntoFocusedView(url))
onView(withId(R.id.tags))
.perform(click())
.perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
onView(withId(R.id.spoutsSpinner))
.perform(click())
onData(hasToString("RSS Feed")).perform(click())
onView(withId(R.id.saveBtn))
.perform(click())
onView(withText(sourceName)).check(matches(isDisplayed()))
}
@@ -0,0 +1,110 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.RelativeLayout
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.graphics.drawable.toBitmap
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.Root
import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.hamcrest.TypeSafeMatcher
fun withError(
@StringRes id: Int,
): TypeSafeMatcher<View?> {
return object : TypeSafeMatcher<View?>() {
override fun matchesSafely(view: View?): Boolean {
if (view != null && (view !is EditText || view.error == null)) {
return false
}
val context = view!!.context
return (view as EditText).error.toString() == context.getString(id)
}
override fun describeTo(description: Description?) {
// Nothing
}
}
}
fun isPopupWindow(): Matcher<Root> = isPlatformPopup()
fun withDrawable(
@DrawableRes id: Int,
) = object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("ImageView with drawable same as drawable with id $id")
}
@Suppress("detekt:SwallowedException")
override fun matchesSafely(view: View): Boolean {
val context = view.context
val expectedBitmap = context.getDrawable(id)!!.toBitmap()
try {
return view is ImageView && view.drawable.toBitmap().sameAs(expectedBitmap)
} catch (e: Exception) {
return false
}
}
}
fun hasBottombarItemText(
@StringRes id: Int,
): Matcher<View>? =
allOf(
withResourceName("fixed_bottom_navigation_icon"),
withParent(
allOf(
withResourceName("fixed_bottom_navigation_icon_container"),
hasSibling(withText(id)),
),
),
)
fun withSettingsCheckboxWidget(
@StringRes id: Int,
): Matcher<View>? =
allOf(
withId(android.R.id.switch_widget),
withParent(
withSettingsCheckboxFrame(id),
),
)
fun withSettingsCheckboxFrame(
@StringRes id: Int,
): Matcher<View>? =
allOf(
withId(android.R.id.widget_frame),
hasSibling(
allOf(
withClassName(Matchers.equalTo(RelativeLayout::class.java.name)),
withChild(
withText(id),
),
),
),
)
fun openMenu() {
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>(),
)
}
@@ -0,0 +1,118 @@
package bou.amine.apps.readerforselfossv2.android
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isClickable
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isFocused
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.isSelected
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class HomeActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Before
fun init() {
loginAndInitHome()
}
@Test
fun testMenu() {
onView(withId(R.id.action_search)).check(matches(isDisplayed())).check(
matches(
isClickable(),
),
)
onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check(
matches(
isClickable(),
),
)
openMenu()
onView(withText(R.string.readAll)).check(matches(isDisplayed()))
onView(withText(R.string.menu_home_sources)).check(matches(isDisplayed()))
onView(withText(R.string.title_activity_settings)).check(matches(isDisplayed()))
onView(withText(R.string.menu_home_refresh)).check(matches(isDisplayed()))
onView(withText(R.string.issue_tracker_link)).check(matches(isDisplayed()))
onView(withText(R.string.action_disconnect)).check(matches(isDisplayed()))
}
@Test
fun testMenuActions() {
onView(withId(R.id.action_search)).perform(click())
onView(
withId(R.id.search_src_text),
).check(matches(isFocused()))
onView(isRoot()).perform(ViewActions.pressBack())
onView(withId(R.id.action_filter)).perform(click())
onView(
withText(R.string.filter_item_sources),
).check(matches(isDisplayed()))
onView(
withText(R.string.filter_item_tags),
).check(matches(isDisplayed()))
onView(
withId(R.id.floatingActionButton2),
).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
onView(withText(R.string.readAll)).perform(click())
onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
onView(withText(R.string.menu_home_sources)).perform(click())
onView(withId(R.id.fab)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
onView(withText(R.string.menu_home_refresh)).perform(click())
onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
/*onView(withText(R.string.issue_tracker_link)).perform(click())
onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()*/
onView(withText(R.string.action_disconnect)).perform(click())
onView(withText(R.string.confirm_disconnect_title)).check(matches(isDisplayed()))
}
@Test
fun testEmptyView() {
onView(withId(R.id.emptyText)).check(matches(isDisplayed()))
onView(
hasBottombarItemText(R.string.tab_new),
).check(matches(isDisplayed())).check(matches(isSelected()))
onView(
hasBottombarItemText(R.string.tab_read),
).check(matches(isDisplayed())).check(matches(not(isSelected())))
onView(
hasBottombarItemText(R.string.tab_favs),
).check(matches(isDisplayed())).check(matches(not(isSelected())))
}
}
@@ -0,0 +1,79 @@
package bou.amine.apps.readerforselfossv2.android
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isClickable
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class LoginActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Before
fun registerIdlingResource() {
IdlingRegistry
.getInstance()
.register(CountingIdlingResourceSingleton.countingIdlingResource)
}
@After
fun unregisterIdlingResource() {
IdlingRegistry
.getInstance()
.unregister(CountingIdlingResourceSingleton.countingIdlingResource)
}
@Test
fun viewIsInitialized() {
onView(withId(R.id.urlView)).check(matches(isDisplayed()))
onView(withId(R.id.selfSigned))
.check(matches(isDisplayed()))
.check(matches(isNotChecked()))
.check(
matches(isClickable()),
)
onView(withId(R.id.withLogin))
.check(matches(isDisplayed()))
.check(matches(isNotChecked()))
.check(
matches(isClickable()),
)
}
@Test
fun urlError() {
performLogin("10.0.2.2:8888")
onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
}
@Test
fun multiError() {
onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.signInButton)).perform(click())
onView(withText(R.string.warning_wrong_url)).check(matches(isDisplayed()))
}
@Test
fun connect() {
performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
}
}
@@ -0,0 +1,181 @@
package bou.amine.apps.readerforselfossv2.android
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.isFocused
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityGeneralTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Before
fun init() {
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext(),
)
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_general)).perform(click())
}
@Suppress("detekt:LongMethod")
@Test
fun testGeneral() {
onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
onView(
withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title),
).check(
matches(
allOf(
isDisplayed(),
not(isChecked()),
),
),
)
onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check(
matches(
allOf(
isDisplayed(),
isChecked(),
),
),
)
onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
matches(
allOf(
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
isEnabled(),
),
)
onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
matches(
allOf(
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxWidget(R.string.card_height_title)).check(
matches(
allOf(
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(
matches(
not(isEnabled()),
),
)
onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check(
matches(
allOf(
isDisplayed(),
isChecked(),
),
),
)
onView(withId(R.id.settings)).perform(swipeUp())
onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check(
matches(
allOf(
isDisplayed(),
not(isChecked()),
),
),
)
}
@Suppress("detekt:ForbiddenComment")
@Test
fun testGeneralActionsNumberItems() {
onView(withText(R.string.pref_api_items_number_title)).perform(click())
onView(withId(android.R.id.edit)).check(matches(isFocused()))
// Value check
onView(
withId(android.R.id.edit),
).perform(replaceText("AVC"))
.check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+
onView(
withId(android.R.id.edit),
).perform(replaceText("-1"))
.check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+
onView(
withId(android.R.id.edit),
).perform(replaceText("300"))
.check(matches(withText("")))
onView(
withId(android.R.id.edit),
).perform(typeTextIntoFocusedView("300"))
.check(matches(withText("30")))
onView(
withId(android.R.id.edit),
).perform(replaceText("10"))
.check(matches(withText("10")))
onView(isRoot()).perform(ViewActions.pressBack())
// Value saving
changeAndCancelSetting("20", "10") {
onView(withText(R.string.pref_api_items_number_title)).perform(click())
}
changeAndSaveSetting("20", "10") {
onView(withText(R.string.pref_api_items_number_title)).perform(click())
}
}
@Test
fun testGeneralActionsCheckboxes() {
// article viewer settings
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
isEnabled(),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
not(isEnabled()),
),
)
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled())))
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled()))
}
}
@@ -0,0 +1,174 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityOfflineTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
lateinit var context: Context
@Before
fun init() {
activityRule.scenario.onActivity { activity ->
context = activity.window.context
}
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext(),
)
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_offline)).perform(click())
}
@Suppress("detekt:LongMethod")
@Test
fun testOffline() {
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
matches(
allOf(
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_items_caching)).check(
matches(
allOf(
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
matches(
isEnabled(),
),
)
onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
matches(
allOf(isNotEnabled(), isDisplayed()),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_refresh_when_charging)).check(
matches(
allOf(
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches(
isNotEnabled(),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_notify_new_items)).check(
matches(
allOf(
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches(
isNotEnabled(),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).check(
matches(
allOf(
isDisplayed(),
isChecked(),
),
),
)
}
@Suppress("detekt:LongMethod")
@Test
fun testOfflineActions() {
onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed()))
onView(withText(R.string.pref_switch_items_caching)).perform(click())
onView(withText(R.string.pref_switch_items_caching_on)).check(matches(isDisplayed()))
onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
matches(
isEnabled(),
),
)
onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
matches(
isNotEnabled(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches(
isNotEnabled(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches(
isNotEnabled(),
),
)
onView(withText(R.string.pref_switch_periodic_refresh_off)).check(
matches(
isDisplayed(),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).perform(click())
onView(withText(R.string.pref_switch_periodic_refresh_on)).check(
matches(
isDisplayed(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_periodic_refresh_minutes_title)).check(
matches(
isEnabled(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches(
isEnabled(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches(
isEnabled(),
),
)
changeAndCancelSetting("360", "123") {
onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click())
}
changeAndSaveSetting("360", "123") {
onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click())
}
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).perform(click())
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).perform(click())
onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).perform(click())
}
}
@@ -0,0 +1,85 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityReaderTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
lateinit var context: Context
@Before
fun init() {
activityRule.scenario.onActivity { activity ->
context = activity.window.context
}
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext(),
)
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_viewer)).perform(click())
}
@Test
fun testReader() {
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check(
matches(
allOf(
isDisplayed(),
not(
isChecked(),
),
),
),
)
onView(withText(R.string.pref_content_reader_font_size)).check(matches(isDisplayed()))
onView(withText(R.string.settings_reader_font)).check(matches(isDisplayed()))
}
@Test
fun testReaderActions() {
onView(withText(R.string.pref_switch_actions_pager_scroll_off)).check(
matches(
isDisplayed(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).perform(click())
onView(withText(R.string.pref_switch_actions_pager_scroll_on)).check(
matches(
isDisplayed(),
),
)
onView(withText(R.string.pref_content_reader_font_size)).perform(click())
changeAndCancelSetting("16", "10") {
onView(withText(R.string.pref_content_reader_font_size)).perform(click())
}
changeAndSaveSetting("16", "10") {
onView(withText(R.string.pref_content_reader_font_size)).perform(click())
}
testPreferencesFromArray(context, R.array.preloaded_fonts_values) {
onView(withText(R.string.settings_reader_font)).perform(click())
}
}
}
@@ -0,0 +1,92 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isSelected
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
lateinit var context: Context
@Before
fun init() {
activityRule.scenario.onActivity { activity ->
context = activity.window.context
}
loginAndInitHome()
openMenu()
onView(withText(R.string.title_activity_settings)).perform(click())
}
@Test
fun testAllSettings() {
onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_viewer)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_theme)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_links)).check(matches(isDisplayed()))
onView(withText(R.string.pref_switch_disable_acra)).check(
matches(
allOf(
isDisplayed(),
not(isSelected()),
),
),
)
onView(withText(R.string.action_about)).check(matches(isDisplayed()))
}
@Test
fun testThemes() {
testPreferencesFromArray(context, R.array.ModeTitles) {
onView(withText(R.string.pref_header_theme)).perform(click())
}
}
@Test
fun testExperimentail() {
onView(withText(R.string.pref_header_experimental)).perform(click())
changeAndCancelSetting("", "10") {
onView(withText(R.string.pref_api_timeout)).perform(click())
}
changeAndSaveSetting("", "10") {
onView(withText(R.string.pref_api_timeout)).perform(click())
}
}
@Test
fun testBugReports() {
onView(withText(R.string.pref_switch_disable_acra)).perform(click())
}
@Test
fun testLinks() {
onView(withText(R.string.pref_header_links)).perform(click())
onView(withText(R.string.issue_tracker_link)).check(matches(isDisplayed()))
onView(withText(R.string.issue_tracker_summary)).check(matches(isDisplayed()))
onView(withText(R.string.source_code)).check(matches(isDisplayed()))
onView(withText(R.string.translation)).check(matches(isDisplayed()))
}
@Test
fun testAbout() {
onView(withText(R.string.action_about)).perform(click())
onView(withText("ACRA")).check(matches(isDisplayed()))
}
}
@@ -0,0 +1,82 @@
package bou.amine.apps.readerforselfossv2.android
import androidx.test.espresso.AmbiguousViewMatcherException
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.UUID
@RunWith(AndroidJUnit4::class)
@LargeTest
class SourcesActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
lateinit var sourceName: String
@Before
fun init() {
sourceName = UUID.randomUUID().toString().substring(0, 15)
loginAndInitHome()
goToSources()
}
@Test
fun addSource() {
testAddSourceWithUrl(
"https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10",
sourceName,
)
}
@Suppress("detekt:SwallowedException")
@Test
fun addSourceCheckContent() {
testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName)
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
onView(withText(R.string.menu_home_refresh)).perform(click())
onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
onView(
withId(android.R.id.button1),
).perform(click())
Thread.sleep(10000)
onView(withId(R.id.swipeRefreshLayout)).perform(swipeDown())
Thread.sleep(2000)
try {
onView(withId(R.id.sourceTitleAndDate)).check(matches(isDisplayed()))
} catch (e: AmbiguousViewMatcherException) {
assert(true)
}
goToSources()
}
@After
fun deleteTheCreatedSource() {
onView(withText(sourceName)).check(matches(isDisplayed()))
onView(withId(R.id.deleteBtn)).perform(click())
onView(withText(sourceName)).check(doesNotExist())
}
private fun goToSources() {
openMenu()
onView(withText(R.string.menu_home_sources))
.perform(click())
}
}
@@ -12,7 +12,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.view.doOnNextLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
@@ -24,8 +28,10 @@ import bou.amine.apps.readerforselfossv2.android.background.LoadingWorker
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding
import bou.amine.apps.readerforselfossv2.android.fragments.FilterSheetFragment
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@@ -43,9 +49,12 @@ import org.kodein.di.instance
import java.security.MessageDigest
import java.util.concurrent.TimeUnit
private const val MIN_WIDTH_CARD_DP = 300
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware {
class HomeActivity :
AppCompatActivity(),
SearchView.OnQueryTextListener,
DIAware {
private var items: ArrayList<SelfossModel.Item> = ArrayList()
private var elementsShown: ItemType = ItemType.UNREAD
@@ -59,29 +68,30 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener
private lateinit var binding: ActivityHomeBinding
private var recyclerAdapter: RecyclerView.Adapter<*>? = null
private var recyclerAdapter: ItemsAdapter<out RecyclerView.ViewHolder>? = null
private var fromTabShortcut: Boolean = false
private val settingsLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
appSettingsService.refreshUserSettings()
}
private val settingsLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
appSettingsService.refreshUserSettings()
}
override val di by closestDI()
private val repository : Repository by instance()
private val appSettingsService : AppSettingsService by instance()
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
val view = binding.root
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
if (fromTabShortcut) {
elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
elementsShown =
ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
}
setContentView(view)
@@ -92,10 +102,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleSwipeRefreshLayout()
if (appSettingsService.isItemCachingEnabled()) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
repository.tryToCacheItemsAndGetNewOnes()
CountingIdlingResourceSingleton.decrement()
}
}
}
@@ -104,48 +115,54 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.swipeRefreshLayout.setColorSchemeResources(
R.color.refresh_progress_1,
R.color.refresh_progress_2,
R.color.refresh_progress_3
R.color.refresh_progress_3,
)
binding.swipeRefreshLayout.setOnRefreshListener {
repository.offlineOverride = false
lastFetchDone = false
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
getElementsAccordingToTab()
binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement()
}
}
val swipeDirs = if (appSettingsService.getPublicAccess()) {
0
} else {
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
}
val swipeDirs =
if (appSettingsService.getPublicAccess()) {
0
} else {
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
}
val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback(
0,
swipeDirs
swipeDirs,
) {
override fun getSwipeDirs(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
viewHolder: RecyclerView.ViewHolder,
): Int =
if (elementsShown == ItemType.STARRED) {
0
} else {
super.getSwipeDirs(
recyclerView,
viewHolder
viewHolder,
)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
target: RecyclerView.ViewHolder,
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {
override fun onSwiped(
viewHolder: RecyclerView.ViewHolder,
swipeDir: Int,
) {
val position = viewHolder.bindingAdapterPosition
val i = items.elementAtOrNull(position)
@@ -159,11 +176,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getElementsAccordingToTab()
}
} else {
Toast.makeText(
this@HomeActivity,
"Found null when swiping at positon $position.",
Toast.LENGTH_LONG
).show()
Toast
.makeText(
this@HomeActivity,
"Found null when swiping at positon $position.",
Toast.LENGTH_LONG,
).show()
}
}
}
@@ -171,7 +189,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
}
private fun updateBottomBarBadgeCount(badge: TextBadgeItem, count: Int) {
private fun updateBottomBarBadgeCount(
badge: TextBadgeItem,
count: Int,
) {
if (count > 0) {
badge
.setText(count.toString())
@@ -181,17 +202,23 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
}
@Suppress("detekt:LongMethod")
private fun handleBottomBar() {
tabNewBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
tabArchiveBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
tabStarredBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
tabNewBadge =
TextBadgeItem()
.setText("")
.setHideOnSelect(false)
.hide(false)
tabArchiveBadge =
TextBadgeItem()
.setText("")
.setHideOnSelect(false)
.hide(false)
tabStarredBadge =
TextBadgeItem()
.setText("")
.setHideOnSelect(false)
.hide(false)
if (appSettingsService.isDisplayUnreadCountEnabled()) {
lifecycleScope.launch {
@@ -218,19 +245,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val tabNew =
BottomNavigationItem(
R.drawable.ic_tab_fiber_new_black_24dp,
getString(R.string.tab_new)
)
.setBadgeItem(tabNewBadge)
getString(R.string.tab_new),
).setBadgeItem(tabNewBadge)
val tabArchive =
BottomNavigationItem(
R.drawable.ic_tab_archive_black_24dp,
getString(R.string.tab_read)
)
.setBadgeItem(tabArchiveBadge)
getString(R.string.tab_read),
).setBadgeItem(tabArchiveBadge)
val tabStarred =
BottomNavigationItem(
R.drawable.ic_tab_favorite_black_24dp,
getString(R.string.tab_favs)
getString(R.string.tab_favs),
).setActiveColorResource(R.color.pink)
.setBadgeItem(tabStarredBadge)
@@ -260,28 +285,28 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleBottomBarActions()
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
handleGdprDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
handleRecurringTask()
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
repository.handleDBActions()
CountingIdlingResourceSingleton.decrement()
}
getElementsAccordingToTab()
}
private fun handleGDPRDialog(GDPRShown: Boolean) {
private fun handleGdprDialog(gdprShown: Boolean) {
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
if (!GDPRShown) {
if (!gdprShown) {
val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK"
"OK",
) { dialog, _ ->
appSettingsService.settings.putBoolean("GDPR_shown", true)
dialog.dismiss()
@@ -298,37 +323,43 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
when (currentManager) {
is StaggeredGridLayoutManager ->
if (!appSettingsService.isCardViewEnabled()) {
layoutManager = GridLayoutManager(
this,
calculateNoOfColumns()
)
layoutManager =
GridLayoutManager(
this,
calculateNoOfColumns(),
)
binding.recyclerView.layoutManager = layoutManager
}
is GridLayoutManager ->
if (appSettingsService.isCardViewEnabled()) {
layoutManager = StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL
)
layoutManager =
StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL,
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager
}
else ->
if (currentManager == null) {
if (!appSettingsService.isCardViewEnabled()) {
layoutManager = GridLayoutManager(
this,
calculateNoOfColumns()
)
layoutManager =
GridLayoutManager(
this,
calculateNoOfColumns(),
)
binding.recyclerView.layoutManager = layoutManager
} else {
layoutManager = StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL
)
layoutManager =
StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL,
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager
}
}
@@ -336,39 +367,42 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
private fun handleBottomBarActions() {
binding.bottomBar.setTabSelectedListener(object : BottomNavigationBar.OnTabSelectedListener {
override fun onTabUnselected(position: Int) = Unit
binding.bottomBar.setTabSelectedListener(
object : BottomNavigationBar.OnTabSelectedListener {
override fun onTabUnselected(position: Int) = Unit
override fun onTabReselected(position: Int) {
override fun onTabReselected(position: Int) {
when (val layoutManager = binding.recyclerView.adapter) {
is StaggeredGridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPositions(null)[0] == 0) {
getElementsAccordingToTab()
} else {
layoutManager.scrollToPositionWithOffset(0, 0)
}
when (val layoutManager = binding.recyclerView.adapter) {
is StaggeredGridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPositions(null)[0] == 0) {
getElementsAccordingToTab()
} else {
layoutManager.scrollToPositionWithOffset(0, 0)
}
is GridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
getElementsAccordingToTab()
} else {
layoutManager.scrollToPositionWithOffset(0, 0)
}
else -> Unit
is GridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
getElementsAccordingToTab()
} else {
layoutManager.scrollToPositionWithOffset(0, 0)
}
else -> Unit
}
}
}
override fun onTabSelected(position: Int) {
offset = 0
lastFetchDone = false
override fun onTabSelected(position: Int) {
offset = 0
lastFetchDone = false
elementsShown = ItemType.fromInt(position + 1)
getElementsAccordingToTab()
binding.recyclerView.scrollToPosition(0)
elementsShown = ItemType.fromInt(position + 1)
getElementsAccordingToTab()
binding.recyclerView.scrollToPosition(0)
fetchOnEmptyList()
}
})
fetchOnEmptyList()
}
},
)
}
fun fetchOnEmptyList() {
@@ -378,31 +412,38 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
private fun handleInfiniteScroll() {
recyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(localRecycler: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) {
val lastVisibleItem = getLastVisibleItem()
recyclerViewScrollListener =
object : RecyclerView.OnScrollListener() {
override fun onScrolled(
localRecycler: RecyclerView,
dx: Int,
dy: Int,
) {
if (dy > 0) {
val lastVisibleItem = getLastVisibleItem()
if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) {
getElementsAccordingToTab(appendResults = true)
if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) {
getElementsAccordingToTab(appendResults = true)
}
}
}
}
}
binding.recyclerView.clearOnScrollListeners()
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
}
private fun getLastVisibleItem() : Int {
return when (val manager = binding.recyclerView.layoutManager) {
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
null
).last()
private fun getLastVisibleItem(): Int =
when (val manager = binding.recyclerView.layoutManager) {
is StaggeredGridLayoutManager ->
manager
.findLastCompletelyVisibleItemPositions(
null,
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0
}
}
private fun mayBeEmpty() =
if (items.isEmpty()) {
@@ -411,73 +452,81 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.emptyText.visibility = View.GONE
}
fun getElementsAccordingToTab(
appendResults: Boolean = false
) {
offset = if (appendResults && items.size > 0) {
items.size - 1
} else {
0
}
fun getElementsAccordingToTab(appendResults: Boolean = false) {
offset =
if (appendResults && items.size > 0) {
items.size - 1
} else {
0
}
firstVisible = if (appendResults) firstVisible else 0
getItems(appendResults, elementsShown)
}
private fun getItems(appendResults: Boolean, itemType: ItemType) {
private fun getItems(
appendResults: Boolean,
itemType: ItemType,
) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = true
repository.displayedItems = itemType
items = if (appendResults) {
repository.getOlderItems()
} else {
repository.getNewerItems()
}
items =
if (appendResults) {
repository.getOlderItems()
} else {
repository.getNewerItems()
}
binding.swipeRefreshLayout.isRefreshing = false
handleListResult()
CountingIdlingResourceSingleton.decrement()
}
}
private fun handleListResult(appendResults: Boolean = false) {
if (appendResults) {
val oldManager = binding.recyclerView.layoutManager
firstVisible = when (oldManager) {
is StaggeredGridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPositions(null).last()
is GridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPosition()
else -> 0
}
firstVisible =
when (oldManager) {
is StaggeredGridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPositions(null).last()
is GridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPosition()
else -> 0
}
}
if (recyclerAdapter == null) {
if (appSettingsService.isCardViewEnabled()) {
recyclerAdapter =
ItemCardAdapter(
this,
items,
) {
updateItems(it)
}
ItemCardAdapter(
this,
items,
) {
updateItems(it)
}
} else {
recyclerAdapter =
ItemListAdapter(
this,
items,
) {
updateItems(it)
}
ItemListAdapter(
this,
items,
) {
updateItems(it)
}
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
this@HomeActivity,
DividerItemDecoration.VERTICAL
)
DividerItemDecoration.VERTICAL,
),
)
}
binding.recyclerView.adapter = recyclerAdapter
} else {
(recyclerAdapter as ItemsAdapter<*>).updateAllItems(items)
recyclerAdapter!!.updateAllItems(items)
}
reloadBadges()
@@ -486,8 +535,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun reloadBadges() {
if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.IO).launch {
repository.reloadBadges()
CountingIdlingResourceSingleton.decrement()
}
}
}
@@ -495,7 +546,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun calculateNoOfColumns(): Int {
val displayMetrics = resources.displayMetrics
val dpWidth = displayMetrics.widthPixels / displayMetrics.density
return (dpWidth / 300).toInt()
return (dpWidth / MIN_WIDTH_CARD_DP).toInt()
}
override fun onQueryTextChange(p0: String?): Boolean {
@@ -523,14 +574,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.getActionView() as SearchView
val searchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
return true
}
private fun needsConfirmation(titleRes: Int, messageRes: Int, doFn: () -> Unit) {
AlertDialog.Builder(this@HomeActivity)
private fun needsConfirmation(
titleRes: Int,
messageRes: Int,
doFn: () -> Unit,
) {
AlertDialog
.Builder(this@HomeActivity)
.setMessage(messageRes)
.setTitle(titleRes)
.setPositiveButton(android.R.string.ok) { _, _ -> doFn() }
@@ -539,82 +595,106 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
.show()
}
@Suppress("detekt:ReturnCount", "detekt:LongMethod")
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.issue_tracker -> {
baseContext.openUrlInBrowser(AppSettingsService.BUG_URL)
return true
}
R.id.action_filter -> {
val filterSheetFragment = FilterSheetFragment()
filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG)
return true
}
R.id.refresh -> {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote()
if (updatedRemote) {
Toast.makeText(
this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG
)
.show()
Toast
.makeText(
this@HomeActivity,
R.string.refresh_success_response,
Toast.LENGTH_LONG,
).show()
} else {
Toast.makeText(
this@HomeActivity,
R.string.refresh_failer_message,
Toast.LENGTH_SHORT
).show()
Toast
.makeText(
this@HomeActivity,
R.string.refresh_failer_message,
Toast.LENGTH_SHORT,
).show()
}
CountingIdlingResourceSingleton.decrement()
}
}
return true
}
R.id.readAll -> {
if (elementsShown == ItemType.UNREAD) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
binding.swipeRefreshLayout.isRefreshing = true
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items)
if (success) {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_read,
Toast.LENGTH_SHORT
).show()
Toast
.makeText(
this@HomeActivity,
R.string.all_posts_read,
Toast.LENGTH_SHORT,
).show()
tabNewBadge.removeBadge()
getElementsAccordingToTab()
} else {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_not_read,
Toast.LENGTH_SHORT
).show()
Toast
.makeText(
this@HomeActivity,
R.string.all_posts_not_read,
Toast.LENGTH_SHORT,
).show()
}
handleListResult()
binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement()
}
}
}
return true
}
R.id.action_disconnect -> {
runBlocking {
repository.logout()
needsConfirmation(
R.string.confirm_disconnect_title,
R.string.confirm_disconnect_description,
) {
runBlocking {
repository.logout()
}
val intent = Intent(this, LoginActivity::class.java)
this.startActivity(intent)
finish()
}
val intent = Intent(this, LoginActivity::class.java)
this.startActivity(intent)
finish()
return true
}
R.id.action_settings -> {
settingsLauncher.launch(Intent(this, SettingsActivity::class.java))
return true
}
R.id.action_sources -> {
startActivity(Intent(this, SourcesActivity::class.java))
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
@@ -633,20 +713,30 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun handleRecurringTask() {
if (appSettingsService.isPeriodicRefreshEnabled()) {
val myConstraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
.setRequiresStorageNotLow(true)
.build()
val myConstraints =
Constraints
.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
.setRequiresStorageNotLow(true)
.build()
val backgroundWork =
PeriodicWorkRequestBuilder<LoadingWorker>(appSettingsService.getRefreshMinutes(), TimeUnit.MINUTES)
.setConstraints(myConstraints)
PeriodicWorkRequestBuilder<LoadingWorker>(
appSettingsService.getRefreshMinutes(),
TimeUnit.MINUTES,
).setConstraints(myConstraints)
.addTag("selfoss-loading")
.build()
WorkManager.getInstance(baseContext).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork)
WorkManager
.getInstance(
baseContext,
).enqueueUniquePeriodicWork(
"selfoss-loading",
ExistingPeriodicWorkPolicy.KEEP,
backgroundWork,
)
}
}
}
@@ -11,8 +11,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
@@ -32,27 +32,44 @@ 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
}
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 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 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
}
}
override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {
// Nothing
}
}
binding.root.setTransitionListener(transitionListener)
}
@@ -67,10 +84,11 @@ class ImageActivity : AppCompatActivity() {
return super.onOptionsItemSelected(item)
}
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
private inner class ScreenSlidePagerAdapter(
fa: FragmentActivity,
) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allImages.size
override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])
}
}
}
@@ -4,6 +4,7 @@ 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
@@ -16,6 +17,7 @@ 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.testing.CountingIdlingResourceSingleton
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@@ -28,9 +30,11 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
private const val MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED = 3
class LoginActivity : AppCompatActivity(), DIAware {
class LoginActivity :
AppCompatActivity(),
DIAware {
private var inValidCount: Int = 0
private var isWithLogin = false
@@ -40,7 +44,6 @@ class LoginActivity : AppCompatActivity(), DIAware {
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -69,7 +72,6 @@ class LoginActivity : AppCompatActivity(), DIAware {
}
private fun handleActions() {
binding.passwordView.setOnEditorActionListener(
TextView.OnEditorActionListener { _, id, _ ->
if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
@@ -77,7 +79,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
return@OnEditorActionListener true
}
false
}
},
)
binding.signInButton.setOnClickListener { attemptLogin() }
@@ -98,16 +100,21 @@ 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() {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
repository.updateApiInformation()
ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString())
ACRA.errorReporter.putCustomData(
"SELFOSS_API_VERSION",
appSettingsService.getApiVersion().toString(),
)
CountingIdlingResourceSingleton.decrement()
}
val intent = Intent(this, HomeActivity::class.java)
startActivity(intent)
@@ -123,50 +130,69 @@ class LoginActivity : AppCompatActivity(), DIAware {
}
private fun attemptLogin() {
// Reset errors.
binding.urlView.error = null
binding.loginView.error = null
binding.passwordView.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()
.trim()
val login =
binding.loginView.text
.toString()
.trim()
val password =
binding.passwordView.text
.toString()
.trim()
failInvalidUrl(url)
failLoginDetails(password, login)
showProgress(true)
appSettingsService.updateSelfSigned(binding.selfSigned.isChecked)
repository.refreshLoginInformation(url, login, password)
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
val result = repository.login()
if (result) {
val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance()
if (!errorFetching && !displaySelfossOnly) {
goToMain()
} else {
if (displaySelfossOnly) {
Toast.makeText(
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
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)
CountingIdlingResourceSingleton.decrement()
}
}
private fun failLoginDetails(
password: String,
login: String
login: String,
) {
var lastFocusedView: View? = null
var cancel = false
@@ -193,13 +219,13 @@ class LoginActivity : AppCompatActivity(), DIAware {
cancel = true
binding.urlView.error = getString(R.string.login_url_problem)
inValidCount++
if (inValidCount == 3) {
if (inValidCount == MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED) {
val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.warning_wrong_url))
alertDialog.setMessage(getString(R.string.text_wrong_url))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK"
"OK",
) { dialog, _ -> dialog.dismiss() }
alertDialog.show()
inValidCount = 0
@@ -208,7 +234,10 @@ class LoginActivity : AppCompatActivity(), DIAware {
maybeCancelAndFocusView(cancel, focusView)
}
private fun maybeCancelAndFocusView(cancel: Boolean, focusView: View?) {
private fun maybeCancelAndFocusView(
cancel: Boolean,
focusView: View?,
) {
if (cancel) {
focusView?.requestFocus()
}
@@ -222,12 +251,13 @@ 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
@@ -235,12 +265,13 @@ class LoginActivity : AppCompatActivity(), DIAware {
.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
}
},
)
}
@@ -251,13 +282,25 @@ 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.BUG_URL))
startActivity(browserIntent)
return true
}
R.id.about -> {
LibsBuilder()
.withAboutIconShown(true)
.withAboutVersionShown(true)
.withAboutSpecial2("Bug reports")
.withAboutSpecial2Description(AppSettingsService.BUG_URL)
.withAboutSpecial1("Project Page")
.withAboutSpecial1Description(AppSettingsService.SOURCE_URL)
.start(this)
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -8,7 +8,6 @@ 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)
@@ -9,10 +9,11 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
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.di.networkModule
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.github.ln_12.library.ConnectivityStatus
@@ -29,23 +30,29 @@ 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 {
import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.bind
import org.kodein.di.instance
import org.kodein.di.singleton
class MyApp :
MultiDexApplication(),
DIAware {
override val di by DI.lazy {
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) }
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) }
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(),
isConnectionAvailable,
instance(),
)
}
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
}
@@ -55,6 +62,7 @@ class MyApp : MultiDexApplication(), DIAware {
private val connectivityStatus: ConnectivityStatus by instance()
private val driverFactory: DriverFactory by instance()
@Suppress("detekt:ForbiddenComment")
// TODO: handle with the "previous" way
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
@@ -70,24 +78,26 @@ class MyApp : MultiDexApplication(), DIAware {
ProcessLifecycleOwner.get().lifecycle.addObserver(
AppLifeCycleObserver(
connectivityStatus,
repository
)
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
}
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()
Toast
.makeText(
applicationContext,
toastMessage,
Toast.LENGTH_SHORT,
).show()
}
}
}
@@ -100,37 +110,38 @@ class MyApp : MultiDexApplication(), DIAware {
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
)
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
// required
text = getString(R.string.crash_toast_text)
length = Toast.LENGTH_SHORT
}
httpSender {
uri =
"https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/
"https://bugs.amine-bouabdallaoui.fr/report" // best guess, you may need to adjust this
basicAuthLogin = "qMEscjj89Gwt6cPR"
basicAuthPassword = "Yo58QFlGzFaWlBzP"
httpMethod = HttpSender.Method.POST
@@ -144,15 +155,16 @@ 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(AppSettingsService.SYNC_CHANNEL_ID, 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(
AppSettingsService.NEW_ITEMS_CHANNEL,
newItemsChannelname,
newItemsChannelimportance,
)
notificationManager.createNotificationChannel(mChannel)
notificationManager.createNotificationChannel(newItemsChannelmChannel)
@@ -163,9 +175,11 @@ class MyApp : MultiDexApplication(), DIAware {
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, e ->
if (e is NoClassDefFoundError && e.stackTrace.asList().any {
if (e is NoClassDefFoundError &&
e.stackTrace.asList().any {
it.toString().contains("android.view.ViewDebug")
}) {
}
) {
// Nothing
} else {
oldHandler.uncaughtException(thread, e)
@@ -175,9 +189,8 @@ class MyApp : MultiDexApplication(), DIAware {
class AppLifeCycleObserver(
val connectivityStatus: ConnectivityStatus,
val repository: Repository
val repository: Repository,
) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
repository.connectionMonitored = true
@@ -190,4 +203,4 @@ class MyApp : MultiDexApplication(), DIAware {
super.onPause(owner)
}
}
}
}
@@ -22,8 +22,9 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
class ReaderActivity : AppCompatActivity(), DIAware {
class ReaderActivity :
AppCompatActivity(),
DIAware {
private var currentItem: Int = 0
private lateinit var toolbarMenu: Menu
@@ -52,6 +53,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
showMenuItem(false)
}
@Suppress("detekt:SwallowedException")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityReaderBinding.inflate(layoutInflater)
@@ -74,7 +76,6 @@ class ReaderActivity : AppCompatActivity(), DIAware {
try {
readItem(allItems[currentItem])
} catch (e: IndexOutOfBoundsException) {
e.sendSilentlyWithAcraWithName("out of bound > size = ${allItems.size} currentItem = $currentItem")
finish()
}
@@ -101,35 +102,37 @@ class ReaderActivity : AppCompatActivity(), DIAware {
oldInstanceState.clear()
}
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
FragmentStateAdapter(fa) {
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 {
return when (keyCode) {
override fun onKeyDown(
keyCode: Int,
event: KeyEvent?,
): Boolean =
when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
val currentFragment =
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
currentFragment.scrollDown()
currentFragment.volumeButtonScrollDown()
true
}
KeyEvent.KEYCODE_VOLUME_UP -> {
val currentFragment =
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
currentFragment.scrollUp()
currentFragment.volumeButtonScrollUp()
true
}
else -> {
super.onKeyDown(keyCode, event)
}
}
}
private fun alignmentMenu() {
val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT
@@ -153,10 +156,8 @@ class ReaderActivity : AppCompatActivity(), DIAware {
canFavorite()
}
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
@@ -167,7 +168,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
}
readItem(allItems[position])
}
}
},
)
}
@@ -191,6 +192,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
onBackPressedDispatcher.onBackPressed()
return true
}
R.id.star -> {
if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch {
@@ -204,10 +206,12 @@ class ReaderActivity : AppCompatActivity(), DIAware {
afterSave()
}
}
R.id.align_left -> {
switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
refreshFragment()
}
R.id.align_justify -> {
switchAlignmentSetting(AppSettingsService.JUSTIFY)
refreshFragment()
@@ -8,6 +8,7 @@ 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.android.testing.CountingIdlingResourceSingleton
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import kotlinx.coroutines.CoroutineScope
@@ -17,12 +18,13 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
class SourcesActivity : AppCompatActivity(), DIAware {
class SourcesActivity :
AppCompatActivity(),
DIAware {
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?) {
binding = ActivitySourcesBinding.inflate(layoutInflater)
@@ -37,7 +39,8 @@ class SourcesActivity : AppCompatActivity(), DIAware {
supportActionBar?.setDisplayShowHomeEnabled(true)
binding.fab.rippleColor = resources.getColor(R.color.colorAccentDark)
binding.fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
binding.fab.backgroundTintList =
ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
}
override fun onStop() {
@@ -54,22 +57,27 @@ class SourcesActivity : AppCompatActivity(), DIAware {
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSourcesDetails()
if (response.isNotEmpty()) {
items = response
val mAdapter = SourcesListAdapter(
this@SourcesActivity, items
)
val mAdapter =
SourcesListAdapter(
this@SourcesActivity,
items,
)
binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
} else {
Toast.makeText(
this@SourcesActivity,
R.string.cant_get_sources,
Toast.LENGTH_SHORT
).show()
Toast
.makeText(
this@SourcesActivity,
R.string.cant_get_sources,
Toast.LENGTH_SHORT,
).show()
}
CountingIdlingResourceSingleton.decrement()
}
binding.fab.setOnClickListener {
@@ -21,9 +21,9 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
class UpsertSourceActivity : AppCompatActivity(), DIAware {
class UpsertSourceActivity :
AppCompatActivity(),
DIAware {
private var existingSource: SelfossModel.SourceDetail? = null
private var mSpoutsValue: String? = null
@@ -58,7 +58,6 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.title = resources.getString(title)
maybeGetDetailsFromIntentSharing(intent)
binding.saveBtn.setOnClickListener {
@@ -86,28 +85,35 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
}
}
@Suppress("detekt:SwallowedException")
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]
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
}
}
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()
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
}
@@ -127,7 +133,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
ArrayAdapter(
this@UpsertSourceActivity,
android.R.layout.simple_spinner_item,
itemsStrings
itemsStrings,
)
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spoutsSpinner.adapter = spinnerArrayAdapter
@@ -144,9 +150,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
}
}
private fun maybeGetDetailsFromIntentSharing(
intent: Intent
) {
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))
@@ -170,32 +174,35 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
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(),
)
}
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()
Toast
.makeText(
this@UpsertSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT,
).show()
}
}
}
@@ -1,7 +1,6 @@
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
@@ -10,10 +9,10 @@ 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.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
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.openItemUrl
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
@@ -31,10 +30,10 @@ import org.kodein.di.instance
class ItemCardAdapter(
override val app: Activity,
override var items: ArrayList<SelfossModel.Item>,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
override val items: ArrayList<SelfossModel.Item>,
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext
override lateinit var binding: CardItemBinding
private val imageMaxHeight: Int =
c.resources.getDimension(R.dimen.card_image_max_height).toInt()
@@ -42,15 +41,53 @@ class ItemCardAdapter(
override val repository: Repository by instance()
override val appSettingsService: AppSettingsService by instance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): ViewHolder {
binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
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.openItemUrlInBrowserAsNewTask(items[position])
}
}
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
@@ -62,7 +99,13 @@ class ItemCardAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate()
binding.sourceTitleAndDate.text =
try {
itm.sourceAuthorAndDate()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")
itm.sourceAuthorOnly()
}
if (!appSettingsService.isFullHeightCardsEnabled()) {
binding.itemImage.maxHeight = imageMaxHeight
@@ -86,53 +129,7 @@ class ItemCardAdapter(
}
}
override fun getItemCount(): Int {
return items.size
}
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
handleClickListeners()
handleLinkOpening()
}
private fun handleClickListeners() {
binding.favButton.setOnClickListener {
val item = items[bindingAdapterPosition]
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[bindingAdapterPosition]
c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded())
}
binding.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[bindingAdapterPosition])
}
}
private fun handleLinkOpening() {
binding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl(
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(),
app
)
}
}
}
inner class ViewHolder(
val binding: CardItemBinding,
) : RecyclerView.ViewHolder(binding.root)
}
@@ -1,15 +1,14 @@
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.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@@ -22,34 +21,47 @@ import org.kodein.di.instance
class ItemListAdapter(
override val app: Activity,
override var items: ArrayList<SelfossModel.Item>,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
override val items: ArrayList<SelfossModel.Item>,
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val c: Context = app.baseContext
override lateinit var binding: ListItemBinding
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 val appSettingsService: AppSettingsService by instance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): ViewHolder {
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.sourceTitleAndDate.text = itm.sourceAuthorAndDate()
binding.sourceTitleAndDate.text =
try {
itm.sourceAuthorAndDate()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")
itm.sourceAuthorOnly()
}
if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else {
@@ -61,24 +73,7 @@ class ItemListAdapter(
}
}
override fun getItemCount(): Int = items.size
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
handleLinkOpening()
}
private fun handleLinkOpening() {
binding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl(
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(),
app
)
}
}
}
inner class ViewHolder(
val binding: ListItemBinding,
) : RecyclerView.ViewHolder(binding.root)
}
@@ -1,10 +1,13 @@
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.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@@ -15,29 +18,38 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.DIAware
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware {
abstract var items: ArrayList<SelfossModel.Item>
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
RecyclerView.Adapter<VH>(),
DIAware {
abstract val items: ArrayList<SelfossModel.Item>
abstract val repository: Repository
abstract val binding: ViewBinding
abstract val appSettingsService: AppSettingsService
abstract val app: Activity
abstract val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
abstract val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit
protected val c: Context get() = app.baseContext
fun updateAllItems(items: ArrayList<SelfossModel.Item>) {
this.items = items
this.items.clear()
this.items.addAll(items)
updateHomeItems(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)
}
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)
@@ -45,16 +57,19 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show()
}