Compare commits
123
Commits
v123102851
...
b5817865e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5817865e4 | ||
|
|
02d503e03a | ||
|
|
24b9320d6d | ||
|
|
ceba58e98f | ||
|
|
c3ee07dd85 | ||
|
|
93d99192b3 | ||
|
|
359dec2ca0 | ||
|
|
62354ec70a | ||
|
|
18a17251ac | ||
|
|
5e91724ee2 | ||
|
|
212d259a33 | ||
|
|
3bf60f1146 | ||
|
|
ef13e300f0 | ||
|
|
f170d1157d | ||
|
|
af4752f0f0 | ||
|
|
f0fa1a17b6 | ||
|
|
bb84d1541c | ||
|
|
c9227b2c1c | ||
|
|
6eaad0c7c5 | ||
|
|
a1c98aa7d0 | ||
|
|
d5ec118679 | ||
|
|
a1c0241a58 | ||
|
|
f38936f9b4 | ||
|
|
a90ccec707 | ||
|
|
2564b19726 | ||
|
|
61c7bb20cc | ||
|
|
6a0f5baf0a | ||
|
|
39f9505c00 | ||
|
|
6a6d447456 | ||
|
|
0bb4fe6aed | ||
|
|
7df4c3368c | ||
|
|
c69635b5ae | ||
|
|
3a829df70e | ||
|
|
7a0202689f | ||
|
|
b20f6888f5 | ||
|
|
6b96eb358d | ||
|
|
dfc1bf9fa3 | ||
|
|
b173664ff0 | ||
|
|
bc20a421ae | ||
|
|
794500355a | ||
|
|
44f9dd53d3 | ||
|
|
717d6b664c | ||
|
|
e23289a3dc | ||
|
|
2f5ebe2420 | ||
|
|
1893904135 | ||
|
|
a4cb28ba81 | ||
|
|
ae3cada1c7 | ||
|
|
309500276f | ||
|
|
ce255b23cd | ||
|
|
3b3a575dae | ||
|
|
7bcf4574b4 | ||
|
|
c79ab5e92b | ||
|
|
54dbda76ab | ||
|
|
11c39ae87c | ||
|
|
6645902ec8 | ||
|
|
0a07a5dfad | ||
|
|
d88d38fd3b | ||
|
|
28fe38aa17 | ||
|
|
d524c30732 | ||
|
|
8c00aa65da | ||
|
|
ae81261cb1 | ||
|
|
03c567ee33 | ||
|
|
d23dd82fc2 | ||
|
|
2e7a168424 | ||
|
|
5bc2f614af | ||
|
|
934c112db5 | ||
|
|
ad7549a89f | ||
|
|
fb9ceecabd | ||
|
|
61b9fd30e0 | ||
|
|
806e56e20b | ||
|
|
cd8b7aaf9d | ||
|
|
c25ad7621e | ||
|
|
63da3b9fe7 | ||
|
|
1d99eeb633 | ||
|
|
162a350a8f | ||
|
|
27c1bba146 | ||
|
|
b7f3a9877a | ||
|
|
47f78754dc | ||
|
|
1bdfb143ac | ||
|
|
d81ced3964 | ||
|
|
fbafece1fa | ||
|
|
cbed8f07cb | ||
|
|
f54fcc3ba1 | ||
|
|
aad93ef722 | ||
|
|
9e83af0302 | ||
|
|
24b86e66b4 | ||
|
|
641c444061 | ||
|
|
0902c61544 | ||
|
|
6790152a0b | ||
|
|
46d1ba418e | ||
|
|
436373d0ad | ||
|
|
5b9b51c02d | ||
|
|
b81abe384a | ||
|
|
851f862dbe | ||
|
|
8d7e302af8 | ||
|
|
236e1cca90 | ||
|
|
3a33cb4510 | ||
|
|
0bf9ca9a49 | ||
|
|
61e0087894 | ||
|
|
1ec05d9913 | ||
|
|
859bd91bbb | ||
|
|
204b736c53 | ||
|
|
f24609c143 | ||
|
|
b94d7dc537 | ||
|
|
41910cc4cd | ||
|
|
db166ca9d4 | ||
|
|
db0d5a4a85 | ||
|
|
3bc0d7cf95 | ||
|
|
8f464d95fd | ||
|
|
5ccd6a3368 | ||
|
|
cdbded246e | ||
|
|
750c7758bd | ||
|
|
22f8b14ecd | ||
|
|
6e27d6d4e6 | ||
|
|
14ff4dbd05 | ||
|
|
390c2d0cf3 | ||
|
|
e58914ef58 | ||
|
|
a03f08fca1 | ||
|
|
8e9b87f00c | ||
|
|
f765224a86 | ||
|
|
14d2219eb8 | ||
|
|
137580ccf9 | ||
|
|
f101d22f54 |
No files matched your search
-170
@@ -1,170 +0,0 @@
|
|||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: test
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Lint
|
|
||||||
failure: ignore
|
|
||||||
image: mingc/android-build-box:latest
|
|
||||||
commands:
|
|
||||||
- echo "---------------------------------------------------------"
|
|
||||||
- echo "Install linters..."
|
|
||||||
- curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
|
|
||||||
- curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip && unzip detekt-cli-1.23.1.zip
|
|
||||||
- echo "---------------------------------------------------------"
|
|
||||||
- echo "Linting..."
|
|
||||||
- ktlint || true
|
|
||||||
- echo "---------------------------------------------------------"
|
|
||||||
- echo "Detecting..."
|
|
||||||
- ./detekt-cli-1.23.1/bin/detekt-cli --all-rules || true
|
|
||||||
- echo "---------------------------------------------------------"
|
|
||||||
command_timeout: 1m
|
|
||||||
- name: BuildAndTest
|
|
||||||
image: mingc/android-build-box:latest
|
|
||||||
commands:
|
|
||||||
- echo "---------------------------------------------------------"
|
|
||||||
- echo "Configure gradle..."
|
|
||||||
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
|
|
||||||
- echo "---------------------------------------------------------"
|
|
||||||
- echo "Configure java..."
|
|
||||||
- . ~/.bash_profile
|
|
||||||
- jenv global 17.0
|
|
||||||
- java --version
|
|
||||||
- date
|
|
||||||
- echo "---------------------------------------------------------"
|
|
||||||
- echo "Building and testing..."
|
|
||||||
- ./gradlew build
|
|
||||||
- echo "---------------------------------------------------------"
|
|
||||||
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
|
|
||||||
- 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]"
|
|
||||||
environment:
|
|
||||||
TZ: Europe/Paris
|
|
||||||
|
|
||||||
- name: git-push
|
|
||||||
image: appleboy/drone-git-push
|
|
||||||
settings:
|
|
||||||
branch: master
|
|
||||||
remote:
|
|
||||||
from_secret: remoteUrl
|
|
||||||
followtags: true
|
|
||||||
ssh_key:
|
|
||||||
from_secret: privateKey
|
|
||||||
skip_verify: true
|
|
||||||
|
|
||||||
- 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\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
|
|
||||||
- echo "---------------------------------------------------------"
|
|
||||||
- echo "Generate APK"
|
|
||||||
- ./gradlew :androidApp:assembleGithubConfigRelease
|
|
||||||
- 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
|
|
||||||
@@ -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 @@
|
|||||||
|
project_id_env: CROWDIN_PROJECT_ID
|
||||||
|
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||||
|
base_path: "../../../"
|
||||||
|
|
||||||
|
files:
|
||||||
|
- source: /androidApp/src/main/res/values/strings.xml
|
||||||
|
translation: /androidApp/src/main/res/values-%android_code%/%original_file_name%
|
||||||
|
translate_attributes: '0'
|
||||||
|
content_segmentation: '0'
|
||||||
|
preserve_hierarchy: true
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
selfoss:
|
||||||
|
container_name: selfoss
|
||||||
|
image: rsprta/selfoss
|
||||||
|
network_mode: "host"
|
||||||
|
ports:
|
||||||
|
- "8888:8888"
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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: "Check android app changes"
|
||||||
|
id: check-android-changes
|
||||||
|
uses: tj-actions/changed-files@v45
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
androidApp/src/**
|
||||||
|
- name: Fetch tags
|
||||||
|
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||||
|
run: git fetch --tags -p
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
cache: gradle
|
||||||
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
|
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||||
|
- uses: android-actions/setup-android@v3
|
||||||
|
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||||
|
- name: Configure gradle...
|
||||||
|
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||||
|
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties
|
||||||
|
- name: Build and test
|
||||||
|
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||||
|
run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest # These tests will be done
|
||||||
|
# TESTS ARE RUN LOCALLY
|
||||||
|
# - 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
|
||||||
|
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||||
|
run: |
|
||||||
|
./gradlew :koverHtmlReport
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||||
|
with:
|
||||||
|
name: coverage
|
||||||
|
path: build/reports/kover/html
|
||||||
|
retention-days: 1
|
||||||
|
overwrite: true
|
||||||
|
include-hidden-files: true
|
||||||
|
# TESTS ARE RUN LOCALLY
|
||||||
|
# - name: Clean
|
||||||
|
# if: always()
|
||||||
|
# run: |
|
||||||
|
# docker compose -f .gitea/workflows/assets/docker-compose.yml stop
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: Coverage
|
||||||
|
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
|
||||||
|
- 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: Tests
|
||||||
|
if: steps.avd-cache.outputs.cache-hit != 'true'
|
||||||
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
|
with:
|
||||||
|
api-level: 29
|
||||||
|
arch: x86_64
|
||||||
|
script: ./gradlew androidApp:connectedAndroidTest
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: coverage-espresso
|
||||||
|
path: build/reports/coverage/androidTest/githubConfig/debug/connected
|
||||||
|
retention-days: 1
|
||||||
|
overwrite: true
|
||||||
|
include-hidden-files: true
|
||||||
|
- name: Clean
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
docker compose -f .gitea/workflows/assets/docker-compose.yml stop
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
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
|
||||||
|
ref: master
|
||||||
|
- 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: master
|
||||||
|
- 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
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
name: Check PR code
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
EspressoReports:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
uses: ./.gitea/workflows/common_coverage.yml
|
||||||
|
# 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'
|
||||||
|
# translations:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - name: Check out repository code
|
||||||
|
# uses: actions/checkout@v4
|
||||||
|
# with:
|
||||||
|
# fetch-depth: 0
|
||||||
|
# - name: "Check translations changes"
|
||||||
|
# id: check-translations-changes
|
||||||
|
# uses: tj-actions/changed-files@v45
|
||||||
|
# with:
|
||||||
|
# files: |
|
||||||
|
# androidApp/src/main/res/values/strings.xml
|
||||||
|
# - name: upload translation sources
|
||||||
|
# if: steps.check-api-changes.outputs.any_modified == 'true'
|
||||||
|
# uses: crowdin/github-action@v2
|
||||||
|
# with:
|
||||||
|
# config: './.gitea/workflows/assets/crowdin.yml'
|
||||||
|
# upload_sources: true
|
||||||
|
# upload_translations: false
|
||||||
|
# download_translations: false
|
||||||
|
# create_pull_request: false
|
||||||
|
# push_translations: false
|
||||||
|
# env:
|
||||||
|
# CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
# CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
|
# - name: wait
|
||||||
|
# if: steps.check-api-changes.outputs.any_modified == 'true'
|
||||||
|
# run: sleep 10s
|
||||||
|
# - name: download translations
|
||||||
|
# if: steps.check-api-changes.outputs.any_modified == 'true'
|
||||||
|
# uses: crowdin/github-action@v2
|
||||||
|
# with:
|
||||||
|
# config: './.gitea/workflows/assets/crowdin.yml'
|
||||||
|
# upload_sources: false
|
||||||
|
# upload_translations: false
|
||||||
|
# download_translations: true
|
||||||
|
# create_pull_request: false
|
||||||
|
# push_translations: false
|
||||||
|
# env:
|
||||||
|
# CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
# CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
|
# - name: Check for uncommitted changes
|
||||||
|
# if: steps.check-api-changes.outputs.any_modified == 'true'
|
||||||
|
# id: check-changes
|
||||||
|
# uses: mskri/check-uncommitted-changes-action@v1.0.1
|
||||||
|
# - name: Commit Changes
|
||||||
|
# if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != ''
|
||||||
|
# run: |
|
||||||
|
# git config --global user.email aminecmi+giteadrone@pm.me
|
||||||
|
# git config --global user.name giteadrone
|
||||||
|
# git add ./androidApp/src/main/res/*
|
||||||
|
# git commit -m "translation: translation files"
|
||||||
|
# - name: Push changes
|
||||||
|
# if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != ''
|
||||||
|
# uses: appleboy/git-push-action@v1.0.0
|
||||||
|
# with:
|
||||||
|
# author_name: giteadrone
|
||||||
|
# author_email: aminecmi+giteadrone@pm.me
|
||||||
|
# remote: ${{ secrets.REMOTE_URL }}
|
||||||
|
# ssh_key: ${{ secrets.PRIVATE_KEY }}
|
||||||
|
# branch: ${{ github.head_ref || github.ref_name }}
|
||||||
|
# build:
|
||||||
|
# needs: Lint
|
||||||
|
# uses: ./.gitea/workflows/common_build.yml
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
name: Check master code
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: ./.gitea/workflows/common_build.yml
|
||||||
@@ -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.
|
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.
|
### What I can't help you with.
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -320,4 +320,9 @@ fabric.properties
|
|||||||
# End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin,androidstudio,android,xcode,swift
|
# End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin,androidstudio,android,xcode,swift
|
||||||
|
|
||||||
|
|
||||||
crowdin.properties
|
crowdin.properties
|
||||||
|
|
||||||
|
.kotlin/
|
||||||
|
build-cache/
|
||||||
|
|
||||||
|
act
|
||||||
+242
@@ -1,3 +1,245 @@
|
|||||||
|
**v125030711
|
||||||
|
|
||||||
|
- Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master
|
||||||
|
- chore: check changes for translations and android.
|
||||||
|
- fix: initial status loading issues.
|
||||||
|
- Merge pull request 'chore: new connectivity dep. Closes #84.' (#189) from connectivity into master
|
||||||
|
- chore: new connectivity dep. Closes #84.
|
||||||
|
- Changelog for v125030681
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125030681
|
||||||
|
|
||||||
|
- chore: do not send reports on simulators.
|
||||||
|
- Merge pull request 'chore: do not send reports on simulators.' (#188) from chore-acra-simulator into master
|
||||||
|
- chore: do not send reports on simulators.
|
||||||
|
- Merge pull request 'fix: Url validation was not failing login. Added tests.' (#186) from fix-invalid-url into master
|
||||||
|
- Merge pull request 'chore: crowding ci integration.' (#187) from chore-crowdin-ci into master
|
||||||
|
- chore: we don't need to check if the url is valid in upsert screen.
|
||||||
|
- fix: Url validation was not failing login. Added tests.
|
||||||
|
- chore: crowding ci integration.
|
||||||
|
- Show a confirmation dialog before deleting sources (#185)
|
||||||
|
- Changelog for v125020581
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125020581
|
||||||
|
|
||||||
|
- fix: url can be empty ?
|
||||||
|
- Changelog for v125020471
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125020471
|
||||||
|
|
||||||
|
- chore: no more docker-compose.
|
||||||
|
- bump: gradle plugin.
|
||||||
|
- Merge pull request 'fix: check index exists.' (#183) from fix-index into master
|
||||||
|
- fix: check index exists.
|
||||||
|
- Changelog for v125020411
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125020411
|
||||||
|
|
||||||
|
- Merge pull request 'bump' (#182) from bump into master
|
||||||
|
- chore: non transiant R classes.
|
||||||
|
- Merge pull request 'fix: One more missing context.' (#181) from fix-one-more-context into master
|
||||||
|
- bump
|
||||||
|
- fix: One more missing context.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125010241
|
||||||
|
|
||||||
|
- Merge pull request 'fix: Link not opening.' (#178) from fix-open-link into master
|
||||||
|
- refactor: context fragments issues.
|
||||||
|
- logs: Context issues.
|
||||||
|
- fix: Handle empty url issue, again.
|
||||||
|
- fix: Link not opening.
|
||||||
|
- Changelog for v125010201
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125010201
|
||||||
|
|
||||||
|
- fix: Handle empty url issue.
|
||||||
|
- Merge pull request 'Removed the floating bar.' (#177) from floating-bar into master
|
||||||
|
- chore: changing actions in reader fragment.
|
||||||
|
- Changelog for v125010131
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125010131
|
||||||
|
|
||||||
|
- fix: reload the adapter when it's needed. Fixes #128. (#176)
|
||||||
|
- feat: basic auth and images loading. Fixes #172. (#175)
|
||||||
|
- Changelog for v125010111
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125010111
|
||||||
|
|
||||||
|
- Debug trying to fix context issues. (#174)
|
||||||
|
- Changelog for v125010031
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**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**
|
**v123061811**
|
||||||
|
|
||||||
- feat: Added confirmation dialog for disconnect item menu.
|
- feat: Added confirmation dialog for disconnect item menu.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# ReaderForSelfoss-multiplatform [](https://build.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform)
|
# ReaderForSelfoss-multiplatform [](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/actions?workflow=on_push.yml&actor=0&status=0)
|
||||||
|
|
||||||
[](https://crowdin.com/project/readerforselfoss)
|
[](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>
|
<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 ?
|
## 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>
|
<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/).
|
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
|
## Useful links
|
||||||
|
|
||||||
- [Check what changed](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/CHANGELOG.md)
|
- [Check what changed](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/CHANGELOG.md)
|
||||||
- [See what I'm doing](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/projects/1)
|
- [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-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/issues)
|
- [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)
|
- [Help translation the app](https://crowdin.com/project/readerforselfoss)
|
||||||
|
|
||||||
## Contributors (V1) (Alphabetical order) ❤️
|
## Contributors (V1) (Alphabetical order) ❤️
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
/build
|
/build
|
||||||
|
.kotlin/
|
||||||
+89
-61
@@ -1,7 +1,7 @@
|
|||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
val ignoreGitVersion: String by project
|
val ignoreGitVersion: String by project
|
||||||
val acraVersion = "5.9.7"
|
val acraVersion = "5.12.0"
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
@@ -9,30 +9,41 @@ plugins {
|
|||||||
kotlin("kapt")
|
kotlin("kapt")
|
||||||
id("com.mikepenz.aboutlibraries.plugin")
|
id("com.mikepenz.aboutlibraries.plugin")
|
||||||
id("org.jetbrains.kotlinx.kover")
|
id("org.jetbrains.kotlinx.kover")
|
||||||
|
id("app.cash.sqldelight") version "2.0.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
|
fun Project.execWithOutput(
|
||||||
val result: String = ByteArrayOutputStream().use { outputStream ->
|
cmd: String,
|
||||||
project.exec {
|
ignore: Boolean = false,
|
||||||
commandLine = cmd.split(" ")
|
): String {
|
||||||
standardOutput = outputStream
|
val result: String =
|
||||||
isIgnoreExitValue = ignore
|
ByteArrayOutputStream().use { outputStream ->
|
||||||
|
project.exec {
|
||||||
|
commandLine = cmd.split(" ")
|
||||||
|
standardOutput = outputStream
|
||||||
|
isIgnoreExitValue = ignore
|
||||||
|
}
|
||||||
|
outputStream.toString()
|
||||||
}
|
}
|
||||||
outputStream.toString()
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun gitVersion(): String {
|
fun gitVersion(): String {
|
||||||
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
|
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
|
||||||
val process = if (maybeTagOfCurrentCommit.isEmpty()) {
|
val process =
|
||||||
println("No tag on current commit. Will take the latest one.")
|
if (maybeTagOfCurrentCommit.isEmpty()) {
|
||||||
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
|
println("No tag on current commit. Will take the latest one.")
|
||||||
} else {
|
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
|
||||||
println("Tag found on current commit")
|
} else {
|
||||||
execWithOutput("git -C ../ describe --contains HEAD")
|
println("Tag found on current commit")
|
||||||
}
|
execWithOutput("git -C ../ describe --contains HEAD")
|
||||||
return process.replace("^0", "").replace("'", "").substring(1).replace("\\.", "").trim()
|
}
|
||||||
|
return process
|
||||||
|
.replace("^0", "")
|
||||||
|
.replace("'", "")
|
||||||
|
.substring(1)
|
||||||
|
.replace("\\.", "")
|
||||||
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun versionCodeFromGit(): Int {
|
fun versionCodeFromGit(): Int {
|
||||||
@@ -65,14 +76,14 @@ android {
|
|||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
compileSdk = 34
|
compileSdk = 35
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
}
|
}
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "bou.amine.apps.readerforselfossv2.android"
|
applicationId = "bou.amine.apps.readerforselfossv2.android"
|
||||||
minSdk = 25
|
minSdk = 25
|
||||||
targetSdk = 34
|
targetSdk = 34 // 35 when edge-to-edge is handled
|
||||||
versionCode = versionCodeFromGit()
|
versionCode = versionCodeFromGit()
|
||||||
versionName = versionNameFromGit()
|
versionName = versionNameFromGit()
|
||||||
|
|
||||||
@@ -84,6 +95,8 @@ android {
|
|||||||
|
|
||||||
// tests
|
// tests
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
testInstrumentationRunnerArguments["clearPackageData"] = "true"
|
||||||
|
testInstrumentationRunnerArguments["useTestStorageService"] = "true"
|
||||||
}
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
@@ -97,6 +110,8 @@ android {
|
|||||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
||||||
}
|
}
|
||||||
getByName("debug") {
|
getByName("debug") {
|
||||||
|
isTestCoverageEnabled = true
|
||||||
|
enableAndroidTestCoverage = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flavorDimensions.add("build")
|
flavorDimensions.add("build")
|
||||||
@@ -107,34 +122,38 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
namespace = "bou.amine.apps.readerforselfossv2.android"
|
namespace = "bou.amine.apps.readerforselfossv2.android"
|
||||||
|
testOptions {
|
||||||
|
animationsDisabled = true
|
||||||
|
execution = "ANDROIDX_TEST_ORCHESTRATOR"
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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(project(":shared"))
|
||||||
implementation("com.google.android.material:material:1.9.0")
|
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
|
|
||||||
|
|
||||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
|
|
||||||
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
|
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
|
||||||
|
|
||||||
// Android Support
|
// Android Support
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
implementation("com.google.android.material:material:1.9.0")
|
implementation("androidx.recyclerview:recyclerview:1.4.0-rc01")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
|
||||||
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
||||||
implementation("androidx.vectordrawable:vectordrawable:1.2.0-beta01")
|
implementation("androidx.vectordrawable:vectordrawable:1.2.0")
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
implementation("androidx.annotation:annotation:1.7.0")
|
implementation("androidx.annotation:annotation:1.9.1")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.8.1")
|
implementation("androidx.work:work-runtime-ktx:2.10.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.2.0")
|
||||||
implementation("org.jsoup:jsoup:1.15.4")
|
implementation("org.jsoup:jsoup:1.18.3")
|
||||||
|
|
||||||
//multidex
|
// multidex
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("androidx.multidex:multidex:2.0.1")
|
||||||
|
|
||||||
// About
|
// About
|
||||||
@@ -145,48 +164,56 @@ dependencies {
|
|||||||
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
|
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
|
||||||
|
|
||||||
// glide
|
// glide
|
||||||
kapt("com.github.bumptech.glide:compiler:4.15.0")
|
kapt("com.github.bumptech.glide:compiler:4.16.0")
|
||||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.15.0")
|
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
|
||||||
|
|
||||||
// Themes
|
// Themes
|
||||||
implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
|
implementation("com.leinardi.android:speed-dial:3.3.0")
|
||||||
|
|
||||||
// Pager
|
// Pager
|
||||||
implementation("me.relex:circleindicator:2.1.6")
|
implementation("me.relex:circleindicator:2.1.6")
|
||||||
implementation("androidx.viewpager2:viewpager2:1.1.0-beta02")
|
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
||||||
|
|
||||||
//Dependency Injection
|
// Dependency Injection
|
||||||
implementation("org.kodein.di:kodein-di:7.14.0")
|
implementation("org.kodein.di:kodein-di:7.23.1")
|
||||||
implementation("org.kodein.di:kodein-di-framework-android-x:7.14.0")
|
implementation("org.kodein.di:kodein-di-framework-android-x:7.23.1")
|
||||||
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.14.0")
|
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.23.1")
|
||||||
|
|
||||||
//Settings
|
// Settings
|
||||||
implementation("com.russhwolf:multiplatform-settings-no-arg:0.9")
|
implementation("com.russhwolf:multiplatform-settings-no-arg:1.3.0")
|
||||||
|
|
||||||
//Logging
|
// Logging
|
||||||
implementation("io.github.aakira:napier:2.6.1")
|
implementation("io.github.aakira:napier:2.7.1")
|
||||||
|
|
||||||
//PhotoView
|
// PhotoView
|
||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx:1.12.0")
|
implementation("androidx.core:core-ktx:1.15.0")
|
||||||
|
|
||||||
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
|
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
|
||||||
|
|
||||||
// Network information
|
|
||||||
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
|
|
||||||
|
|
||||||
// SQLDELIGHT
|
// SQLDELIGHT
|
||||||
implementation("com.squareup.sqldelight:android-driver:1.5.4")
|
implementation("app.cash.sqldelight:android-driver:2.0.2")
|
||||||
|
|
||||||
//test
|
// test
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("io.mockk:mockk:1.12.0")
|
testImplementation("io.mockk:mockk:1.13.14")
|
||||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
|
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
|
||||||
|
androidTestImplementation("androidx.test:runner:1.7.0-alpha01")
|
||||||
|
androidTestImplementation("androidx.test:rules:1.7.0-alpha01")
|
||||||
|
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.6.0-alpha02")
|
||||||
|
androidTestUtil("androidx.test.services:test-services:1.6.0-alpha02")
|
||||||
|
testImplementation("org.robolectric:robolectric:4.14.1")
|
||||||
|
testImplementation("androidx.test:core-ktx:1.7.0-alpha01")
|
||||||
|
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
|
||||||
|
|
||||||
implementation("ch.acra:acra-http:$acraVersion")
|
implementation("ch.acra:acra-http:$acraVersion")
|
||||||
implementation("ch.acra:acra-toast:$acraVersion")
|
implementation("ch.acra:acra-toast:$acraVersion")
|
||||||
|
implementation("com.google.auto.service:auto-service:1.1.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
@@ -194,11 +221,12 @@ tasks.withType<Test> {
|
|||||||
useJUnit()
|
useJUnit()
|
||||||
testLogging {
|
testLogging {
|
||||||
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||||
events = setOf(
|
events =
|
||||||
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
|
setOf(
|
||||||
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
|
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
|
||||||
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR
|
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
|
||||||
)
|
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR,
|
||||||
|
)
|
||||||
showStandardStreams = true
|
showStandardStreams = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,4 +239,4 @@ aboutLibraries {
|
|||||||
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||||
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||||
duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
||||||
}
|
}
|
||||||
Vendored
+9
@@ -86,3 +86,12 @@
|
|||||||
|
|
||||||
-dontwarn io.mockk.**
|
-dontwarn io.mockk.**
|
||||||
-keep class 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
|
||||||
+163
@@ -0,0 +1,163 @@
|
|||||||
|
package bou.amine.apps.readerforselfossv2.android
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.ArrayRes
|
||||||
|
import androidx.test.espresso.Espresso
|
||||||
|
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.base.DefaultFailureHandler
|
||||||
|
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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||||
|
import androidx.test.uiautomator.UiDevice
|
||||||
|
import androidx.test.uiautomator.UiSelector
|
||||||
|
import org.hamcrest.CoreMatchers.allOf
|
||||||
|
import org.hamcrest.Matchers.hasToString
|
||||||
|
import org.junit.Before
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
|
||||||
|
open class WithANRException {
|
||||||
|
// Running count of the number of Android Not Responding dialogues to prevent endless dismissal.
|
||||||
|
private var anrCount = 0
|
||||||
|
|
||||||
|
// `RootViewWithoutFocusException` class is private, need to match the message (instead of using type matching).
|
||||||
|
private val rootViewWithoutFocusExceptionMsg =
|
||||||
|
java.lang.String.format(
|
||||||
|
Locale.ROOT,
|
||||||
|
"Waited for the root of the view hierarchy to have " +
|
||||||
|
"window focus and not request layout for 10 seconds. If you specified a non " +
|
||||||
|
"default root matcher, it may be picking a root that never takes focus. " +
|
||||||
|
"Root:",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUpHandler() {
|
||||||
|
Espresso.setFailureHandler { error, viewMatcher ->
|
||||||
|
|
||||||
|
if (error.message!!.contains(rootViewWithoutFocusExceptionMsg) && anrCount < 3) {
|
||||||
|
anrCount++
|
||||||
|
handleAnrDialogue()
|
||||||
|
} else { // chain all failures down to the default espresso handler
|
||||||
|
DefaultFailureHandler(getInstrumentation().targetContext).handle(error, viewMatcher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleAnrDialogue() {
|
||||||
|
val device = UiDevice.getInstance(getInstrumentation())
|
||||||
|
// If running the device in English Locale
|
||||||
|
val waitButton = device.findObject(UiSelector().textContains("wait"))
|
||||||
|
if (waitButton.exists()) waitButton.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
@@ -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>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
+118
@@ -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 : WithANRException() {
|
||||||
|
@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(com.google.android.material.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())))
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
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 : WithANRException() {
|
||||||
|
@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.login_url_problem)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun connectError() {
|
||||||
|
performLogin("http://10.0.2.2:8889")
|
||||||
|
onView(withId(R.id.urlView)).perform(click())
|
||||||
|
onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun urlSlashError() {
|
||||||
|
performLogin("https://google.fr/toto")
|
||||||
|
onView(withId(R.id.urlView)).perform(click())
|
||||||
|
onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
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 : WithANRException() {
|
||||||
|
@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(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() {
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
+174
@@ -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 : WithANRException() {
|
||||||
|
@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())
|
||||||
|
}
|
||||||
|
}
|
||||||
+85
@@ -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 : WithANRException() {
|
||||||
|
@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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
@@ -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 : WithANRException() {
|
||||||
|
@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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
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 : WithANRException() {
|
||||||
|
@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(R.string.confirm_delete_title)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(android.R.id.button1)).perform(click())
|
||||||
|
onView(withText(sourceName)).check(doesNotExist())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun goToSources() {
|
||||||
|
openMenu()
|
||||||
|
onView(withText(R.string.menu_home_sources))
|
||||||
|
.perform(click())
|
||||||
|
}
|
||||||
|
}
|
||||||
+281
-193
@@ -12,7 +12,11 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.view.doOnNextLayout
|
import androidx.core.view.doOnNextLayout
|
||||||
import androidx.lifecycle.lifecycleScope
|
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.Constraints
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
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.databinding.ActivityHomeBinding
|
||||||
import bou.amine.apps.readerforselfossv2.android.fragments.FilterSheetFragment
|
import bou.amine.apps.readerforselfossv2.android.fragments.FilterSheetFragment
|
||||||
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
|
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.maybeShow
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
|
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowserAsNewTask
|
||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
@@ -43,9 +49,12 @@ import org.kodein.di.instance
|
|||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.concurrent.TimeUnit
|
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 items: ArrayList<SelfossModel.Item> = ArrayList()
|
||||||
|
|
||||||
private var elementsShown: ItemType = ItemType.UNREAD
|
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 recyclerViewScrollListener: RecyclerView.OnScrollListener
|
||||||
private lateinit var binding: ActivityHomeBinding
|
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 var fromTabShortcut: Boolean = false
|
||||||
|
|
||||||
private val settingsLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
private val settingsLauncher =
|
||||||
appSettingsService.refreshUserSettings()
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
}
|
appSettingsService.refreshUserSettings()
|
||||||
|
}
|
||||||
|
|
||||||
override val di by closestDI()
|
override val di by closestDI()
|
||||||
private val repository : Repository by instance()
|
private val repository: Repository by instance()
|
||||||
private val appSettingsService : AppSettingsService by instance()
|
private val appSettingsService: AppSettingsService by instance()
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||||
val view = binding.root
|
val view = binding.root
|
||||||
|
|
||||||
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
|
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
|
||||||
repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
|
repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
|
||||||
|
|
||||||
if (fromTabShortcut) {
|
if (fromTabShortcut) {
|
||||||
elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
|
elementsShown =
|
||||||
|
ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
|
||||||
}
|
}
|
||||||
|
|
||||||
setContentView(view)
|
setContentView(view)
|
||||||
@@ -92,10 +102,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
|
|
||||||
handleSwipeRefreshLayout()
|
handleSwipeRefreshLayout()
|
||||||
|
|
||||||
|
|
||||||
if (appSettingsService.isItemCachingEnabled()) {
|
if (appSettingsService.isItemCachingEnabled()) {
|
||||||
|
CountingIdlingResourceSingleton.increment()
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
repository.tryToCacheItemsAndGetNewOnes()
|
repository.tryToCacheItemsAndGetNewOnes()
|
||||||
|
CountingIdlingResourceSingleton.decrement()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,48 +115,54 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
binding.swipeRefreshLayout.setColorSchemeResources(
|
binding.swipeRefreshLayout.setColorSchemeResources(
|
||||||
R.color.refresh_progress_1,
|
R.color.refresh_progress_1,
|
||||||
R.color.refresh_progress_2,
|
R.color.refresh_progress_2,
|
||||||
R.color.refresh_progress_3
|
R.color.refresh_progress_3,
|
||||||
)
|
)
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||||
repository.offlineOverride = false
|
repository.offlineOverride = false
|
||||||
lastFetchDone = false
|
lastFetchDone = false
|
||||||
|
CountingIdlingResourceSingleton.increment()
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
getElementsAccordingToTab()
|
getElementsAccordingToTab()
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
|
CountingIdlingResourceSingleton.decrement()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val swipeDirs = if (appSettingsService.getPublicAccess()) {
|
val swipeDirs =
|
||||||
0
|
if (appSettingsService.getPublicAccess()) {
|
||||||
} else {
|
0
|
||||||
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
|
} else {
|
||||||
}
|
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
val simpleItemTouchCallback =
|
val simpleItemTouchCallback =
|
||||||
object : ItemTouchHelper.SimpleCallback(
|
object : ItemTouchHelper.SimpleCallback(
|
||||||
0,
|
0,
|
||||||
swipeDirs
|
swipeDirs,
|
||||||
) {
|
) {
|
||||||
override fun getSwipeDirs(
|
override fun getSwipeDirs(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
viewHolder: RecyclerView.ViewHolder
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
): Int =
|
): Int =
|
||||||
if (elementsShown == ItemType.STARRED) {
|
if (elementsShown == ItemType.STARRED) {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
super.getSwipeDirs(
|
super.getSwipeDirs(
|
||||||
recyclerView,
|
recyclerView,
|
||||||
viewHolder
|
viewHolder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMove(
|
override fun onMove(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
target: RecyclerView.ViewHolder
|
target: RecyclerView.ViewHolder,
|
||||||
): Boolean = false
|
): Boolean = false
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {
|
override fun onSwiped(
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
swipeDir: Int,
|
||||||
|
) {
|
||||||
val position = viewHolder.bindingAdapterPosition
|
val position = viewHolder.bindingAdapterPosition
|
||||||
val i = items.elementAtOrNull(position)
|
val i = items.elementAtOrNull(position)
|
||||||
|
|
||||||
@@ -159,11 +176,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
getElementsAccordingToTab()
|
getElementsAccordingToTab()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast
|
||||||
this@HomeActivity,
|
.makeText(
|
||||||
"Found null when swiping at positon $position.",
|
this@HomeActivity,
|
||||||
Toast.LENGTH_LONG
|
"Found null when swiping at positon $position.",
|
||||||
).show()
|
Toast.LENGTH_LONG,
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +189,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
|
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateBottomBarBadgeCount(badge: TextBadgeItem, count: Int) {
|
private fun updateBottomBarBadgeCount(
|
||||||
|
badge: TextBadgeItem,
|
||||||
|
count: Int,
|
||||||
|
) {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
badge
|
badge
|
||||||
.setText(count.toString())
|
.setText(count.toString())
|
||||||
@@ -181,17 +202,23 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:LongMethod")
|
||||||
private fun handleBottomBar() {
|
private fun handleBottomBar() {
|
||||||
|
tabNewBadge =
|
||||||
tabNewBadge = TextBadgeItem()
|
TextBadgeItem()
|
||||||
.setText("")
|
.setText("")
|
||||||
.setHideOnSelect(false).hide(false)
|
.setHideOnSelect(false)
|
||||||
tabArchiveBadge = TextBadgeItem()
|
.hide(false)
|
||||||
.setText("")
|
tabArchiveBadge =
|
||||||
.setHideOnSelect(false).hide(false)
|
TextBadgeItem()
|
||||||
tabStarredBadge = TextBadgeItem()
|
.setText("")
|
||||||
.setText("")
|
.setHideOnSelect(false)
|
||||||
.setHideOnSelect(false).hide(false)
|
.hide(false)
|
||||||
|
tabStarredBadge =
|
||||||
|
TextBadgeItem()
|
||||||
|
.setText("")
|
||||||
|
.setHideOnSelect(false)
|
||||||
|
.hide(false)
|
||||||
|
|
||||||
if (appSettingsService.isDisplayUnreadCountEnabled()) {
|
if (appSettingsService.isDisplayUnreadCountEnabled()) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -218,19 +245,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
val tabNew =
|
val tabNew =
|
||||||
BottomNavigationItem(
|
BottomNavigationItem(
|
||||||
R.drawable.ic_tab_fiber_new_black_24dp,
|
R.drawable.ic_tab_fiber_new_black_24dp,
|
||||||
getString(R.string.tab_new)
|
getString(R.string.tab_new),
|
||||||
)
|
).setBadgeItem(tabNewBadge)
|
||||||
.setBadgeItem(tabNewBadge)
|
|
||||||
val tabArchive =
|
val tabArchive =
|
||||||
BottomNavigationItem(
|
BottomNavigationItem(
|
||||||
R.drawable.ic_tab_archive_black_24dp,
|
R.drawable.ic_tab_archive_black_24dp,
|
||||||
getString(R.string.tab_read)
|
getString(R.string.tab_read),
|
||||||
)
|
).setBadgeItem(tabArchiveBadge)
|
||||||
.setBadgeItem(tabArchiveBadge)
|
|
||||||
val tabStarred =
|
val tabStarred =
|
||||||
BottomNavigationItem(
|
BottomNavigationItem(
|
||||||
R.drawable.ic_tab_favorite_black_24dp,
|
R.drawable.ic_tab_favorite_black_24dp,
|
||||||
getString(R.string.tab_favs)
|
getString(R.string.tab_favs),
|
||||||
).setActiveColorResource(R.color.pink)
|
).setActiveColorResource(R.color.pink)
|
||||||
.setBadgeItem(tabStarredBadge)
|
.setBadgeItem(tabStarredBadge)
|
||||||
|
|
||||||
@@ -260,28 +285,28 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
|
|
||||||
handleBottomBarActions()
|
handleBottomBarActions()
|
||||||
|
|
||||||
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
|
handleGdprDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
|
||||||
|
|
||||||
handleRecurringTask()
|
handleRecurringTask()
|
||||||
|
CountingIdlingResourceSingleton.increment()
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
repository.handleDBActions()
|
repository.handleDBActions()
|
||||||
|
CountingIdlingResourceSingleton.decrement()
|
||||||
}
|
}
|
||||||
|
|
||||||
getElementsAccordingToTab()
|
getElementsAccordingToTab()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleGdprDialog(gdprShown: Boolean) {
|
||||||
private fun handleGDPRDialog(GDPRShown: Boolean) {
|
|
||||||
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
|
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
|
||||||
messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
|
messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
|
||||||
if (!GDPRShown) {
|
if (!gdprShown) {
|
||||||
val alertDialog = AlertDialog.Builder(this).create()
|
val alertDialog = AlertDialog.Builder(this).create()
|
||||||
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
|
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
|
||||||
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
|
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
|
||||||
alertDialog.setButton(
|
alertDialog.setButton(
|
||||||
AlertDialog.BUTTON_NEUTRAL,
|
AlertDialog.BUTTON_NEUTRAL,
|
||||||
"OK"
|
"OK",
|
||||||
) { dialog, _ ->
|
) { dialog, _ ->
|
||||||
appSettingsService.settings.putBoolean("GDPR_shown", true)
|
appSettingsService.settings.putBoolean("GDPR_shown", true)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
@@ -292,83 +317,86 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
|
|
||||||
private fun reloadLayoutManager() {
|
private fun reloadLayoutManager() {
|
||||||
val currentManager = binding.recyclerView.layoutManager
|
val currentManager = binding.recyclerView.layoutManager
|
||||||
val layoutManager: RecyclerView.LayoutManager
|
|
||||||
|
|
||||||
// This will only update the layout manager if settings changed
|
fun gridLayoutManager() {
|
||||||
|
val layoutManager =
|
||||||
|
GridLayoutManager(
|
||||||
|
this,
|
||||||
|
calculateNoOfColumns(),
|
||||||
|
)
|
||||||
|
binding.recyclerView.layoutManager = layoutManager
|
||||||
|
}
|
||||||
|
|
||||||
|
fun staggererdGridLayoutManager() {
|
||||||
|
var layoutManager =
|
||||||
|
StaggeredGridLayoutManager(
|
||||||
|
calculateNoOfColumns(),
|
||||||
|
StaggeredGridLayoutManager.VERTICAL,
|
||||||
|
)
|
||||||
|
layoutManager.gapStrategy =
|
||||||
|
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
||||||
|
binding.recyclerView.layoutManager = layoutManager
|
||||||
|
}
|
||||||
|
|
||||||
when (currentManager) {
|
when (currentManager) {
|
||||||
is StaggeredGridLayoutManager ->
|
is StaggeredGridLayoutManager ->
|
||||||
if (!appSettingsService.isCardViewEnabled()) {
|
if (!appSettingsService.isCardViewEnabled()) {
|
||||||
layoutManager = GridLayoutManager(
|
gridLayoutManager()
|
||||||
this,
|
|
||||||
calculateNoOfColumns()
|
|
||||||
)
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is GridLayoutManager ->
|
is GridLayoutManager ->
|
||||||
if (appSettingsService.isCardViewEnabled()) {
|
if (appSettingsService.isCardViewEnabled()) {
|
||||||
layoutManager = StaggeredGridLayoutManager(
|
staggererdGridLayoutManager()
|
||||||
calculateNoOfColumns(),
|
|
||||||
StaggeredGridLayoutManager.VERTICAL
|
|
||||||
)
|
|
||||||
layoutManager.gapStrategy =
|
|
||||||
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else ->
|
else ->
|
||||||
if (currentManager == null) {
|
if (currentManager == null) {
|
||||||
if (!appSettingsService.isCardViewEnabled()) {
|
if (!appSettingsService.isCardViewEnabled()) {
|
||||||
layoutManager = GridLayoutManager(
|
gridLayoutManager()
|
||||||
this,
|
|
||||||
calculateNoOfColumns()
|
|
||||||
)
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
} else {
|
} else {
|
||||||
layoutManager = StaggeredGridLayoutManager(
|
staggererdGridLayoutManager()
|
||||||
calculateNoOfColumns(),
|
|
||||||
StaggeredGridLayoutManager.VERTICAL
|
|
||||||
)
|
|
||||||
layoutManager.gapStrategy =
|
|
||||||
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleBottomBarActions() {
|
private fun handleBottomBarActions() {
|
||||||
binding.bottomBar.setTabSelectedListener(object : BottomNavigationBar.OnTabSelectedListener {
|
binding.bottomBar.setTabSelectedListener(
|
||||||
override fun onTabUnselected(position: Int) = Unit
|
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 GridLayoutManager ->
|
||||||
is StaggeredGridLayoutManager ->
|
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
|
||||||
if (layoutManager.findFirstCompletelyVisibleItemPositions(null)[0] == 0) {
|
getElementsAccordingToTab()
|
||||||
getElementsAccordingToTab()
|
} else {
|
||||||
} else {
|
layoutManager.scrollToPositionWithOffset(0, 0)
|
||||||
layoutManager.scrollToPositionWithOffset(0, 0)
|
}
|
||||||
}
|
|
||||||
is GridLayoutManager ->
|
else -> Unit
|
||||||
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
|
}
|
||||||
getElementsAccordingToTab()
|
|
||||||
} else {
|
|
||||||
layoutManager.scrollToPositionWithOffset(0, 0)
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTabSelected(position: Int) {
|
override fun onTabSelected(position: Int) {
|
||||||
offset = 0
|
offset = 0
|
||||||
lastFetchDone = false
|
lastFetchDone = false
|
||||||
|
|
||||||
elementsShown = ItemType.fromInt(position + 1)
|
elementsShown = ItemType.fromInt(position + 1)
|
||||||
getElementsAccordingToTab()
|
getElementsAccordingToTab()
|
||||||
binding.recyclerView.scrollToPosition(0)
|
binding.recyclerView.scrollToPosition(0)
|
||||||
|
|
||||||
fetchOnEmptyList()
|
fetchOnEmptyList()
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchOnEmptyList() {
|
fun fetchOnEmptyList() {
|
||||||
@@ -378,31 +406,38 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleInfiniteScroll() {
|
private fun handleInfiniteScroll() {
|
||||||
recyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
|
recyclerViewScrollListener =
|
||||||
override fun onScrolled(localRecycler: RecyclerView, dx: Int, dy: Int) {
|
object : RecyclerView.OnScrollListener() {
|
||||||
if (dy > 0) {
|
override fun onScrolled(
|
||||||
val lastVisibleItem = getLastVisibleItem()
|
localRecycler: RecyclerView,
|
||||||
|
dx: Int,
|
||||||
|
dy: Int,
|
||||||
|
) {
|
||||||
|
if (dy > 0) {
|
||||||
|
val lastVisibleItem = getLastVisibleItem()
|
||||||
|
|
||||||
if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) {
|
if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) {
|
||||||
getElementsAccordingToTab(appendResults = true)
|
getElementsAccordingToTab(appendResults = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
binding.recyclerView.clearOnScrollListeners()
|
binding.recyclerView.clearOnScrollListeners()
|
||||||
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
|
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLastVisibleItem() : Int {
|
private fun getLastVisibleItem(): Int =
|
||||||
return when (val manager = binding.recyclerView.layoutManager) {
|
when (val manager = binding.recyclerView.layoutManager) {
|
||||||
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
|
is StaggeredGridLayoutManager ->
|
||||||
null
|
manager
|
||||||
).last()
|
.findLastCompletelyVisibleItemPositions(
|
||||||
|
null,
|
||||||
|
).last()
|
||||||
|
|
||||||
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
|
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun mayBeEmpty() =
|
private fun mayBeEmpty() =
|
||||||
if (items.isEmpty()) {
|
if (items.isEmpty()) {
|
||||||
@@ -411,73 +446,87 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
binding.emptyText.visibility = View.GONE
|
binding.emptyText.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getElementsAccordingToTab(
|
fun getElementsAccordingToTab(appendResults: Boolean = false) {
|
||||||
appendResults: Boolean = false
|
offset =
|
||||||
) {
|
if (appendResults && items.size > 0) {
|
||||||
offset = if (appendResults && items.size > 0) {
|
items.size - 1
|
||||||
items.size - 1
|
} else {
|
||||||
} else {
|
0
|
||||||
0
|
}
|
||||||
}
|
|
||||||
firstVisible = if (appendResults) firstVisible else 0
|
firstVisible = if (appendResults) firstVisible else 0
|
||||||
|
|
||||||
getItems(appendResults, elementsShown)
|
getItems(appendResults, elementsShown)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getItems(appendResults: Boolean, itemType: ItemType) {
|
private fun getItems(
|
||||||
|
appendResults: Boolean,
|
||||||
|
itemType: ItemType,
|
||||||
|
) {
|
||||||
|
CountingIdlingResourceSingleton.increment()
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
binding.swipeRefreshLayout.isRefreshing = true
|
binding.swipeRefreshLayout.isRefreshing = true
|
||||||
repository.displayedItems = itemType
|
repository.displayedItems = itemType
|
||||||
items = if (appendResults) {
|
items =
|
||||||
repository.getOlderItems()
|
if (appendResults) {
|
||||||
} else {
|
repository.getOlderItems()
|
||||||
repository.getNewerItems()
|
} else {
|
||||||
}
|
repository.getNewerItems()
|
||||||
|
}
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
handleListResult()
|
handleListResult()
|
||||||
|
CountingIdlingResourceSingleton.decrement()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleListResult(appendResults: Boolean = false) {
|
private fun handleListResult(appendResults: Boolean = false) {
|
||||||
|
val oldManager = binding.recyclerView.layoutManager
|
||||||
if (appendResults) {
|
if (appendResults) {
|
||||||
val oldManager = binding.recyclerView.layoutManager
|
firstVisible =
|
||||||
firstVisible = when (oldManager) {
|
when (oldManager) {
|
||||||
is StaggeredGridLayoutManager ->
|
is StaggeredGridLayoutManager ->
|
||||||
oldManager.findFirstCompletelyVisibleItemPositions(null).last()
|
oldManager.findFirstCompletelyVisibleItemPositions(null).last()
|
||||||
is GridLayoutManager ->
|
|
||||||
oldManager.findFirstCompletelyVisibleItemPosition()
|
is GridLayoutManager ->
|
||||||
else -> 0
|
oldManager.findFirstCompletelyVisibleItemPosition()
|
||||||
}
|
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recyclerAdapter == null) {
|
@Suppress("detekt:ComplexCondition")
|
||||||
|
if (recyclerAdapter == null ||
|
||||||
|
(
|
||||||
|
(recyclerAdapter is ItemListAdapter && appSettingsService.isCardViewEnabled()) ||
|
||||||
|
(recyclerAdapter is ItemCardAdapter && !appSettingsService.isCardViewEnabled())
|
||||||
|
)
|
||||||
|
) {
|
||||||
if (appSettingsService.isCardViewEnabled()) {
|
if (appSettingsService.isCardViewEnabled()) {
|
||||||
recyclerAdapter =
|
recyclerAdapter =
|
||||||
ItemCardAdapter(
|
ItemCardAdapter(
|
||||||
this,
|
this,
|
||||||
items,
|
items,
|
||||||
) {
|
) {
|
||||||
updateItems(it)
|
updateItems(it)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
recyclerAdapter =
|
recyclerAdapter =
|
||||||
ItemListAdapter(
|
ItemListAdapter(
|
||||||
this,
|
this,
|
||||||
items,
|
items,
|
||||||
) {
|
) {
|
||||||
updateItems(it)
|
updateItems(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.recyclerView.addItemDecoration(
|
binding.recyclerView.addItemDecoration(
|
||||||
DividerItemDecoration(
|
DividerItemDecoration(
|
||||||
this@HomeActivity,
|
this@HomeActivity,
|
||||||
DividerItemDecoration.VERTICAL
|
DividerItemDecoration.VERTICAL,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
binding.recyclerView.adapter = recyclerAdapter
|
binding.recyclerView.adapter = recyclerAdapter
|
||||||
} else {
|
} else {
|
||||||
(recyclerAdapter as ItemsAdapter<*>).updateAllItems(items)
|
recyclerAdapter!!.updateAllItems(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadBadges()
|
reloadBadges()
|
||||||
@@ -486,8 +535,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
|
|
||||||
private fun reloadBadges() {
|
private fun reloadBadges() {
|
||||||
if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
|
if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
|
||||||
|
CountingIdlingResourceSingleton.increment()
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
repository.reloadBadges()
|
repository.reloadBadges()
|
||||||
|
CountingIdlingResourceSingleton.decrement()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,7 +546,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
private fun calculateNoOfColumns(): Int {
|
private fun calculateNoOfColumns(): Int {
|
||||||
val displayMetrics = resources.displayMetrics
|
val displayMetrics = resources.displayMetrics
|
||||||
val dpWidth = displayMetrics.widthPixels / displayMetrics.density
|
val dpWidth = displayMetrics.widthPixels / displayMetrics.density
|
||||||
return (dpWidth / 300).toInt()
|
return (dpWidth / MIN_WIDTH_CARD_DP).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueryTextChange(p0: String?): Boolean {
|
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 searchItem = menu.findItem(R.id.action_search)
|
||||||
val searchView = searchItem.getActionView() as SearchView
|
val searchView = searchItem.actionView as SearchView
|
||||||
searchView.setOnQueryTextListener(this)
|
searchView.setOnQueryTextListener(this)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun needsConfirmation(titleRes: Int, messageRes: Int, doFn: () -> Unit) {
|
private fun needsConfirmation(
|
||||||
AlertDialog.Builder(this@HomeActivity)
|
titleRes: Int,
|
||||||
|
messageRes: Int,
|
||||||
|
doFn: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog
|
||||||
|
.Builder(this@HomeActivity)
|
||||||
.setMessage(messageRes)
|
.setMessage(messageRes)
|
||||||
.setTitle(titleRes)
|
.setTitle(titleRes)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> doFn() }
|
.setPositiveButton(android.R.string.ok) { _, _ -> doFn() }
|
||||||
@@ -539,67 +595,86 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:ReturnCount", "detekt:LongMethod")
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
|
R.id.issue_tracker -> {
|
||||||
|
baseContext.openUrlInBrowserAsNewTask(AppSettingsService.BUG_URL)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.action_filter -> {
|
R.id.action_filter -> {
|
||||||
val filterSheetFragment = FilterSheetFragment()
|
val filterSheetFragment = FilterSheetFragment()
|
||||||
filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG)
|
filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.refresh -> {
|
R.id.refresh -> {
|
||||||
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
|
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
|
||||||
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
|
||||||
|
CountingIdlingResourceSingleton.increment()
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||