Compare commits
	
		
			67 Commits
		
	
	
		
			v124123651
			...
			6626784e8b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6626784e8b | |||
| 1b2e9edc8c | |||
| 7c65a63315 | |||
| 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 | 
							
								
								
									
										36
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					root = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[*]
 | 
				
			||||||
 | 
					insert_final_newline = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[.editorconfig]
 | 
				
			||||||
 | 
					insert_final_newline = false
 | 
				
			||||||
 | 
					ij_kotlin_line_break_after_multiline_when_entry = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[*.{kt,kts}]
 | 
				
			||||||
 | 
					#  Disable wildcard imports entirely
 | 
				
			||||||
 | 
					ij_kotlin_name_count_to_use_star_import = 2147483647
 | 
				
			||||||
 | 
					ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
 | 
				
			||||||
 | 
					end_of_line = lf
 | 
				
			||||||
 | 
					ij_kotlin_allow_trailing_comma = true
 | 
				
			||||||
 | 
					ij_kotlin_allow_trailing_comma_on_call_site = true
 | 
				
			||||||
 | 
					ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
 | 
				
			||||||
 | 
					ij_kotlin_indent_before_arrow_on_new_line = false
 | 
				
			||||||
 | 
					ij_kotlin_line_break_after_multiline_when_entry = true
 | 
				
			||||||
 | 
					ij_kotlin_packages_to_use_import_on_demand = unset
 | 
				
			||||||
 | 
					indent_size = 4
 | 
				
			||||||
 | 
					indent_style = space
 | 
				
			||||||
 | 
					ktlint_argument_list_wrapping_ignore_when_parameter_count_greater_or_equal_than = unset
 | 
				
			||||||
 | 
					ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = 4
 | 
				
			||||||
 | 
					ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
 | 
				
			||||||
 | 
					ktlint_code_style = ktlint_official
 | 
				
			||||||
 | 
					ktlint_enum_entry_name_casing = upper_or_camel_cases
 | 
				
			||||||
 | 
					ktlint_function_naming_ignore_when_annotated_with = unset
 | 
				
			||||||
 | 
					ktlint_function_signature_body_expression_wrapping = multiline
 | 
				
			||||||
 | 
					ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2
 | 
				
			||||||
 | 
					ktlint_ignore_back_ticked_identifier = false
 | 
				
			||||||
 | 
					ktlint_property_naming_constant_naming = screaming_snake_case
 | 
				
			||||||
 | 
					max_line_length = 140
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[**/build]
 | 
				
			||||||
 | 
					ktlint = disabled
 | 
				
			||||||
							
								
								
									
										10
									
								
								.gitea/workflows/assets/crowdin.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.gitea/workflows/assets/crowdin.yml
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
@@ -10,36 +10,52 @@ jobs:
 | 
				
			|||||||
        uses: actions/checkout@v4
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          fetch-depth: 0
 | 
					          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
 | 
					      - name: Fetch tags
 | 
				
			||||||
 | 
					        if: steps.check-android-changes.outputs.any_modified == 'true'
 | 
				
			||||||
        run: git fetch --tags -p
 | 
					        run: git fetch --tags -p
 | 
				
			||||||
      - uses: actions/setup-java@v4
 | 
					      - uses: actions/setup-java@v4
 | 
				
			||||||
 | 
					        if: steps.check-android-changes.outputs.any_modified == 'true'
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          distribution: 'temurin'
 | 
					          distribution: 'temurin'
 | 
				
			||||||
          java-version: '17'
 | 
					          java-version: '17'
 | 
				
			||||||
          cache: gradle
 | 
					          cache: gradle
 | 
				
			||||||
      - uses: gradle/actions/setup-gradle@v3
 | 
					      - uses: gradle/actions/setup-gradle@v3
 | 
				
			||||||
 | 
					        if: steps.check-android-changes.outputs.any_modified == 'true'
 | 
				
			||||||
      - uses: android-actions/setup-android@v3
 | 
					      - uses: android-actions/setup-android@v3
 | 
				
			||||||
 | 
					        if: steps.check-android-changes.outputs.any_modified == 'true'
 | 
				
			||||||
      - name: Configure gradle...
 | 
					      - 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
 | 
					        run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties
 | 
				
			||||||
      - name: Build and test
 | 
					      - 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
 | 
					        run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest # These tests will be done
 | 
				
			||||||
      - uses: KengoTODA/actions-setup-docker-compose@v1
 | 
					      # TESTS ARE RUN LOCALLY
 | 
				
			||||||
        with:
 | 
					      #      - uses: KengoTODA/actions-setup-docker-compose@v1
 | 
				
			||||||
          version: "2.23.3"
 | 
					      #        with:
 | 
				
			||||||
      - name: run selfoss
 | 
					      #          version: "2.23.3"
 | 
				
			||||||
        run: |
 | 
					      #      - name: run selfoss
 | 
				
			||||||
          docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
 | 
					      #        run: |
 | 
				
			||||||
 | 
					      #          docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
 | 
				
			||||||
      - name: coverage
 | 
					      - name: coverage
 | 
				
			||||||
 | 
					        if: steps.check-android-changes.outputs.any_modified == 'true'
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          ./gradlew :koverHtmlReport
 | 
					          ./gradlew :koverHtmlReport
 | 
				
			||||||
      - uses: actions/upload-artifact@v3
 | 
					      - uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        if: steps.check-android-changes.outputs.any_modified == 'true'
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: coverage
 | 
					          name: coverage
 | 
				
			||||||
          path: build/reports/kover/html
 | 
					          path: build/reports/kover/html
 | 
				
			||||||
          retention-days: 1
 | 
					          retention-days: 1
 | 
				
			||||||
          overwrite: true
 | 
					          overwrite: true
 | 
				
			||||||
          include-hidden-files: true
 | 
					          include-hidden-files: true
 | 
				
			||||||
      - name: Clean
 | 
					#      TESTS ARE RUN LOCALLY
 | 
				
			||||||
        if: always()
 | 
					#      - name: Clean
 | 
				
			||||||
        run: |
 | 
					#        if: always()
 | 
				
			||||||
          docker compose -f .gitea/workflows/assets/docker-compose.yml stop
 | 
					#        run: |
 | 
				
			||||||
 | 
					#          docker compose -f .gitea/workflows/assets/docker-compose.yml stop
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										65
									
								
								.gitea/workflows/common_coverage.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								.gitea/workflows/common_coverage.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					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 "ignoreGitVersion=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: Set env url
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          export SELFOSS_URL=172.17.0.1:8888
 | 
				
			||||||
 | 
					      # https://github.com/ReactiveCircus/android-emulator-runner/issues/385
 | 
				
			||||||
 | 
					      - name: Kill crashpad_handler processes
 | 
				
			||||||
 | 
					        if: always()
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          pkill -SIGTERM crashpad_handler || true
 | 
				
			||||||
 | 
					          sleep 5
 | 
				
			||||||
 | 
					          pkill -SIGKILL crashpad_handler || true
 | 
				
			||||||
 | 
					      - name: Tests
 | 
				
			||||||
 | 
					        uses: reactivecircus/android-emulator-runner@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          api-level: 29
 | 
				
			||||||
 | 
					          script: |
 | 
				
			||||||
 | 
					            ./gradlew androidApp:connectedAndroidTest
 | 
				
			||||||
 | 
					            killall -INT crashpad_handler || true
 | 
				
			||||||
 | 
					      - uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        if: failure()
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: failure-espresso
 | 
				
			||||||
 | 
					          path: build/reports/androidTests/connected/screenshots
 | 
				
			||||||
 | 
					          retention-days: 2
 | 
				
			||||||
 | 
					          overwrite: true
 | 
				
			||||||
 | 
					          include-hidden-files: true
 | 
				
			||||||
 | 
					      - 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
 | 
				
			||||||
@@ -16,6 +16,7 @@ jobs:
 | 
				
			|||||||
        uses: actions/checkout@v4
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          fetch-depth: 0
 | 
					          fetch-depth: 0
 | 
				
			||||||
 | 
					          ref: master
 | 
				
			||||||
      - name: Config git
 | 
					      - name: Config git
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          git config --global user.email aminecmi+giteadrone@pm.me
 | 
					          git config --global user.email aminecmi+giteadrone@pm.me
 | 
				
			||||||
@@ -50,7 +51,7 @@ jobs:
 | 
				
			|||||||
          followtags: true
 | 
					          followtags: true
 | 
				
			||||||
          ssh_key: ${{ secrets.PRIVATE_KEY }}
 | 
					          ssh_key: ${{ secrets.PRIVATE_KEY }}
 | 
				
			||||||
          tags: true
 | 
					          tags: true
 | 
				
			||||||
          branch: release
 | 
					          branch: master
 | 
				
			||||||
      - name: copy file via ssh password
 | 
					      - name: copy file via ssh password
 | 
				
			||||||
        uses: appleboy/scp-action@v0.1.7
 | 
					        uses: appleboy/scp-action@v0.1.7
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
@@ -124,4 +125,4 @@ jobs:
 | 
				
			|||||||
          priority: high
 | 
					          priority: high
 | 
				
			||||||
          convert_markdown: true
 | 
					          convert_markdown: true
 | 
				
			||||||
          body: Nouveau fichier de mapping pour la version ${{ steps.version.outputs.VERSION }}
 | 
					          body: Nouveau fichier de mapping pour la version ${{ steps.version.outputs.VERSION }}
 | 
				
			||||||
          attachments: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
 | 
					          attachments: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,23 +5,158 @@ on:
 | 
				
			|||||||
      - master
 | 
					      - master
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  Lint:
 | 
					  BuildAndTestAndCoverage:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - name: Check out repository code
 | 
					      - name: Check out repository code
 | 
				
			||||||
        uses: actions/checkout@v4
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          fetch-depth: 0
 | 
				
			||||||
 | 
					      - name: Fetch tags
 | 
				
			||||||
 | 
					        run: git fetch --tags -p
 | 
				
			||||||
      - uses: actions/setup-java@v4
 | 
					      - uses: actions/setup-java@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          distribution: 'temurin'
 | 
					          distribution: 'temurin'
 | 
				
			||||||
          java-version: '17'
 | 
					          java-version: '17'
 | 
				
			||||||
          cache: gradle
 | 
					      - uses: gradle/actions/setup-gradle@v3
 | 
				
			||||||
      - name: Install klint
 | 
					      - uses: android-actions/setup-android@v3
 | 
				
			||||||
        run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
 | 
					      - name: Configure gradle...
 | 
				
			||||||
      - name: Install detekt
 | 
					        run: mkdir -p ~/.gradle && echo "ignoreGitVersion=true" >> ~/.gradle/gradle.properties
 | 
				
			||||||
        run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip && unzip detekt-cli-1.23.1.zip
 | 
					      - uses: KengoTODA/actions-setup-docker-compose@v1
 | 
				
			||||||
      - name: Linting...
 | 
					        with:
 | 
				
			||||||
        run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' || true
 | 
					          version: "2.23.3"
 | 
				
			||||||
      - name: Detecting...
 | 
					      - name: run selfoss
 | 
				
			||||||
        run: ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
 | 
					        run: |
 | 
				
			||||||
  build:
 | 
					          docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
 | 
				
			||||||
    uses: ./.gitea/workflows/common_build.yml
 | 
					      # https://github.com/ReactiveCircus/android-emulator-runner/issues/385
 | 
				
			||||||
 | 
					      - name: Kill crashpad_handler processes
 | 
				
			||||||
 | 
					        if: always()
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          pkill -SIGTERM crashpad_handler || true
 | 
				
			||||||
 | 
					          sleep 5
 | 
				
			||||||
 | 
					          pkill -SIGKILL crashpad_handler || true
 | 
				
			||||||
 | 
					      - name: Change url until I find a better way to do it
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          sed -i "s/val defaultUrl = \"http:\/\/10\.0\.2\.2\:8888\"/val defaultUrl = \"http:\/\/172\.17\.0\.1\:8888\"/g" ./androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt
 | 
				
			||||||
 | 
					      - name: Tests
 | 
				
			||||||
 | 
					        uses: reactivecircus/android-emulator-runner@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          api-level: 29
 | 
				
			||||||
 | 
					          script: |
 | 
				
			||||||
 | 
					            ./gradlew androidApp:clearScreenshotsTask || true
 | 
				
			||||||
 | 
					            ./gradlew androidApp:createScreenshotDirectory
 | 
				
			||||||
 | 
					            adb logcat -G 16M
 | 
				
			||||||
 | 
					            ./gradlew JacocoDebugCodeCoverage || true
 | 
				
			||||||
 | 
					            ./gradlew androidApp:fetchScreenshots
 | 
				
			||||||
 | 
					            adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' 'LogApiCalls:D' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt
 | 
				
			||||||
 | 
					      - uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        if: always()
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: screenshot-espresso
 | 
				
			||||||
 | 
					          path: androidApp/build/reports/androidTests/connected/screenshots
 | 
				
			||||||
 | 
					          retention-days: 2
 | 
				
			||||||
 | 
					          overwrite: true
 | 
				
			||||||
 | 
					          include-hidden-files: true
 | 
				
			||||||
 | 
					      - uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        if: always()
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: result-espresso
 | 
				
			||||||
 | 
					          path: androidApp/build/reports/androidTests/connected/debug/flavors/githubConfig
 | 
				
			||||||
 | 
					          retention-days: 1
 | 
				
			||||||
 | 
					          overwrite: true
 | 
				
			||||||
 | 
					          include-hidden-files: true
 | 
				
			||||||
 | 
					      - uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: coverage-espresso
 | 
				
			||||||
 | 
					          path: androidApp/build/reports/jacoco/JacocoDebugCodeCoverage
 | 
				
			||||||
 | 
					          retention-days: 1
 | 
				
			||||||
 | 
					          overwrite: true
 | 
				
			||||||
 | 
					          include-hidden-files: true
 | 
				
			||||||
 | 
					      - name: Clean
 | 
				
			||||||
 | 
					        if: always()
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          docker compose -f .gitea/workflows/assets/docker-compose.yml stop
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -323,4 +323,6 @@ fabric.properties
 | 
				
			|||||||
crowdin.properties
 | 
					crowdin.properties
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.kotlin/
 | 
					.kotlin/
 | 
				
			||||||
build-cache/
 | 
					build-cache/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					act
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										124
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										124
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,3 +1,127 @@
 | 
				
			|||||||
 | 
					**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
 | 
					**v124123641
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Chore: no tests on build.
 | 
					- Chore: no tests on build.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,42 @@ 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"
 | 
				
			||||||
 | 
					    jacoco
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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 {
 | 
				
			||||||
@@ -53,6 +65,15 @@ fun versionNameFromGit(): String {
 | 
				
			|||||||
    return gitVersion()
 | 
					    return gitVersion()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					val exclusions =
 | 
				
			||||||
 | 
					    listOf(
 | 
				
			||||||
 | 
					        "**/R.class",
 | 
				
			||||||
 | 
					        "**/R\$*.class",
 | 
				
			||||||
 | 
					        "**/BuildConfig.*",
 | 
				
			||||||
 | 
					        "**/Manifest*.*",
 | 
				
			||||||
 | 
					        "**/*Test*.*",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
android {
 | 
					android {
 | 
				
			||||||
    compileOptions {
 | 
					    compileOptions {
 | 
				
			||||||
        isCoreLibraryDesugaringEnabled = true
 | 
					        isCoreLibraryDesugaringEnabled = true
 | 
				
			||||||
@@ -65,14 +86,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,7 +105,7 @@ android {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        // tests
 | 
					        // tests
 | 
				
			||||||
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
 | 
					        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
 | 
				
			||||||
        testInstrumentationRunnerArguments["clearPackageData"] = "true"
 | 
					        testInstrumentationRunnerArguments["useTestStorageService"] = "true"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    packaging {
 | 
					    packaging {
 | 
				
			||||||
        resources {
 | 
					        resources {
 | 
				
			||||||
@@ -98,6 +119,44 @@ 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
 | 
				
			||||||
 | 
					            installation {
 | 
				
			||||||
 | 
					                installOptions("-g", "-r")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val androidTests = "connectedAndroidTest"
 | 
				
			||||||
 | 
					            tasks.register<JacocoReport>("JacocoDebugCodeCoverage") {
 | 
				
			||||||
 | 
					                // Depend on unit tests and Android tests tasks
 | 
				
			||||||
 | 
					                dependsOn(listOf(androidTests))
 | 
				
			||||||
 | 
					                // Set task grouping and description
 | 
				
			||||||
 | 
					                group = "Reporting"
 | 
				
			||||||
 | 
					                description = "Execute UI and unit tests, generate and combine Jacoco coverage report"
 | 
				
			||||||
 | 
					                // Configure reports to generate both XML and HTML formats
 | 
				
			||||||
 | 
					                reports {
 | 
				
			||||||
 | 
					                    xml.required.set(true)
 | 
				
			||||||
 | 
					                    html.required.set(true)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                // Set source directories to the main source directory
 | 
				
			||||||
 | 
					                sourceDirectories.setFrom(layout.projectDirectory.dir("src/main"))
 | 
				
			||||||
 | 
					                // Set class directories to compiled Java and Kotlin classes, excluding specified exclusions
 | 
				
			||||||
 | 
					                classDirectories.setFrom(
 | 
				
			||||||
 | 
					                    files(
 | 
				
			||||||
 | 
					                        fileTree(layout.buildDirectory.dir("intermediates/javac/")) {
 | 
				
			||||||
 | 
					                            exclude(exclusions)
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/")) {
 | 
				
			||||||
 | 
					                            exclude(exclusions)
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                // Collect execution data from .exec and .ec files generated during test execution
 | 
				
			||||||
 | 
					                executionData.setFrom(
 | 
				
			||||||
 | 
					                    files(
 | 
				
			||||||
 | 
					                        fileTree(layout.buildDirectory) { include(listOf("**/*.exec", "**/*.ec")) },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    flavorDimensions.add("build")
 | 
					    flavorDimensions.add("build")
 | 
				
			||||||
@@ -110,96 +169,90 @@ android {
 | 
				
			|||||||
    namespace = "bou.amine.apps.readerforselfossv2.android"
 | 
					    namespace = "bou.amine.apps.readerforselfossv2.android"
 | 
				
			||||||
    testOptions {
 | 
					    testOptions {
 | 
				
			||||||
        animationsDisabled = true
 | 
					        animationsDisabled = true
 | 
				
			||||||
        execution = "ANDROIDX_TEST_ORCHESTRATOR"
 | 
					 | 
				
			||||||
        unitTests {
 | 
					        unitTests {
 | 
				
			||||||
            isIncludeAndroidResources = true
 | 
					            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
 | 
				
			||||||
    implementation("com.mikepenz:aboutlibraries-core:10.5.1")
 | 
					    implementation("com.mikepenz:aboutlibraries-core:11.6.3")
 | 
				
			||||||
    implementation("com.mikepenz:aboutlibraries:10.5.1")
 | 
					    implementation("com.mikepenz:aboutlibraries:11.6.3")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Material-ish things
 | 
					    // Material-ish things
 | 
				
			||||||
    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.6.2")
 | 
					    androidTestImplementation("androidx.test:runner:1.7.0-alpha01")
 | 
				
			||||||
    androidTestImplementation("androidx.test:rules:1.6.1")
 | 
					    androidTestImplementation("androidx.test:rules:1.7.0-alpha01")
 | 
				
			||||||
    androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
 | 
					    androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
 | 
				
			||||||
    implementation("androidx.test.espresso:espresso-idling-resource:3.6.1")
 | 
					    implementation("androidx.test.espresso:espresso-idling-resource:3.6.1")
 | 
				
			||||||
    androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1")
 | 
					    androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1")
 | 
				
			||||||
    androidTestUtil("androidx.test:orchestrator:1.5.1")
 | 
					    androidTestUtil("androidx.test.services:test-services:1.6.0-alpha02")
 | 
				
			||||||
    testImplementation("org.robolectric:robolectric:4.14.1")
 | 
					    testImplementation("org.robolectric:robolectric:4.14.1")
 | 
				
			||||||
    testImplementation("androidx.test:core-ktx:1.6.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")
 | 
				
			||||||
@@ -211,16 +264,24 @@ 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
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (this.name == "connectedAndroidTest") {
 | 
				
			||||||
 | 
					        configure<JacocoTaskExtension> {
 | 
				
			||||||
 | 
					            isIncludeNoLocationClasses = true
 | 
				
			||||||
 | 
					            excludes = listOf("jdk.internal.*")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
aboutLibraries {
 | 
					aboutLibraries {
 | 
				
			||||||
 | 
					    excludeFields = arrayOf("generated")
 | 
				
			||||||
    offlineMode = true
 | 
					    offlineMode = true
 | 
				
			||||||
    fetchRemoteLicense = false
 | 
					    fetchRemoteLicense = false
 | 
				
			||||||
    fetchRemoteFunding = false
 | 
					    fetchRemoteFunding = false
 | 
				
			||||||
@@ -228,4 +289,31 @@ 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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					val clearScreenshotsTask =
 | 
				
			||||||
 | 
					    tasks.register<Exec>("clearScreenshots") {
 | 
				
			||||||
 | 
					        println("AMINE : clear")
 | 
				
			||||||
 | 
					        commandLine = listOf("adb", "shell", "rm", "-r", "/storage/emulated/0/Pictures/selfoss_tests/screenshots/*")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					val createScreenshotDirectoryTask =
 | 
				
			||||||
 | 
					    tasks.register<Exec>("createScreenshotDirectory") {
 | 
				
			||||||
 | 
					        println("AMINE : create directory")
 | 
				
			||||||
 | 
					        group = "reporting"
 | 
				
			||||||
 | 
					        commandLine = listOf("adb", "shell", "mkdir", "-p", "/storage/emulated/0/Pictures/selfoss_tests/screenshots")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tasks.register<Exec>("fetchScreenshots") {
 | 
				
			||||||
 | 
					    val reportsDirectory = file("$buildDir/reports/androidTests/connected")
 | 
				
			||||||
 | 
					    println("AMINE : fetch")
 | 
				
			||||||
 | 
					    group = "reporting"
 | 
				
			||||||
 | 
					    executable(android.adbExecutable.toString())
 | 
				
			||||||
 | 
					    commandLine = listOf("adb", "pull", "/storage/emulated/0/Pictures/selfoss_tests/screenshots", reportsDirectory.toString())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    finalizedBy(clearScreenshotsTask)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    doFirst {
 | 
				
			||||||
 | 
					        reportsDirectory.mkdirs()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.android
 | 
					package bou.amine.apps.readerforselfossv2.android
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.app.Activity
 | 
					 | 
				
			||||||
import androidx.test.espresso.Espresso.onView
 | 
					import androidx.test.espresso.Espresso.onView
 | 
				
			||||||
import androidx.test.espresso.IdlingRegistry
 | 
					import androidx.test.espresso.IdlingRegistry
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions.click
 | 
					import androidx.test.espresso.action.ViewActions.click
 | 
				
			||||||
@@ -16,59 +15,73 @@ import androidx.test.filters.LargeTest
 | 
				
			|||||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
 | 
					import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
 | 
				
			||||||
import org.junit.After
 | 
					import org.junit.After
 | 
				
			||||||
import org.junit.Before
 | 
					import org.junit.Before
 | 
				
			||||||
 | 
					import org.junit.FixMethodOrder
 | 
				
			||||||
import org.junit.Rule
 | 
					import org.junit.Rule
 | 
				
			||||||
import org.junit.Test
 | 
					import org.junit.Test
 | 
				
			||||||
import org.junit.runner.RunWith
 | 
					import org.junit.runner.RunWith
 | 
				
			||||||
 | 
					import org.junit.runners.MethodSorters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
 | 
				
			||||||
@RunWith(AndroidJUnit4::class)
 | 
					@RunWith(AndroidJUnit4::class)
 | 
				
			||||||
 | 
					@FixMethodOrder(MethodSorters.NAME_ASCENDING)
 | 
				
			||||||
@LargeTest
 | 
					@LargeTest
 | 
				
			||||||
class LoginActivityTest {
 | 
					class `1-LoginActivityTest` : WithANRException() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @get:Rule
 | 
					    @get:Rule
 | 
				
			||||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
					    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun getActivity(): Activity? {
 | 
					 | 
				
			||||||
        var activity: Activity? = null
 | 
					 | 
				
			||||||
        activityRule.scenario.onActivity {
 | 
					 | 
				
			||||||
            activity = it
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return activity
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Before
 | 
					    @Before
 | 
				
			||||||
    fun registerIdlingResource() {
 | 
					    fun registerIdlingResource() {
 | 
				
			||||||
        IdlingRegistry.getInstance()
 | 
					        IdlingRegistry
 | 
				
			||||||
 | 
					            .getInstance()
 | 
				
			||||||
            .register(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
					            .register(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @After
 | 
					    @After
 | 
				
			||||||
    fun unregisterIdlingResource() {
 | 
					    fun unregisterIdlingResource() {
 | 
				
			||||||
        IdlingRegistry.getInstance()
 | 
					        IdlingRegistry
 | 
				
			||||||
 | 
					            .getInstance()
 | 
				
			||||||
            .unregister(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
					            .unregister(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun viewIsInitialized() {
 | 
					    fun `1-viewIsInitialized`() {
 | 
				
			||||||
        onView(withId(R.id.urlView)).check(matches(isDisplayed()))
 | 
					        onView(withId(R.id.urlView)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked()))
 | 
					        onView(withId(R.id.selfSigned))
 | 
				
			||||||
 | 
					            .check(matches(isDisplayed()))
 | 
				
			||||||
 | 
					            .check(matches(isNotChecked()))
 | 
				
			||||||
            .check(
 | 
					            .check(
 | 
				
			||||||
                matches(isClickable())
 | 
					                matches(isClickable()),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        onView(withId(R.id.withLogin)).check(matches(isDisplayed()))
 | 
					        onView(withId(R.id.withLogin))
 | 
				
			||||||
            .check(matches(isNotChecked())).check(
 | 
					            .check(matches(isDisplayed()))
 | 
				
			||||||
                matches(isClickable())
 | 
					            .check(matches(isNotChecked()))
 | 
				
			||||||
 | 
					            .check(
 | 
				
			||||||
 | 
					                matches(isClickable()),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun urlError() {
 | 
					    fun `2-urlError`() {
 | 
				
			||||||
        performLogin("10.0.2.2:8888")
 | 
					        performLogin("10.0.2.2:8888")
 | 
				
			||||||
        onView(withId(R.id.urlView)).perform(click())
 | 
					        onView(withId(R.id.urlView)).perform(click())
 | 
				
			||||||
 | 
					        onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem)))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    fun `3-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 `4-connectError`() {
 | 
				
			||||||
 | 
					        performLogin("http://10.0.2.2:8889")
 | 
				
			||||||
        onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
 | 
					        onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun multiError() {
 | 
					    fun `5-multiError`() {
 | 
				
			||||||
        onView(withId(R.id.signInButton)).perform(click())
 | 
					        onView(withId(R.id.signInButton)).perform(click())
 | 
				
			||||||
        onView(withId(R.id.signInButton)).perform(click())
 | 
					        onView(withId(R.id.signInButton)).perform(click())
 | 
				
			||||||
        onView(withId(R.id.signInButton)).perform(click())
 | 
					        onView(withId(R.id.signInButton)).perform(click())
 | 
				
			||||||
@@ -76,8 +89,10 @@ class LoginActivityTest {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun connect() {
 | 
					    fun `6-connect`() {
 | 
				
			||||||
        performLogin()
 | 
					        performLogin()
 | 
				
			||||||
        onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
 | 
				
			||||||
 | 
					        onView(withText("OK")).perform(click())
 | 
				
			||||||
 | 
					        checkHomeLoadingDone()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,9 +1,7 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.android
 | 
					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.onView
 | 
				
			||||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
 | 
					import androidx.test.espresso.IdlingRegistry
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions
 | 
					import androidx.test.espresso.action.ViewActions
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions.click
 | 
					import androidx.test.espresso.action.ViewActions.click
 | 
				
			||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
					import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
				
			||||||
@@ -17,39 +15,44 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
 | 
				
			|||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
 | 
					import androidx.test.ext.junit.rules.ActivityScenarioRule
 | 
				
			||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
 | 
					import androidx.test.ext.junit.runners.AndroidJUnit4
 | 
				
			||||||
import androidx.test.filters.LargeTest
 | 
					import androidx.test.filters.LargeTest
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
 | 
				
			||||||
import org.hamcrest.CoreMatchers.not
 | 
					import org.hamcrest.CoreMatchers.not
 | 
				
			||||||
import org.junit.Before
 | 
					import org.junit.Before
 | 
				
			||||||
 | 
					import org.junit.FixMethodOrder
 | 
				
			||||||
import org.junit.Rule
 | 
					import org.junit.Rule
 | 
				
			||||||
import org.junit.Test
 | 
					import org.junit.Test
 | 
				
			||||||
import org.junit.runner.RunWith
 | 
					import org.junit.runner.RunWith
 | 
				
			||||||
 | 
					import org.junit.runners.MethodSorters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
 | 
				
			||||||
@RunWith(AndroidJUnit4::class)
 | 
					@RunWith(AndroidJUnit4::class)
 | 
				
			||||||
 | 
					@FixMethodOrder(MethodSorters.NAME_ASCENDING)
 | 
				
			||||||
@LargeTest
 | 
					@LargeTest
 | 
				
			||||||
class HomeActivityTest {
 | 
					class `2-HomeActivityTest` : WithANRException() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @get:Rule
 | 
					    @get:Rule
 | 
				
			||||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
					    val activityRule = ActivityScenarioRule(HomeActivity::class.java)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Before
 | 
					    @Before
 | 
				
			||||||
    fun init() {
 | 
					    fun registerIdlingResource() {
 | 
				
			||||||
        loginAndInitHome()
 | 
					        IdlingRegistry
 | 
				
			||||||
 | 
					            .getInstance()
 | 
				
			||||||
 | 
					            .register(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
				
			||||||
 | 
					        checkHomeLoadingDone()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testMenu() {
 | 
					    fun testMenu() {
 | 
				
			||||||
        onView(withId(R.id.action_search)).check(matches(isDisplayed())).check(
 | 
					        onView(withId(R.id.action_search)).check(matches(isDisplayed())).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isClickable()
 | 
					                isClickable(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check(
 | 
					        onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isClickable()
 | 
					                isClickable(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					 | 
				
			||||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        openMenu()
 | 
				
			||||||
        onView(withText(R.string.readAll)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.readAll)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(withText(R.string.menu_home_sources)).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.title_activity_settings)).check(matches(isDisplayed()))
 | 
				
			||||||
@@ -62,59 +65,47 @@ class HomeActivityTest {
 | 
				
			|||||||
    fun testMenuActions() {
 | 
					    fun testMenuActions() {
 | 
				
			||||||
        onView(withId(R.id.action_search)).perform(click())
 | 
					        onView(withId(R.id.action_search)).perform(click())
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            withId(R.id.search_src_text)
 | 
					            withId(com.google.android.material.R.id.search_src_text),
 | 
				
			||||||
        ).check(matches(isFocused()))
 | 
					        ).check(matches(isFocused()))
 | 
				
			||||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
					        onView(isRoot()).perform(ViewActions.pressBack())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        onView(withId(R.id.action_filter)).perform(click())
 | 
					        onView(withId(R.id.action_filter)).perform(click())
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            withText(R.string.filter_item_sources)
 | 
					            withText(R.string.filter_item_sources),
 | 
				
			||||||
        ).check(matches(isDisplayed()))
 | 
					        ).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            withText(R.string.filter_item_tags)
 | 
					            withText(R.string.filter_item_tags),
 | 
				
			||||||
        ).check(matches(isDisplayed()))
 | 
					        ).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            withId(R.id.floatingActionButton2)
 | 
					            withId(R.id.floatingActionButton2),
 | 
				
			||||||
        ).check(matches(isDisplayed()))
 | 
					        ).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
					        onView(isRoot()).perform(ViewActions.pressBack())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					        openMenu()
 | 
				
			||||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        onView(withText(R.string.readAll)).perform(click())
 | 
					        onView(withText(R.string.readAll)).perform(click())
 | 
				
			||||||
        onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
					        onView(isRoot()).perform(ViewActions.pressBack())
 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					        openMenu()
 | 
				
			||||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        onView(withText(R.string.menu_home_sources)).perform(click())
 | 
					        onView(withText(R.string.menu_home_sources)).perform(click())
 | 
				
			||||||
        onView(withId(R.id.fab)).check(matches(isDisplayed()))
 | 
					        onView(withId(R.id.fab)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
					        onView(isRoot()).perform(ViewActions.pressBack())
 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					        openMenu()
 | 
				
			||||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
					        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
				
			||||||
        onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
					        onView(isRoot()).perform(ViewActions.pressBack())
 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					        openMenu()
 | 
				
			||||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        onView(withText(R.string.menu_home_refresh)).perform(click())
 | 
					        onView(withText(R.string.menu_home_refresh)).perform(click())
 | 
				
			||||||
        onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
					        onView(isRoot()).perform(ViewActions.pressBack())
 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					        openMenu()
 | 
				
			||||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /*onView(withText(R.string.issue_tracker_link)).perform(click())
 | 
					        /*onView(withText(R.string.issue_tracker_link)).perform(click())
 | 
				
			||||||
        onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
					        onView(isRoot()).perform(ViewActions.pressBack())
 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					        openMenu()*/
 | 
				
			||||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
					 | 
				
			||||||
        )*/
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        onView(withText(R.string.action_disconnect)).perform(click())
 | 
					        onView(withText(R.string.action_disconnect)).perform(click())
 | 
				
			||||||
        onView(withText(R.string.confirm_disconnect_title)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.confirm_disconnect_title)).check(matches(isDisplayed()))
 | 
				
			||||||
@@ -124,14 +115,13 @@ class HomeActivityTest {
 | 
				
			|||||||
    fun testEmptyView() {
 | 
					    fun testEmptyView() {
 | 
				
			||||||
        onView(withId(R.id.emptyText)).check(matches(isDisplayed()))
 | 
					        onView(withId(R.id.emptyText)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            hasBottombarItemText(R.string.tab_new)
 | 
					            hasBottombarItemText(R.string.tab_new),
 | 
				
			||||||
        ).check(matches(isDisplayed())).check(matches(isSelected()))
 | 
					        ).check(matches(isDisplayed())).check(matches(isSelected()))
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            hasBottombarItemText(R.string.tab_read)
 | 
					            hasBottombarItemText(R.string.tab_read),
 | 
				
			||||||
        ).check(matches(isDisplayed())).check(matches(not(isSelected())))
 | 
					        ).check(matches(isDisplayed())).check(matches(not(isSelected())))
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            hasBottombarItemText(R.string.tab_favs)
 | 
					            hasBottombarItemText(R.string.tab_favs),
 | 
				
			||||||
        ).check(matches(isDisplayed())).check(matches(not(isSelected())))
 | 
					        ).check(matches(isDisplayed())).check(matches(not(isSelected())))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,9 +1,8 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.android
 | 
					package bou.amine.apps.readerforselfossv2.android
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.content.Context
 | 
					import android.content.Context
 | 
				
			||||||
import androidx.test.core.app.ApplicationProvider
 | 
					 | 
				
			||||||
import androidx.test.espresso.Espresso.onView
 | 
					import androidx.test.espresso.Espresso.onView
 | 
				
			||||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
 | 
					import androidx.test.espresso.IdlingRegistry
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions.click
 | 
					import androidx.test.espresso.action.ViewActions.click
 | 
				
			||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
					import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 | 
				
			||||||
@@ -12,6 +11,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
 | 
				
			|||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
 | 
					import androidx.test.ext.junit.rules.ActivityScenarioRule
 | 
				
			||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
 | 
					import androidx.test.ext.junit.runners.AndroidJUnit4
 | 
				
			||||||
import androidx.test.filters.LargeTest
 | 
					import androidx.test.filters.LargeTest
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
 | 
				
			||||||
import org.hamcrest.CoreMatchers.allOf
 | 
					import org.hamcrest.CoreMatchers.allOf
 | 
				
			||||||
import org.hamcrest.CoreMatchers.not
 | 
					import org.hamcrest.CoreMatchers.not
 | 
				
			||||||
import org.junit.Before
 | 
					import org.junit.Before
 | 
				
			||||||
@@ -21,10 +21,11 @@ import org.junit.runner.RunWith
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@RunWith(AndroidJUnit4::class)
 | 
					@RunWith(AndroidJUnit4::class)
 | 
				
			||||||
@LargeTest
 | 
					@LargeTest
 | 
				
			||||||
class SettingsActivityTest {
 | 
					@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
 | 
				
			||||||
 | 
					class `3-SettingsActivityTest` : WithANRException() {
 | 
				
			||||||
    @get:Rule
 | 
					    @get:Rule
 | 
				
			||||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
					    val activityRule = ActivityScenarioRule(HomeActivity::class.java)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    lateinit var context: Context
 | 
					    lateinit var context: Context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Before
 | 
					    @Before
 | 
				
			||||||
@@ -32,17 +33,15 @@ class SettingsActivityTest {
 | 
				
			|||||||
        activityRule.scenario.onActivity { activity ->
 | 
					        activityRule.scenario.onActivity { activity ->
 | 
				
			||||||
            context = activity.window.context
 | 
					            context = activity.window.context
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        loginAndInitHome()
 | 
					        IdlingRegistry
 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					            .getInstance()
 | 
				
			||||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
					            .register(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
				
			||||||
        )
 | 
					        openMenu()
 | 
				
			||||||
        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
					        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testAllSettings() {
 | 
					    fun testAllSettings() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
        onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
 | 
					        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_viewer)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed()))
 | 
				
			||||||
@@ -52,14 +51,13 @@ class SettingsActivityTest {
 | 
				
			|||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(),
 | 
					                    isDisplayed(),
 | 
				
			||||||
                    not(isSelected())
 | 
					                    not(isSelected()),
 | 
				
			||||||
                )
 | 
					                ),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withText(R.string.action_about)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.action_about)).check(matches(isDisplayed()))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testThemes() {
 | 
					    fun testThemes() {
 | 
				
			||||||
        testPreferencesFromArray(context, R.array.ModeTitles) {
 | 
					        testPreferencesFromArray(context, R.array.ModeTitles) {
 | 
				
			||||||
@@ -67,7 +65,6 @@ class SettingsActivityTest {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testExperimentail() {
 | 
					    fun testExperimentail() {
 | 
				
			||||||
        onView(withText(R.string.pref_header_experimental)).perform(click())
 | 
					        onView(withText(R.string.pref_header_experimental)).perform(click())
 | 
				
			||||||
@@ -77,15 +74,16 @@ class SettingsActivityTest {
 | 
				
			|||||||
        changeAndSaveSetting("", "10") {
 | 
					        changeAndSaveSetting("", "10") {
 | 
				
			||||||
            onView(withText(R.string.pref_api_timeout)).perform(click())
 | 
					            onView(withText(R.string.pref_api_timeout)).perform(click())
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        changeAndSaveSetting("", "60") {
 | 
				
			||||||
 | 
					            onView(withText(R.string.pref_api_timeout)).perform(click())
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testBugReports() {
 | 
					    fun testBugReports() {
 | 
				
			||||||
        onView(withText(R.string.pref_switch_disable_acra)).perform(click())
 | 
					        onView(withText(R.string.pref_switch_disable_acra)).perform(click())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testLinks() {
 | 
					    fun testLinks() {
 | 
				
			||||||
        onView(withText(R.string.pref_header_links)).perform(click())
 | 
					        onView(withText(R.string.pref_header_links)).perform(click())
 | 
				
			||||||
@@ -95,10 +93,9 @@ class SettingsActivityTest {
 | 
				
			|||||||
        onView(withText(R.string.translation)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.translation)).check(matches(isDisplayed()))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testAbout() {
 | 
					    fun testAbout() {
 | 
				
			||||||
        onView(withText(R.string.action_about)).perform(click())
 | 
					        onView(withText(R.string.action_about)).perform(click())
 | 
				
			||||||
        onView(withText("ACRA")).check(matches(isDisplayed()))
 | 
					        onView(withText("ACRA")).check(matches(isDisplayed()))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -3,9 +3,11 @@ package bou.amine.apps.readerforselfossv2.android
 | 
				
			|||||||
import androidx.test.core.app.ApplicationProvider
 | 
					import androidx.test.core.app.ApplicationProvider
 | 
				
			||||||
import androidx.test.espresso.Espresso.onView
 | 
					import androidx.test.espresso.Espresso.onView
 | 
				
			||||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
 | 
					import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
 | 
				
			||||||
 | 
					import androidx.test.espresso.IdlingRegistry
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions
 | 
					import androidx.test.espresso.action.ViewActions
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions.click
 | 
					import androidx.test.espresso.action.ViewActions.click
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions.replaceText
 | 
					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.action.ViewActions.typeTextIntoFocusedView
 | 
				
			||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
					import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isChecked
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isChecked
 | 
				
			||||||
@@ -18,99 +20,101 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
 | 
				
			|||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
 | 
					import androidx.test.ext.junit.rules.ActivityScenarioRule
 | 
				
			||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
 | 
					import androidx.test.ext.junit.runners.AndroidJUnit4
 | 
				
			||||||
import androidx.test.filters.LargeTest
 | 
					import androidx.test.filters.LargeTest
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
 | 
				
			||||||
import org.hamcrest.CoreMatchers.allOf
 | 
					import org.hamcrest.CoreMatchers.allOf
 | 
				
			||||||
import org.hamcrest.CoreMatchers.not
 | 
					import org.hamcrest.CoreMatchers.not
 | 
				
			||||||
import org.junit.Before
 | 
					import org.junit.Before
 | 
				
			||||||
 | 
					import org.junit.FixMethodOrder
 | 
				
			||||||
import org.junit.Rule
 | 
					import org.junit.Rule
 | 
				
			||||||
import org.junit.Test
 | 
					import org.junit.Test
 | 
				
			||||||
import org.junit.runner.RunWith
 | 
					import org.junit.runner.RunWith
 | 
				
			||||||
 | 
					import org.junit.runners.MethodSorters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
 | 
				
			||||||
@RunWith(AndroidJUnit4::class)
 | 
					@RunWith(AndroidJUnit4::class)
 | 
				
			||||||
 | 
					@FixMethodOrder(MethodSorters.NAME_ASCENDING)
 | 
				
			||||||
@LargeTest
 | 
					@LargeTest
 | 
				
			||||||
class SettingsActivityGeneralTest {
 | 
					class `4-SettingsActivityGeneralTest` : WithANRException() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @get:Rule
 | 
					    @get:Rule
 | 
				
			||||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
					    val activityRule = ActivityScenarioRule(HomeActivity::class.java)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Before
 | 
					    @Before
 | 
				
			||||||
    fun init() {
 | 
					    fun init() {
 | 
				
			||||||
        loginAndInitHome()
 | 
					        IdlingRegistry
 | 
				
			||||||
 | 
					            .getInstance()
 | 
				
			||||||
 | 
					            .register(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					        openActionBarOverflowOrOptionsMenu(
 | 
				
			||||||
            ApplicationProvider.getApplicationContext()
 | 
					            ApplicationProvider.getApplicationContext(),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
					        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
				
			||||||
        onView(withText(R.string.pref_header_general)).perform(click())
 | 
					        onView(withText(R.string.pref_header_general)).perform(click())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Suppress("detekt:LongMethod")
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testGeneral() {
 | 
					    fun testGeneral() {
 | 
				
			||||||
        onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title)
 | 
					            withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title),
 | 
				
			||||||
        ).check(
 | 
					        ).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), not(isChecked())
 | 
					                    isDisplayed(),
 | 
				
			||||||
                )
 | 
					                    not(isChecked()),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check(
 | 
					        onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), isChecked()
 | 
					                    isDisplayed(),
 | 
				
			||||||
                )
 | 
					                    isChecked(),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
        )
 | 
					            ),
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
 | 
					 | 
				
			||||||
            matches(
 | 
					 | 
				
			||||||
                allOf(
 | 
					 | 
				
			||||||
                    isDisplayed(), not(isChecked())
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
 | 
					 | 
				
			||||||
            matches(
 | 
					 | 
				
			||||||
                isEnabled()
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
 | 
					        onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), not(isChecked())
 | 
					                    isDisplayed(),
 | 
				
			||||||
                )
 | 
					                    not(isChecked()),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.card_height_title)).check(
 | 
					        onView(withSettingsCheckboxWidget(R.string.card_height_title)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), not(isChecked())
 | 
					                    isDisplayed(),
 | 
				
			||||||
                )
 | 
					                    not(isChecked()),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(
 | 
					        onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                not(isEnabled())
 | 
					                not(isEnabled()),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check(
 | 
					        onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), isChecked()
 | 
					                    isDisplayed(),
 | 
				
			||||||
                )
 | 
					                    isChecked(),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        onView(withId(R.id.settings)).perform(swipeUp())
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check(
 | 
					        onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), not(isChecked())
 | 
					                    isDisplayed(),
 | 
				
			||||||
                )
 | 
					                    not(isChecked()),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Suppress("detekt:ForbiddenComment")
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testGeneralActionsNumberItems() {
 | 
					    fun testGeneralActionsNumberItems() {
 | 
				
			||||||
        onView(withText(R.string.pref_api_items_number_title)).perform(click())
 | 
					        onView(withText(R.string.pref_api_items_number_title)).perform(click())
 | 
				
			||||||
@@ -118,25 +122,25 @@ class SettingsActivityGeneralTest {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        // Value check
 | 
					        // Value check
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            withId(android.R.id.edit)
 | 
					            withId(android.R.id.edit),
 | 
				
			||||||
        ).perform(replaceText("AVC"))
 | 
					        ).perform(replaceText("AVC"))
 | 
				
			||||||
            .check(matches(withText("")))
 | 
					            .check(matches(withText("")))
 | 
				
			||||||
        // TODO: should check message error. Not working for api level 30+
 | 
					        // TODO: should check message error. Not working for api level 30+
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            withId(android.R.id.edit)
 | 
					            withId(android.R.id.edit),
 | 
				
			||||||
        ).perform(replaceText("-1"))
 | 
					        ).perform(replaceText("-1"))
 | 
				
			||||||
            .check(matches(withText("")))
 | 
					            .check(matches(withText("")))
 | 
				
			||||||
        // TODO: should check message error. Not working for api level 30+
 | 
					        // TODO: should check message error. Not working for api level 30+
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            withId(android.R.id.edit)
 | 
					            withId(android.R.id.edit),
 | 
				
			||||||
        ).perform(replaceText("300"))
 | 
					        ).perform(replaceText("300"))
 | 
				
			||||||
            .check(matches(withText("")))
 | 
					            .check(matches(withText("")))
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            withId(android.R.id.edit)
 | 
					            withId(android.R.id.edit),
 | 
				
			||||||
        ).perform(typeTextIntoFocusedView("300"))
 | 
					        ).perform(typeTextIntoFocusedView("300"))
 | 
				
			||||||
            .check(matches(withText("30")))
 | 
					            .check(matches(withText("30")))
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            withId(android.R.id.edit)
 | 
					            withId(android.R.id.edit),
 | 
				
			||||||
        ).perform(replaceText("10"))
 | 
					        ).perform(replaceText("10"))
 | 
				
			||||||
            .check(matches(withText("10")))
 | 
					            .check(matches(withText("10")))
 | 
				
			||||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
					        onView(isRoot()).perform(ViewActions.pressBack())
 | 
				
			||||||
@@ -152,21 +156,8 @@ class SettingsActivityGeneralTest {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testGeneralActionsCheckboxes() {
 | 
					    fun testGeneralActionsCheckboxes() {
 | 
				
			||||||
        // article viewer settings
 | 
					 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
 | 
					 | 
				
			||||||
            matches(
 | 
					 | 
				
			||||||
                isEnabled()
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click())
 | 
					 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
 | 
					 | 
				
			||||||
            matches(
 | 
					 | 
				
			||||||
                not(isEnabled())
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled())))
 | 
					        onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled())))
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click())
 | 
					        onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click())
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled()))
 | 
					        onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled()))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,31 +1,34 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.android
 | 
					package bou.amine.apps.readerforselfossv2.android
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.content.Context
 | 
					import android.content.Context
 | 
				
			||||||
import androidx.test.core.app.ApplicationProvider
 | 
					 | 
				
			||||||
import androidx.test.espresso.Espresso.onView
 | 
					import androidx.test.espresso.Espresso.onView
 | 
				
			||||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
 | 
					import androidx.test.espresso.IdlingRegistry
 | 
				
			||||||
 | 
					import androidx.test.espresso.action.ViewActions
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions.click
 | 
					import androidx.test.espresso.action.ViewActions.click
 | 
				
			||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
					import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isChecked
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isChecked
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 | 
				
			||||||
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isRoot
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
 | 
					import androidx.test.espresso.matcher.ViewMatchers.withText
 | 
				
			||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
 | 
					import androidx.test.ext.junit.rules.ActivityScenarioRule
 | 
				
			||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
 | 
					import androidx.test.ext.junit.runners.AndroidJUnit4
 | 
				
			||||||
import androidx.test.filters.LargeTest
 | 
					import androidx.test.filters.LargeTest
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
 | 
				
			||||||
import org.hamcrest.CoreMatchers.allOf
 | 
					import org.hamcrest.CoreMatchers.allOf
 | 
				
			||||||
import org.hamcrest.CoreMatchers.not
 | 
					import org.hamcrest.CoreMatchers.not
 | 
				
			||||||
 | 
					import org.junit.After
 | 
				
			||||||
import org.junit.Before
 | 
					import org.junit.Before
 | 
				
			||||||
import org.junit.Rule
 | 
					import org.junit.Rule
 | 
				
			||||||
import org.junit.Test
 | 
					import org.junit.Test
 | 
				
			||||||
import org.junit.runner.RunWith
 | 
					import org.junit.runner.RunWith
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
 | 
				
			||||||
@RunWith(AndroidJUnit4::class)
 | 
					@RunWith(AndroidJUnit4::class)
 | 
				
			||||||
@LargeTest
 | 
					@LargeTest
 | 
				
			||||||
class SettingsActivityReaderTest {
 | 
					class `5-SettingsActivityReaderTest` : WithANRException() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @get:Rule
 | 
					    @get:Rule
 | 
				
			||||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
					    val activityRule = ActivityScenarioRule(SettingsActivity::class.java)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    lateinit var context: Context
 | 
					    lateinit var context: Context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -34,24 +37,28 @@ class SettingsActivityReaderTest {
 | 
				
			|||||||
        activityRule.scenario.onActivity { activity ->
 | 
					        activityRule.scenario.onActivity { activity ->
 | 
				
			||||||
            context = activity.window.context
 | 
					            context = activity.window.context
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        loginAndInitHome()
 | 
					        IdlingRegistry
 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					            .getInstance()
 | 
				
			||||||
            ApplicationProvider.getApplicationContext()
 | 
					            .register(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
					 | 
				
			||||||
        onView(withText(R.string.pref_header_viewer)).perform(click())
 | 
					        onView(withText(R.string.pref_header_viewer)).perform(click())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @After
 | 
				
			||||||
 | 
					    fun back() {
 | 
				
			||||||
 | 
					        onView(isRoot()).perform(ViewActions.pressBack())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testReader() {
 | 
					    fun testReader() {
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check(
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), not(
 | 
					                    isDisplayed(),
 | 
				
			||||||
                        isChecked()
 | 
					                    not(
 | 
				
			||||||
                    )
 | 
					                        isChecked(),
 | 
				
			||||||
                )
 | 
					                    ),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withText(R.string.pref_content_reader_font_size)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.pref_content_reader_font_size)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(withText(R.string.settings_reader_font)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.settings_reader_font)).check(matches(isDisplayed()))
 | 
				
			||||||
@@ -61,14 +68,14 @@ class SettingsActivityReaderTest {
 | 
				
			|||||||
    fun testReaderActions() {
 | 
					    fun testReaderActions() {
 | 
				
			||||||
        onView(withText(R.string.pref_switch_actions_pager_scroll_off)).check(
 | 
					        onView(withText(R.string.pref_switch_actions_pager_scroll_off)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isDisplayed()
 | 
					                isDisplayed(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).perform(click())
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).perform(click())
 | 
				
			||||||
        onView(withText(R.string.pref_switch_actions_pager_scroll_on)).check(
 | 
					        onView(withText(R.string.pref_switch_actions_pager_scroll_on)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isDisplayed()
 | 
					                isDisplayed(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        onView(withText(R.string.pref_content_reader_font_size)).perform(click())
 | 
					        onView(withText(R.string.pref_content_reader_font_size)).perform(click())
 | 
				
			||||||
@@ -83,4 +90,4 @@ class SettingsActivityReaderTest {
 | 
				
			|||||||
            onView(withText(R.string.settings_reader_font)).perform(click())
 | 
					            onView(withText(R.string.settings_reader_font)).perform(click())
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,33 +1,36 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.android
 | 
					package bou.amine.apps.readerforselfossv2.android
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.content.Context
 | 
					import android.content.Context
 | 
				
			||||||
import androidx.test.core.app.ApplicationProvider
 | 
					 | 
				
			||||||
import androidx.test.espresso.Espresso.onView
 | 
					import androidx.test.espresso.Espresso.onView
 | 
				
			||||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
 | 
					import androidx.test.espresso.IdlingRegistry
 | 
				
			||||||
 | 
					import androidx.test.espresso.action.ViewActions
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions.click
 | 
					import androidx.test.espresso.action.ViewActions.click
 | 
				
			||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
					import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isChecked
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isChecked
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isEnabled
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
 | 
				
			||||||
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isRoot
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
 | 
					import androidx.test.espresso.matcher.ViewMatchers.withText
 | 
				
			||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
 | 
					import androidx.test.ext.junit.rules.ActivityScenarioRule
 | 
				
			||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
 | 
					import androidx.test.ext.junit.runners.AndroidJUnit4
 | 
				
			||||||
import androidx.test.filters.LargeTest
 | 
					import androidx.test.filters.LargeTest
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
 | 
				
			||||||
import org.hamcrest.CoreMatchers.allOf
 | 
					import org.hamcrest.CoreMatchers.allOf
 | 
				
			||||||
import org.hamcrest.CoreMatchers.not
 | 
					import org.hamcrest.CoreMatchers.not
 | 
				
			||||||
 | 
					import org.junit.After
 | 
				
			||||||
import org.junit.Before
 | 
					import org.junit.Before
 | 
				
			||||||
import org.junit.Rule
 | 
					import org.junit.Rule
 | 
				
			||||||
import org.junit.Test
 | 
					import org.junit.Test
 | 
				
			||||||
import org.junit.runner.RunWith
 | 
					import org.junit.runner.RunWith
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
 | 
				
			||||||
@RunWith(AndroidJUnit4::class)
 | 
					@RunWith(AndroidJUnit4::class)
 | 
				
			||||||
@LargeTest
 | 
					@LargeTest
 | 
				
			||||||
class SettingsActivityOfflineTest {
 | 
					class `6-SettingsActivityOfflineTest` : WithANRException() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @get:Rule
 | 
					    @get:Rule
 | 
				
			||||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
					    val activityRule = ActivityScenarioRule(SettingsActivity::class.java)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    lateinit var context: Context
 | 
					    lateinit var context: Context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,74 +39,84 @@ class SettingsActivityOfflineTest {
 | 
				
			|||||||
        activityRule.scenario.onActivity { activity ->
 | 
					        activityRule.scenario.onActivity { activity ->
 | 
				
			||||||
            context = activity.window.context
 | 
					            context = activity.window.context
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        loginAndInitHome()
 | 
					        IdlingRegistry
 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					            .getInstance()
 | 
				
			||||||
            ApplicationProvider.getApplicationContext()
 | 
					            .register(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
					 | 
				
			||||||
        onView(withText(R.string.pref_header_offline)).perform(click())
 | 
					        onView(withText(R.string.pref_header_offline)).perform(click())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @After
 | 
				
			||||||
 | 
					    fun back() {
 | 
				
			||||||
 | 
					        onView(isRoot()).perform(ViewActions.pressBack())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Suppress("detekt:LongMethod")
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testOffline() {
 | 
					    fun testOffline() {
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
 | 
					        onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), not(isChecked())
 | 
					                    isDisplayed(),
 | 
				
			||||||
                )
 | 
					                    not(isChecked()),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_items_caching)).check(
 | 
					        onView(withSettingsCheckboxWidget(R.string.pref_switch_items_caching)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), not(isChecked())
 | 
					                    isDisplayed(),
 | 
				
			||||||
                )
 | 
					                    not(isChecked()),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isEnabled()
 | 
					                isEnabled(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
 | 
					        onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(isNotEnabled(), isDisplayed())
 | 
					                allOf(isNotEnabled(), isDisplayed()),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_refresh_when_charging)).check(
 | 
					        onView(withSettingsCheckboxWidget(R.string.pref_switch_refresh_when_charging)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), not(isChecked())
 | 
					                    isDisplayed(),
 | 
				
			||||||
                )
 | 
					                    not(isChecked()),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isNotEnabled()
 | 
					                isNotEnabled(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_notify_new_items)).check(
 | 
					        onView(withSettingsCheckboxWidget(R.string.pref_switch_notify_new_items)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), not(isChecked())
 | 
					                    isDisplayed(),
 | 
				
			||||||
                )
 | 
					                    not(isChecked()),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isNotEnabled()
 | 
					                isNotEnabled(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).check(
 | 
					        onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                allOf(
 | 
					                allOf(
 | 
				
			||||||
                    isDisplayed(), isChecked()
 | 
					                    isDisplayed(),
 | 
				
			||||||
                )
 | 
					                    isChecked(),
 | 
				
			||||||
            )
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Suppress("detekt:LongMethod")
 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testOfflineActions() {
 | 
					    fun testOfflineActions() {
 | 
				
			||||||
        onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed()))
 | 
				
			||||||
@@ -111,50 +124,50 @@ class SettingsActivityOfflineTest {
 | 
				
			|||||||
        onView(withText(R.string.pref_switch_items_caching_on)).check(matches(isDisplayed()))
 | 
					        onView(withText(R.string.pref_switch_items_caching_on)).check(matches(isDisplayed()))
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isEnabled()
 | 
					                isEnabled(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
 | 
					        onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isNotEnabled()
 | 
					                isNotEnabled(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isNotEnabled()
 | 
					                isNotEnabled(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isNotEnabled()
 | 
					                isNotEnabled(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        onView(withText(R.string.pref_switch_periodic_refresh_off)).check(
 | 
					        onView(withText(R.string.pref_switch_periodic_refresh_off)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isDisplayed()
 | 
					                isDisplayed(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).perform(click())
 | 
					        onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).perform(click())
 | 
				
			||||||
        onView(withText(R.string.pref_switch_periodic_refresh_on)).check(
 | 
					        onView(withText(R.string.pref_switch_periodic_refresh_on)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isDisplayed()
 | 
					                isDisplayed(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.pref_periodic_refresh_minutes_title)).check(
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_periodic_refresh_minutes_title)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isEnabled()
 | 
					                isEnabled(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isEnabled()
 | 
					                isEnabled(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
 | 
				
			||||||
            matches(
 | 
					            matches(
 | 
				
			||||||
                isEnabled()
 | 
					                isEnabled(),
 | 
				
			||||||
            )
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        changeAndCancelSetting("360", "123") {
 | 
					        changeAndCancelSetting("360", "123") {
 | 
				
			||||||
            onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click())
 | 
					            onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click())
 | 
				
			||||||
@@ -166,4 +179,4 @@ class SettingsActivityOfflineTest {
 | 
				
			|||||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).perform(click())
 | 
					        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).perform(click())
 | 
				
			||||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).perform(click())
 | 
					        onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).perform(click())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					package bou.amine.apps.readerforselfossv2.android
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import androidx.test.espresso.AmbiguousViewMatcherException
 | 
				
			||||||
 | 
					import androidx.test.espresso.Espresso.onView
 | 
				
			||||||
 | 
					import androidx.test.espresso.IdlingRegistry
 | 
				
			||||||
 | 
					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 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
 | 
				
			||||||
 | 
					import java.util.UUID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
 | 
				
			||||||
 | 
					@RunWith(AndroidJUnit4::class)
 | 
				
			||||||
 | 
					@LargeTest
 | 
				
			||||||
 | 
					class `7-SourcesActivityTest` : WithANRException() {
 | 
				
			||||||
 | 
					    @get:Rule
 | 
				
			||||||
 | 
					    val activityRule = ActivityScenarioRule(HomeActivity::class.java)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lateinit var sourceName: String
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Before
 | 
				
			||||||
 | 
					    fun init() {
 | 
				
			||||||
 | 
					        IdlingRegistry
 | 
				
			||||||
 | 
					            .getInstance()
 | 
				
			||||||
 | 
					            .register(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
				
			||||||
 | 
					        sourceName = UUID.randomUUID().toString().substring(0, 15)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,83 +1,106 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.android
 | 
					package bou.amine.apps.readerforselfossv2.android
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.content.Context
 | 
					import android.content.Context
 | 
				
			||||||
 | 
					import android.graphics.Bitmap
 | 
				
			||||||
 | 
					import android.os.Environment.DIRECTORY_PICTURES
 | 
				
			||||||
 | 
					import android.os.Environment.getExternalStoragePublicDirectory
 | 
				
			||||||
 | 
					import android.util.Log
 | 
				
			||||||
import androidx.annotation.ArrayRes
 | 
					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.Espresso.onView
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions.click
 | 
					import androidx.test.espresso.action.ViewActions.click
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions.replaceText
 | 
					import androidx.test.espresso.action.ViewActions.replaceText
 | 
				
			||||||
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
 | 
					import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
 | 
				
			||||||
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
 | 
					import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
 | 
				
			||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
					import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
				
			||||||
 | 
					import androidx.test.espresso.base.DefaultFailureHandler
 | 
				
			||||||
 | 
					import androidx.test.espresso.matcher.RootMatchers.isDialog
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isChecked
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isChecked
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
 | 
					import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.withId
 | 
					import androidx.test.espresso.matcher.ViewMatchers.withId
 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
 | 
					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.CoreMatchers.allOf
 | 
				
			||||||
 | 
					import org.hamcrest.CoreMatchers.not
 | 
				
			||||||
 | 
					import org.hamcrest.Matchers.hasToString
 | 
				
			||||||
 | 
					import org.junit.BeforeClass
 | 
				
			||||||
 | 
					import java.io.BufferedOutputStream
 | 
				
			||||||
 | 
					import java.io.File
 | 
				
			||||||
 | 
					import java.io.FileOutputStream
 | 
				
			||||||
 | 
					import java.io.IOException
 | 
				
			||||||
 | 
					import java.util.Locale
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// For now, do not move this as it is modified by the integration tests
 | 
				
			||||||
 | 
					val defaultUrl = "http://10.0.2.2:8888"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun performLogin(someUrl: String? = null) {
 | 
					fun performLogin(someUrl: String? = null) {
 | 
				
			||||||
 | 
					    Log.i("AUTOMATION", "The url used will be ${if (!someUrl.isNullOrEmpty()) someUrl else defaultUrl}")
 | 
				
			||||||
    onView(withId(R.id.urlView)).perform(click()).perform(
 | 
					    onView(withId(R.id.urlView)).perform(click()).perform(
 | 
				
			||||||
        typeTextIntoFocusedView(
 | 
					        typeTextIntoFocusedView(
 | 
				
			||||||
            if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888"
 | 
					            if (!someUrl.isNullOrEmpty()) someUrl else defaultUrl,
 | 
				
			||||||
        )
 | 
					        ),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    onView(withId(R.id.signInButton)).perform(click())
 | 
					    onView(withId(R.id.signInButton)).perform(click())
 | 
				
			||||||
    Thread.sleep(10000)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun loginAndInitHome() {
 | 
					fun changeAndCancelSetting(
 | 
				
			||||||
 | 
					    oldValue: String,
 | 
				
			||||||
    performLogin()
 | 
					    newValue: String,
 | 
				
			||||||
    onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
 | 
					    openSettingItem: () -> Unit,
 | 
				
			||||||
    onView(withText("OK")).perform(click())
 | 
					) {
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fun changeAndCancelSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) {
 | 
					 | 
				
			||||||
    openSettingItem()
 | 
					    openSettingItem()
 | 
				
			||||||
    onView(
 | 
					    onView(
 | 
				
			||||||
        withId(android.R.id.edit)
 | 
					        withId(android.R.id.edit),
 | 
				
			||||||
    ).perform(replaceText(newValue))
 | 
					    ).perform(replaceText(newValue))
 | 
				
			||||||
    onView(
 | 
					    onView(
 | 
				
			||||||
        withId(android.R.id.button2)
 | 
					        withId(android.R.id.button2),
 | 
				
			||||||
    ).perform(click())
 | 
					    ).perform(click())
 | 
				
			||||||
    openSettingItem()
 | 
					    openSettingItem()
 | 
				
			||||||
    onView(
 | 
					    onView(
 | 
				
			||||||
        withId(android.R.id.edit)
 | 
					        withId(android.R.id.edit),
 | 
				
			||||||
    ).check(matches(withText(oldValue)))
 | 
					    ).check(matches(withText(oldValue)))
 | 
				
			||||||
    onView(
 | 
					    onView(
 | 
				
			||||||
        withText(newValue)
 | 
					        withText(newValue),
 | 
				
			||||||
    ).check(doesNotExist())
 | 
					    ).check(doesNotExist())
 | 
				
			||||||
    onView(
 | 
					    onView(
 | 
				
			||||||
        withId(android.R.id.button2)
 | 
					        withId(android.R.id.button2),
 | 
				
			||||||
    ).perform(click())
 | 
					    ).perform(click())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun changeAndSaveSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) {
 | 
					fun changeAndSaveSetting(
 | 
				
			||||||
 | 
					    oldValue: String,
 | 
				
			||||||
 | 
					    newValue: String,
 | 
				
			||||||
 | 
					    openSettingItem: () -> Unit,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
    openSettingItem()
 | 
					    openSettingItem()
 | 
				
			||||||
    onView(
 | 
					    onView(
 | 
				
			||||||
        withId(android.R.id.edit)
 | 
					        withId(android.R.id.edit),
 | 
				
			||||||
    ).perform(replaceText(newValue))
 | 
					    ).perform(replaceText(newValue))
 | 
				
			||||||
    onView(
 | 
					    onView(
 | 
				
			||||||
        withId(android.R.id.button1)
 | 
					        withId(android.R.id.button1),
 | 
				
			||||||
    ).perform(click())
 | 
					    ).perform(click())
 | 
				
			||||||
    openSettingItem()
 | 
					    openSettingItem()
 | 
				
			||||||
    onView(
 | 
					    onView(
 | 
				
			||||||
        withId(android.R.id.edit)
 | 
					        withId(android.R.id.edit),
 | 
				
			||||||
    ).check(matches(withText(newValue)))
 | 
					    ).check(matches(withText(newValue)))
 | 
				
			||||||
    if (oldValue.isNotEmpty()) {
 | 
					    if (oldValue.isNotEmpty()) {
 | 
				
			||||||
        onView(
 | 
					        onView(
 | 
				
			||||||
            withText(oldValue)
 | 
					            withText(oldValue),
 | 
				
			||||||
        ).check(doesNotExist())
 | 
					        ).check(doesNotExist())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    onView(
 | 
					    onView(
 | 
				
			||||||
        withId(android.R.id.button2)
 | 
					        withId(android.R.id.button2),
 | 
				
			||||||
    ).perform(click())
 | 
					    ).perform(click())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun testPreferencesFromArray(
 | 
					fun testPreferencesFromArray(
 | 
				
			||||||
    context: Context,
 | 
					    context: Context,
 | 
				
			||||||
    @ArrayRes arrayRes: Int,
 | 
					    @ArrayRes arrayRes: Int,
 | 
				
			||||||
    openSettingItem: () -> Unit
 | 
					    openSettingItem: () -> Unit,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
    openSettingItem()
 | 
					    openSettingItem()
 | 
				
			||||||
    context.resources.getStringArray(arrayRes).forEach { res ->
 | 
					    context.resources.getStringArray(arrayRes).forEach { res ->
 | 
				
			||||||
@@ -87,4 +110,122 @@ fun testPreferencesFromArray(
 | 
				
			|||||||
        openSettingItem()
 | 
					        openSettingItem()
 | 
				
			||||||
        onView(withText(res)).check(matches(allOf(isDisplayed(), isChecked())))
 | 
					        onView(withText(res)).check(matches(allOf(isDisplayed(), isChecked())))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun goToSources() {
 | 
				
			||||||
 | 
					    openMenu()
 | 
				
			||||||
 | 
					    onView(withText(R.string.menu_home_sources))
 | 
				
			||||||
 | 
					        .perform(click())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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()))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun checkHomeLoadingDone() {
 | 
				
			||||||
 | 
					    onView(withId(R.id.swipeRefreshLayout)).inRoot(not(isDialog())).perform(waitUntilNotLoading(300000))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("detekt:UtilityClassWithPublicConstructor")
 | 
				
			||||||
 | 
					open class WithANRException {
 | 
				
			||||||
 | 
					    companion object {
 | 
				
			||||||
 | 
					        // 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:",
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        private val otherException = "System Ul isn't responding"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @JvmStatic
 | 
				
			||||||
 | 
					        @BeforeClass
 | 
				
			||||||
 | 
					        fun setUpHandler() {
 | 
				
			||||||
 | 
					            Espresso.setFailureHandler { error, viewMatcher ->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                takeScreenshot()
 | 
				
			||||||
 | 
					                if (error.message!!.contains(otherException)) {
 | 
				
			||||||
 | 
					                    handleAnrDialogue()
 | 
				
			||||||
 | 
					                } else if (error.message!!.contains(rootViewWithoutFocusExceptionMsg) &&
 | 
				
			||||||
 | 
					                    anrCount < 20
 | 
				
			||||||
 | 
					                ) {
 | 
				
			||||||
 | 
					                    anrCount++
 | 
				
			||||||
 | 
					                    handleAnrDialogue()
 | 
				
			||||||
 | 
					                } else { // chain all failures down to the default espresso handler
 | 
				
			||||||
 | 
					                    Log.e("AMINE", "AMINE : ${error.message}")
 | 
				
			||||||
 | 
					                    println("AMINE : ${error.message}")
 | 
				
			||||||
 | 
					                    DefaultFailureHandler(getInstrumentation().targetContext).handle(error, viewMatcher)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun takeScreenshot() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val folder =
 | 
				
			||||||
 | 
					            File(
 | 
				
			||||||
 | 
					                File(
 | 
				
			||||||
 | 
					                    getExternalStoragePublicDirectory(DIRECTORY_PICTURES),
 | 
				
			||||||
 | 
					                    "selfoss_tests",
 | 
				
			||||||
 | 
					                ).absolutePath,
 | 
				
			||||||
 | 
					                "screenshots",
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        if (!folder.exists()) {
 | 
				
			||||||
 | 
					            folder.mkdirs()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var out: BufferedOutputStream? = null
 | 
				
			||||||
 | 
					        val size = folder.list().size + 1
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            out = BufferedOutputStream(FileOutputStream(folder.path + "/" + size + ".png"))
 | 
				
			||||||
 | 
					            bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
 | 
				
			||||||
 | 
					            Log.d("Screenshots", "Screenshot taken")
 | 
				
			||||||
 | 
					        } catch (e: IOException) {
 | 
				
			||||||
 | 
					            Log.e("Screenshots", "Could not save the screenshot", e)
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            if (out != null) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    out.close()
 | 
				
			||||||
 | 
					                } catch (e: IOException) {
 | 
				
			||||||
 | 
					                    Log.e("Screenshots", "Could not save the screenshot", e)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } catch (ex: IOException) {
 | 
				
			||||||
 | 
					        Log.e("Screenshots", "Could not take the screenshot", ex)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,160 @@
 | 
				
			|||||||
 | 
					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.core.view.isVisible
 | 
				
			||||||
 | 
					import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
 | 
				
			||||||
 | 
					import androidx.test.core.app.ApplicationProvider
 | 
				
			||||||
 | 
					import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
 | 
				
			||||||
 | 
					import androidx.test.espresso.PerformException
 | 
				
			||||||
 | 
					import androidx.test.espresso.Root
 | 
				
			||||||
 | 
					import androidx.test.espresso.UiController
 | 
				
			||||||
 | 
					import androidx.test.espresso.ViewAction
 | 
				
			||||||
 | 
					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 androidx.test.espresso.util.HumanReadables
 | 
				
			||||||
 | 
					import androidx.test.espresso.util.TreeIterables
 | 
				
			||||||
 | 
					import org.hamcrest.CoreMatchers.allOf
 | 
				
			||||||
 | 
					import org.hamcrest.CoreMatchers.any
 | 
				
			||||||
 | 
					import org.hamcrest.Description
 | 
				
			||||||
 | 
					import org.hamcrest.Matcher
 | 
				
			||||||
 | 
					import org.hamcrest.Matchers
 | 
				
			||||||
 | 
					import org.hamcrest.TypeSafeMatcher
 | 
				
			||||||
 | 
					import java.util.concurrent.TimeoutException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 waitUntilNotLoading(millis: Long): ViewAction {
 | 
				
			||||||
 | 
					    return object : ViewAction {
 | 
				
			||||||
 | 
					        override fun getConstraints(): Matcher<View> = any(View::class.java)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        override fun getDescription(): String = "wait for a specific view is not loading during $millis millis."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        override fun perform(
 | 
				
			||||||
 | 
					            uiController: UiController,
 | 
				
			||||||
 | 
					            view: View?,
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            uiController.loopMainThreadUntilIdle()
 | 
				
			||||||
 | 
					            val startTime = System.currentTimeMillis()
 | 
				
			||||||
 | 
					            val endTime = startTime + millis
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            do {
 | 
				
			||||||
 | 
					                // either the empty view is displayed
 | 
				
			||||||
 | 
					                for (child in TreeIterables.breadthFirstViewTraversal(view)) {
 | 
				
			||||||
 | 
					                    // found view with required ID
 | 
				
			||||||
 | 
					                    if (withId(R.id.emptyText).matches(child) && child.isVisible) {
 | 
				
			||||||
 | 
					                        return
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // or the refresh layout is refreshing
 | 
				
			||||||
 | 
					                if (view is SwipeRefreshLayout && !view.isRefreshing) {
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                uiController.loopMainThreadForAtLeast(100)
 | 
				
			||||||
 | 
					            } while (System.currentTimeMillis() < endTime)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // timeout happens
 | 
				
			||||||
 | 
					            throw PerformException
 | 
				
			||||||
 | 
					                .Builder()
 | 
				
			||||||
 | 
					                .withActionDescription(this.description)
 | 
				
			||||||
 | 
					                .withViewDescription(HumanReadables.describe(view))
 | 
				
			||||||
 | 
					                .withCause(TimeoutException())
 | 
				
			||||||
 | 
					                .build()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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>(),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,65 +0,0 @@
 | 
				
			|||||||
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.action.ViewActions.scrollCompletelyTo
 | 
					 | 
				
			||||||
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
 | 
					 | 
				
			||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
					 | 
				
			||||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 | 
					 | 
				
			||||||
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.Before
 | 
					 | 
				
			||||||
import org.junit.Rule
 | 
					 | 
				
			||||||
import org.junit.Test
 | 
					 | 
				
			||||||
import org.junit.runner.RunWith
 | 
					 | 
				
			||||||
import java.util.UUID
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@RunWith(AndroidJUnit4::class)
 | 
					 | 
				
			||||||
@LargeTest
 | 
					 | 
				
			||||||
class SourcesActivityTest {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @get:Rule
 | 
					 | 
				
			||||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    lateinit var sourceName: String
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Before
 | 
					 | 
				
			||||||
    fun init() {
 | 
					 | 
				
			||||||
        sourceName = UUID.randomUUID().toString().substring(0, 15)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        loginAndInitHome()
 | 
					 | 
				
			||||||
        openActionBarOverflowOrOptionsMenu(
 | 
					 | 
				
			||||||
            ApplicationProvider.getApplicationContext<Context>()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        onView(withText(R.string.menu_home_sources))
 | 
					 | 
				
			||||||
            .perform(click())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Test
 | 
					 | 
				
			||||||
    fun addSource() {
 | 
					 | 
				
			||||||
        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("https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10"))
 | 
					 | 
				
			||||||
        onView(withId(R.id.tags))
 | 
					 | 
				
			||||||
            .perform(click()).perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
 | 
					 | 
				
			||||||
        onView(withId(R.id.spoutsSpinner))
 | 
					 | 
				
			||||||
            .perform(click())
 | 
					 | 
				
			||||||
        onView(withText("RSS Feed"))
 | 
					 | 
				
			||||||
            .perform(scrollCompletelyTo())
 | 
					 | 
				
			||||||
            .perform(click())
 | 
					 | 
				
			||||||
        onView(withId(R.id.saveBtn))
 | 
					 | 
				
			||||||
            .perform(click())
 | 
					 | 
				
			||||||
        onView(withText(sourceName)).check(matches(isDisplayed()))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,101 +0,0 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.android
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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.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) {
 | 
					 | 
				
			||||||
                return false
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            val context = view.context
 | 
					 | 
				
			||||||
            if (view !is EditText) {
 | 
					 | 
				
			||||||
                return false
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (view.error == null) {
 | 
					 | 
				
			||||||
                return false
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return view.error.toString() == context.getString(id)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        override fun describeTo(description: Description?) {
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fun isPopupWindow(): Matcher<Root> {
 | 
					 | 
				
			||||||
    return 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")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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>? {
 | 
					 | 
				
			||||||
    return allOf(
 | 
					 | 
				
			||||||
        withResourceName("fixed_bottom_navigation_icon"),
 | 
					 | 
				
			||||||
        withParent(
 | 
					 | 
				
			||||||
            allOf(
 | 
					 | 
				
			||||||
                withResourceName("fixed_bottom_navigation_icon_container"),
 | 
					 | 
				
			||||||
                hasSibling(withText(id))
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fun withSettingsCheckboxWidget(@StringRes id: Int): Matcher<View>? {
 | 
					 | 
				
			||||||
    return allOf(
 | 
					 | 
				
			||||||
        withId(android.R.id.switch_widget),
 | 
					 | 
				
			||||||
        withParent(
 | 
					 | 
				
			||||||
            withSettingsCheckboxFrame(id)
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fun withSettingsCheckboxFrame(@StringRes id: Int): Matcher<View>? {
 | 
					 | 
				
			||||||
    return allOf(
 | 
					 | 
				
			||||||
        withId(android.R.id.widget_frame),
 | 
					 | 
				
			||||||
        hasSibling(
 | 
					 | 
				
			||||||
            allOf(
 | 
					 | 
				
			||||||
                withClassName(Matchers.equalTo(RelativeLayout::class.java.name)),
 | 
					 | 
				
			||||||
                withChild(
 | 
					 | 
				
			||||||
                    withText(id)
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										87
									
								
								androidApp/src/debug/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								androidApp/src/debug/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    xmlns:tools="http://schemas.android.com/tools">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="android.permission.INTERNET" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <application
 | 
				
			||||||
 | 
					        android:name=".MyApp"
 | 
				
			||||||
 | 
					        android:allowBackup="false"
 | 
				
			||||||
 | 
					        android:configChanges="uiMode"
 | 
				
			||||||
 | 
					        android:dataExtractionRules="@xml/data_extraction_rules"
 | 
				
			||||||
 | 
					        android:fullBackupContent="false"
 | 
				
			||||||
 | 
					        android:icon="@mipmap/ic_launcher"
 | 
				
			||||||
 | 
					        android:label="@string/app_name"
 | 
				
			||||||
 | 
					        android:networkSecurityConfig="@xml/network_security_config"
 | 
				
			||||||
 | 
					        android:requestLegacyExternalStorage="true"
 | 
				
			||||||
 | 
					        android:supportsRtl="true"
 | 
				
			||||||
 | 
					        android:theme="@style/NoBar"
 | 
				
			||||||
 | 
					        tools:replace="android:allowBackup">
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".MainActivity"
 | 
				
			||||||
 | 
					            android:exported="true"
 | 
				
			||||||
 | 
					            android:theme="@style/SplashTheme">
 | 
				
			||||||
 | 
					            <intent-filter>
 | 
				
			||||||
 | 
					                <action android:name="android.intent.action.MAIN" />
 | 
				
			||||||
 | 
					                <category android:name="android.intent.category.LAUNCHER" />
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <meta-data
 | 
				
			||||||
 | 
					                android:name="android.app.shortcuts"
 | 
				
			||||||
 | 
					                android:resource="@xml/shortcuts" />
 | 
				
			||||||
 | 
					        </activity>
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".LoginActivity"
 | 
				
			||||||
 | 
					            android:label="@string/title_activity_login"></activity>
 | 
				
			||||||
 | 
					        <activity android:name=".HomeActivity"></activity>
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".settings.SettingsActivity"
 | 
				
			||||||
 | 
					            android:label="@string/title_activity_settings"
 | 
				
			||||||
 | 
					            android:parentActivityName=".HomeActivity">
 | 
				
			||||||
 | 
					            <meta-data
 | 
				
			||||||
 | 
					                android:name="android.support.PARENT_ACTIVITY"
 | 
				
			||||||
 | 
					                android:value=".HomeActivity" />
 | 
				
			||||||
 | 
					        </activity>
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".SourcesActivity"
 | 
				
			||||||
 | 
					            android:parentActivityName=".HomeActivity">
 | 
				
			||||||
 | 
					            <meta-data
 | 
				
			||||||
 | 
					                android:name="android.support.PARENT_ACTIVITY"
 | 
				
			||||||
 | 
					                android:value=".HomeActivity" />
 | 
				
			||||||
 | 
					        </activity>
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".UpsertSourceActivity"
 | 
				
			||||||
 | 
					            android:exported="true"
 | 
				
			||||||
 | 
					            android:parentActivityName=".SourcesActivity">
 | 
				
			||||||
 | 
					            <meta-data
 | 
				
			||||||
 | 
					                android:name="android.support.PARENT_ACTIVITY"
 | 
				
			||||||
 | 
					                android:value=".SourcesActivity" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <intent-filter>
 | 
				
			||||||
 | 
					                <action android:name="android.intent.action.SEND" />
 | 
				
			||||||
 | 
					                <category android:name="android.intent.category.DEFAULT" />
 | 
				
			||||||
 | 
					                <data android:mimeType="text/plain" />
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					        </activity>
 | 
				
			||||||
 | 
					        <activity android:name=".ReaderActivity"></activity>
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".ImageActivity"
 | 
				
			||||||
 | 
					            android:theme="@style/Theme.AppCompat.ImageActivity"></activity>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <meta-data
 | 
				
			||||||
 | 
					            android:name="android.webkit.WebView.MetricsOptOut"
 | 
				
			||||||
 | 
					            android:value="true" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <meta-data
 | 
				
			||||||
 | 
					            android:name="android.webkit.WebView.EnableSafeBrowsing"
 | 
				
			||||||
 | 
					            android:value="true" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <meta-data
 | 
				
			||||||
 | 
					            android:name="android.max_aspect"
 | 
				
			||||||
 | 
					            android:value="2.1" />
 | 
				
			||||||
 | 
					    </application>
 | 
				
			||||||
 | 
					</manifest>
 | 
				
			||||||
@@ -31,7 +31,7 @@ import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
 | 
				
			|||||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
 | 
					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.openUrlInBrowser
 | 
					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
 | 
				
			||||||
@@ -49,7 +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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware {
 | 
					private const val MIN_WIDTH_CARD_DP = 300
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
@@ -99,7 +104,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if (appSettingsService.isItemCachingEnabled()) {
 | 
					        if (appSettingsService.isItemCachingEnabled()) {
 | 
				
			||||||
            CountingIdlingResourceSingleton.increment()
 | 
					            CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
            CoroutineScope(Dispatchers.Main).launch {
 | 
					            CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
                repository.tryToCacheItemsAndGetNewOnes()
 | 
					                repository.tryToCacheItemsAndGetNewOnes()
 | 
				
			||||||
                CountingIdlingResourceSingleton.decrement()
 | 
					                CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -115,12 +120,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
        binding.swipeRefreshLayout.setOnRefreshListener {
 | 
					        binding.swipeRefreshLayout.setOnRefreshListener {
 | 
				
			||||||
            repository.offlineOverride = false
 | 
					            repository.offlineOverride = false
 | 
				
			||||||
            lastFetchDone = false
 | 
					            lastFetchDone = false
 | 
				
			||||||
            CountingIdlingResourceSingleton.increment()
 | 
					            getElementsAccordingToTab()
 | 
				
			||||||
            CoroutineScope(Dispatchers.Main).launch {
 | 
					            binding.swipeRefreshLayout.isRefreshing = false
 | 
				
			||||||
                getElementsAccordingToTab()
 | 
					 | 
				
			||||||
                binding.swipeRefreshLayout.isRefreshing = false
 | 
					 | 
				
			||||||
                CountingIdlingResourceSingleton.decrement()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val swipeDirs =
 | 
					        val swipeDirs =
 | 
				
			||||||
@@ -171,11 +172,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()
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -196,19 +198,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)
 | 
				
			||||||
 | 
					                .hide(false)
 | 
				
			||||||
        tabArchiveBadge =
 | 
					        tabArchiveBadge =
 | 
				
			||||||
            TextBadgeItem()
 | 
					            TextBadgeItem()
 | 
				
			||||||
                .setText("")
 | 
					                .setText("")
 | 
				
			||||||
                .setHideOnSelect(false).hide(false)
 | 
					                .setHideOnSelect(false)
 | 
				
			||||||
 | 
					                .hide(false)
 | 
				
			||||||
        tabStarredBadge =
 | 
					        tabStarredBadge =
 | 
				
			||||||
            TextBadgeItem()
 | 
					            TextBadgeItem()
 | 
				
			||||||
                .setText("")
 | 
					                .setText("")
 | 
				
			||||||
                .setHideOnSelect(false).hide(false)
 | 
					                .setHideOnSelect(false)
 | 
				
			||||||
 | 
					                .hide(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (appSettingsService.isDisplayUnreadCountEnabled()) {
 | 
					        if (appSettingsService.isDisplayUnreadCountEnabled()) {
 | 
				
			||||||
            lifecycleScope.launch {
 | 
					            lifecycleScope.launch {
 | 
				
			||||||
@@ -236,14 +242,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
            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,
 | 
				
			||||||
@@ -277,11 +281,11 @@ 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()
 | 
					        CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
					        CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
            repository.handleDBActions()
 | 
					            repository.handleDBActions()
 | 
				
			||||||
            CountingIdlingResourceSingleton.decrement()
 | 
					            CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -289,10 +293,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
        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))
 | 
				
			||||||
@@ -309,50 +313,44 @@ 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 =
 | 
					                    staggererdGridLayoutManager()
 | 
				
			||||||
                        StaggeredGridLayoutManager(
 | 
					 | 
				
			||||||
                            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 =
 | 
					                        staggererdGridLayoutManager()
 | 
				
			||||||
                            StaggeredGridLayoutManager(
 | 
					 | 
				
			||||||
                                calculateNoOfColumns(),
 | 
					 | 
				
			||||||
                                StaggeredGridLayoutManager.VERTICAL,
 | 
					 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                        layoutManager.gapStrategy =
 | 
					 | 
				
			||||||
                            StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
 | 
					 | 
				
			||||||
                        binding.recyclerView.layoutManager = layoutManager
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -425,17 +423,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
        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 ->
 | 
					            is StaggeredGridLayoutManager ->
 | 
				
			||||||
                manager.findLastCompletelyVisibleItemPositions(
 | 
					                manager
 | 
				
			||||||
                    null,
 | 
					                    .findLastCompletelyVisibleItemPositions(
 | 
				
			||||||
                ).last()
 | 
					                        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()) {
 | 
				
			||||||
@@ -461,8 +459,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
        itemType: ItemType,
 | 
					        itemType: ItemType,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        CountingIdlingResourceSingleton.increment()
 | 
					        CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
					        binding.swipeRefreshLayout.isRefreshing = true
 | 
				
			||||||
            binding.swipeRefreshLayout.isRefreshing = true
 | 
					        CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
            repository.displayedItems = itemType
 | 
					            repository.displayedItems = itemType
 | 
				
			||||||
            items =
 | 
					            items =
 | 
				
			||||||
                if (appendResults) {
 | 
					                if (appendResults) {
 | 
				
			||||||
@@ -470,15 +468,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    repository.getNewerItems()
 | 
					                    repository.getNewerItems()
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            binding.swipeRefreshLayout.isRefreshing = false
 | 
					            CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
            handleListResult()
 | 
					            launch(Dispatchers.Main) {
 | 
				
			||||||
 | 
					                binding.swipeRefreshLayout.isRefreshing = false
 | 
				
			||||||
 | 
					                handleListResult()
 | 
				
			||||||
 | 
					                CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            CountingIdlingResourceSingleton.decrement()
 | 
					            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 ->
 | 
				
			||||||
@@ -491,7 +493,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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(
 | 
				
			||||||
@@ -538,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 {
 | 
				
			||||||
@@ -577,7 +585,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
        messageRes: Int,
 | 
					        messageRes: Int,
 | 
				
			||||||
        doFn: () -> Unit,
 | 
					        doFn: () -> Unit,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        AlertDialog.Builder(this@HomeActivity)
 | 
					        AlertDialog
 | 
				
			||||||
 | 
					            .Builder(this@HomeActivity)
 | 
				
			||||||
            .setMessage(messageRes)
 | 
					            .setMessage(messageRes)
 | 
				
			||||||
            .setTitle(titleRes)
 | 
					            .setTitle(titleRes)
 | 
				
			||||||
            .setPositiveButton(android.R.string.ok) { _, _ -> doFn() }
 | 
					            .setPositiveButton(android.R.string.ok) { _, _ -> doFn() }
 | 
				
			||||||
@@ -586,10 +595,11 @@ 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 -> {
 | 
					            R.id.issue_tracker -> {
 | 
				
			||||||
                baseContext.openUrlInBrowser(AppSettingsService.trackerUrl)
 | 
					                baseContext.openUrlInBrowserAsNewTask(AppSettingsService.BUG_URL)
 | 
				
			||||||
                return true
 | 
					                return true
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -603,21 +613,26 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
                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()
 | 
					                    CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
                    CoroutineScope(Dispatchers.Main).launch {
 | 
					                    CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
                        val updatedRemote = repository.updateRemote()
 | 
					                        val updatedRemote = repository.updateRemote()
 | 
				
			||||||
                        if (updatedRemote) {
 | 
					                        CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
                            Toast.makeText(
 | 
					                        launch(Dispatchers.Main) {
 | 
				
			||||||
                                this@HomeActivity,
 | 
					                            if (updatedRemote) {
 | 
				
			||||||
                                R.string.refresh_success_response,
 | 
					                                Toast
 | 
				
			||||||
                                Toast.LENGTH_LONG,
 | 
					                                    .makeText(
 | 
				
			||||||
                            )
 | 
					                                        this@HomeActivity,
 | 
				
			||||||
                                .show()
 | 
					                                        R.string.refresh_success_response,
 | 
				
			||||||
                        } else {
 | 
					                                        Toast.LENGTH_LONG,
 | 
				
			||||||
                            Toast.makeText(
 | 
					                                    ).show()
 | 
				
			||||||
                                this@HomeActivity,
 | 
					                            } else {
 | 
				
			||||||
                                R.string.refresh_failer_message,
 | 
					                                Toast
 | 
				
			||||||
                                Toast.LENGTH_SHORT,
 | 
					                                    .makeText(
 | 
				
			||||||
                            ).show()
 | 
					                                        this@HomeActivity,
 | 
				
			||||||
 | 
					                                        R.string.refresh_failer_message,
 | 
				
			||||||
 | 
					                                        Toast.LENGTH_SHORT,
 | 
				
			||||||
 | 
					                                    ).show()
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        CountingIdlingResourceSingleton.decrement()
 | 
					                        CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
@@ -628,30 +643,34 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
            R.id.readAll -> {
 | 
					            R.id.readAll -> {
 | 
				
			||||||
                if (elementsShown == ItemType.UNREAD) {
 | 
					                if (elementsShown == ItemType.UNREAD) {
 | 
				
			||||||
                    needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
 | 
					                    needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
 | 
				
			||||||
                        binding.swipeRefreshLayout.isRefreshing = true
 | 
					 | 
				
			||||||
                        CountingIdlingResourceSingleton.increment()
 | 
					                        CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
                        CoroutineScope(Dispatchers.Main).launch {
 | 
					                        binding.swipeRefreshLayout.isRefreshing = true
 | 
				
			||||||
 | 
					                        CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
                            val success = repository.markAllAsRead(items)
 | 
					                            val success = repository.markAllAsRead(items)
 | 
				
			||||||
                            if (success) {
 | 
					                            CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
                                Toast.makeText(
 | 
					                            launch(Dispatchers.Main) {
 | 
				
			||||||
                                    this@HomeActivity,
 | 
					                                if (success) {
 | 
				
			||||||
                                    R.string.all_posts_read,
 | 
					                                    Toast
 | 
				
			||||||
                                    Toast.LENGTH_SHORT,
 | 
					                                        .makeText(
 | 
				
			||||||
                                ).show()
 | 
					                                            this@HomeActivity,
 | 
				
			||||||
                                tabNewBadge.removeBadge()
 | 
					                                            R.string.all_posts_read,
 | 
				
			||||||
 | 
					                                            Toast.LENGTH_SHORT,
 | 
				
			||||||
 | 
					                                        ).show()
 | 
				
			||||||
 | 
					                                    tabNewBadge.removeBadge()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                getElementsAccordingToTab()
 | 
					                                    getElementsAccordingToTab()
 | 
				
			||||||
                            } else {
 | 
					                                } else {
 | 
				
			||||||
                                Toast.makeText(
 | 
					                                    Toast
 | 
				
			||||||
                                    this@HomeActivity,
 | 
					                                        .makeText(
 | 
				
			||||||
                                    R.string.all_posts_not_read,
 | 
					                                            this@HomeActivity,
 | 
				
			||||||
                                    Toast.LENGTH_SHORT,
 | 
					                                            R.string.all_posts_not_read,
 | 
				
			||||||
                                ).show()
 | 
					                                            Toast.LENGTH_SHORT,
 | 
				
			||||||
 | 
					                                        ).show()
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                binding.swipeRefreshLayout.isRefreshing = false
 | 
				
			||||||
 | 
					                                CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            handleListResult()
 | 
					 | 
				
			||||||
                            binding.swipeRefreshLayout.isRefreshing = false
 | 
					 | 
				
			||||||
                            CountingIdlingResourceSingleton.decrement()
 | 
					                            CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@@ -661,7 +680,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
            R.id.action_disconnect -> {
 | 
					            R.id.action_disconnect -> {
 | 
				
			||||||
                needsConfirmation(
 | 
					                needsConfirmation(
 | 
				
			||||||
                    R.string.confirm_disconnect_title,
 | 
					                    R.string.confirm_disconnect_title,
 | 
				
			||||||
                    R.string.confirm_disconnect_description
 | 
					                    R.string.confirm_disconnect_description,
 | 
				
			||||||
                ) {
 | 
					                ) {
 | 
				
			||||||
                    runBlocking {
 | 
					                    runBlocking {
 | 
				
			||||||
                        repository.logout()
 | 
					                        repository.logout()
 | 
				
			||||||
@@ -702,7 +721,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
    private fun handleRecurringTask() {
 | 
					    private fun handleRecurringTask() {
 | 
				
			||||||
        if (appSettingsService.isPeriodicRefreshEnabled()) {
 | 
					        if (appSettingsService.isPeriodicRefreshEnabled()) {
 | 
				
			||||||
            val myConstraints =
 | 
					            val myConstraints =
 | 
				
			||||||
                Constraints.Builder()
 | 
					                Constraints
 | 
				
			||||||
 | 
					                    .Builder()
 | 
				
			||||||
                    .setRequiresBatteryNotLow(true)
 | 
					                    .setRequiresBatteryNotLow(true)
 | 
				
			||||||
                    .setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
 | 
					                    .setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
 | 
				
			||||||
                    .setRequiresStorageNotLow(true)
 | 
					                    .setRequiresStorageNotLow(true)
 | 
				
			||||||
@@ -711,19 +731,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
				
			|||||||
            val backgroundWork =
 | 
					            val backgroundWork =
 | 
				
			||||||
                PeriodicWorkRequestBuilder<LoadingWorker>(
 | 
					                PeriodicWorkRequestBuilder<LoadingWorker>(
 | 
				
			||||||
                    appSettingsService.getRefreshMinutes(),
 | 
					                    appSettingsService.getRefreshMinutes(),
 | 
				
			||||||
                    TimeUnit.MINUTES
 | 
					                    TimeUnit.MINUTES,
 | 
				
			||||||
                )
 | 
					                ).setConstraints(myConstraints)
 | 
				
			||||||
                    .setConstraints(myConstraints)
 | 
					 | 
				
			||||||
                    .addTag("selfoss-loading")
 | 
					                    .addTag("selfoss-loading")
 | 
				
			||||||
                    .build()
 | 
					                    .build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            WorkManager.getInstance(
 | 
					            WorkManager
 | 
				
			||||||
                baseContext,
 | 
					                .getInstance(
 | 
				
			||||||
            ).enqueueUniquePeriodicWork(
 | 
					                    baseContext,
 | 
				
			||||||
                "selfoss-loading",
 | 
					                ).enqueueUniquePeriodicWork(
 | 
				
			||||||
                ExistingPeriodicWorkPolicy.KEEP,
 | 
					                    "selfoss-loading",
 | 
				
			||||||
                backgroundWork
 | 
					                    ExistingPeriodicWorkPolicy.KEEP,
 | 
				
			||||||
            )
 | 
					                    backgroundWork,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -84,7 +84,9 @@ class ImageActivity : AppCompatActivity() {
 | 
				
			|||||||
        return super.onOptionsItemSelected(item)
 | 
					        return super.onOptionsItemSelected(item)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
 | 
					    private inner class ScreenSlidePagerAdapter(
 | 
				
			||||||
 | 
					        fa: FragmentActivity,
 | 
				
			||||||
 | 
					    ) : FragmentStateAdapter(fa) {
 | 
				
			||||||
        override fun getItemCount(): Int = allImages.size
 | 
					        override fun getItemCount(): Int = allImages.size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])
 | 
					        override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,11 @@ import org.kodein.di.DIAware
 | 
				
			|||||||
import org.kodein.di.android.closestDI
 | 
					import org.kodein.di.android.closestDI
 | 
				
			||||||
import org.kodein.di.instance
 | 
					import org.kodein.di.instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LoginActivity : AppCompatActivity(), DIAware {
 | 
					private const val MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED = 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LoginActivity :
 | 
				
			||||||
 | 
					    AppCompatActivity(),
 | 
				
			||||||
 | 
					    DIAware {
 | 
				
			||||||
    private var inValidCount: Int = 0
 | 
					    private var inValidCount: Int = 0
 | 
				
			||||||
    private var isWithLogin = false
 | 
					    private var isWithLogin = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -104,11 +108,11 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private fun goToMain() {
 | 
					    private fun goToMain() {
 | 
				
			||||||
        CountingIdlingResourceSingleton.increment()
 | 
					        CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
					        CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
            repository.updateApiInformation()
 | 
					            repository.updateApiInformation()
 | 
				
			||||||
            ACRA.errorReporter.putCustomData(
 | 
					            ACRA.errorReporter.putCustomData(
 | 
				
			||||||
                "SELFOSS_API_VERSION",
 | 
					                "SELFOSS_API_VERSION",
 | 
				
			||||||
                appSettingsService.getApiVersion().toString()
 | 
					                appSettingsService.getApiVersion().toString(),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            CountingIdlingResourceSingleton.decrement()
 | 
					            CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -123,6 +127,9 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
        binding.urlView.error = getString(R.string.wrong_infos)
 | 
					        binding.urlView.error = getString(R.string.wrong_infos)
 | 
				
			||||||
        binding.loginView.error = getString(R.string.wrong_infos)
 | 
					        binding.loginView.error = getString(R.string.wrong_infos)
 | 
				
			||||||
        binding.passwordView.error = getString(R.string.wrong_infos)
 | 
					        binding.passwordView.error = getString(R.string.wrong_infos)
 | 
				
			||||||
 | 
					        binding.urlView.requestFocus()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        showProgress(false)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun attemptLogin() {
 | 
					    private fun attemptLogin() {
 | 
				
			||||||
@@ -132,13 +139,23 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
        binding.passwordView.error = null
 | 
					        binding.passwordView.error = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Store values at the time of the login attempt.
 | 
					        // Store values at the time of the login attempt.
 | 
				
			||||||
        val url = binding.urlView.text.toString().trim()
 | 
					        val url =
 | 
				
			||||||
        val login = binding.loginView.text.toString().trim()
 | 
					            binding.urlView.text
 | 
				
			||||||
        val password = binding.passwordView.text.toString().trim()
 | 
					                .toString()
 | 
				
			||||||
 | 
					                .trim()
 | 
				
			||||||
        failInvalidUrl(url)
 | 
					        val login =
 | 
				
			||||||
        failLoginDetails(password, login)
 | 
					            binding.loginView.text
 | 
				
			||||||
 | 
					                .toString()
 | 
				
			||||||
 | 
					                .trim()
 | 
				
			||||||
 | 
					        val password =
 | 
				
			||||||
 | 
					            binding.passwordView.text
 | 
				
			||||||
 | 
					                .toString()
 | 
				
			||||||
 | 
					                .trim()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val cancelUrl = failInvalidUrl(url)
 | 
				
			||||||
 | 
					        if (cancelUrl) return
 | 
				
			||||||
 | 
					        val cancelDetails = failLoginDetails(password, login)
 | 
				
			||||||
 | 
					        if (cancelDetails) return
 | 
				
			||||||
        showProgress(true)
 | 
					        showProgress(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        appSettingsService.updateSelfSigned(binding.selfSigned.isChecked)
 | 
					        appSettingsService.updateSelfSigned(binding.selfSigned.isChecked)
 | 
				
			||||||
@@ -146,40 +163,48 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
        repository.refreshLoginInformation(url, login, password)
 | 
					        repository.refreshLoginInformation(url, login, password)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        CountingIdlingResourceSingleton.increment()
 | 
					        CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
					        CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                repository.updateApiInformation()
 | 
					                repository.updateApiInformation()
 | 
				
			||||||
 | 
					                val result = repository.login()
 | 
				
			||||||
 | 
					                CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
 | 
					                launch(Dispatchers.Main) {
 | 
				
			||||||
 | 
					                    if (result) {
 | 
				
			||||||
 | 
					                        val errorFetching = repository.checkIfFetchFails()
 | 
				
			||||||
 | 
					                        if (!errorFetching) {
 | 
				
			||||||
 | 
					                            goToMain()
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            preferenceError()
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        preferenceError()
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            } catch (e: Exception) {
 | 
					            } catch (e: Exception) {
 | 
				
			||||||
                if (e.message?.startsWith("No transformation found") == true) {
 | 
					                CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
                    Toast.makeText(
 | 
					                launch(Dispatchers.Main) {
 | 
				
			||||||
                        applicationContext,
 | 
					                    if (e.message?.startsWith("No transformation found") == true) {
 | 
				
			||||||
                        R.string.application_selfoss_only,
 | 
					                        Toast
 | 
				
			||||||
                        Toast.LENGTH_LONG,
 | 
					                            .makeText(
 | 
				
			||||||
                    ).show()
 | 
					                                applicationContext,
 | 
				
			||||||
                    preferenceError()
 | 
					                                R.string.application_selfoss_only,
 | 
				
			||||||
                    showProgress(false)
 | 
					                                Toast.LENGTH_LONG,
 | 
				
			||||||
 | 
					                            ).show()
 | 
				
			||||||
 | 
					                        preferenceError()
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					            } finally {
 | 
				
			||||||
 | 
					                CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            val result = repository.login()
 | 
					 | 
				
			||||||
            if (result) {
 | 
					 | 
				
			||||||
                val errorFetching = repository.checkIfFetchFails()
 | 
					 | 
				
			||||||
                if (!errorFetching) {
 | 
					 | 
				
			||||||
                    goToMain()
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    preferenceError()
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                preferenceError()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            showProgress(false)
 | 
					 | 
				
			||||||
            CountingIdlingResourceSingleton.decrement()
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun failLoginDetails(
 | 
					    private fun failLoginDetails(
 | 
				
			||||||
        password: String,
 | 
					        password: String,
 | 
				
			||||||
        login: String,
 | 
					        login: String,
 | 
				
			||||||
    ) {
 | 
					    ): Boolean {
 | 
				
			||||||
        var lastFocusedView: View? = null
 | 
					        var lastFocusedView: View? = null
 | 
				
			||||||
        var cancel = false
 | 
					        var cancel = false
 | 
				
			||||||
        if (isWithLogin) {
 | 
					        if (isWithLogin) {
 | 
				
			||||||
@@ -196,16 +221,17 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        maybeCancelAndFocusView(cancel, lastFocusedView)
 | 
					        maybeCancelAndFocusView(cancel, lastFocusedView)
 | 
				
			||||||
 | 
					        return cancel
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun failInvalidUrl(url: String) {
 | 
					    private fun failInvalidUrl(url: String): Boolean {
 | 
				
			||||||
        val focusView = binding.urlView
 | 
					        val focusView = binding.urlView
 | 
				
			||||||
        var cancel = false
 | 
					        var cancel = false
 | 
				
			||||||
        if (url.isBaseUrlInvalid()) {
 | 
					        if (url.isBaseUrlInvalid()) {
 | 
				
			||||||
            cancel = true
 | 
					            cancel = true
 | 
				
			||||||
            binding.urlView.error = getString(R.string.login_url_problem)
 | 
					            binding.urlView.error = getString(R.string.login_url_problem)
 | 
				
			||||||
            inValidCount++
 | 
					            inValidCount++
 | 
				
			||||||
            if (inValidCount == 3) {
 | 
					            if (inValidCount == MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED) {
 | 
				
			||||||
                val alertDialog = AlertDialog.Builder(this).create()
 | 
					                val alertDialog = AlertDialog.Builder(this).create()
 | 
				
			||||||
                alertDialog.setTitle(getString(R.string.warning_wrong_url))
 | 
					                alertDialog.setTitle(getString(R.string.warning_wrong_url))
 | 
				
			||||||
                alertDialog.setMessage(getString(R.string.text_wrong_url))
 | 
					                alertDialog.setMessage(getString(R.string.text_wrong_url))
 | 
				
			||||||
@@ -218,6 +244,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        maybeCancelAndFocusView(cancel, focusView)
 | 
					        maybeCancelAndFocusView(cancel, focusView)
 | 
				
			||||||
 | 
					        return cancel
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun maybeCancelAndFocusView(
 | 
					    private fun maybeCancelAndFocusView(
 | 
				
			||||||
@@ -270,7 +297,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
        return when (item.itemId) {
 | 
					        return when (item.itemId) {
 | 
				
			||||||
            R.id.issue_tracker -> {
 | 
					            R.id.issue_tracker -> {
 | 
				
			||||||
                val browserIntent =
 | 
					                val browserIntent =
 | 
				
			||||||
                    Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
 | 
					                    Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.BUG_URL))
 | 
				
			||||||
                startActivity(browserIntent)
 | 
					                startActivity(browserIntent)
 | 
				
			||||||
                return true
 | 
					                return true
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -280,9 +307,9 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
                    .withAboutIconShown(true)
 | 
					                    .withAboutIconShown(true)
 | 
				
			||||||
                    .withAboutVersionShown(true)
 | 
					                    .withAboutVersionShown(true)
 | 
				
			||||||
                    .withAboutSpecial2("Bug reports")
 | 
					                    .withAboutSpecial2("Bug reports")
 | 
				
			||||||
                    .withAboutSpecial2Description(AppSettingsService.trackerUrl)
 | 
					                    .withAboutSpecial2Description(AppSettingsService.BUG_URL)
 | 
				
			||||||
                    .withAboutSpecial1("Project Page")
 | 
					                    .withAboutSpecial1("Project Page")
 | 
				
			||||||
                    .withAboutSpecial1Description(AppSettingsService.sourceUrl)
 | 
					                    .withAboutSpecial1Description(AppSettingsService.SOURCE_URL)
 | 
				
			||||||
                    .start(this)
 | 
					                    .start(this)
 | 
				
			||||||
                true
 | 
					                true
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -290,4 +317,4 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
            else -> super.onOptionsItemSelected(item)
 | 
					            else -> super.onOptionsItemSelected(item)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,19 +9,17 @@ import androidx.lifecycle.DefaultLifecycleObserver
 | 
				
			|||||||
import androidx.lifecycle.LifecycleOwner
 | 
					import androidx.lifecycle.LifecycleOwner
 | 
				
			||||||
import androidx.lifecycle.ProcessLifecycleOwner
 | 
					import androidx.lifecycle.ProcessLifecycleOwner
 | 
				
			||||||
import androidx.multidex.MultiDexApplication
 | 
					import androidx.multidex.MultiDexApplication
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.DI.networkModule
 | 
					 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
 | 
					import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
 | 
					 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
 | 
					import bou.amine.apps.readerforselfossv2.dao.DriverFactory
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
 | 
					import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.di.networkModule
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
					import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
					import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
				
			||||||
import com.github.ln_12.library.ConnectivityStatus
 | 
					import bou.amine.apps.readerforselfossv2.service.ConnectivityService
 | 
				
			||||||
import io.github.aakira.napier.DebugAntilog
 | 
					import io.github.aakira.napier.DebugAntilog
 | 
				
			||||||
import io.github.aakira.napier.Napier
 | 
					import io.github.aakira.napier.Napier
 | 
				
			||||||
import kotlinx.coroutines.CoroutineScope
 | 
					import kotlinx.coroutines.CoroutineScope
 | 
				
			||||||
import kotlinx.coroutines.Dispatchers
 | 
					import kotlinx.coroutines.Dispatchers
 | 
				
			||||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
					 | 
				
			||||||
import kotlinx.coroutines.launch
 | 
					import kotlinx.coroutines.launch
 | 
				
			||||||
import org.acra.ACRA
 | 
					import org.acra.ACRA
 | 
				
			||||||
import org.acra.ReportField
 | 
					import org.acra.ReportField
 | 
				
			||||||
@@ -36,32 +34,29 @@ import org.kodein.di.bind
 | 
				
			|||||||
import org.kodein.di.instance
 | 
					import org.kodein.di.instance
 | 
				
			||||||
import org.kodein.di.singleton
 | 
					import org.kodein.di.singleton
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MyApp : MultiDexApplication(), DIAware {
 | 
					class MyApp :
 | 
				
			||||||
 | 
					    MultiDexApplication(),
 | 
				
			||||||
 | 
					    DIAware {
 | 
				
			||||||
    override val di by DI.lazy {
 | 
					    override val di by DI.lazy {
 | 
				
			||||||
        bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) }
 | 
					        bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) }
 | 
				
			||||||
        import(networkModule)
 | 
					        import(networkModule)
 | 
				
			||||||
        bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
 | 
					        bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
 | 
				
			||||||
        bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
 | 
					        bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
 | 
				
			||||||
 | 
					        bind<ConnectivityService>() with singleton { ConnectivityService() }
 | 
				
			||||||
        bind<Repository>() with
 | 
					        bind<Repository>() with
 | 
				
			||||||
                singleton {
 | 
					            singleton {
 | 
				
			||||||
                    Repository(
 | 
					                Repository(
 | 
				
			||||||
                        instance(),
 | 
					                    instance(),
 | 
				
			||||||
                        instance(),
 | 
					                    instance(),
 | 
				
			||||||
                        isConnectionAvailable,
 | 
					                    instance(),
 | 
				
			||||||
                        instance(),
 | 
					                    instance(),
 | 
				
			||||||
                    )
 | 
					                )
 | 
				
			||||||
                }
 | 
					            }
 | 
				
			||||||
        bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
 | 
					 | 
				
			||||||
        bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val repository: Repository by instance()
 | 
					    private val repository: Repository by instance()
 | 
				
			||||||
    private val viewModel: AppViewModel by instance()
 | 
					 | 
				
			||||||
    private val connectivityStatus: ConnectivityStatus by instance()
 | 
					 | 
				
			||||||
    private val driverFactory: DriverFactory by instance()
 | 
					    private val driverFactory: DriverFactory by instance()
 | 
				
			||||||
 | 
					    private val connectivityService: ConnectivityService by instance()
 | 
				
			||||||
    // TODO: handle with the "previous" way
 | 
					 | 
				
			||||||
    private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onCreate() {
 | 
					    override fun onCreate() {
 | 
				
			||||||
        super.onCreate()
 | 
					        super.onCreate()
 | 
				
			||||||
@@ -74,13 +69,12 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            ProcessLifecycleOwner.get().lifecycle.addObserver(
 | 
					            ProcessLifecycleOwner.get().lifecycle.addObserver(
 | 
				
			||||||
                AppLifeCycleObserver(
 | 
					                AppLifeCycleObserver(
 | 
				
			||||||
                    connectivityStatus,
 | 
					                    connectivityService,
 | 
				
			||||||
                    repository,
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            CoroutineScope(Dispatchers.Main).launch {
 | 
					            CoroutineScope(Dispatchers.Default).launch {
 | 
				
			||||||
                viewModel.networkAvailableProvider.collect { networkAvailable ->
 | 
					                connectivityService.networkAvailableProvider.collect { networkAvailable ->
 | 
				
			||||||
                    val toastMessage =
 | 
					                    val toastMessage =
 | 
				
			||||||
                        if (networkAvailable) {
 | 
					                        if (networkAvailable) {
 | 
				
			||||||
                            repository.handleDBActions()
 | 
					                            repository.handleDBActions()
 | 
				
			||||||
@@ -89,11 +83,12 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
				
			|||||||
                            R.string.network_connectivity_lost
 | 
					                            R.string.network_connectivity_lost
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Toast.makeText(
 | 
					                    Toast
 | 
				
			||||||
                        applicationContext,
 | 
					                        .makeText(
 | 
				
			||||||
                        toastMessage,
 | 
					                            applicationContext,
 | 
				
			||||||
                        Toast.LENGTH_SHORT,
 | 
					                            toastMessage,
 | 
				
			||||||
                    ).show()
 | 
					                            Toast.LENGTH_SHORT,
 | 
				
			||||||
 | 
					                        ).show()
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -105,6 +100,7 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
				
			|||||||
        super.attachBaseContext(base)
 | 
					        super.attachBaseContext(base)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initAcra {
 | 
					        initAcra {
 | 
				
			||||||
 | 
					            sendReportsInDevMode = false
 | 
				
			||||||
            reportFormat = StringFormat.JSON
 | 
					            reportFormat = StringFormat.JSON
 | 
				
			||||||
            reportContent =
 | 
					            reportContent =
 | 
				
			||||||
                listOf(
 | 
					                listOf(
 | 
				
			||||||
@@ -151,13 +147,13 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            val name = getString(R.string.notification_channel_sync)
 | 
					            val name = getString(R.string.notification_channel_sync)
 | 
				
			||||||
            val importance = NotificationManager.IMPORTANCE_LOW
 | 
					            val importance = NotificationManager.IMPORTANCE_LOW
 | 
				
			||||||
            val mChannel = NotificationChannel(AppSettingsService.syncChannelId, name, importance)
 | 
					            val mChannel = NotificationChannel(AppSettingsService.SYNC_CHANNEL_ID, name, importance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            val newItemsChannelname = getString(R.string.new_items_channel_sync)
 | 
					            val newItemsChannelname = getString(R.string.new_items_channel_sync)
 | 
				
			||||||
            val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
 | 
					            val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
 | 
				
			||||||
            val newItemsChannelmChannel =
 | 
					            val newItemsChannelmChannel =
 | 
				
			||||||
                NotificationChannel(
 | 
					                NotificationChannel(
 | 
				
			||||||
                    AppSettingsService.newItemsChannelId,
 | 
					                    AppSettingsService.NEW_ITEMS_CHANNEL,
 | 
				
			||||||
                    newItemsChannelname,
 | 
					                    newItemsChannelname,
 | 
				
			||||||
                    newItemsChannelimportance,
 | 
					                    newItemsChannelimportance,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
@@ -184,19 +180,16 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class AppLifeCycleObserver(
 | 
					    class AppLifeCycleObserver(
 | 
				
			||||||
        val connectivityStatus: ConnectivityStatus,
 | 
					        val connectivityService: ConnectivityService,
 | 
				
			||||||
        val repository: Repository,
 | 
					 | 
				
			||||||
    ) : DefaultLifecycleObserver {
 | 
					    ) : DefaultLifecycleObserver {
 | 
				
			||||||
        override fun onResume(owner: LifecycleOwner) {
 | 
					        override fun onResume(owner: LifecycleOwner) {
 | 
				
			||||||
            super.onResume(owner)
 | 
					            super.onResume(owner)
 | 
				
			||||||
            repository.connectionMonitored = true
 | 
					            connectivityService.start()
 | 
				
			||||||
            connectivityStatus.start()
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        override fun onPause(owner: LifecycleOwner) {
 | 
					        override fun onPause(owner: LifecycleOwner) {
 | 
				
			||||||
            repository.connectionMonitored = false
 | 
					            connectivityService.stop()
 | 
				
			||||||
            connectivityStatus.stop()
 | 
					 | 
				
			||||||
            super.onPause(owner)
 | 
					            super.onPause(owner)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,10 +22,12 @@ import org.kodein.di.DIAware
 | 
				
			|||||||
import org.kodein.di.android.closestDI
 | 
					import org.kodein.di.android.closestDI
 | 
				
			||||||
import org.kodein.di.instance
 | 
					import org.kodein.di.instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReaderActivity : AppCompatActivity(), DIAware {
 | 
					class ReaderActivity :
 | 
				
			||||||
 | 
					    AppCompatActivity(),
 | 
				
			||||||
 | 
					    DIAware {
 | 
				
			||||||
    private var currentItem: Int = 0
 | 
					    private var currentItem: Int = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private lateinit var toolbarMenu: Menu
 | 
					    private var toolbarMenu: Menu? = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private lateinit var binding: ActivityReaderBinding
 | 
					    private lateinit var binding: ActivityReaderBinding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -35,22 +37,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
    private val repository: Repository by instance()
 | 
					    private val repository: Repository by instance()
 | 
				
			||||||
    private val appSettingsService: AppSettingsService by instance()
 | 
					    private val appSettingsService: AppSettingsService by instance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun showMenuItem(willAddToFavorite: Boolean) {
 | 
					    @Suppress("detekt:SwallowedException")
 | 
				
			||||||
        if (willAddToFavorite) {
 | 
					 | 
				
			||||||
            toolbarMenu.findItem(R.id.star).icon?.setTint(Color.WHITE)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            toolbarMenu.findItem(R.id.star).icon?.setTint(Color.RED)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private fun canFavorite() {
 | 
					 | 
				
			||||||
        showMenuItem(true)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private fun canRemoveFromFavorite() {
 | 
					 | 
				
			||||||
        showMenuItem(false)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
					    override fun onCreate(savedInstanceState: Bundle?) {
 | 
				
			||||||
        super.onCreate(savedInstanceState)
 | 
					        super.onCreate(savedInstanceState)
 | 
				
			||||||
        binding = ActivityReaderBinding.inflate(layoutInflater)
 | 
					        binding = ActivityReaderBinding.inflate(layoutInflater)
 | 
				
			||||||
@@ -70,14 +57,21 @@ class ReaderActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
            finish()
 | 
					            finish()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        readItem()
 | 
				
			||||||
            readItem(allItems[currentItem])
 | 
					 | 
				
			||||||
        } catch (e: IndexOutOfBoundsException) {
 | 
					 | 
				
			||||||
            finish()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        binding.pager.adapter = ScreenSlidePagerAdapter(this)
 | 
					        binding.pager.adapter = ScreenSlidePagerAdapter(this)
 | 
				
			||||||
        binding.pager.setCurrentItem(currentItem, false)
 | 
					        binding.pager.setCurrentItem(currentItem, false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        binding.pager.registerOnPageChangeCallback(
 | 
				
			||||||
 | 
					            object : ViewPager2.OnPageChangeCallback() {
 | 
				
			||||||
 | 
					                override fun onPageSelected(position: Int) {
 | 
				
			||||||
 | 
					                    super.onPageSelected(position)
 | 
				
			||||||
 | 
					                    currentItem = position
 | 
				
			||||||
 | 
					                    updateStarIcon()
 | 
				
			||||||
 | 
					                    readItem()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onResume() {
 | 
					    override fun onResume() {
 | 
				
			||||||
@@ -86,21 +80,30 @@ class ReaderActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
        binding.indicator.setViewPager(binding.pager)
 | 
					        binding.indicator.setViewPager(binding.pager)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun readItem(item: SelfossModel.Item) {
 | 
					    private fun readItem() {
 | 
				
			||||||
        if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess()) {
 | 
					        val item = allItems.getOrNull(currentItem)
 | 
				
			||||||
 | 
					        if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess() && item != null) {
 | 
				
			||||||
            CoroutineScope(Dispatchers.IO).launch {
 | 
					            CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
                repository.markAsRead(item)
 | 
					                repository.markAsRead(item)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun updateStarIcon() {
 | 
				
			||||||
 | 
					        if (toolbarMenu != null) {
 | 
				
			||||||
 | 
					            val isStarred = allItems.getOrNull(currentItem)?.starred ?: false
 | 
				
			||||||
 | 
					            toolbarMenu!!.findItem(R.id.star)?.icon?.setTint(if (isStarred) Color.RED else Color.WHITE)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onSaveInstanceState(oldInstanceState: Bundle) {
 | 
					    override fun onSaveInstanceState(oldInstanceState: Bundle) {
 | 
				
			||||||
        super.onSaveInstanceState(oldInstanceState)
 | 
					        super.onSaveInstanceState(oldInstanceState)
 | 
				
			||||||
        oldInstanceState.clear()
 | 
					        oldInstanceState.clear()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
 | 
					    private inner class ScreenSlidePagerAdapter(
 | 
				
			||||||
        FragmentStateAdapter(fa) {
 | 
					        fa: FragmentActivity,
 | 
				
			||||||
 | 
					    ) : FragmentStateAdapter(fa) {
 | 
				
			||||||
        override fun getItemCount(): Int = allItems.size
 | 
					        override fun getItemCount(): Int = allItems.size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
 | 
					        override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
 | 
				
			||||||
@@ -109,35 +112,37 @@ class ReaderActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
    override fun onKeyDown(
 | 
					    override fun onKeyDown(
 | 
				
			||||||
        keyCode: Int,
 | 
					        keyCode: Int,
 | 
				
			||||||
        event: KeyEvent?,
 | 
					        event: KeyEvent?,
 | 
				
			||||||
    ): Boolean {
 | 
					    ): Boolean =
 | 
				
			||||||
        return when (keyCode) {
 | 
					        when (keyCode) {
 | 
				
			||||||
            KeyEvent.KEYCODE_VOLUME_DOWN -> {
 | 
					            KeyEvent.KEYCODE_VOLUME_DOWN -> {
 | 
				
			||||||
                val currentFragment =
 | 
					                val currentFragment =
 | 
				
			||||||
                    supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
 | 
					                    supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
 | 
				
			||||||
                currentFragment.scrollDown()
 | 
					                currentFragment.volumeButtonScrollDown()
 | 
				
			||||||
                true
 | 
					                true
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            KeyEvent.KEYCODE_VOLUME_UP -> {
 | 
					            KeyEvent.KEYCODE_VOLUME_UP -> {
 | 
				
			||||||
                val currentFragment =
 | 
					                val currentFragment =
 | 
				
			||||||
                    supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
 | 
					                    supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
 | 
				
			||||||
                currentFragment.scrollUp()
 | 
					                currentFragment.volumeButtonScrollUp()
 | 
				
			||||||
                true
 | 
					                true
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            else -> {
 | 
					            else -> {
 | 
				
			||||||
                super.onKeyDown(keyCode, event)
 | 
					                super.onKeyDown(keyCode, event)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun alignmentMenu() {
 | 
					    private fun alignmentMenu() {
 | 
				
			||||||
        val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT
 | 
					        val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT
 | 
				
			||||||
        toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify
 | 
					        if (toolbarMenu != null) {
 | 
				
			||||||
        toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify
 | 
					            toolbarMenu!!.findItem(R.id.align_left).isVisible = !showJustify
 | 
				
			||||||
 | 
					            toolbarMenu!!.findItem(R.id.align_justify).isVisible = showJustify
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
 | 
					    override fun onCreateOptionsMenu(menu: Menu): Boolean {
 | 
				
			||||||
        val inflater = menuInflater
 | 
					        menuInflater.inflate(R.menu.reader_menu, menu)
 | 
				
			||||||
        inflater.inflate(R.menu.reader_menu, menu)
 | 
					 | 
				
			||||||
        toolbarMenu = menu
 | 
					        toolbarMenu = menu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        alignmentMenu()
 | 
					        alignmentMenu()
 | 
				
			||||||
@@ -145,82 +150,50 @@ class ReaderActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
        if (appSettingsService.getPublicAccess()) {
 | 
					        if (appSettingsService.getPublicAccess()) {
 | 
				
			||||||
            menu.removeItem(R.id.star)
 | 
					            menu.removeItem(R.id.star)
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            if (allItems.isNotEmpty() && allItems[currentItem].starred) {
 | 
					            updateStarIcon()
 | 
				
			||||||
                canRemoveFromFavorite()
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                canFavorite()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            binding.pager.registerOnPageChangeCallback(
 | 
					 | 
				
			||||||
                object : ViewPager2.OnPageChangeCallback() {
 | 
					 | 
				
			||||||
                    override fun onPageSelected(position: Int) {
 | 
					 | 
				
			||||||
                        super.onPageSelected(position)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if (allItems[position].starred) {
 | 
					 | 
				
			||||||
                            canRemoveFromFavorite()
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            canFavorite()
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        readItem(allItems[position])
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return true
 | 
					        return true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
					    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
				
			||||||
        fun afterSave() {
 | 
					 | 
				
			||||||
            allItems[binding.pager.currentItem] =
 | 
					 | 
				
			||||||
                allItems[binding.pager.currentItem].toggleStar()
 | 
					 | 
				
			||||||
            canRemoveFromFavorite()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        fun afterUnsave() {
 | 
					 | 
				
			||||||
            allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar()
 | 
					 | 
				
			||||||
            canFavorite()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        when (item.itemId) {
 | 
					        when (item.itemId) {
 | 
				
			||||||
            android.R.id.home -> {
 | 
					            android.R.id.home -> onBackPressedDispatcher.onBackPressed()
 | 
				
			||||||
                onBackPressedDispatcher.onBackPressed()
 | 
					            R.id.star -> toggleFavorite()
 | 
				
			||||||
                return true
 | 
					            R.id.align_left -> switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
 | 
				
			||||||
            }
 | 
					            R.id.align_justify -> switchAlignmentSetting(AppSettingsService.JUSTIFY)
 | 
				
			||||||
            R.id.star -> {
 | 
					 | 
				
			||||||
                if (allItems[binding.pager.currentItem].starred) {
 | 
					 | 
				
			||||||
                    CoroutineScope(Dispatchers.IO).launch {
 | 
					 | 
				
			||||||
                        repository.unstarr(allItems[binding.pager.currentItem])
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    afterUnsave()
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    CoroutineScope(Dispatchers.IO).launch {
 | 
					 | 
				
			||||||
                        repository.starr(allItems[binding.pager.currentItem])
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    afterSave()
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            R.id.align_left -> {
 | 
					 | 
				
			||||||
                switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
 | 
					 | 
				
			||||||
                refreshFragment()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            R.id.align_justify -> {
 | 
					 | 
				
			||||||
                switchAlignmentSetting(AppSettingsService.JUSTIFY)
 | 
					 | 
				
			||||||
                refreshFragment()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return super.onOptionsItemSelected(item)
 | 
					        return super.onOptionsItemSelected(item)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun switchAlignmentSetting(allignment: Int) {
 | 
					    private fun toggleFavorite() {
 | 
				
			||||||
        appSettingsService.changeAllignment(allignment)
 | 
					        val item = allItems.getOrNull(currentItem) ?: return
 | 
				
			||||||
        alignmentMenu()
 | 
					
 | 
				
			||||||
 | 
					        val starred = item.starred
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
 | 
					            if (starred) {
 | 
				
			||||||
 | 
					                repository.unstarr(item)
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                repository.starr(item)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        item.toggleStar()
 | 
				
			||||||
 | 
					        updateStarIcon()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun refreshFragment() {
 | 
					    private fun switchAlignmentSetting(alignment: Int) {
 | 
				
			||||||
        finish()
 | 
					        appSettingsService.changeAllignment(alignment)
 | 
				
			||||||
        overridePendingTransition(0, 0)
 | 
					        alignmentMenu()
 | 
				
			||||||
        startActivity(intent)
 | 
					
 | 
				
			||||||
        overridePendingTransition(0, 0)
 | 
					        val fragmentManager = supportFragmentManager
 | 
				
			||||||
 | 
					        val fragments = fragmentManager.fragments
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (fragment in fragments) {
 | 
				
			||||||
 | 
					            if (fragment is ArticleFragment) {
 | 
				
			||||||
 | 
					                fragment.refreshAlignment()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,9 @@ import org.kodein.di.DIAware
 | 
				
			|||||||
import org.kodein.di.android.closestDI
 | 
					import org.kodein.di.android.closestDI
 | 
				
			||||||
import org.kodein.di.instance
 | 
					import org.kodein.di.instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SourcesActivity : AppCompatActivity(), DIAware {
 | 
					class SourcesActivity :
 | 
				
			||||||
 | 
					    AppCompatActivity(),
 | 
				
			||||||
 | 
					    DIAware {
 | 
				
			||||||
    private lateinit var binding: ActivitySourcesBinding
 | 
					    private lateinit var binding: ActivitySourcesBinding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override val di by closestDI()
 | 
					    override val di by closestDI()
 | 
				
			||||||
@@ -56,23 +58,28 @@ class SourcesActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
        binding.recyclerView.layoutManager = mLayoutManager
 | 
					        binding.recyclerView.layoutManager = mLayoutManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        CountingIdlingResourceSingleton.increment()
 | 
					        CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
					        CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
            val response = repository.getSourcesDetails()
 | 
					            val response = repository.getSourcesDetails()
 | 
				
			||||||
            if (response.isNotEmpty()) {
 | 
					            CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
                items = response
 | 
					            launch(Dispatchers.Main) {
 | 
				
			||||||
                val mAdapter =
 | 
					                if (response.isNotEmpty()) {
 | 
				
			||||||
                    SourcesListAdapter(
 | 
					                    items = response
 | 
				
			||||||
                        this@SourcesActivity,
 | 
					                    val mAdapter =
 | 
				
			||||||
                        items,
 | 
					                        SourcesListAdapter(
 | 
				
			||||||
                    )
 | 
					                            this@SourcesActivity,
 | 
				
			||||||
                binding.recyclerView.adapter = mAdapter
 | 
					                            items,
 | 
				
			||||||
                mAdapter.notifyDataSetChanged()
 | 
					                        )
 | 
				
			||||||
            } else {
 | 
					                    binding.recyclerView.adapter = mAdapter
 | 
				
			||||||
                Toast.makeText(
 | 
					                    mAdapter.notifyDataSetChanged()
 | 
				
			||||||
                    this@SourcesActivity,
 | 
					                } else {
 | 
				
			||||||
                    R.string.cant_get_sources,
 | 
					                    Toast
 | 
				
			||||||
                    Toast.LENGTH_SHORT,
 | 
					                        .makeText(
 | 
				
			||||||
                ).show()
 | 
					                            this@SourcesActivity,
 | 
				
			||||||
 | 
					                            R.string.cant_get_sources,
 | 
				
			||||||
 | 
					                            Toast.LENGTH_SHORT,
 | 
				
			||||||
 | 
					                        ).show()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            CountingIdlingResourceSingleton.decrement()
 | 
					            CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -81,4 +88,4 @@ class SourcesActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
            startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java))
 | 
					            startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java))
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,11 +9,10 @@ import android.widget.TextView
 | 
				
			|||||||
import android.widget.Toast
 | 
					import android.widget.Toast
 | 
				
			||||||
import androidx.appcompat.app.AppCompatActivity
 | 
					import androidx.appcompat.app.AppCompatActivity
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding
 | 
					import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid
 | 
					import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
 | 
					import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
 | 
				
			||||||
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 kotlinx.coroutines.CoroutineScope
 | 
					import kotlinx.coroutines.CoroutineScope
 | 
				
			||||||
import kotlinx.coroutines.Dispatchers
 | 
					import kotlinx.coroutines.Dispatchers
 | 
				
			||||||
import kotlinx.coroutines.launch
 | 
					import kotlinx.coroutines.launch
 | 
				
			||||||
@@ -21,7 +20,9 @@ import org.kodein.di.DIAware
 | 
				
			|||||||
import org.kodein.di.android.closestDI
 | 
					import org.kodein.di.android.closestDI
 | 
				
			||||||
import org.kodein.di.instance
 | 
					import org.kodein.di.instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
					class UpsertSourceActivity :
 | 
				
			||||||
 | 
					    AppCompatActivity(),
 | 
				
			||||||
 | 
					    DIAware {
 | 
				
			||||||
    private var existingSource: SelfossModel.SourceDetail? = null
 | 
					    private var existingSource: SelfossModel.SourceDetail? = null
 | 
				
			||||||
    private var mSpoutsValue: String? = null
 | 
					    private var mSpoutsValue: String? = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,7 +30,6 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    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()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
					    override fun onCreate(savedInstanceState: Bundle?) {
 | 
				
			||||||
        super.onCreate(savedInstanceState)
 | 
					        super.onCreate(savedInstanceState)
 | 
				
			||||||
@@ -74,15 +74,10 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    override fun onResume() {
 | 
					    override fun onResume() {
 | 
				
			||||||
        super.onResume()
 | 
					        super.onResume()
 | 
				
			||||||
 | 
					        handleSpoutsSpinner()
 | 
				
			||||||
        val baseUrl = appSettingsService.getBaseUrl()
 | 
					 | 
				
			||||||
        if (baseUrl.isEmpty() || baseUrl.isBaseUrlInvalid()) {
 | 
					 | 
				
			||||||
            mustLoginToAddSource()
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            handleSpoutsSpinner()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Suppress("detekt:SwallowedException")
 | 
				
			||||||
    private fun handleSpoutsSpinner() {
 | 
					    private fun handleSpoutsSpinner() {
 | 
				
			||||||
        val spoutsKV = HashMap<String, String>()
 | 
					        val spoutsKV = HashMap<String, String>()
 | 
				
			||||||
        binding.spoutsSpinner.onItemSelectedListener =
 | 
					        binding.spoutsSpinner.onItemSelectedListener =
 | 
				
			||||||
@@ -105,44 +100,51 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fun handleSpoutFailure(networkIssue: Boolean = false) {
 | 
					        fun handleSpoutFailure(networkIssue: Boolean = false) {
 | 
				
			||||||
            Toast.makeText(
 | 
					            Toast
 | 
				
			||||||
                this@UpsertSourceActivity,
 | 
					                .makeText(
 | 
				
			||||||
                if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
 | 
					                    this@UpsertSourceActivity,
 | 
				
			||||||
                Toast.LENGTH_SHORT,
 | 
					                    if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
 | 
				
			||||||
            ).show()
 | 
					                    Toast.LENGTH_SHORT,
 | 
				
			||||||
 | 
					                ).show()
 | 
				
			||||||
            binding.progress.visibility = View.GONE
 | 
					            binding.progress.visibility = View.GONE
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
					        CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
 | 
					        CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                val items = repository.getSpouts()
 | 
					                val items = repository.getSpouts()
 | 
				
			||||||
                if (items.isNotEmpty()) {
 | 
					                CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
                    val itemsStrings = items.map { it.value.name }
 | 
					                launch(Dispatchers.Main) {
 | 
				
			||||||
                    for ((key, value) in items) {
 | 
					                    if (items.isNotEmpty()) {
 | 
				
			||||||
                        spoutsKV[value.name] = key
 | 
					                        val itemsStrings = items.map { it.value.name }
 | 
				
			||||||
 | 
					                        for ((key, value) in items) {
 | 
				
			||||||
 | 
					                            spoutsKV[value.name] = key
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        binding.progress.visibility = View.GONE
 | 
				
			||||||
 | 
					                        binding.formContainer.visibility = View.VISIBLE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        val spinnerArrayAdapter =
 | 
				
			||||||
 | 
					                            ArrayAdapter(
 | 
				
			||||||
 | 
					                                this@UpsertSourceActivity,
 | 
				
			||||||
 | 
					                                android.R.layout.simple_spinner_item,
 | 
				
			||||||
 | 
					                                itemsStrings,
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                        spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
 | 
				
			||||||
 | 
					                        binding.spoutsSpinner.adapter = spinnerArrayAdapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (existingSource != null) {
 | 
				
			||||||
 | 
					                            initFields(items)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        handleSpoutFailure()
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                    CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
                    binding.progress.visibility = View.GONE
 | 
					 | 
				
			||||||
                    binding.formContainer.visibility = View.VISIBLE
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    val spinnerArrayAdapter =
 | 
					 | 
				
			||||||
                        ArrayAdapter(
 | 
					 | 
				
			||||||
                            this@UpsertSourceActivity,
 | 
					 | 
				
			||||||
                            android.R.layout.simple_spinner_item,
 | 
					 | 
				
			||||||
                            itemsStrings,
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                    spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
 | 
					 | 
				
			||||||
                    binding.spoutsSpinner.adapter = spinnerArrayAdapter
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if (existingSource != null) {
 | 
					 | 
				
			||||||
                        initFields(items)
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    handleSpoutFailure()
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } catch (e: NetworkUnavailableException) {
 | 
					            } catch (e: NetworkUnavailableException) {
 | 
				
			||||||
                handleSpoutFailure(networkIssue = true)
 | 
					                handleSpoutFailure(networkIssue = true)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -153,13 +155,6 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun mustLoginToAddSource() {
 | 
					 | 
				
			||||||
        Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
 | 
					 | 
				
			||||||
        val i = Intent(this, LoginActivity::class.java)
 | 
					 | 
				
			||||||
        startActivity(i)
 | 
					 | 
				
			||||||
        finish()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private fun handleSaveSource() {
 | 
					    private fun handleSaveSource() {
 | 
				
			||||||
        val url = binding.sourceUri.text.toString()
 | 
					        val url = binding.sourceUri.text.toString()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -170,8 +165,10 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
            sourceDetailsUnavailable -> {
 | 
					            sourceDetailsUnavailable -> {
 | 
				
			||||||
                Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
 | 
					                Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            else -> {
 | 
					            else -> {
 | 
				
			||||||
                CoroutineScope(Dispatchers.Main).launch {
 | 
					                CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
 | 
					                CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
                    val successfullyAddedSource =
 | 
					                    val successfullyAddedSource =
 | 
				
			||||||
                        if (existingSource != null) {
 | 
					                        if (existingSource != null) {
 | 
				
			||||||
                            repository.updateSource(
 | 
					                            repository.updateSource(
 | 
				
			||||||
@@ -189,15 +186,21 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
				
			|||||||
                                binding.tags.text.toString(),
 | 
					                                binding.tags.text.toString(),
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    if (successfullyAddedSource) {
 | 
					                    CountingIdlingResourceSingleton.increment()
 | 
				
			||||||
                        finish()
 | 
					                    launch(Dispatchers.Main) {
 | 
				
			||||||
                    } else {
 | 
					                        if (successfullyAddedSource) {
 | 
				
			||||||
                        Toast.makeText(
 | 
					                            finish()
 | 
				
			||||||
                            this@UpsertSourceActivity,
 | 
					                        } else {
 | 
				
			||||||
                            R.string.cant_create_source,
 | 
					                            Toast
 | 
				
			||||||
                            Toast.LENGTH_SHORT,
 | 
					                                .makeText(
 | 
				
			||||||
                        ).show()
 | 
					                                    this@UpsertSourceActivity,
 | 
				
			||||||
 | 
					                                    R.string.cant_create_source,
 | 
				
			||||||
 | 
					                                    Toast.LENGTH_SHORT,
 | 
				
			||||||
 | 
					                                ).show()
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                    CountingIdlingResourceSingleton.decrement()
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,7 +49,10 @@ class ItemCardAdapter(
 | 
				
			|||||||
        return ViewHolder(binding)
 | 
					        return ViewHolder(binding)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun handleClickListeners(holderBinding: CardItemBinding, position: Int) {
 | 
					    private fun handleClickListeners(
 | 
				
			||||||
 | 
					        holderBinding: CardItemBinding,
 | 
				
			||||||
 | 
					        position: Int,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
        holderBinding.favButton.setOnClickListener {
 | 
					        holderBinding.favButton.setOnClickListener {
 | 
				
			||||||
            val item = items[position]
 | 
					            val item = items[position]
 | 
				
			||||||
            if (item.starred) {
 | 
					            if (item.starred) {
 | 
				
			||||||
@@ -96,12 +99,13 @@ class ItemCardAdapter(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
 | 
					            binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            binding.sourceTitleAndDate.text = try {
 | 
					            binding.sourceTitleAndDate.text =
 | 
				
			||||||
                itm.sourceAuthorAndDate()
 | 
					                try {
 | 
				
			||||||
            } catch (e: Exception) {
 | 
					                    itm.sourceAuthorAndDate()
 | 
				
			||||||
                e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")
 | 
					                } catch (e: Exception) {
 | 
				
			||||||
                itm.sourceAuthorOnly()
 | 
					                    e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")
 | 
				
			||||||
            }
 | 
					                    itm.sourceAuthorOnly()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!appSettingsService.isFullHeightCardsEnabled()) {
 | 
					            if (!appSettingsService.isFullHeightCardsEnabled()) {
 | 
				
			||||||
                binding.itemImage.maxHeight = imageMaxHeight
 | 
					                binding.itemImage.maxHeight = imageMaxHeight
 | 
				
			||||||
@@ -114,16 +118,18 @@ class ItemCardAdapter(
 | 
				
			|||||||
                binding.itemImage.setImageDrawable(null)
 | 
					                binding.itemImage.setImageDrawable(null)
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                binding.itemImage.visibility = View.VISIBLE
 | 
					                binding.itemImage.visibility = View.VISIBLE
 | 
				
			||||||
                c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage)
 | 
					                c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (itm.getIcon(repository.baseUrl).isEmpty()) {
 | 
					            if (itm.getIcon(repository.baseUrl).isEmpty()) {
 | 
				
			||||||
                binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
 | 
					                binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
 | 
					                c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage, appSettingsService)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
 | 
					    inner class ViewHolder(
 | 
				
			||||||
}
 | 
					        val binding: CardItemBinding,
 | 
				
			||||||
 | 
					    ) : RecyclerView.ViewHolder(binding.root)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,24 +53,27 @@ class ItemListAdapter(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
 | 
					            binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            binding.sourceTitleAndDate.text = try {
 | 
					            binding.sourceTitleAndDate.text =
 | 
				
			||||||
                itm.sourceAuthorAndDate()
 | 
					                try {
 | 
				
			||||||
            } catch (e: Exception) {
 | 
					                    itm.sourceAuthorAndDate()
 | 
				
			||||||
                e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")
 | 
					                } catch (e: Exception) {
 | 
				
			||||||
                itm.sourceAuthorOnly()
 | 
					                    e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")
 | 
				
			||||||
            }
 | 
					                    itm.sourceAuthorOnly()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
 | 
					            if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
 | 
				
			||||||
                if (itm.getIcon(repository.baseUrl).isEmpty()) {
 | 
					                if (itm.getIcon(repository.baseUrl).isEmpty()) {
 | 
				
			||||||
                    binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
 | 
					                    binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
 | 
					                    c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage)
 | 
					                c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
 | 
					    inner class ViewHolder(
 | 
				
			||||||
}
 | 
					        val binding: ListItemBinding,
 | 
				
			||||||
 | 
					    ) : RecyclerView.ViewHolder(binding.root)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,9 @@ import kotlinx.coroutines.Dispatchers
 | 
				
			|||||||
import kotlinx.coroutines.launch
 | 
					import kotlinx.coroutines.launch
 | 
				
			||||||
import org.kodein.di.DIAware
 | 
					import org.kodein.di.DIAware
 | 
				
			||||||
 | 
					
 | 
				
			||||||
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware {
 | 
					abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
 | 
				
			||||||
 | 
					    RecyclerView.Adapter<VH>(),
 | 
				
			||||||
 | 
					    DIAware {
 | 
				
			||||||
    abstract val items: ArrayList<SelfossModel.Item>
 | 
					    abstract val items: ArrayList<SelfossModel.Item>
 | 
				
			||||||
    abstract val repository: Repository
 | 
					    abstract val repository: Repository
 | 
				
			||||||
    abstract val binding: ViewBinding
 | 
					    abstract val binding: ViewBinding
 | 
				
			||||||
@@ -45,8 +47,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
 | 
				
			|||||||
                    app.findViewById(R.id.coordLayout),
 | 
					                    app.findViewById(R.id.coordLayout),
 | 
				
			||||||
                    R.string.marked_as_read,
 | 
					                    R.string.marked_as_read,
 | 
				
			||||||
                    Snackbar.LENGTH_LONG,
 | 
					                    Snackbar.LENGTH_LONG,
 | 
				
			||||||
                )
 | 
					                ).setAction(R.string.undo_string) {
 | 
				
			||||||
                .setAction(R.string.undo_string) {
 | 
					 | 
				
			||||||
                    unreadItemAtIndex(item, position, false)
 | 
					                    unreadItemAtIndex(item, position, false)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,8 +67,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
 | 
				
			|||||||
                    app.findViewById(R.id.coordLayout),
 | 
					                    app.findViewById(R.id.coordLayout),
 | 
				
			||||||
                    R.string.marked_as_unread,
 | 
					                    R.string.marked_as_unread,
 | 
				
			||||||
                    Snackbar.LENGTH_LONG,
 | 
					                    Snackbar.LENGTH_LONG,
 | 
				
			||||||
                )
 | 
					                ).setAction(R.string.undo_string) {
 | 
				
			||||||
                .setAction(R.string.undo_string) {
 | 
					 | 
				
			||||||
                    readItemAtIndex(item, position, false)
 | 
					                    readItemAtIndex(item, position, false)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -77,7 +77,10 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
 | 
				
			|||||||
        s.show()
 | 
					        s.show()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected fun handleLinkOpening(holderBinding: ViewBinding, position: Int) {
 | 
					    protected fun handleLinkOpening(
 | 
				
			||||||
 | 
					        holderBinding: ViewBinding,
 | 
				
			||||||
 | 
					        position: Int,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
        holderBinding.root.setOnClickListener {
 | 
					        holderBinding.root.setOnClickListener {
 | 
				
			||||||
            repository.setReaderItems(items)
 | 
					            repository.setReaderItems(items)
 | 
				
			||||||
            c.openItemUrl(
 | 
					            c.openItemUrl(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,9 +6,8 @@ import android.content.Intent
 | 
				
			|||||||
import android.view.LayoutInflater
 | 
					import android.view.LayoutInflater
 | 
				
			||||||
import android.view.View
 | 
					import android.view.View
 | 
				
			||||||
import android.view.ViewGroup
 | 
					import android.view.ViewGroup
 | 
				
			||||||
import android.widget.Button
 | 
					 | 
				
			||||||
import android.widget.Toast
 | 
					import android.widget.Toast
 | 
				
			||||||
import androidx.constraintlayout.widget.ConstraintLayout
 | 
					import androidx.appcompat.app.AlertDialog
 | 
				
			||||||
import androidx.recyclerview.widget.RecyclerView
 | 
					import androidx.recyclerview.widget.RecyclerView
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
					import bou.amine.apps.readerforselfossv2.android.R
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
 | 
					import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
 | 
				
			||||||
@@ -16,6 +15,7 @@ import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBindi
 | 
				
			|||||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
 | 
				
			||||||
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.utils.getHtmlDecoded
 | 
					import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
 | 
					import bou.amine.apps.readerforselfossv2.utils.getIcon
 | 
				
			||||||
import kotlinx.coroutines.CoroutineScope
 | 
					import kotlinx.coroutines.CoroutineScope
 | 
				
			||||||
@@ -29,68 +29,23 @@ import org.kodein.di.instance
 | 
				
			|||||||
class SourcesListAdapter(
 | 
					class SourcesListAdapter(
 | 
				
			||||||
    private val app: Activity,
 | 
					    private val app: Activity,
 | 
				
			||||||
    private val items: ArrayList<SelfossModel.SourceDetail>,
 | 
					    private val items: ArrayList<SelfossModel.SourceDetail>,
 | 
				
			||||||
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
 | 
					) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(),
 | 
				
			||||||
    private val c: Context = app.baseContext
 | 
					    DIAware {
 | 
				
			||||||
    private lateinit var binding: SourceListItemBinding
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override val di: DI by closestDI(app)
 | 
					    override val di: DI by closestDI(app)
 | 
				
			||||||
    private val repository: Repository by instance()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onCreateViewHolder(
 | 
					    override fun onCreateViewHolder(
 | 
				
			||||||
        parent: ViewGroup,
 | 
					        parent: ViewGroup,
 | 
				
			||||||
        viewType: Int,
 | 
					        viewType: Int,
 | 
				
			||||||
    ): ViewHolder {
 | 
					    ): ViewHolder {
 | 
				
			||||||
        binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 | 
					        val binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 | 
				
			||||||
        return ViewHolder(binding.root)
 | 
					        return ViewHolder(binding)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onBindViewHolder(
 | 
					    override fun onBindViewHolder(
 | 
				
			||||||
        holder: ViewHolder,
 | 
					        holder: ViewHolder,
 | 
				
			||||||
        position: Int,
 | 
					        position: Int,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        val itm = items[position]
 | 
					        holder.bind(items[position], position)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        val deleteBtn: Button = holder.mView.findViewById(R.id.deleteBtn)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        deleteBtn.setOnClickListener {
 | 
					 | 
				
			||||||
            val (id, title) = items[position]
 | 
					 | 
				
			||||||
            CoroutineScope(Dispatchers.IO).launch {
 | 
					 | 
				
			||||||
                val successfullyDeletedSource = repository.deleteSource(id, title)
 | 
					 | 
				
			||||||
                if (successfullyDeletedSource) {
 | 
					 | 
				
			||||||
                    items.removeAt(position)
 | 
					 | 
				
			||||||
                    notifyItemRemoved(position)
 | 
					 | 
				
			||||||
                    notifyItemRangeChanged(position, itemCount)
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    Toast.makeText(
 | 
					 | 
				
			||||||
                        app,
 | 
					 | 
				
			||||||
                        R.string.can_delete_source,
 | 
					 | 
				
			||||||
                        Toast.LENGTH_SHORT,
 | 
					 | 
				
			||||||
                    ).show()
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        holder.mView.setOnClickListener {
 | 
					 | 
				
			||||||
            val source = items[position]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            repository.setSelectedSource(source)
 | 
					 | 
				
			||||||
            app.startActivity(Intent(app, UpsertSourceActivity::class.java))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (itm.getIcon(repository.baseUrl).isEmpty()) {
 | 
					 | 
				
			||||||
            binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!itm.error.isNullOrBlank()) {
 | 
					 | 
				
			||||||
            binding.errorText.visibility = View.VISIBLE
 | 
					 | 
				
			||||||
            binding.errorText.text = itm.error
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            binding.errorText.visibility = View.GONE
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        binding.sourceTitle.text = itm.title.getHtmlDecoded()
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun getItemId(position: Int) = position.toLong()
 | 
					    override fun getItemId(position: Int) = position.toLong()
 | 
				
			||||||
@@ -99,5 +54,73 @@ class SourcesListAdapter(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    override fun getItemCount(): Int = items.size
 | 
					    override fun getItemCount(): Int = items.size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView)
 | 
					    inner class ViewHolder(
 | 
				
			||||||
 | 
					        val binding: SourceListItemBinding,
 | 
				
			||||||
 | 
					    ) : RecyclerView.ViewHolder(binding.root) {
 | 
				
			||||||
 | 
					        private val context: Context = app.applicationContext
 | 
				
			||||||
 | 
					        private val repository: Repository by instance()
 | 
				
			||||||
 | 
					        private val appSettingsService: AppSettingsService by instance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fun bind(
 | 
				
			||||||
 | 
					            source: SelfossModel.SourceDetail,
 | 
				
			||||||
 | 
					            position: Int,
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            binding.apply {
 | 
				
			||||||
 | 
					                sourceTitle.text = source.title.getHtmlDecoded()
 | 
				
			||||||
 | 
					                if (source.getIcon(repository.baseUrl).isEmpty()) {
 | 
				
			||||||
 | 
					                    itemImage.setBackgroundAndText(source.title.getHtmlDecoded())
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    context.circularDrawable(source.getIcon(repository.baseUrl), itemImage, appSettingsService)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                errorText.apply {
 | 
				
			||||||
 | 
					                    visibility = if (!source.error.isNullOrBlank()) View.VISIBLE else View.GONE
 | 
				
			||||||
 | 
					                    text = source.error
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                deleteBtn.setOnClickListener { showDeleteConfirmationDialog(source, position) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                root.setOnClickListener {
 | 
				
			||||||
 | 
					                    repository.setSelectedSource(source)
 | 
				
			||||||
 | 
					                    app.startActivity(Intent(app, UpsertSourceActivity::class.java))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private fun showDeleteConfirmationDialog(
 | 
				
			||||||
 | 
					            source: SelfossModel.SourceDetail,
 | 
				
			||||||
 | 
					            position: Int,
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            AlertDialog
 | 
				
			||||||
 | 
					                .Builder(app)
 | 
				
			||||||
 | 
					                .setTitle(app.getString(R.string.confirm_delete_title))
 | 
				
			||||||
 | 
					                .setMessage(app.getString(R.string.confirm_delete_message, source.title))
 | 
				
			||||||
 | 
					                .setPositiveButton(android.R.string.ok) { _, _ -> deleteSource(source, position) }
 | 
				
			||||||
 | 
					                .setNegativeButton(android.R.string.cancel, null)
 | 
				
			||||||
 | 
					                .show()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private fun deleteSource(
 | 
				
			||||||
 | 
					            source: SelfossModel.SourceDetail,
 | 
				
			||||||
 | 
					            position: Int,
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
 | 
					                val successfullyDeletedSource = repository.deleteSource(source.id, source.title)
 | 
				
			||||||
 | 
					                launch(Dispatchers.Main) {
 | 
				
			||||||
 | 
					                    if (successfullyDeletedSource) {
 | 
				
			||||||
 | 
					                        items.removeAt(position)
 | 
				
			||||||
 | 
					                        notifyItemRemoved(position)
 | 
				
			||||||
 | 
					                        notifyItemRangeChanged(position, itemCount)
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        Toast
 | 
				
			||||||
 | 
					                            .makeText(
 | 
				
			||||||
 | 
					                                app,
 | 
				
			||||||
 | 
					                                R.string.can_delete_source,
 | 
				
			||||||
 | 
					                                Toast.LENGTH_SHORT,
 | 
				
			||||||
 | 
					                            ).show()
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,11 +23,15 @@ import kotlinx.coroutines.Dispatchers
 | 
				
			|||||||
import kotlinx.coroutines.launch
 | 
					import kotlinx.coroutines.launch
 | 
				
			||||||
import org.kodein.di.DIAware
 | 
					import org.kodein.di.DIAware
 | 
				
			||||||
import org.kodein.di.instance
 | 
					import org.kodein.di.instance
 | 
				
			||||||
import java.util.*
 | 
					import java.util.Timer
 | 
				
			||||||
import kotlin.concurrent.schedule
 | 
					import kotlin.concurrent.schedule
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LoadingWorker(val context: Context, params: WorkerParameters) :
 | 
					private const val NOTIFICATION_DELAY = 4000L
 | 
				
			||||||
    Worker(context, params),
 | 
					
 | 
				
			||||||
 | 
					class LoadingWorker(
 | 
				
			||||||
 | 
					    val context: Context,
 | 
				
			||||||
 | 
					    params: WorkerParameters,
 | 
				
			||||||
 | 
					) : Worker(context, params),
 | 
				
			||||||
    DIAware {
 | 
					    DIAware {
 | 
				
			||||||
    override val di by lazy { (applicationContext as MyApp).di }
 | 
					    override val di by lazy { (applicationContext as MyApp).di }
 | 
				
			||||||
    private val repository: Repository by instance()
 | 
					    private val repository: Repository by instance()
 | 
				
			||||||
@@ -40,12 +44,13 @@ class LoadingWorker(val context: Context, params: WorkerParameters) :
 | 
				
			|||||||
                    applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 | 
					                    applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                val notification =
 | 
					                val notification =
 | 
				
			||||||
                    NotificationCompat.Builder(applicationContext, AppSettingsService.syncChannelId)
 | 
					                    NotificationCompat
 | 
				
			||||||
 | 
					                        .Builder(applicationContext, AppSettingsService.SYNC_CHANNEL_ID)
 | 
				
			||||||
                        .setContentTitle(context.getString(R.string.loading_notification_title))
 | 
					                        .setContentTitle(context.getString(R.string.loading_notification_title))
 | 
				
			||||||
                        .setContentText(context.getString(R.string.loading_notification_text))
 | 
					                        .setContentText(context.getString(R.string.loading_notification_text))
 | 
				
			||||||
                        .setOngoing(true)
 | 
					                        .setOngoing(true)
 | 
				
			||||||
                        .setPriority(PRIORITY_LOW)
 | 
					                        .setPriority(PRIORITY_LOW)
 | 
				
			||||||
                        .setChannelId(AppSettingsService.syncChannelId)
 | 
					                        .setChannelId(AppSettingsService.SYNC_CHANNEL_ID)
 | 
				
			||||||
                        .setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
 | 
					                        .setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                notificationManager.notify(1, notification.build())
 | 
					                notificationManager.notify(1, notification.build())
 | 
				
			||||||
@@ -58,7 +63,7 @@ class LoadingWorker(val context: Context, params: WorkerParameters) :
 | 
				
			|||||||
                        handleNewItemsNotification(apiItems, notificationManager)
 | 
					                        handleNewItemsNotification(apiItems, notificationManager)
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                apiItems.map { it.preloadImages(context) }
 | 
					                apiItems.map { it.preloadImages(context, appSettingsService) }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return Result.success()
 | 
					        return Result.success()
 | 
				
			||||||
@@ -87,28 +92,27 @@ class LoadingWorker(val context: Context, params: WorkerParameters) :
 | 
				
			|||||||
                    PendingIntent.getActivity(context, 0, intent, pflags)
 | 
					                    PendingIntent.getActivity(context, 0, intent, pflags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                val newItemsNotification =
 | 
					                val newItemsNotification =
 | 
				
			||||||
                    NotificationCompat.Builder(
 | 
					                    NotificationCompat
 | 
				
			||||||
                        applicationContext,
 | 
					                        .Builder(
 | 
				
			||||||
                        AppSettingsService.newItemsChannelId,
 | 
					                            applicationContext,
 | 
				
			||||||
                    )
 | 
					                            AppSettingsService.NEW_ITEMS_CHANNEL,
 | 
				
			||||||
                        .setContentTitle(context.getString(R.string.new_items_notification_title))
 | 
					                        ).setContentTitle(context.getString(R.string.new_items_notification_title))
 | 
				
			||||||
                        .setContentText(
 | 
					                        .setContentText(
 | 
				
			||||||
                            context.getString(
 | 
					                            context.getString(
 | 
				
			||||||
                                R.string.new_items_notification_text,
 | 
					                                R.string.new_items_notification_text,
 | 
				
			||||||
                                newSize,
 | 
					                                newSize,
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                        )
 | 
					                        ).setPriority(PRIORITY_DEFAULT)
 | 
				
			||||||
                        .setPriority(PRIORITY_DEFAULT)
 | 
					                        .setChannelId(AppSettingsService.NEW_ITEMS_CHANNEL)
 | 
				
			||||||
                        .setChannelId(AppSettingsService.newItemsChannelId)
 | 
					 | 
				
			||||||
                        .setContentIntent(pendingIntent)
 | 
					                        .setContentIntent(pendingIntent)
 | 
				
			||||||
                        .setAutoCancel(true)
 | 
					                        .setAutoCancel(true)
 | 
				
			||||||
                        .setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
 | 
					                        .setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Timer("", false).schedule(4000) {
 | 
					                Timer("", false).schedule(NOTIFICATION_DELAY) {
 | 
				
			||||||
                    notificationManager.notify(2, newItemsNotification.build())
 | 
					                    notificationManager.notify(2, newItemsNotification.build())
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            Timer("", false).schedule(4000) {
 | 
					            Timer("", false).schedule(NOTIFICATION_DELAY) {
 | 
				
			||||||
                notificationManager.cancel(1)
 | 
					                notificationManager.cancel(1)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -1,17 +1,15 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.android.fragments
 | 
					package bou.amine.apps.readerforselfossv2.android.fragments
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Context
 | 
				
			||||||
import android.content.Intent
 | 
					import android.content.Intent
 | 
				
			||||||
import android.content.res.ColorStateList
 | 
					 | 
				
			||||||
import android.content.res.TypedArray
 | 
					import android.content.res.TypedArray
 | 
				
			||||||
import android.graphics.Bitmap
 | 
					import android.graphics.Bitmap
 | 
				
			||||||
import android.graphics.Typeface
 | 
					import android.graphics.Typeface
 | 
				
			||||||
import android.graphics.drawable.ColorDrawable
 | 
					 | 
				
			||||||
import android.os.Bundle
 | 
					import android.os.Bundle
 | 
				
			||||||
import android.util.TypedValue
 | 
					import android.util.TypedValue.DATA_NULL_UNDEFINED
 | 
				
			||||||
import android.view.GestureDetector
 | 
					import android.view.GestureDetector
 | 
				
			||||||
import android.view.InflateException
 | 
					import android.view.InflateException
 | 
				
			||||||
import android.view.LayoutInflater
 | 
					import android.view.LayoutInflater
 | 
				
			||||||
import android.view.MenuItem
 | 
					 | 
				
			||||||
import android.view.MotionEvent
 | 
					import android.view.MotionEvent
 | 
				
			||||||
import android.view.View
 | 
					import android.view.View
 | 
				
			||||||
import android.view.ViewGroup
 | 
					import android.view.ViewGroup
 | 
				
			||||||
@@ -21,7 +19,6 @@ import android.webkit.WebView
 | 
				
			|||||||
import android.webkit.WebViewClient
 | 
					import android.webkit.WebViewClient
 | 
				
			||||||
import android.widget.Toast
 | 
					import android.widget.Toast
 | 
				
			||||||
import androidx.appcompat.app.AlertDialog
 | 
					import androidx.appcompat.app.AlertDialog
 | 
				
			||||||
import androidx.core.widget.NestedScrollView
 | 
					 | 
				
			||||||
import androidx.fragment.app.Fragment
 | 
					import androidx.fragment.app.Fragment
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.ImageActivity
 | 
					import bou.amine.apps.readerforselfossv2.android.ImageActivity
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
					import bou.amine.apps.readerforselfossv2.android.R
 | 
				
			||||||
@@ -30,25 +27,27 @@ import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
 | 
				
			|||||||
import bou.amine.apps.readerforselfossv2.android.model.toModel
 | 
					import bou.amine.apps.readerforselfossv2.android.model.toModel
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.model.toParcelable
 | 
					import bou.amine.apps.readerforselfossv2.android.model.toParcelable
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.bottombar.addHomeMadeActionItem
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.getColorFromAttr
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapFitCenter
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.glide.getGlideImageForResource
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowserAsNewTask
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.shareLink
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.model.MercuryModel
 | 
					import bou.amine.apps.readerforselfossv2.model.MercuryModel
 | 
				
			||||||
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.rest.MercuryApi
 | 
					import bou.amine.apps.readerforselfossv2.rest.MercuryApi
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
					import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.service.ConnectivityService
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
 | 
					import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.getImages
 | 
					import bou.amine.apps.readerforselfossv2.utils.getImages
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
 | 
					import bou.amine.apps.readerforselfossv2.utils.getThumbnail
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
 | 
					import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
 | 
				
			||||||
import com.bumptech.glide.Glide
 | 
					import com.leinardi.android.speeddial.SpeedDialView
 | 
				
			||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
					 | 
				
			||||||
import com.bumptech.glide.request.RequestOptions
 | 
					 | 
				
			||||||
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
 | 
					 | 
				
			||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
 | 
					 | 
				
			||||||
import kotlinx.coroutines.CoroutineScope
 | 
					import kotlinx.coroutines.CoroutineScope
 | 
				
			||||||
import kotlinx.coroutines.Dispatchers
 | 
					import kotlinx.coroutines.Dispatchers
 | 
				
			||||||
import kotlinx.coroutines.launch
 | 
					import kotlinx.coroutines.launch
 | 
				
			||||||
@@ -63,28 +62,38 @@ import java.util.Locale
 | 
				
			|||||||
import java.util.concurrent.ExecutionException
 | 
					import java.util.concurrent.ExecutionException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private const val IMAGE_JPG = "image/jpg"
 | 
					private const val IMAGE_JPG = "image/jpg"
 | 
				
			||||||
 | 
					private const val IMAGE_PNG = "image/png"
 | 
				
			||||||
 | 
					private const val IMAGE_WEBP = "image/webp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ArticleFragment : Fragment(), DIAware {
 | 
					private const val WHITE_COLOR_HEX = 0xFFFFFF
 | 
				
			||||||
    private var fontSize: Int = 16
 | 
					
 | 
				
			||||||
 | 
					private const val DEFAULT_FONT_SIZE = 16
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ArticleFragment :
 | 
				
			||||||
 | 
					    Fragment(),
 | 
				
			||||||
 | 
					    DIAware {
 | 
				
			||||||
 | 
					    private var colorOnSurface: Int = 0
 | 
				
			||||||
 | 
					    private var colorSurface: Int = 0
 | 
				
			||||||
 | 
					    private var fontSize: Int = DEFAULT_FONT_SIZE
 | 
				
			||||||
    private lateinit var item: SelfossModel.Item
 | 
					    private lateinit var item: SelfossModel.Item
 | 
				
			||||||
    private lateinit var url: String
 | 
					    private var url: String? = null
 | 
				
			||||||
    private lateinit var contentText: String
 | 
					    private lateinit var contentText: String
 | 
				
			||||||
    private lateinit var contentSource: String
 | 
					    private lateinit var contentSource: String
 | 
				
			||||||
    private lateinit var contentImage: String
 | 
					    private lateinit var contentImage: String
 | 
				
			||||||
    private lateinit var contentTitle: String
 | 
					    private lateinit var contentTitle: String
 | 
				
			||||||
    private lateinit var allImages: ArrayList<String>
 | 
					    private lateinit var allImages: ArrayList<String>
 | 
				
			||||||
    private lateinit var fab: FloatingActionButton
 | 
					    private lateinit var fab: SpeedDialView
 | 
				
			||||||
    private lateinit var textAlignment: String
 | 
					    private lateinit var textAlignment: String
 | 
				
			||||||
    private lateinit var binding: FragmentArticleBinding
 | 
					    private lateinit var binding: FragmentArticleBinding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override val di: DI by closestDI()
 | 
					    override val di: 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()
 | 
				
			||||||
 | 
					    private val connectivityService: ConnectivityService by instance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private var typeface: Typeface? = null
 | 
					    private var typeface: Typeface? = null
 | 
				
			||||||
    private var resId: Int = 0
 | 
					    private var resId: Int = 0
 | 
				
			||||||
    private var font = ""
 | 
					    private var font = ""
 | 
				
			||||||
    private var staticBar = false
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val mercuryApi: MercuryApi by instance()
 | 
					    private val mercuryApi: MercuryApi by instance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -96,6 +105,7 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
        item = pi.toModel()
 | 
					        item = pi.toModel()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Suppress("detekt:LongMethod")
 | 
				
			||||||
    override fun onCreateView(
 | 
					    override fun onCreateView(
 | 
				
			||||||
        inflater: LayoutInflater,
 | 
					        inflater: LayoutInflater,
 | 
				
			||||||
        container: ViewGroup?,
 | 
					        container: ViewGroup?,
 | 
				
			||||||
@@ -110,35 +120,27 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
                e.sendSilentlyWithAcra()
 | 
					                e.sendSilentlyWithAcra()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            colorOnSurface = getColorFromAttr(com.google.android.material.R.attr.colorOnSurface)
 | 
				
			||||||
 | 
					            colorSurface = getColorFromAttr(com.google.android.material.R.attr.colorSurface)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            contentText = item.content
 | 
					            contentText = item.content
 | 
				
			||||||
            contentTitle = item.title.getHtmlDecoded()
 | 
					            contentTitle = item.title.getHtmlDecoded()
 | 
				
			||||||
            contentImage = item.getThumbnail(repository.baseUrl)
 | 
					            contentImage = item.getThumbnail(repository.baseUrl)
 | 
				
			||||||
            contentSource = try {
 | 
					            contentSource =
 | 
				
			||||||
                item.sourceAuthorAndDate()
 | 
					                try {
 | 
				
			||||||
            } catch (e: Exception) {
 | 
					                    item.sourceAuthorAndDate()
 | 
				
			||||||
                e.sendSilentlyWithAcraWithName("Article Fragment parse date")
 | 
					                } catch (e: Exception) {
 | 
				
			||||||
                item.sourceAuthorOnly()
 | 
					                    e.sendSilentlyWithAcraWithName("Article Fragment parse date")
 | 
				
			||||||
            }
 | 
					                    item.sourceAuthorOnly()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            allImages = item.getImages()
 | 
					            allImages = item.getImages()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            fontSize = appSettingsService.getFontSize()
 | 
					            fontSize = appSettingsService.getFontSize()
 | 
				
			||||||
            staticBar = appSettingsService.isStaticBarEnabled()
 | 
					 | 
				
			||||||
            font = appSettingsService.getFont()
 | 
					            font = appSettingsService.getFont()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            refreshAlignment()
 | 
					            refreshAlignment()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            fab = binding.fab
 | 
					            handleFloatingToolbar()
 | 
				
			||||||
 | 
					 | 
				
			||||||
            fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            fab.rippleColor = resources.getColor(R.color.colorAccentDark)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            val floatingToolbar: FloatingToolbar = handleFloatingToolbar()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (staticBar) {
 | 
					 | 
				
			||||||
                fab.hide()
 | 
					 | 
				
			||||||
                floatingToolbar.show()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            binding.source.text = contentSource
 | 
					            binding.source.text = contentSource
 | 
				
			||||||
            if (typeface != null) {
 | 
					            if (typeface != null) {
 | 
				
			||||||
@@ -146,37 +148,20 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            handleContent()
 | 
					            handleContent()
 | 
				
			||||||
 | 
					 | 
				
			||||||
            binding.nestedScrollView.setOnScrollChangeListener(
 | 
					 | 
				
			||||||
                NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
 | 
					 | 
				
			||||||
                    if (scrollY > oldScrollY) {
 | 
					 | 
				
			||||||
                        floatingToolbar.hide()
 | 
					 | 
				
			||||||
                        fab.hide()
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        if (staticBar) {
 | 
					 | 
				
			||||||
                            floatingToolbar.show()
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        } catch (e: InflateException) {
 | 
					        } catch (e: InflateException) {
 | 
				
			||||||
            e.sendSilentlyWithAcraWithName("webview not available")
 | 
					            e.sendSilentlyWithAcraWithName("webview not available")
 | 
				
			||||||
            try {
 | 
					            maybeIfContext {
 | 
				
			||||||
                AlertDialog.Builder(requireContext())
 | 
					                AlertDialog
 | 
				
			||||||
                    .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
 | 
					                    .Builder(it)
 | 
				
			||||||
                    .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
 | 
					                    .setMessage(it.getString(R.string.webview_dialog_issue_message))
 | 
				
			||||||
 | 
					                    .setTitle(it.getString(R.string.webview_dialog_issue_title))
 | 
				
			||||||
                    .setPositiveButton(
 | 
					                    .setPositiveButton(
 | 
				
			||||||
                        android.R.string.ok,
 | 
					                        android.R.string.ok,
 | 
				
			||||||
                    ) { _, _ ->
 | 
					                    ) { _, _ ->
 | 
				
			||||||
                        appSettingsService.disableArticleViewer()
 | 
					                        appSettingsService.disableArticleViewer()
 | 
				
			||||||
                        requireActivity().finish()
 | 
					                        requireActivity().finish()
 | 
				
			||||||
                    }
 | 
					                    }.create()
 | 
				
			||||||
                    .create()
 | 
					 | 
				
			||||||
                    .show()
 | 
					                    .show()
 | 
				
			||||||
            } catch (e: IllegalStateException) {
 | 
					 | 
				
			||||||
                e.sendSilentlyWithAcraWithName("Context required is null")
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -185,8 +170,8 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private fun handleContent() {
 | 
					    private fun handleContent() {
 | 
				
			||||||
        if (contentText.isEmptyOrNullOrNullString()) {
 | 
					        if (contentText.isEmptyOrNullOrNullString()) {
 | 
				
			||||||
            if (repository.isNetworkAvailable()) {
 | 
					            if (connectivityService.isNetworkAvailable() && url.isUrlValid()) {
 | 
				
			||||||
                getContentFromMercury()
 | 
					                getContentFromMercury(url!!)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            binding.titleView.text = contentTitle
 | 
					            binding.titleView.text = contentTitle
 | 
				
			||||||
@@ -198,82 +183,99 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
 | 
					            if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
 | 
				
			||||||
                binding.imageView.visibility = View.VISIBLE
 | 
					                binding.imageView.visibility = View.VISIBLE
 | 
				
			||||||
                Glide
 | 
					                maybeIfContext { it.bitmapFitCenter(contentImage, binding.imageView, appSettingsService) }
 | 
				
			||||||
                    .with(requireContext())
 | 
					 | 
				
			||||||
                    .asBitmap()
 | 
					 | 
				
			||||||
                    .load(contentImage)
 | 
					 | 
				
			||||||
                    .apply(RequestOptions.fitCenterTransform())
 | 
					 | 
				
			||||||
                    .into(binding.imageView)
 | 
					 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                binding.imageView.visibility = View.GONE
 | 
					                binding.imageView.visibility = View.GONE
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun handleFloatingToolbar(): FloatingToolbar {
 | 
					    private fun handleFloatingToolbar() {
 | 
				
			||||||
        val floatingToolbar: FloatingToolbar = binding.floatingToolbar
 | 
					        fab = binding.speedDial
 | 
				
			||||||
        if (appSettingsService.getPublicAccess()) {
 | 
					        fab.mainFabClosedIconColor = colorOnSurface
 | 
				
			||||||
            floatingToolbar.setMenu(R.menu.reader_toolbar_no_read)
 | 
					        fab.mainFabOpenedIconColor = colorOnSurface
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        floatingToolbar.attachFab(fab)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent))
 | 
					        maybeIfContext { handleFloatingToolbarActionItems(it) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        floatingToolbar.setClickListener(
 | 
					        fab.setOnActionSelectedListener { actionItem ->
 | 
				
			||||||
            object : FloatingToolbar.ItemClickListener {
 | 
					            when (actionItem.id) {
 | 
				
			||||||
                override fun onItemClick(item: MenuItem) {
 | 
					                R.id.share_action -> requireActivity().shareLink(url, contentTitle)
 | 
				
			||||||
                    when (item.itemId) {
 | 
					                R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
 | 
				
			||||||
                        R.id.share_action -> requireActivity().shareLink(url, contentTitle)
 | 
					                R.id.unread_action ->
 | 
				
			||||||
                        R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
 | 
					                    if (this@ArticleFragment.item.unread) {
 | 
				
			||||||
                        R.id.unread_action ->
 | 
					                        CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
                            try {
 | 
					                            repository.markAsRead(this@ArticleFragment.item)
 | 
				
			||||||
                                if (this@ArticleFragment.item.unread) {
 | 
					                        }
 | 
				
			||||||
                                    CoroutineScope(Dispatchers.IO).launch {
 | 
					                        this@ArticleFragment.item.unread = false
 | 
				
			||||||
                                        repository.markAsRead(this@ArticleFragment.item)
 | 
					                        maybeIfContext {
 | 
				
			||||||
                                    }
 | 
					                            Toast
 | 
				
			||||||
                                    this@ArticleFragment.item.unread = false
 | 
					                                .makeText(
 | 
				
			||||||
                                    Toast.makeText(
 | 
					                                    it,
 | 
				
			||||||
                                        requireContext(),
 | 
					                                    R.string.marked_as_read,
 | 
				
			||||||
                                        R.string.marked_as_read,
 | 
					                                    Toast.LENGTH_LONG,
 | 
				
			||||||
                                        Toast.LENGTH_LONG,
 | 
					                                ).show()
 | 
				
			||||||
                                    ).show()
 | 
					                        }
 | 
				
			||||||
                                } else {
 | 
					                    } else {
 | 
				
			||||||
                                    CoroutineScope(Dispatchers.IO).launch {
 | 
					                        CoroutineScope(Dispatchers.IO).launch {
 | 
				
			||||||
                                        repository.unmarkAsRead(this@ArticleFragment.item)
 | 
					                            repository.unmarkAsRead(this@ArticleFragment.item)
 | 
				
			||||||
                                    }
 | 
					                        }
 | 
				
			||||||
                                    this@ArticleFragment.item.unread = true
 | 
					                        this@ArticleFragment.item.unread = true
 | 
				
			||||||
                                    Toast.makeText(
 | 
					                        maybeIfContext {
 | 
				
			||||||
                                        context,
 | 
					                            Toast
 | 
				
			||||||
                                        R.string.marked_as_unread,
 | 
					                                .makeText(
 | 
				
			||||||
                                        Toast.LENGTH_LONG,
 | 
					                                    it,
 | 
				
			||||||
                                    ).show()
 | 
					                                    R.string.marked_as_unread,
 | 
				
			||||||
                                }
 | 
					                                    Toast.LENGTH_LONG,
 | 
				
			||||||
                            } catch (e: IllegalStateException) {
 | 
					                                ).show()
 | 
				
			||||||
                                e.sendSilentlyWithAcraWithName("Context required is null")
 | 
					                        }
 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        else -> Unit
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                override fun onItemLongClick(item: MenuItem?) {
 | 
					                else -> Unit
 | 
				
			||||||
                    // We do nothing
 | 
					            }
 | 
				
			||||||
                }
 | 
					            false
 | 
				
			||||||
            },
 | 
					        }
 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        return floatingToolbar
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun refreshAlignment() {
 | 
					    private fun handleFloatingToolbarActionItems(c: Context) {
 | 
				
			||||||
 | 
					        fab.addHomeMadeActionItem(
 | 
				
			||||||
 | 
					            R.id.share_action,
 | 
				
			||||||
 | 
					            resources.getDrawable(R.drawable.ic_share_white_24dp),
 | 
				
			||||||
 | 
					            R.string.reader_action_share,
 | 
				
			||||||
 | 
					            colorOnSurface,
 | 
				
			||||||
 | 
					            colorSurface,
 | 
				
			||||||
 | 
					            c,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        fab.addHomeMadeActionItem(
 | 
				
			||||||
 | 
					            R.id.open_action,
 | 
				
			||||||
 | 
					            resources.getDrawable(R.drawable.ic_open_in_browser_white_24dp),
 | 
				
			||||||
 | 
					            R.string.reader_action_open,
 | 
				
			||||||
 | 
					            colorOnSurface,
 | 
				
			||||||
 | 
					            colorSurface,
 | 
				
			||||||
 | 
					            c,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        fab.addHomeMadeActionItem(
 | 
				
			||||||
 | 
					            R.id.unread_action,
 | 
				
			||||||
 | 
					            resources.getDrawable(R.drawable.ic_baseline_white_eye_24dp),
 | 
				
			||||||
 | 
					            R.string.unmark,
 | 
				
			||||||
 | 
					            colorOnSurface,
 | 
				
			||||||
 | 
					            colorSurface,
 | 
				
			||||||
 | 
					            c,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun refreshAlignment() {
 | 
				
			||||||
        textAlignment =
 | 
					        textAlignment =
 | 
				
			||||||
            when (appSettingsService.getActiveAllignment()) {
 | 
					            when (appSettingsService.getActiveAllignment()) {
 | 
				
			||||||
                1 -> "justify"
 | 
					                1 -> "justify"
 | 
				
			||||||
                2 -> "left"
 | 
					                2 -> "left"
 | 
				
			||||||
                else -> "justify"
 | 
					                else -> "justify"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        htmlToWebview()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun getContentFromMercury() {
 | 
					    @Suppress("detekt:SwallowedException")
 | 
				
			||||||
 | 
					    private fun getContentFromMercury(url: String) {
 | 
				
			||||||
        binding.progressBar.visibility = View.VISIBLE
 | 
					        binding.progressBar.visibility = View.VISIBLE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        CoroutineScope(Dispatchers.Main).launch {
 | 
					        CoroutineScope(Dispatchers.Main).launch {
 | 
				
			||||||
@@ -311,17 +313,12 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun handleLeadImage(lead_image_url: String?) {
 | 
					    private fun handleLeadImage(leadImageUrl: String?) {
 | 
				
			||||||
        if (!lead_image_url.isNullOrEmpty() && context != null) {
 | 
					        if (!leadImageUrl.isNullOrEmpty()) {
 | 
				
			||||||
            binding.imageView.visibility = View.VISIBLE
 | 
					            maybeIfContext {
 | 
				
			||||||
            Glide
 | 
					                binding.imageView.visibility = View.VISIBLE
 | 
				
			||||||
                .with(requireContext())
 | 
					                it.bitmapFitCenter(leadImageUrl, binding.imageView, appSettingsService)
 | 
				
			||||||
                .asBitmap()
 | 
					            }
 | 
				
			||||||
                .load(
 | 
					 | 
				
			||||||
                    lead_image_url,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .apply(RequestOptions.fitCenterTransform())
 | 
					 | 
				
			||||||
                .into(binding.imageView)
 | 
					 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            binding.imageView.visibility = View.GONE
 | 
					            binding.imageView.visibility = View.GONE
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -334,140 +331,137 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
                override fun shouldOverrideUrlLoading(
 | 
					                override fun shouldOverrideUrlLoading(
 | 
				
			||||||
                    view: WebView?,
 | 
					                    view: WebView?,
 | 
				
			||||||
                    url: String,
 | 
					                    url: String,
 | 
				
			||||||
                ): Boolean {
 | 
					                ): Boolean =
 | 
				
			||||||
                    return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
 | 
					                    if (url.isUrlValid() &&
 | 
				
			||||||
                        requireContext().openUrlInBrowser(url)
 | 
					                        binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
 | 
				
			||||||
 | 
					                    ) {
 | 
				
			||||||
 | 
					                        maybeIfContext { it.openUrlInBrowserAsNewTask(url) }
 | 
				
			||||||
                        true
 | 
					                        true
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        false
 | 
					                        false
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @Suppress("detekt:SwallowedException", "detekt:ReturnCount")
 | 
				
			||||||
                @Deprecated("Deprecated in Java")
 | 
					                @Deprecated("Deprecated in Java")
 | 
				
			||||||
                override fun shouldInterceptRequest(
 | 
					                override fun shouldInterceptRequest(
 | 
				
			||||||
                    view: WebView,
 | 
					                    view: WebView,
 | 
				
			||||||
                    url: String,
 | 
					                    url: String,
 | 
				
			||||||
                ): WebResourceResponse? {
 | 
					                ): WebResourceResponse? {
 | 
				
			||||||
                    val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
 | 
					                    val (mime: String?, compression: Bitmap.CompressFormat) =
 | 
				
			||||||
                    if (url.lowercase(Locale.US).contains(".jpg") ||
 | 
					                        if (url
 | 
				
			||||||
                        url.lowercase(Locale.US)
 | 
					                                .lowercase(Locale.US)
 | 
				
			||||||
                            .contains(".jpeg")
 | 
					                                .contains(".jpg") ||
 | 
				
			||||||
                    ) {
 | 
					                            url.lowercase(Locale.US).contains(".jpeg")
 | 
				
			||||||
                        try {
 | 
					                        ) {
 | 
				
			||||||
                            val image =
 | 
					                            Pair(IMAGE_JPG, Bitmap.CompressFormat.JPEG)
 | 
				
			||||||
                                Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
 | 
					                        } else if (url.lowercase(Locale.US).contains(".png")) {
 | 
				
			||||||
                                    .get()
 | 
					                            Pair(IMAGE_PNG, Bitmap.CompressFormat.PNG)
 | 
				
			||||||
                            return WebResourceResponse(
 | 
					                        } else if (url.lowercase(Locale.US).contains(".webp")) {
 | 
				
			||||||
                                IMAGE_JPG,
 | 
					                            Pair(IMAGE_WEBP, Bitmap.CompressFormat.WEBP)
 | 
				
			||||||
                                "UTF-8",
 | 
					                        } else {
 | 
				
			||||||
                                getBitmapInputStream(image, Bitmap.CompressFormat.JPEG),
 | 
					                            return super.shouldInterceptRequest(view, url)
 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                        } catch (e: ExecutionException) {
 | 
					 | 
				
			||||||
                            // Do nothing
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    } else if (url.lowercase(Locale.US).contains(".png")) {
 | 
					 | 
				
			||||||
                        try {
 | 
					 | 
				
			||||||
                            val image =
 | 
					 | 
				
			||||||
                                Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
 | 
					 | 
				
			||||||
                                    .get()
 | 
					 | 
				
			||||||
                            return WebResourceResponse(
 | 
					 | 
				
			||||||
                                IMAGE_JPG,
 | 
					 | 
				
			||||||
                                "UTF-8",
 | 
					 | 
				
			||||||
                                getBitmapInputStream(image, Bitmap.CompressFormat.PNG),
 | 
					 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                        } catch (e: ExecutionException) {
 | 
					 | 
				
			||||||
                            // Do nothing
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    } else if (url.lowercase(Locale.US).contains(".webp")) {
 | 
					 | 
				
			||||||
                        try {
 | 
					 | 
				
			||||||
                            val image =
 | 
					 | 
				
			||||||
                                Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
 | 
					 | 
				
			||||||
                                    .get()
 | 
					 | 
				
			||||||
                            return WebResourceResponse(
 | 
					 | 
				
			||||||
                                IMAGE_JPG,
 | 
					 | 
				
			||||||
                                "UTF-8",
 | 
					 | 
				
			||||||
                                getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
 | 
					 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                        } catch (e: ExecutionException) {
 | 
					 | 
				
			||||||
                            // Do nothing
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    return super.shouldInterceptRequest(view, url)
 | 
					                    try {
 | 
				
			||||||
 | 
					                        val image = view.getGlideImageForResource(url, appSettingsService)
 | 
				
			||||||
 | 
					                        return WebResourceResponse(
 | 
				
			||||||
 | 
					                            mime,
 | 
				
			||||||
 | 
					                            "UTF-8",
 | 
				
			||||||
 | 
					                            getBitmapInputStream(image, compression),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    } catch (e: ExecutionException) {
 | 
				
			||||||
 | 
					                        return super.shouldInterceptRequest(view, url)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Suppress("detekt:LongMethod", "detekt:ImplicitDefaultLocale")
 | 
				
			||||||
    private fun htmlToWebview() {
 | 
					    private fun htmlToWebview() {
 | 
				
			||||||
        try {
 | 
					        maybeIfContext {
 | 
				
			||||||
            val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
 | 
					            val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
 | 
				
			||||||
            val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
 | 
					            val a: TypedArray = it.obtainStyledAttributes(resId, attrs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            binding.webcontent.settings.standardFontFamily = a.getString(0)
 | 
					            binding.webcontent.settings.standardFontFamily = a.getString(0)
 | 
				
			||||||
            binding.webcontent.visibility = View.VISIBLE
 | 
					            ""
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        binding.webcontent.visibility = View.VISIBLE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            val colorOnSurface = TypedValue()
 | 
					        val colorSurfaceString =
 | 
				
			||||||
            requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
 | 
					            String.format(
 | 
				
			||||||
 | 
					                "#%06X",
 | 
				
			||||||
 | 
					                WHITE_COLOR_HEX and (if (colorSurface != DATA_NULL_UNDEFINED) colorSurface else WHITE_COLOR_HEX),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            val colorSurface = TypedValue()
 | 
					        val colorOnSurfaceString =
 | 
				
			||||||
            requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
 | 
					            String.format(
 | 
				
			||||||
 | 
					                "#%06X",
 | 
				
			||||||
 | 
					                WHITE_COLOR_HEX and (if (colorOnSurface != DATA_NULL_UNDEFINED) colorOnSurface else 0),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            binding.webcontent.settings.useWideViewPort = true
 | 
					        binding.webcontent.settings.useWideViewPort = true
 | 
				
			||||||
            binding.webcontent.settings.loadWithOverviewMode = true
 | 
					        binding.webcontent.settings.loadWithOverviewMode = true
 | 
				
			||||||
            binding.webcontent.settings.javaScriptEnabled = false
 | 
					        binding.webcontent.settings.javaScriptEnabled = false
 | 
				
			||||||
 | 
					 | 
				
			||||||
            handleImageLoading()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        handleImageLoading()
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
            val gestureDetector =
 | 
					            val gestureDetector =
 | 
				
			||||||
                GestureDetector(
 | 
					                GestureDetector(
 | 
				
			||||||
                    activity,
 | 
					                    activity,
 | 
				
			||||||
                    object : GestureDetector.SimpleOnGestureListener() {
 | 
					                    object : GestureDetector.SimpleOnGestureListener() {
 | 
				
			||||||
                        override fun onSingleTapUp(e: MotionEvent): Boolean {
 | 
					                        override fun onSingleTapUp(e: MotionEvent): Boolean = performClick()
 | 
				
			||||||
                            return performClick()
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
 | 
					            binding.webcontent.setOnTouchListener { _, event ->
 | 
				
			||||||
 | 
					                gestureDetector.onTouchEvent(
 | 
				
			||||||
            binding.webcontent.settings.layoutAlgorithm =
 | 
					                    event,
 | 
				
			||||||
                WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
 | 
					                )
 | 
				
			||||||
 | 
					 | 
				
			||||||
            var baseUrl: String? = null
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                val itemUrl = URL(url)
 | 
					 | 
				
			||||||
                baseUrl = itemUrl.protocol + "://" + itemUrl.host
 | 
					 | 
				
			||||||
            } catch (e: MalformedURLException) {
 | 
					 | 
				
			||||||
                e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (e: IllegalStateException) {
 | 
				
			||||||
 | 
					            e.sendSilentlyWithAcraWithName("Gesture detector issue ?")
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            val fontName =
 | 
					        binding.webcontent.settings.layoutAlgorithm =
 | 
				
			||||||
 | 
					            WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var baseUrl: String? = null
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            val itemUrl = URL(url.orEmpty())
 | 
				
			||||||
 | 
					            baseUrl = itemUrl.protocol + "://" + itemUrl.host
 | 
				
			||||||
 | 
					        } catch (e: MalformedURLException) {
 | 
				
			||||||
 | 
					            e.sendSilentlyWithAcraWithName("htmlToWebview > ${url.orEmpty()}")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val fontName: String =
 | 
				
			||||||
 | 
					            maybeIfContext {
 | 
				
			||||||
                when (font) {
 | 
					                when (font) {
 | 
				
			||||||
                    getString(R.string.open_sans_font_id) -> "Open Sans"
 | 
					                    it.getString(R.string.open_sans_font_id) -> "Open Sans"
 | 
				
			||||||
                    getString(R.string.roboto_font_id) -> "Roboto"
 | 
					                    it.getString(R.string.roboto_font_id) -> "Roboto"
 | 
				
			||||||
                    getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
 | 
					                    it.getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
 | 
				
			||||||
                    else -> ""
 | 
					                    else -> ""
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					            }?.toString().orEmpty()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            val fontLinkAndStyle =
 | 
					        val fontLinkAndStyle =
 | 
				
			||||||
                if (font.isNotEmpty()) {
 | 
					            if (fontName.isNotEmpty()) {
 | 
				
			||||||
                    """<link href="https://fonts.googleapis.com/css?family=${
 | 
					                """<link href="https://fonts.googleapis.com/css?family=${
 | 
				
			||||||
                        fontName.replace(
 | 
					                    fontName.replace(
 | 
				
			||||||
                            " ",
 | 
					                        " ",
 | 
				
			||||||
                            "+",
 | 
					                        "+",
 | 
				
			||||||
                        )
 | 
					                    )
 | 
				
			||||||
                    }" rel="stylesheet">
 | 
					                }" rel="stylesheet">
 | 
				
			||||||
                |<style>
 | 
					                |<style>
 | 
				
			||||||
                |   * {
 | 
					                |   * {
 | 
				
			||||||
                |       font-family: '$fontName';
 | 
					                |       font-family: '$fontName';
 | 
				
			||||||
                |   }
 | 
					                |   }
 | 
				
			||||||
                |</style>
 | 
					                |</style>
 | 
				
			||||||
                    """.trimMargin()
 | 
					                """.trimMargin()
 | 
				
			||||||
                } else {
 | 
					            } else {
 | 
				
			||||||
                    ""
 | 
					                ""
 | 
				
			||||||
                }
 | 
					            }
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
            binding.webcontent.loadDataWithBaseURL(
 | 
					            binding.webcontent.loadDataWithBaseURL(
 | 
				
			||||||
                baseUrl,
 | 
					                baseUrl,
 | 
				
			||||||
                """<html>
 | 
					                """<html>
 | 
				
			||||||
@@ -484,12 +478,12 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
                |        color: ${
 | 
					                |        color: ${
 | 
				
			||||||
                    String.format(
 | 
					                    String.format(
 | 
				
			||||||
                        "#%06X",
 | 
					                        "#%06X",
 | 
				
			||||||
                        0xFFFFFF and resources.getColor(R.color.colorAccent),
 | 
					                        WHITE_COLOR_HEX and (maybeIfContext { it.resources.getColor(R.color.colorAccent) } as Int),
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                } !important;
 | 
					                } !important;
 | 
				
			||||||
                |      }
 | 
					                |      }
 | 
				
			||||||
                |      *:not(a) {
 | 
					                |      *:not(a) {
 | 
				
			||||||
                |        color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)};
 | 
					                |        color: $colorOnSurfaceString;
 | 
				
			||||||
                |      }
 | 
					                |      }
 | 
				
			||||||
                |      * {
 | 
					                |      * {
 | 
				
			||||||
                |        font-size: ${fontSize}px;
 | 
					                |        font-size: ${fontSize}px;
 | 
				
			||||||
@@ -497,26 +491,11 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
                |        word-break: break-word;
 | 
					                |        word-break: break-word;
 | 
				
			||||||
                |        overflow:hidden;
 | 
					                |        overflow:hidden;
 | 
				
			||||||
                |        line-height: 1.5em;
 | 
					                |        line-height: 1.5em;
 | 
				
			||||||
                |        background-color: ${
 | 
					                |        background-color: $colorSurfaceString;
 | 
				
			||||||
                    String.format(
 | 
					 | 
				
			||||||
                        "#%06X",
 | 
					 | 
				
			||||||
                        0xFFFFFF and colorSurface.data,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                |      }
 | 
					                |      }
 | 
				
			||||||
                |      body, html {
 | 
					                |      body, html {
 | 
				
			||||||
                |        background-color: ${
 | 
					                |        background-color: $colorSurfaceString !important;
 | 
				
			||||||
                    String.format(
 | 
					                |        border-color: $colorSurfaceString  !important;
 | 
				
			||||||
                        "#%06X",
 | 
					 | 
				
			||||||
                        0xFFFFFF and colorSurface.data,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                } !important;
 | 
					 | 
				
			||||||
                |        border-color: ${
 | 
					 | 
				
			||||||
                    String.format(
 | 
					 | 
				
			||||||
                        "#%06X",
 | 
					 | 
				
			||||||
                        0xFFFFFF and colorSurface.data,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                }  !important;
 | 
					 | 
				
			||||||
                |        padding: 0 !important;
 | 
					                |        padding: 0 !important;
 | 
				
			||||||
                |        margin: 0 !important;
 | 
					                |        margin: 0 !important;
 | 
				
			||||||
                |      }
 | 
					                |      }
 | 
				
			||||||
@@ -526,12 +505,7 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
                |      pre, code {
 | 
					                |      pre, code {
 | 
				
			||||||
                |        white-space: pre-wrap;
 | 
					                |        white-space: pre-wrap;
 | 
				
			||||||
                |        width:100%;
 | 
					                |        width:100%;
 | 
				
			||||||
                |        background-color: ${
 | 
					                |        background-color: $colorSurfaceString;
 | 
				
			||||||
                    String.format(
 | 
					 | 
				
			||||||
                        "#%06X",
 | 
					 | 
				
			||||||
                        0xFFFFFF and colorSurface.data,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                |      }
 | 
					                |      }
 | 
				
			||||||
                |   </style>
 | 
					                |   </style>
 | 
				
			||||||
                |   $fontLinkAndStyle
 | 
					                |   $fontLinkAndStyle
 | 
				
			||||||
@@ -545,26 +519,24 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
                null,
 | 
					                null,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        } catch (e: IllegalStateException) {
 | 
					        } catch (e: IllegalStateException) {
 | 
				
			||||||
            e.sendSilentlyWithAcraWithName("Context required is null")
 | 
					            e.sendSilentlyWithAcraWithName("Context required is still null ?")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun scrollDown() {
 | 
					    fun volumeButtonScrollDown() {
 | 
				
			||||||
        val height = binding.nestedScrollView.measuredHeight
 | 
					        val height = binding.nestedScrollView.measuredHeight
 | 
				
			||||||
        binding.nestedScrollView.smoothScrollBy(0, height / 2)
 | 
					        binding.nestedScrollView.smoothScrollBy(0, height / 2)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun scrollUp() {
 | 
					    fun volumeButtonScrollUp() {
 | 
				
			||||||
        val height = binding.nestedScrollView.measuredHeight
 | 
					        val height = binding.nestedScrollView.measuredHeight
 | 
				
			||||||
        binding.nestedScrollView.smoothScrollBy(0, -height / 2)
 | 
					        binding.nestedScrollView.smoothScrollBy(0, -height / 2)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun openInBrowserAfterFailing() {
 | 
					    private fun openInBrowserAfterFailing() {
 | 
				
			||||||
        binding.progressBar.visibility = View.GONE
 | 
					        binding.progressBar.visibility = View.GONE
 | 
				
			||||||
        try {
 | 
					        maybeIfContext {
 | 
				
			||||||
            requireContext().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
 | 
					            it.openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
 | 
				
			||||||
        } catch (e: IllegalStateException) {
 | 
					 | 
				
			||||||
            e.sendSilentlyWithAcraWithName("Context required is null")
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -581,10 +553,11 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun performClick(): Boolean {
 | 
					    fun performClick(): Boolean {
 | 
				
			||||||
        if (allImages != null && (
 | 
					        if (allImages != null &&
 | 
				
			||||||
                    binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
 | 
					            (
 | 
				
			||||||
                            binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
 | 
					                binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
 | 
				
			||||||
                    )
 | 
					                    binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        ) {
 | 
					        ) {
 | 
				
			||||||
            val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
 | 
					            val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -596,4 +569,4 @@ class ArticleFragment : Fragment(), DIAware {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        return false
 | 
					        return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.android.fragments
 | 
					package bou.amine.apps.readerforselfossv2.android.fragments
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.content.Context
 | 
					 | 
				
			||||||
import android.graphics.Color
 | 
					import android.graphics.Color
 | 
				
			||||||
import android.graphics.drawable.Drawable
 | 
					import android.graphics.drawable.Drawable
 | 
				
			||||||
import android.graphics.drawable.GradientDrawable
 | 
					import android.graphics.drawable.GradientDrawable
 | 
				
			||||||
@@ -16,11 +15,13 @@ import bou.amine.apps.readerforselfossv2.android.HomeActivity
 | 
				
			|||||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
					import bou.amine.apps.readerforselfossv2.android.R
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
 | 
					import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext
 | 
				
			||||||
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.utils.getColorHexCode
 | 
					import bou.amine.apps.readerforselfossv2.utils.getColorHexCode
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
 | 
					import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
 | 
					import bou.amine.apps.readerforselfossv2.utils.getIcon
 | 
				
			||||||
import com.bumptech.glide.Glide
 | 
					 | 
				
			||||||
import com.bumptech.glide.request.target.ViewTarget
 | 
					import com.bumptech.glide.request.target.ViewTarget
 | 
				
			||||||
import com.bumptech.glide.request.transition.Transition
 | 
					import com.bumptech.glide.request.transition.Transition
 | 
				
			||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
 | 
					import com.google.android.material.bottomsheet.BottomSheetDialogFragment
 | 
				
			||||||
@@ -33,10 +34,15 @@ import org.kodein.di.DIAware
 | 
				
			|||||||
import org.kodein.di.android.x.closestDI
 | 
					import org.kodein.di.android.x.closestDI
 | 
				
			||||||
import org.kodein.di.instance
 | 
					import org.kodein.di.instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
					private const val DRAWABLE_SIZE = 30
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FilterSheetFragment :
 | 
				
			||||||
 | 
					    BottomSheetDialogFragment(),
 | 
				
			||||||
 | 
					    DIAware {
 | 
				
			||||||
    private lateinit var binding: FilterFragmentBinding
 | 
					    private lateinit var binding: FilterFragmentBinding
 | 
				
			||||||
    override val di: DI by closestDI()
 | 
					    override val di: DI by closestDI()
 | 
				
			||||||
    private val repository: Repository by instance()
 | 
					    private val repository: Repository by instance()
 | 
				
			||||||
 | 
					    private val appSettingsService: AppSettingsService by instance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private var selectedChip: Chip? = null
 | 
					    private var selectedChip: Chip? = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,8 +60,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            CoroutineScope(Dispatchers.Main).launch {
 | 
					            CoroutineScope(Dispatchers.Main).launch {
 | 
				
			||||||
                handleTagChips(requireContext())
 | 
					                handleTagChips()
 | 
				
			||||||
                handleSourceChips(requireContext())
 | 
					                handleSourceChips()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                binding.progressBar2.visibility = GONE
 | 
					                binding.progressBar2.visibility = GONE
 | 
				
			||||||
                binding.filterView.visibility = VISIBLE
 | 
					                binding.filterView.visibility = VISIBLE
 | 
				
			||||||
@@ -73,16 +79,24 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
				
			|||||||
        return binding.root
 | 
					        return binding.root
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private suspend fun handleSourceChips(context: Context) {
 | 
					    private suspend fun handleSourceChips() {
 | 
				
			||||||
        val sourceGroup = binding.sourcesGroup
 | 
					        val sourceGroup = binding.sourcesGroup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
 | 
					        repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
 | 
				
			||||||
            val c = Chip(context)
 | 
					            val c: Chip? =
 | 
				
			||||||
 | 
					                maybeIfContext {
 | 
				
			||||||
 | 
					                    Chip(it)
 | 
				
			||||||
 | 
					                } as Chip?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (c == null) {
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            c.ellipsize = TextUtils.TruncateAt.END
 | 
					            c.ellipsize = TextUtils.TruncateAt.END
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Glide.with(context)
 | 
					            maybeIfContext {
 | 
				
			||||||
                .load(source.getIcon(repository.baseUrl))
 | 
					                it.imageIntoViewTarget(
 | 
				
			||||||
                .into(
 | 
					                    source.getIcon(repository.baseUrl),
 | 
				
			||||||
                    object : ViewTarget<Chip?, Drawable?>(c) {
 | 
					                    object : ViewTarget<Chip?, Drawable?>(c) {
 | 
				
			||||||
                        override fun onResourceReady(
 | 
					                        override fun onResourceReady(
 | 
				
			||||||
                            resource: Drawable,
 | 
					                            resource: Drawable,
 | 
				
			||||||
@@ -95,7 +109,9 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
				
			|||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
 | 
					                    appSettingsService,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            c.text = source.title.getHtmlDecoded()
 | 
					            c.text = source.title.getHtmlDecoded()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -131,13 +147,17 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private suspend fun handleTagChips(context: Context) {
 | 
					    private suspend fun handleTagChips() {
 | 
				
			||||||
        val tagGroup = binding.tagsGroup
 | 
					        val tagGroup = binding.tagsGroup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val tags = repository.getTags()
 | 
					        val tags = repository.getTags()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        tags.forEachIndexed { _, tag ->
 | 
					        tags.forEachIndexed { _, tag ->
 | 
				
			||||||
            val c = Chip(context)
 | 
					            val c: Chip? = maybeIfContext { Chip(it) } as Chip?
 | 
				
			||||||
 | 
					            if (c == null) {
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            c.ellipsize = TextUtils.TruncateAt.END
 | 
					            c.ellipsize = TextUtils.TruncateAt.END
 | 
				
			||||||
            c.text = tag.tag
 | 
					            c.text = tag.tag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -153,8 +173,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
				
			|||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    gd.setColor(gdColor)
 | 
					                    gd.setColor(gdColor)
 | 
				
			||||||
                    gd.shape = GradientDrawable.RECTANGLE
 | 
					                    gd.shape = GradientDrawable.RECTANGLE
 | 
				
			||||||
                    gd.setSize(30, 30)
 | 
					                    gd.setSize(DRAWABLE_SIZE, DRAWABLE_SIZE)
 | 
				
			||||||
                    gd.cornerRadius = 30F
 | 
					                    gd.cornerRadius = DRAWABLE_SIZE.toFloat()
 | 
				
			||||||
                    c.chipIcon = gd
 | 
					                    c.chipIcon = gd
 | 
				
			||||||
                } catch (e: Exception) {
 | 
					                } catch (e: Exception) {
 | 
				
			||||||
                    e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
 | 
					                    e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
 | 
				
			||||||
@@ -190,4 +210,4 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
				
			|||||||
    companion object {
 | 
					    companion object {
 | 
				
			||||||
        const val TAG = "FilterModalBottomSheet"
 | 
					        const val TAG = "FilterModalBottomSheet"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,15 +6,21 @@ import android.view.View
 | 
				
			|||||||
import android.view.ViewGroup
 | 
					import android.view.ViewGroup
 | 
				
			||||||
import androidx.fragment.app.Fragment
 | 
					import androidx.fragment.app.Fragment
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding
 | 
					import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding
 | 
				
			||||||
import com.bumptech.glide.Glide
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapWithCache
 | 
				
			||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
					import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
				
			||||||
import com.bumptech.glide.request.RequestOptions
 | 
					import org.kodein.di.DI
 | 
				
			||||||
 | 
					import org.kodein.di.DIAware
 | 
				
			||||||
 | 
					import org.kodein.di.android.x.closestDI
 | 
				
			||||||
 | 
					import org.kodein.di.instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImageFragment : Fragment() {
 | 
					class ImageFragment :
 | 
				
			||||||
 | 
					    Fragment(),
 | 
				
			||||||
 | 
					    DIAware {
 | 
				
			||||||
 | 
					    override val di: DI by closestDI()
 | 
				
			||||||
 | 
					    private val appSettingsService: AppSettingsService by instance()
 | 
				
			||||||
    private lateinit var imageUrl: String
 | 
					    private lateinit var imageUrl: String
 | 
				
			||||||
    private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
 | 
					 | 
				
			||||||
    private var _binding: FragmentImageBinding? = null
 | 
					    private var _binding: FragmentImageBinding? = null
 | 
				
			||||||
    private val binding get() = _binding
 | 
					    val binding get() = _binding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
					    override fun onCreate(savedInstanceState: Bundle?) {
 | 
				
			||||||
        super.onCreate(savedInstanceState)
 | 
					        super.onCreate(savedInstanceState)
 | 
				
			||||||
@@ -31,11 +37,7 @@ class ImageFragment : Fragment() {
 | 
				
			|||||||
        val view = binding?.root
 | 
					        val view = binding?.root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        binding!!.photoView.visibility = View.VISIBLE
 | 
					        binding!!.photoView.visibility = View.VISIBLE
 | 
				
			||||||
        Glide.with(requireActivity())
 | 
					        requireActivity().bitmapWithCache(imageUrl, binding!!.photoView, appSettingsService)
 | 
				
			||||||
            .asBitmap()
 | 
					 | 
				
			||||||
            .apply(glideOptions)
 | 
					 | 
				
			||||||
            .load(imageUrl)
 | 
					 | 
				
			||||||
            .into(binding!!.photoView)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return view
 | 
					        return view
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,23 +3,21 @@ package bou.amine.apps.readerforselfossv2.android.model
 | 
				
			|||||||
import android.content.Context
 | 
					import android.content.Context
 | 
				
			||||||
import android.webkit.URLUtil
 | 
					import android.webkit.URLUtil
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.glide.preloadImage
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
					import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.getImages
 | 
					import bou.amine.apps.readerforselfossv2.utils.getImages
 | 
				
			||||||
import com.bumptech.glide.Glide
 | 
					 | 
				
			||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
					 | 
				
			||||||
import com.bumptech.glide.request.RequestOptions
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun SelfossModel.Item.preloadImages(context: Context): Boolean {
 | 
					fun SelfossModel.Item.preloadImages(
 | 
				
			||||||
 | 
					    context: Context,
 | 
				
			||||||
 | 
					    appSettingsService: AppSettingsService,
 | 
				
			||||||
 | 
					): Boolean {
 | 
				
			||||||
    val imageUrls = this.getImages()
 | 
					    val imageUrls = this.getImages()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        for (url in imageUrls) {
 | 
					        for (url in imageUrls) {
 | 
				
			||||||
            if (URLUtil.isValidUrl(url)) {
 | 
					            if (URLUtil.isValidUrl(url)) {
 | 
				
			||||||
                Glide.with(context).asBitmap()
 | 
					                context.preloadImage(url, appSettingsService)
 | 
				
			||||||
                    .apply(glideOptions)
 | 
					 | 
				
			||||||
                    .load(url).submit()
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    } catch (e: Error) {
 | 
					    } catch (e: Error) {
 | 
				
			||||||
@@ -40,4 +38,4 @@ fun String.toTextDrawableString(): String {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return textDrawable.toString()
 | 
					    return textDrawable.toString()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,12 +17,19 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBin
 | 
				
			|||||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
					import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.API_ITEMS_NUMBER
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.CURRENT_THEME
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.READER_FONT_SIZE
 | 
				
			||||||
import com.mikepenz.aboutlibraries.LibsBuilder
 | 
					import com.mikepenz.aboutlibraries.LibsBuilder
 | 
				
			||||||
import org.kodein.di.DIAware
 | 
					import org.kodein.di.DIAware
 | 
				
			||||||
import org.kodein.di.android.closestDI
 | 
					import org.kodein.di.android.closestDI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private const val TITLE_TAG = "settingsActivityTitle"
 | 
					private const val TITLE_TAG = "settingsActivityTitle"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const val MAX_ITEMS_NUMBER = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val MIN_ITEMS_NUMBER = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SettingsActivity :
 | 
					class SettingsActivity :
 | 
				
			||||||
    AppCompatActivity(),
 | 
					    AppCompatActivity(),
 | 
				
			||||||
    PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
 | 
					    PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
 | 
				
			||||||
@@ -61,15 +68,14 @@ class SettingsActivity :
 | 
				
			|||||||
        outState.putCharSequence(TITLE_TAG, title)
 | 
					        outState.putCharSequence(TITLE_TAG, title)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onSupportNavigateUp(): Boolean {
 | 
					    override fun onSupportNavigateUp(): Boolean =
 | 
				
			||||||
        return if (supportFragmentManager.popBackStackImmediate()) {
 | 
					        if (supportFragmentManager.popBackStackImmediate()) {
 | 
				
			||||||
            supportActionBar?.title = getText(R.string.title_activity_settings)
 | 
					            supportActionBar?.title = getText(R.string.title_activity_settings)
 | 
				
			||||||
            false
 | 
					            false
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            super.onBackPressed()
 | 
					            super.onBackPressed()
 | 
				
			||||||
            true
 | 
					            true
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun onPreferenceStartFragment(
 | 
					    override fun onPreferenceStartFragment(
 | 
				
			||||||
        caller: PreferenceFragmentCompat,
 | 
					        caller: PreferenceFragmentCompat,
 | 
				
			||||||
@@ -78,15 +84,17 @@ class SettingsActivity :
 | 
				
			|||||||
        // Instantiate the new Fragment
 | 
					        // Instantiate the new Fragment
 | 
				
			||||||
        val args = pref.extras
 | 
					        val args = pref.extras
 | 
				
			||||||
        val fragment =
 | 
					        val fragment =
 | 
				
			||||||
            supportFragmentManager.fragmentFactory.instantiate(
 | 
					            supportFragmentManager.fragmentFactory
 | 
				
			||||||
                classLoader,
 | 
					                .instantiate(
 | 
				
			||||||
                pref.fragment.toString(),
 | 
					                    classLoader,
 | 
				
			||||||
            ).apply {
 | 
					                    pref.fragment.toString(),
 | 
				
			||||||
                arguments = args
 | 
					                ).apply {
 | 
				
			||||||
                setTargetFragment(caller, 0)
 | 
					                    arguments = args
 | 
				
			||||||
            }
 | 
					                    setTargetFragment(caller, 0)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
        // Replace the existing Fragment with the new Fragment
 | 
					        // Replace the existing Fragment with the new Fragment
 | 
				
			||||||
        supportFragmentManager.beginTransaction()
 | 
					        supportFragmentManager
 | 
				
			||||||
 | 
					            .beginTransaction()
 | 
				
			||||||
            .replace(R.id.settings, fragment)
 | 
					            .replace(R.id.settings, fragment)
 | 
				
			||||||
            .addToBackStack(null)
 | 
					            .addToBackStack(null)
 | 
				
			||||||
            .commit()
 | 
					            .commit()
 | 
				
			||||||
@@ -102,10 +110,10 @@ class SettingsActivity :
 | 
				
			|||||||
        ) {
 | 
					        ) {
 | 
				
			||||||
            setPreferencesFromResource(R.xml.pref_main, rootKey)
 | 
					            setPreferencesFromResource(R.xml.pref_main, rootKey)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
 | 
					            preferenceManager.findPreference<Preference>(CURRENT_THEME)?.onPreferenceChangeListener =
 | 
				
			||||||
                Preference.OnPreferenceChangeListener { _, newValue ->
 | 
					                Preference.OnPreferenceChangeListener { _, newValue ->
 | 
				
			||||||
                    AppCompatDelegate.setDefaultNightMode(
 | 
					                    AppCompatDelegate.setDefaultNightMode(
 | 
				
			||||||
                        newValue.toString().toInt()
 | 
					                        newValue.toString().toInt(),
 | 
				
			||||||
                    ) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
 | 
					                    ) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
 | 
				
			||||||
                    true
 | 
					                    true
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@@ -131,7 +139,7 @@ class SettingsActivity :
 | 
				
			|||||||
            setPreferencesFromResource(R.xml.pref_general, rootKey)
 | 
					            setPreferencesFromResource(R.xml.pref_general, rootKey)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            val editTextPreference =
 | 
					            val editTextPreference =
 | 
				
			||||||
                preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number")
 | 
					                preferenceManager.findPreference<EditTextPreference>(API_ITEMS_NUMBER)
 | 
				
			||||||
            editTextPreference?.setOnBindEditTextListener { editText ->
 | 
					            editTextPreference?.setOnBindEditTextListener { editText ->
 | 
				
			||||||
                editText.inputType = InputType.TYPE_CLASS_NUMBER
 | 
					                editText.inputType = InputType.TYPE_CLASS_NUMBER
 | 
				
			||||||
                editText.filters =
 | 
					                editText.filters =
 | 
				
			||||||
@@ -139,13 +147,14 @@ class SettingsActivity :
 | 
				
			|||||||
                        InputFilter { source, _, _, dest, _, _ ->
 | 
					                        InputFilter { source, _, _, dest, _, _ ->
 | 
				
			||||||
                            try {
 | 
					                            try {
 | 
				
			||||||
                                val input: Int = (dest.toString() + source.toString()).toInt()
 | 
					                                val input: Int = (dest.toString() + source.toString()).toInt()
 | 
				
			||||||
                                if (input in 1..200) return@InputFilter null
 | 
					                                if (input in MIN_ITEMS_NUMBER..MAX_ITEMS_NUMBER) return@InputFilter null
 | 
				
			||||||
                            } catch (nfe: NumberFormatException) {
 | 
					                            } catch (nfe: NumberFormatException) {
 | 
				
			||||||
                                Toast.makeText(
 | 
					                                Toast
 | 
				
			||||||
                                    activity,
 | 
					                                    .makeText(
 | 
				
			||||||
                                    R.string.items_number_should_be_number,
 | 
					                                        activity,
 | 
				
			||||||
                                    Toast.LENGTH_LONG
 | 
					                                        R.string.items_number_should_be_number,
 | 
				
			||||||
                                ).show()
 | 
					                                        Toast.LENGTH_LONG,
 | 
				
			||||||
 | 
					                                    ).show()
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            ""
 | 
					                            ""
 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
@@ -161,7 +170,7 @@ class SettingsActivity :
 | 
				
			|||||||
        ) {
 | 
					        ) {
 | 
				
			||||||
            setPreferencesFromResource(R.xml.pref_viewer, rootKey)
 | 
					            setPreferencesFromResource(R.xml.pref_viewer, rootKey)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size")
 | 
					            val fontSize = preferenceManager.findPreference<EditTextPreference>(READER_FONT_SIZE)
 | 
				
			||||||
            fontSize?.setOnBindEditTextListener { editText ->
 | 
					            fontSize?.setOnBindEditTextListener { editText ->
 | 
				
			||||||
                editText.inputType = InputType.TYPE_CLASS_NUMBER
 | 
					                editText.inputType = InputType.TYPE_CLASS_NUMBER
 | 
				
			||||||
                editText.addTextChangedListener {
 | 
					                editText.addTextChangedListener {
 | 
				
			||||||
@@ -218,23 +227,6 @@ class SettingsActivity :
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class ThemePreferenceFragment : PreferenceFragmentCompat() {
 | 
					 | 
				
			||||||
        override fun onCreatePreferences(
 | 
					 | 
				
			||||||
            savedInstanceState: Bundle?,
 | 
					 | 
				
			||||||
            rootKey: String?,
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
            setPreferencesFromResource(R.xml.pref_theme, rootKey)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
 | 
					 | 
				
			||||||
                Preference.OnPreferenceChangeListener { _, newValue ->
 | 
					 | 
				
			||||||
                    AppCompatDelegate.setDefaultNightMode(
 | 
					 | 
				
			||||||
                        newValue.toString().toInt()
 | 
					 | 
				
			||||||
                    ) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
 | 
					 | 
				
			||||||
                    true
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class LinksPreferenceFragment : PreferenceFragmentCompat() {
 | 
					    class LinksPreferenceFragment : PreferenceFragmentCompat() {
 | 
				
			||||||
        private fun openUrl(url: String) {
 | 
					        private fun openUrl(url: String) {
 | 
				
			||||||
            context?.openUrlInBrowser(url)
 | 
					            context?.openUrlInBrowser(url)
 | 
				
			||||||
@@ -248,19 +240,19 @@ class SettingsActivity :
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
 | 
					            preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
 | 
				
			||||||
                Preference.OnPreferenceClickListener {
 | 
					                Preference.OnPreferenceClickListener {
 | 
				
			||||||
                    openUrl(AppSettingsService.trackerUrl)
 | 
					                    openUrl(AppSettingsService.BUG_URL)
 | 
				
			||||||
                    true
 | 
					                    true
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener =
 | 
					            preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener =
 | 
				
			||||||
                Preference.OnPreferenceClickListener {
 | 
					                Preference.OnPreferenceClickListener {
 | 
				
			||||||
                    openUrl(AppSettingsService.sourceUrl)
 | 
					                    openUrl(AppSettingsService.SOURCE_URL)
 | 
				
			||||||
                    false
 | 
					                    false
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener =
 | 
					            preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener =
 | 
				
			||||||
                Preference.OnPreferenceClickListener {
 | 
					                Preference.OnPreferenceClickListener {
 | 
				
			||||||
                    openUrl(AppSettingsService.translationUrl)
 | 
					                    openUrl(AppSettingsService.TRANSLATION_URL)
 | 
				
			||||||
                    false
 | 
					                    false
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -274,4 +266,4 @@ class SettingsActivity :
 | 
				
			|||||||
            setPreferencesFromResource(R.xml.pref_experimental, rootKey)
 | 
					            setPreferencesFromResource(R.xml.pref_experimental, rootKey)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.testing
 | 
				
			|||||||
import androidx.test.espresso.idling.CountingIdlingResource
 | 
					import androidx.test.espresso.idling.CountingIdlingResource
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object CountingIdlingResourceSingleton {
 | 
					object CountingIdlingResourceSingleton {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    private const val RESOURCE = "GLOBAL"
 | 
					    private const val RESOURCE = "GLOBAL"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @JvmField
 | 
					    @JvmField
 | 
				
			||||||
@@ -18,4 +17,4 @@ object CountingIdlingResourceSingleton {
 | 
				
			|||||||
            countingIdlingResource.decrement()
 | 
					            countingIdlingResource.decrement()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@ package bou.amine.apps.readerforselfossv2.android.testing
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import android.os.Build
 | 
					import android.os.Build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
class TestingHelper {
 | 
					class TestingHelper {
 | 
				
			||||||
    fun isUnitTest(): Boolean {
 | 
					    fun isUnitTest(): Boolean {
 | 
				
			||||||
        var device = Build.DEVICE
 | 
					        var device = Build.DEVICE
 | 
				
			||||||
@@ -16,4 +15,4 @@ class TestingHelper {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        return device == "robolectric" && product == "robolectric"
 | 
					        return device == "robolectric" && product == "robolectric"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,23 +2,60 @@ package bou.amine.apps.readerforselfossv2.android.utils
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import android.content.Context
 | 
					import android.content.Context
 | 
				
			||||||
import android.content.Intent
 | 
					import android.content.Intent
 | 
				
			||||||
 | 
					import android.util.TypedValue
 | 
				
			||||||
 | 
					import androidx.annotation.AttrRes
 | 
				
			||||||
 | 
					import androidx.annotation.ColorInt
 | 
				
			||||||
 | 
					import androidx.fragment.app.Fragment
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
					import bou.amine.apps.readerforselfossv2.android.R
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
 | 
					import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun Context.shareLink(
 | 
					fun Context.shareLink(
 | 
				
			||||||
    itemUrl: String,
 | 
					    itemUrl: String?,
 | 
				
			||||||
    itemTitle: String,
 | 
					    itemTitle: String,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
    val sendIntent = Intent()
 | 
					    if (itemUrl.isUrlValid()) {
 | 
				
			||||||
    sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
					        val sendIntent = Intent()
 | 
				
			||||||
    sendIntent.action = Intent.ACTION_SEND
 | 
					        sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
				
			||||||
    sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl.toStringUriWithHttp())
 | 
					        sendIntent.action = Intent.ACTION_SEND
 | 
				
			||||||
    sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
 | 
					        sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl!!.toStringUriWithHttp())
 | 
				
			||||||
    sendIntent.type = "text/plain"
 | 
					        sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
 | 
				
			||||||
    startActivity(
 | 
					        sendIntent.type = "text/plain"
 | 
				
			||||||
        Intent.createChooser(
 | 
					        startActivity(
 | 
				
			||||||
            sendIntent,
 | 
					            Intent
 | 
				
			||||||
            getString(R.string.share),
 | 
					                .createChooser(
 | 
				
			||||||
        ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
 | 
					                    sendIntent,
 | 
				
			||||||
    )
 | 
					                    getString(R.string.share),
 | 
				
			||||||
 | 
					                ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@ColorInt
 | 
				
			||||||
 | 
					fun Fragment.getColorFromAttr(
 | 
				
			||||||
 | 
					    @AttrRes attrColor: Int,
 | 
				
			||||||
 | 
					    resolveRefs: Boolean = true,
 | 
				
			||||||
 | 
					): Int {
 | 
				
			||||||
 | 
					    val typedValue = TypedValue()
 | 
				
			||||||
 | 
					    maybeIfContextWithLog { this.requireContext().theme.resolveAttribute(attrColor, typedValue, resolveRefs) }
 | 
				
			||||||
 | 
					    return typedValue.data
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("detekt:SwallowedException")
 | 
				
			||||||
 | 
					fun Fragment.maybeIfContext(fn: (Context) -> Any): Any? {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        return fn(this.requireContext())
 | 
				
			||||||
 | 
					    } catch (e: Exception) {
 | 
				
			||||||
 | 
					        // Do nothing
 | 
				
			||||||
 | 
					        return null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun Fragment.maybeIfContextWithLog(fn: (Context) -> Any): Any? {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        return fn(this.requireContext())
 | 
				
			||||||
 | 
					    } catch (e: Exception) {
 | 
				
			||||||
 | 
					        e.sendSilentlyWithAcraWithName("Fragment context issue...")
 | 
				
			||||||
 | 
					        return null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,7 +59,5 @@ class CircleImageView
 | 
				
			|||||||
            textView.text = text.toTextDrawableString()
 | 
					            textView.text = text.toTextDrawableString()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private fun colorFromIdentifier(key: String): Int {
 | 
					        private fun colorFromIdentifier(key: String): Int = colorScheme[abs(key.hashCode()) % colorScheme.size]
 | 
				
			||||||
            return colorScheme[abs(key.hashCode()) % colorScheme.size]
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,35 +15,35 @@ import android.widget.Toast
 | 
				
			|||||||
import bou.amine.apps.readerforselfossv2.android.R
 | 
					import bou.amine.apps.readerforselfossv2.android.R
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.ReaderActivity
 | 
					import bou.amine.apps.readerforselfossv2.android.ReaderActivity
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
					import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
 | 
					import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
 | 
				
			||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
 | 
					import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
fun Context.openItemUrl(
 | 
					fun Context.openItemUrl(
 | 
				
			||||||
    currentItem: Int,
 | 
					    currentItem: Int,
 | 
				
			||||||
    linkDecoded: String,
 | 
					    linkDecoded: String?,
 | 
				
			||||||
    articleViewer: Boolean,
 | 
					    articleViewer: Boolean,
 | 
				
			||||||
    app: Activity,
 | 
					    app: Activity,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
    if (!linkDecoded.isUrlValid()) {
 | 
					    if (!linkDecoded.isUrlValid()) {
 | 
				
			||||||
        Toast.makeText(
 | 
					        Toast
 | 
				
			||||||
            this,
 | 
					            .makeText(
 | 
				
			||||||
            this.getString(R.string.cant_open_invalid_url),
 | 
					                this,
 | 
				
			||||||
            Toast.LENGTH_LONG,
 | 
					                this.getString(R.string.cant_open_invalid_url),
 | 
				
			||||||
        ).show()
 | 
					                Toast.LENGTH_LONG,
 | 
				
			||||||
 | 
					            ).show()
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        if (articleViewer) {
 | 
					        if (articleViewer) {
 | 
				
			||||||
            val intent = Intent(this, ReaderActivity::class.java)
 | 
					            val intent = Intent(this, ReaderActivity::class.java)
 | 
				
			||||||
            intent.putExtra("currentItem", currentItem)
 | 
					            intent.putExtra("currentItem", currentItem)
 | 
				
			||||||
            app.startActivity(intent)
 | 
					            app.startActivity(intent)
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            this.openUrlInBrowserAsNewTask(linkDecoded)
 | 
					            this.openUrlInBrowserAsNewTask(linkDecoded!!)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun String.isUrlValid(): Boolean =
 | 
					fun String?.isUrlValid(): Boolean =
 | 
				
			||||||
    this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
 | 
					    !this.isEmptyOrNullOrNullString() && this!!.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun String.isBaseUrlInvalid(): Boolean {
 | 
					fun String.isBaseUrlInvalid(): Boolean {
 | 
				
			||||||
    val baseUrl = this.toHttpUrlOrNull()
 | 
					    val baseUrl = this.toHttpUrlOrNull()
 | 
				
			||||||
@@ -57,15 +57,16 @@ fun String.isBaseUrlInvalid(): Boolean {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) {
 | 
					fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) {
 | 
				
			||||||
    this.openUrlInBrowserAsNewTask(i.getLinkDecoded().toStringUriWithHttp())
 | 
					    this.openUrlInBrowserAsNewTask(i.getLinkDecoded())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun Context.openUrlInBrowserAsNewTask(url: String) {
 | 
					fun Context.openUrlInBrowserAsNewTask(url: String?) {
 | 
				
			||||||
 | 
					    if (url.isUrlValid()) {
 | 
				
			||||||
    val intent = Intent(Intent.ACTION_VIEW)
 | 
					        val intent = Intent(Intent.ACTION_VIEW)
 | 
				
			||||||
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
					        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
				
			||||||
    intent.data = Uri.parse(url)
 | 
					        intent.data = Uri.parse(url)
 | 
				
			||||||
    this.mayBeStartActivity(intent)
 | 
					        this.mayBeStartActivity(intent)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun Context.openUrlInBrowser(url: String) {
 | 
					fun Context.openUrlInBrowser(url: String) {
 | 
				
			||||||
@@ -74,13 +75,13 @@ fun Context.openUrlInBrowser(url: String) {
 | 
				
			|||||||
    this.mayBeStartActivity(intent)
 | 
					    this.mayBeStartActivity(intent)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("detekt:SwallowedException")
 | 
				
			||||||
fun Context.mayBeStartActivity(intent: Intent) {
 | 
					fun Context.mayBeStartActivity(intent: Intent) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        this.startActivity(intent)
 | 
					        this.startActivity(intent)
 | 
				
			||||||
    } catch (e: ActivityNotFoundException) {
 | 
					    } catch (e: ActivityNotFoundException) {
 | 
				
			||||||
        Toast.makeText(this, getString(R.string.no_browser), Toast.LENGTH_SHORT).show()
 | 
					        Toast.makeText(this, getString(R.string.no_browser), Toast.LENGTH_SHORT).show()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LinkOnTouchListener : View.OnTouchListener {
 | 
					class LinkOnTouchListener : View.OnTouchListener {
 | 
				
			||||||
@@ -122,4 +123,4 @@ class LinkOnTouchListener : View.OnTouchListener {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        return ret
 | 
					        return ret
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,4 +6,4 @@ import org.acra.ktx.sendSilentlyWithAcra
 | 
				
			|||||||
fun Throwable.sendSilentlyWithAcraWithName(name: String) {
 | 
					fun Throwable.sendSilentlyWithAcraWithName(name: String) {
 | 
				
			||||||
    ACRA.errorReporter.putCustomData("error_source", name)
 | 
					    ACRA.errorReporter.putCustomData("error_source", name)
 | 
				
			||||||
    this.sendSilentlyWithAcra()
 | 
					    this.sendSilentlyWithAcra()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,22 +8,19 @@ import org.acra.config.CoreConfiguration
 | 
				
			|||||||
import org.acra.config.ReportingAdministrator
 | 
					import org.acra.config.ReportingAdministrator
 | 
				
			||||||
import org.acra.data.CrashReportData
 | 
					import org.acra.data.CrashReportData
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
@AutoService(ReportingAdministrator::class)
 | 
					@AutoService(ReportingAdministrator::class)
 | 
				
			||||||
class AcraReportingAdministrator : ReportingAdministrator {
 | 
					class AcraReportingAdministrator : ReportingAdministrator {
 | 
				
			||||||
    override fun shouldStartCollecting(
 | 
					    override fun shouldStartCollecting(
 | 
				
			||||||
        context: Context,
 | 
					        context: Context,
 | 
				
			||||||
        config: CoreConfiguration,
 | 
					        config: CoreConfiguration,
 | 
				
			||||||
        reportBuilder: ReportBuilder
 | 
					        reportBuilder: ReportBuilder,
 | 
				
			||||||
    ): Boolean {
 | 
					    ): Boolean =
 | 
				
			||||||
        return reportBuilder.exception !is DeadSystemException
 | 
					        reportBuilder.exception !is DeadSystemException &&
 | 
				
			||||||
    }
 | 
					            (reportBuilder.exception != null && reportBuilder.exception!!::class.simpleName != "CannotDeliverBroadcastException")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun shouldSendReport(
 | 
					    override fun shouldSendReport(
 | 
				
			||||||
        context: Context,
 | 
					        context: Context,
 | 
				
			||||||
        config: CoreConfiguration,
 | 
					        config: CoreConfiguration,
 | 
				
			||||||
        crashReportData: CrashReportData
 | 
					        crashReportData: CrashReportData,
 | 
				
			||||||
    ): Boolean {
 | 
					    ): Boolean = crashReportData.get("BRAND") != "redroid" && !crashReportData.get("PHONE_MODEL").toString().startsWith("sdk_gphone")
 | 
				
			||||||
        return crashReportData.get("BRAND") != "redroid"
 | 
					}
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,13 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.android.utils.bottombar
 | 
					package bou.amine.apps.readerforselfossv2.android.utils.bottombar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Context
 | 
				
			||||||
 | 
					import android.graphics.drawable.Drawable
 | 
				
			||||||
 | 
					import androidx.annotation.IdRes
 | 
				
			||||||
 | 
					import androidx.annotation.StringRes
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.android.R
 | 
				
			||||||
import com.ashokvarma.bottomnavigation.TextBadgeItem
 | 
					import com.ashokvarma.bottomnavigation.TextBadgeItem
 | 
				
			||||||
 | 
					import com.leinardi.android.speeddial.SpeedDialActionItem
 | 
				
			||||||
 | 
					import com.leinardi.android.speeddial.SpeedDialView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun TextBadgeItem.removeBadge(): TextBadgeItem {
 | 
					fun TextBadgeItem.removeBadge(): TextBadgeItem {
 | 
				
			||||||
    this.setText("")
 | 
					    this.setText("")
 | 
				
			||||||
@@ -9,3 +16,25 @@ fun TextBadgeItem.removeBadge(): TextBadgeItem {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this
 | 
					fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("detekt:LongParameterList")
 | 
				
			||||||
 | 
					fun SpeedDialView.addHomeMadeActionItem(
 | 
				
			||||||
 | 
					    @IdRes actionId: Int,
 | 
				
			||||||
 | 
					    actionIcon: Drawable,
 | 
				
			||||||
 | 
					    @StringRes labelId: Int,
 | 
				
			||||||
 | 
					    colorOnSurface: Int,
 | 
				
			||||||
 | 
					    colorSurface: Int,
 | 
				
			||||||
 | 
					    context: Context,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    this.addActionItem(
 | 
				
			||||||
 | 
					        SpeedDialActionItem
 | 
				
			||||||
 | 
					            .Builder(actionId, actionIcon)
 | 
				
			||||||
 | 
					            .setFabBackgroundColor(context.resources.getColor(R.color.colorAccent))
 | 
				
			||||||
 | 
					            .setFabImageTintColor(colorOnSurface)
 | 
				
			||||||
 | 
					            .setLabel(context.getString(labelId))
 | 
				
			||||||
 | 
					            .setLabelClickable(false)
 | 
				
			||||||
 | 
					            .setLabelBackgroundColor(colorOnSurface)
 | 
				
			||||||
 | 
					            .setLabelColor(colorSurface)
 | 
				
			||||||
 | 
					            .create(),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,40 +2,135 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import android.content.Context
 | 
					import android.content.Context
 | 
				
			||||||
import android.graphics.Bitmap
 | 
					import android.graphics.Bitmap
 | 
				
			||||||
 | 
					import android.graphics.drawable.Drawable
 | 
				
			||||||
 | 
					import android.webkit.WebView
 | 
				
			||||||
import android.widget.ImageView
 | 
					import android.widget.ImageView
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
 | 
					import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
 | 
				
			||||||
import com.bumptech.glide.Glide
 | 
					import com.bumptech.glide.Glide
 | 
				
			||||||
 | 
					import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
				
			||||||
 | 
					import com.bumptech.glide.load.model.GlideUrl
 | 
				
			||||||
 | 
					import com.bumptech.glide.load.model.LazyHeaders
 | 
				
			||||||
import com.bumptech.glide.request.RequestOptions
 | 
					import com.bumptech.glide.request.RequestOptions
 | 
				
			||||||
 | 
					import com.bumptech.glide.request.target.ViewTarget
 | 
				
			||||||
 | 
					import com.google.android.material.chip.Chip
 | 
				
			||||||
import java.io.ByteArrayInputStream
 | 
					import java.io.ByteArrayInputStream
 | 
				
			||||||
import java.io.ByteArrayOutputStream
 | 
					import java.io.ByteArrayOutputStream
 | 
				
			||||||
import java.io.InputStream
 | 
					import java.io.InputStream
 | 
				
			||||||
 | 
					import kotlin.io.encoding.Base64
 | 
				
			||||||
 | 
					import kotlin.io.encoding.ExperimentalEncodingApi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val PRELOAD_IMAGE_TIMEOUT = 10000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("detekt:ReturnCount")
 | 
				
			||||||
 | 
					@OptIn(ExperimentalEncodingApi::class)
 | 
				
			||||||
 | 
					fun String.toGlideUrl(appSettingsService: AppSettingsService): Any { // GlideUrl Or String
 | 
				
			||||||
 | 
					    if (this.isEmptyOrNullOrNullString()) {
 | 
				
			||||||
 | 
					        return ""
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (appSettingsService.getBasicUserName().isNotEmpty()) {
 | 
				
			||||||
 | 
					        val authString = "${appSettingsService.getBasicUserName()}:${appSettingsService.getBasicPassword()}"
 | 
				
			||||||
 | 
					        val authBuf = Base64.encode(authString.toByteArray(Charsets.UTF_8))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return GlideUrl(
 | 
				
			||||||
 | 
					            this,
 | 
				
			||||||
 | 
					            LazyHeaders
 | 
				
			||||||
 | 
					                .Builder()
 | 
				
			||||||
 | 
					                .addHeader("Authorization", "Basic $authBuf")
 | 
				
			||||||
 | 
					                .build(),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        return GlideUrl(
 | 
				
			||||||
 | 
					            this,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun WebView.getGlideImageForResource(
 | 
				
			||||||
 | 
					    url: String,
 | 
				
			||||||
 | 
					    appSettingsService: AppSettingsService,
 | 
				
			||||||
 | 
					) = Glide
 | 
				
			||||||
 | 
					    .with(this)
 | 
				
			||||||
 | 
					    .asBitmap()
 | 
				
			||||||
 | 
					    .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL))
 | 
				
			||||||
 | 
					    .load(url.toGlideUrl(appSettingsService))
 | 
				
			||||||
 | 
					    .submit()
 | 
				
			||||||
 | 
					    .get()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun Context.preloadImage(
 | 
				
			||||||
 | 
					    url: String,
 | 
				
			||||||
 | 
					    appSettingsService: AppSettingsService,
 | 
				
			||||||
 | 
					) = Glide
 | 
				
			||||||
 | 
					    .with(this)
 | 
				
			||||||
 | 
					    .asBitmap()
 | 
				
			||||||
 | 
					    .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(PRELOAD_IMAGE_TIMEOUT))
 | 
				
			||||||
 | 
					    .load(url.toGlideUrl(appSettingsService))
 | 
				
			||||||
 | 
					    .submit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun Context.imageIntoViewTarget(
 | 
				
			||||||
 | 
					    url: String,
 | 
				
			||||||
 | 
					    target: ViewTarget<Chip?, Drawable?>,
 | 
				
			||||||
 | 
					    appSettingsService: AppSettingsService,
 | 
				
			||||||
 | 
					) = Glide
 | 
				
			||||||
 | 
					    .with(this)
 | 
				
			||||||
 | 
					    .load(url.toGlideUrl(appSettingsService))
 | 
				
			||||||
 | 
					    .into(target)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun Context.bitmapWithCache(
 | 
				
			||||||
 | 
					    url: String,
 | 
				
			||||||
 | 
					    iv: ImageView,
 | 
				
			||||||
 | 
					    appSettingsService: AppSettingsService,
 | 
				
			||||||
 | 
					) = Glide
 | 
				
			||||||
 | 
					    .with(this)
 | 
				
			||||||
 | 
					    .asBitmap()
 | 
				
			||||||
 | 
					    .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL))
 | 
				
			||||||
 | 
					    .load(url.toGlideUrl(appSettingsService))
 | 
				
			||||||
 | 
					    .into(iv)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun Context.bitmapCenterCrop(
 | 
					fun Context.bitmapCenterCrop(
 | 
				
			||||||
    url: String,
 | 
					    url: String,
 | 
				
			||||||
    iv: ImageView,
 | 
					    iv: ImageView,
 | 
				
			||||||
) = Glide.with(this)
 | 
					    appSettingsService: AppSettingsService,
 | 
				
			||||||
 | 
					) = Glide
 | 
				
			||||||
 | 
					    .with(this)
 | 
				
			||||||
    .asBitmap()
 | 
					    .asBitmap()
 | 
				
			||||||
    .load(url)
 | 
					    .load(url.toGlideUrl(appSettingsService))
 | 
				
			||||||
    .apply(RequestOptions.centerCropTransform())
 | 
					    .apply(RequestOptions.centerCropTransform())
 | 
				
			||||||
    .into(iv)
 | 
					    .into(iv)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun Context.bitmapFitCenter(
 | 
				
			||||||
 | 
					    url: String,
 | 
				
			||||||
 | 
					    iv: ImageView,
 | 
				
			||||||
 | 
					    appSettingsService: AppSettingsService,
 | 
				
			||||||
 | 
					) = Glide
 | 
				
			||||||
 | 
					    .with(this)
 | 
				
			||||||
 | 
					    .asBitmap()
 | 
				
			||||||
 | 
					    .load(url.toGlideUrl(appSettingsService))
 | 
				
			||||||
 | 
					    .apply(RequestOptions.fitCenterTransform())
 | 
				
			||||||
 | 
					    .into(iv)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun Context.circularDrawable(
 | 
					fun Context.circularDrawable(
 | 
				
			||||||
    url: String,
 | 
					    url: String,
 | 
				
			||||||
    view: CircleImageView,
 | 
					    view: CircleImageView,
 | 
				
			||||||
 | 
					    appSettingsService: AppSettingsService,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
    view.textView.text = ""
 | 
					    view.textView.text = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Glide.with(this)
 | 
					    Glide
 | 
				
			||||||
        .load(url)
 | 
					        .with(this)
 | 
				
			||||||
 | 
					        .load(url.toGlideUrl(appSettingsService))
 | 
				
			||||||
        .into(view.imageView)
 | 
					        .into(view.imageView)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val BITMAP_INPUT_STREAM_COMPRESSION_QUALITY = 80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun getBitmapInputStream(
 | 
					fun getBitmapInputStream(
 | 
				
			||||||
    bitmap: Bitmap,
 | 
					    bitmap: Bitmap,
 | 
				
			||||||
    compressFormat: Bitmap.CompressFormat,
 | 
					    compressFormat: Bitmap.CompressFormat,
 | 
				
			||||||
): InputStream {
 | 
					): InputStream {
 | 
				
			||||||
    val byteArrayOutputStream = ByteArrayOutputStream()
 | 
					    val byteArrayOutputStream = ByteArrayOutputStream()
 | 
				
			||||||
    bitmap.compress(compressFormat, 80, byteArrayOutputStream)
 | 
					    bitmap.compress(compressFormat, BITMAP_INPUT_STREAM_COMPRESSION_QUALITY, byteArrayOutputStream)
 | 
				
			||||||
    val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
 | 
					    val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
 | 
				
			||||||
    return ByteArrayInputStream(bitmapData)
 | 
					    return ByteArrayInputStream(bitmapData)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.utils.network
 | 
				
			|||||||
import android.content.Context
 | 
					import android.content.Context
 | 
				
			||||||
import android.net.ConnectivityManager
 | 
					import android.net.ConnectivityManager
 | 
				
			||||||
import android.net.NetworkCapabilities
 | 
					import android.net.NetworkCapabilities
 | 
				
			||||||
import android.os.Build
 | 
					 | 
				
			||||||
import com.google.android.material.snackbar.Snackbar
 | 
					import com.google.android.material.snackbar.Snackbar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
lateinit var s: Snackbar
 | 
					lateinit var s: Snackbar
 | 
				
			||||||
@@ -11,19 +10,13 @@ lateinit var s: Snackbar
 | 
				
			|||||||
fun isNetworkAccessible(context: Context): Boolean {
 | 
					fun isNetworkAccessible(context: Context): Boolean {
 | 
				
			||||||
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
 | 
					    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 | 
					    val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false
 | 
				
			||||||
        val network = connectivityManager.activeNetwork ?: return false
 | 
					 | 
				
			||||||
        val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return when {
 | 
					    return when {
 | 
				
			||||||
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
 | 
					        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
 | 
				
			||||||
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
 | 
					        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
 | 
				
			||||||
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
 | 
					        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
 | 
				
			||||||
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
 | 
					        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
 | 
				
			||||||
            else -> false
 | 
					        else -> false
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        val network = connectivityManager.activeNetworkInfo ?: return false
 | 
					 | 
				
			||||||
        return network.isConnectedOrConnecting
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,31 +0,0 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.android.viewmodel
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import androidx.lifecycle.ViewModel
 | 
					 | 
				
			||||||
import androidx.lifecycle.viewModelScope
 | 
					 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
					 | 
				
			||||||
import kotlinx.coroutines.flow.MutableSharedFlow
 | 
					 | 
				
			||||||
import kotlinx.coroutines.flow.asSharedFlow
 | 
					 | 
				
			||||||
import kotlinx.coroutines.launch
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AppViewModel(private val repository: Repository) : ViewModel() {
 | 
					 | 
				
			||||||
    private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
 | 
					 | 
				
			||||||
    val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
 | 
					 | 
				
			||||||
    private var wasConnected = true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    init {
 | 
					 | 
				
			||||||
        viewModelScope.launch {
 | 
					 | 
				
			||||||
            repository.isConnectionAvailable.collect { isConnected ->
 | 
					 | 
				
			||||||
                if (repository.connectionMonitored) {
 | 
					 | 
				
			||||||
                    if (isConnected && !wasConnected && repository.connectionMonitored) {
 | 
					 | 
				
			||||||
                        _networkAvailableProvider.emit(true)
 | 
					 | 
				
			||||||
                        wasConnected = true
 | 
					 | 
				
			||||||
                    } else if (!isConnected && wasConnected && repository.connectionMonitored)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            _networkAvailableProvider.emit(false)
 | 
					 | 
				
			||||||
                            wasConnected = false
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -23,6 +23,7 @@
 | 
				
			|||||||
    <LinearLayout
 | 
					    <LinearLayout
 | 
				
			||||||
        android:layout_width="match_parent"
 | 
					        android:layout_width="match_parent"
 | 
				
			||||||
        android:layout_height="match_parent"
 | 
					        android:layout_height="match_parent"
 | 
				
			||||||
 | 
					        android:gravity="center"
 | 
				
			||||||
        android:orientation="vertical"
 | 
					        android:orientation="vertical"
 | 
				
			||||||
        android:padding="@dimen/activity_horizontal_margin">
 | 
					        android:padding="@dimen/activity_horizontal_margin">
 | 
				
			||||||
        <!-- Login progress -->
 | 
					        <!-- Login progress -->
 | 
				
			||||||
@@ -37,7 +38,7 @@
 | 
				
			|||||||
        <LinearLayout
 | 
					        <LinearLayout
 | 
				
			||||||
            android:id="@+id/loginForm"
 | 
					            android:id="@+id/loginForm"
 | 
				
			||||||
            android:layout_width="match_parent"
 | 
					            android:layout_width="match_parent"
 | 
				
			||||||
            android:layout_height="wrap_content"
 | 
					            android:layout_height="match_parent"
 | 
				
			||||||
            android:orientation="vertical">
 | 
					            android:orientation="vertical">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <EditText
 | 
					            <EditText
 | 
				
			||||||
@@ -100,4 +101,4 @@
 | 
				
			|||||||
        </LinearLayout>
 | 
					        </LinearLayout>
 | 
				
			||||||
    </LinearLayout>
 | 
					    </LinearLayout>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</LinearLayout>
 | 
					</LinearLayout>
 | 
				
			||||||
@@ -71,35 +71,13 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    </androidx.core.widget.NestedScrollView>
 | 
					    </androidx.core.widget.NestedScrollView>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <FrameLayout
 | 
					    <com.leinardi.android.speeddial.SpeedDialView
 | 
				
			||||||
        android:layout_width="match_parent"
 | 
					        android:id="@+id/speedDial"
 | 
				
			||||||
 | 
					        android:layout_width="wrap_content"
 | 
				
			||||||
        android:layout_height="wrap_content"
 | 
					        android:layout_height="wrap_content"
 | 
				
			||||||
        android:layout_gravity="start|bottom|end"
 | 
					        android:layout_gravity="bottom|end"
 | 
				
			||||||
        app:layout_constraintBottom_toBottomOf="parent"
 | 
					        app:layout_behavior="@string/speeddial_scrolling_view_snackbar_behavior"
 | 
				
			||||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
					        app:sdMainFabClosedSrc="@drawable/ic_add_white_24dp" />
 | 
				
			||||||
        app:layout_constraintStart_toStartOf="parent">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <com.github.rubensousa.floatingtoolbar.FloatingToolbar
 | 
					 | 
				
			||||||
            android:id="@+id/floatingToolbar"
 | 
					 | 
				
			||||||
            android:layout_width="match_parent"
 | 
					 | 
				
			||||||
            android:layout_height="?attr/actionBarSize"
 | 
					 | 
				
			||||||
            android:layout_gravity="bottom"
 | 
					 | 
				
			||||||
            app:floatingMenu="@menu/reader_toolbar" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <com.google.android.material.floatingactionbutton.FloatingActionButton
 | 
					 | 
				
			||||||
            android:id="@+id/fab"
 | 
					 | 
				
			||||||
            android:layout_width="wrap_content"
 | 
					 | 
				
			||||||
            android:layout_height="wrap_content"
 | 
					 | 
				
			||||||
            android:layout_gravity="end|bottom"
 | 
					 | 
				
			||||||
            android:layout_marginEnd="16dp"
 | 
					 | 
				
			||||||
            android:layout_marginBottom="16dp"
 | 
					 | 
				
			||||||
            android:paddingTop="@dimen/activity_vertical_margin"
 | 
					 | 
				
			||||||
            android:paddingBottom="@dimen/activity_vertical_margin"
 | 
					 | 
				
			||||||
            android:src="@drawable/ic_add_white_24dp"
 | 
					 | 
				
			||||||
            app:backgroundTint="?attr/colorAccent"
 | 
					 | 
				
			||||||
            app:fabSize="mini"
 | 
					 | 
				
			||||||
            app:rippleColor="?attr/colorAccentDark" />
 | 
					 | 
				
			||||||
    </FrameLayout>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <FrameLayout
 | 
					    <FrameLayout
 | 
				
			||||||
        android:id="@+id/progressBar"
 | 
					        android:id="@+id/progressBar"
 | 
				
			||||||
@@ -119,4 +97,5 @@
 | 
				
			|||||||
            android:progressTint="?attr/colorAccent" />
 | 
					            android:progressTint="?attr/colorAccent" />
 | 
				
			||||||
    </FrameLayout>
 | 
					    </FrameLayout>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
 | 
					
 | 
				
			||||||
 | 
					</androidx.coordinatorlayout.widget.CoordinatorLayout>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +0,0 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="utf-8"?>
 | 
					 | 
				
			||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
 | 
					 | 
				
			||||||
    xmlns:app="http://schemas.android.com/apk/res-auto">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <item
 | 
					 | 
				
			||||||
        android:id="@+id/unread_action"
 | 
					 | 
				
			||||||
        android:icon="@drawable/ic_baseline_white_eye_24dp"
 | 
					 | 
				
			||||||
        android:title="@string/unmark"
 | 
					 | 
				
			||||||
        app:showAsAction="ifRoom" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <item
 | 
					 | 
				
			||||||
        android:id="@+id/open_action"
 | 
					 | 
				
			||||||
        android:icon="@drawable/ic_open_in_browser_white_24dp"
 | 
					 | 
				
			||||||
        android:title="@string/reader_action_open"
 | 
					 | 
				
			||||||
        app:showAsAction="ifRoom" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <item
 | 
					 | 
				
			||||||
        android:id="@+id/share_action"
 | 
					 | 
				
			||||||
        android:icon="@drawable/ic_share_white_24dp"
 | 
					 | 
				
			||||||
        android:title="@string/reader_action_share"
 | 
					 | 
				
			||||||
        app:showAsAction="ifRoom" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</menu>
 | 
					 | 
				
			||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"No s'han llegit totes les publicacions"</string>
 | 
					    <string name="all_posts_not_read">"No s'han llegit totes les publicacions"</string>
 | 
				
			||||||
    <string name="all_posts_read">"S'han llegit totes les publicacions"</string>
 | 
					    <string name="all_posts_read">"S'han llegit totes les publicacions"</string>
 | 
				
			||||||
    <string name="undo_string">"Desfés"</string>
 | 
					    <string name="undo_string">"Desfés"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Inicieu la sessió per afegir fonts."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"No es pot obtenir la llista de fonts."</string>
 | 
					    <string name="cant_get_sources">"No es pot obtenir la llista de fonts."</string>
 | 
				
			||||||
    <string name="cant_create_source">"No es pot crear la font."</string>
 | 
					    <string name="cant_create_source">"No es pot crear la font."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Align left</string>
 | 
					    <string name="reader_text_align_left">Align left</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justify</string>
 | 
					    <string name="reader_text_align_justify">Justify</string>
 | 
				
			||||||
    <string name="settings_reader_font">Reader font</string>
 | 
					    <string name="settings_reader_font">Reader font</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"Quant a"</string>
 | 
					    <string name="action_about">"Quant a"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Element llegit"</string>
 | 
					    <string name="marked_as_read">"Element llegit"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Item unread"</string>
 | 
					    <string name="marked_as_unread">"Item unread"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"Nicht alle Beiträge wurden gelesen"</string>
 | 
					    <string name="all_posts_not_read">"Nicht alle Beiträge wurden gelesen"</string>
 | 
				
			||||||
    <string name="all_posts_read">"Alle Beiträge wurden gelesen"</string>
 | 
					    <string name="all_posts_read">"Alle Beiträge wurden gelesen"</string>
 | 
				
			||||||
    <string name="undo_string">"Rückgängig"</string>
 | 
					    <string name="undo_string">"Rückgängig"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Melde dich an um Quellen hinzuzufügen."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"Quellen können nicht abgerufen werden."</string>
 | 
					    <string name="cant_get_sources">"Quellen können nicht abgerufen werden."</string>
 | 
				
			||||||
    <string name="cant_create_source">"Quelle kann nicht gespeichert werden."</string>
 | 
					    <string name="cant_create_source">"Quelle kann nicht gespeichert werden."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Fehler beim Laden der Spouts-Liste aufgrund von Netzwerkproblemen."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Fehler beim Laden der Spouts-Liste aufgrund von Netzwerkproblemen."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Linksbündig</string>
 | 
					    <string name="reader_text_align_left">Linksbündig</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Blocksatz</string>
 | 
					    <string name="reader_text_align_justify">Blocksatz</string>
 | 
				
			||||||
    <string name="settings_reader_font">Schriftgröße im Lesemodus</string>
 | 
					    <string name="settings_reader_font">Schriftgröße im Lesemodus</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Statische untere Leiste im Lesemodus</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">Die untere Leiste wird dauerhaft angezeigt</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">Die untere Leiste kann über einen schwebenden Button angezeigt werden</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Quelle entfernen</string>
 | 
					    <string name="remove_source">Quelle entfernen</string>
 | 
				
			||||||
    <string name="pref_theme_title">Heller/Dunkler Modus</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dunkler Modus</string>
 | 
					    <string name="mode_dark">Dunkler Modus</string>
 | 
				
			||||||
    <string name="mode_system">Systemeinstellungen übernehmen</string>
 | 
					    <string name="mode_system">Systemeinstellungen übernehmen</string>
 | 
				
			||||||
    <string name="mode_light">Heller Modus</string>
 | 
					    <string name="mode_light">Heller Modus</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"Über"</string>
 | 
					    <string name="action_about">"Über"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Artikel gelesen"</string>
 | 
					    <string name="marked_as_read">"Artikel gelesen"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Item unread"</string>
 | 
					    <string name="marked_as_unread">"Item unread"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"No todas las publicaciones fueron leídas"</string>
 | 
					    <string name="all_posts_not_read">"No todas las publicaciones fueron leídas"</string>
 | 
				
			||||||
    <string name="all_posts_read">"Todas las publicaciones fueron leídas"</string>
 | 
					    <string name="all_posts_read">"Todas las publicaciones fueron leídas"</string>
 | 
				
			||||||
    <string name="undo_string">"Deshacer"</string>
 | 
					    <string name="undo_string">"Deshacer"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Iniciar sesión para añadir fuentes."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"No se puede obtener la lista de fuentes."</string>
 | 
					    <string name="cant_get_sources">"No se puede obtener la lista de fuentes."</string>
 | 
				
			||||||
    <string name="cant_create_source">"No se puede crear la fuente."</string>
 | 
					    <string name="cant_create_source">"No se puede crear la fuente."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Alinear a la izquierda</string>
 | 
					    <string name="reader_text_align_left">Alinear a la izquierda</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justificado</string>
 | 
					    <string name="reader_text_align_justify">Justificado</string>
 | 
				
			||||||
    <string name="settings_reader_font">Modo lectura</string>
 | 
					    <string name="settings_reader_font">Modo lectura</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"Acerca de"</string>
 | 
					    <string name="action_about">"Acerca de"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Artículo leído"</string>
 | 
					    <string name="marked_as_read">"Artículo leído"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Artículo no leído"</string>
 | 
					    <string name="marked_as_unread">"Artículo no leído"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"Tous les posts n'ont pas été lus"</string>
 | 
					    <string name="all_posts_not_read">"Tous les posts n'ont pas été lus"</string>
 | 
				
			||||||
    <string name="all_posts_read">"Tous les posts sont lus"</string>
 | 
					    <string name="all_posts_read">"Tous les posts sont lus"</string>
 | 
				
			||||||
    <string name="undo_string">"Annuler"</string>
 | 
					    <string name="undo_string">"Annuler"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Identifiez-vous pour ajouter une source."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"Impossible de récupérer la liste des sources."</string>
 | 
					    <string name="cant_get_sources">"Impossible de récupérer la liste des sources."</string>
 | 
				
			||||||
    <string name="cant_create_source">"Impossible de créer la source."</string>
 | 
					    <string name="cant_create_source">"Impossible de créer la source."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Impossible d'obtenir la liste des spouts en raison d'un problème de réseau."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Impossible d'obtenir la liste des spouts en raison d'un problème de réseau."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Aligner à gauche</string>
 | 
					    <string name="reader_text_align_left">Aligner à gauche</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justifier le texte</string>
 | 
					    <string name="reader_text_align_justify">Justifier le texte</string>
 | 
				
			||||||
    <string name="settings_reader_font">Police du lecteur d\'articles</string>
 | 
					    <string name="settings_reader_font">Police du lecteur d\'articles</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Barre statique pour le visionneur d\'articles</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">La barre sera affichée</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">La barre sera affichée grâce au bouton</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Supprimer la source</string>
 | 
					    <string name="remove_source">Supprimer la source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Thème Clair/Sombre</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Thème sombre</string>
 | 
					    <string name="mode_dark">Thème sombre</string>
 | 
				
			||||||
    <string name="mode_system">Utiliser les paramètres système</string>
 | 
					    <string name="mode_system">Utiliser les paramètres système</string>
 | 
				
			||||||
    <string name="mode_light">Thème clair</string>
 | 
					    <string name="mode_light">Thème clair</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"À propos"</string>
 | 
					    <string name="action_about">"À propos"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Marqué comme lu"</string>
 | 
					    <string name="marked_as_read">"Marqué comme lu"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Marqué comme non lu"</string>
 | 
					    <string name="marked_as_unread">"Marqué comme non lu"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"Non se leron todas as publicacións"</string>
 | 
					    <string name="all_posts_not_read">"Non se leron todas as publicacións"</string>
 | 
				
			||||||
    <string name="all_posts_read">"Leronse todas as publicacións"</string>
 | 
					    <string name="all_posts_read">"Leronse todas as publicacións"</string>
 | 
				
			||||||
    <string name="undo_string">"Desfacer"</string>
 | 
					    <string name="undo_string">"Desfacer"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Accede pra engadir fontes."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"Non se pode obter a lista de fontes."</string>
 | 
					    <string name="cant_get_sources">"Non se pode obter a lista de fontes."</string>
 | 
				
			||||||
    <string name="cant_create_source">"Non se pode crear unha fonte."</string>
 | 
					    <string name="cant_create_source">"Non se pode crear unha fonte."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Non se pode obter a lista de spouts por mor dun erro de rede."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Non se pode obter a lista de spouts por mor dun erro de rede."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Aliñar á esquerda</string>
 | 
					    <string name="reader_text_align_left">Aliñar á esquerda</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Xustificado</string>
 | 
					    <string name="reader_text_align_justify">Xustificado</string>
 | 
				
			||||||
    <string name="settings_reader_font">Modo lector</string>
 | 
					    <string name="settings_reader_font">Modo lector</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Barra inferior estática na vista de artigos</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">A barra inferior mostrarase sempre</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">A barra inferior pode mostrarse a través do botón flotante</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Eliminar fonte</string>
 | 
					    <string name="remove_source">Eliminar fonte</string>
 | 
				
			||||||
    <string name="pref_theme_title">Modo Claro/Escuro</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Modo escuro</string>
 | 
					    <string name="mode_dark">Modo escuro</string>
 | 
				
			||||||
    <string name="mode_system">Seguir axustes do sistema</string>
 | 
					    <string name="mode_system">Seguir axustes do sistema</string>
 | 
				
			||||||
    <string name="mode_light">Modo claro</string>
 | 
					    <string name="mode_light">Modo claro</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"Acerca de"</string>
 | 
					    <string name="action_about">"Acerca de"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Elemento lido"</string>
 | 
					    <string name="marked_as_read">"Elemento lido"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Elemento non lido"</string>
 | 
					    <string name="marked_as_unread">"Elemento non lido"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"Semua pos belum dibaca"</string>
 | 
					    <string name="all_posts_not_read">"Semua pos belum dibaca"</string>
 | 
				
			||||||
    <string name="all_posts_read">"Semua pos sudah dibaca"</string>
 | 
					    <string name="all_posts_read">"Semua pos sudah dibaca"</string>
 | 
				
			||||||
    <string name="undo_string">"Urung"</string>
 | 
					    <string name="undo_string">"Urung"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Masuk untuk menambah sumber."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"Tidak bisa mendapatkan daftar sumber."</string>
 | 
					    <string name="cant_get_sources">"Tidak bisa mendapatkan daftar sumber."</string>
 | 
				
			||||||
    <string name="cant_create_source">"Tidak dapat membuat sumber."</string>
 | 
					    <string name="cant_create_source">"Tidak dapat membuat sumber."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Align left</string>
 | 
					    <string name="reader_text_align_left">Align left</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justify</string>
 | 
					    <string name="reader_text_align_justify">Justify</string>
 | 
				
			||||||
    <string name="settings_reader_font">Reader font</string>
 | 
					    <string name="settings_reader_font">Reader font</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"Tentang"</string>
 | 
					    <string name="action_about">"Tentang"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Membaca item"</string>
 | 
					    <string name="marked_as_read">"Membaca item"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Item unread"</string>
 | 
					    <string name="marked_as_unread">"Item unread"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"All posts weren't read"</string>
 | 
					    <string name="all_posts_not_read">"All posts weren't read"</string>
 | 
				
			||||||
    <string name="all_posts_read">"Tutti i messaggi sono stati letti"</string>
 | 
					    <string name="all_posts_read">"Tutti i messaggi sono stati letti"</string>
 | 
				
			||||||
    <string name="undo_string">"Annulla"</string>
 | 
					    <string name="undo_string">"Annulla"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Autenticati per aggiungere fonti."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"Can't get sources list."</string>
 | 
					    <string name="cant_get_sources">"Can't get sources list."</string>
 | 
				
			||||||
    <string name="cant_create_source">"Can't create source."</string>
 | 
					    <string name="cant_create_source">"Can't create source."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Align left</string>
 | 
					    <string name="reader_text_align_left">Align left</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justify</string>
 | 
					    <string name="reader_text_align_justify">Justify</string>
 | 
				
			||||||
    <string name="settings_reader_font">Reader font</string>
 | 
					    <string name="settings_reader_font">Reader font</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"Informazioni"</string>
 | 
					    <string name="action_about">"Informazioni"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Articolo letto"</string>
 | 
					    <string name="marked_as_read">"Articolo letto"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Item unread"</string>
 | 
					    <string name="marked_as_unread">"Item unread"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"모든 게시물을 읽지 않았습니다."</string>
 | 
					    <string name="all_posts_not_read">"모든 게시물을 읽지 않았습니다."</string>
 | 
				
			||||||
    <string name="all_posts_read">"모든 게시물을 읽었습니다."</string>
 | 
					    <string name="all_posts_read">"모든 게시물을 읽었습니다."</string>
 | 
				
			||||||
    <string name="undo_string">"실행 취소"</string>
 | 
					    <string name="undo_string">"실행 취소"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"로그인 소스를 추가 해야 합니다."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"소스 리스트를 얻을 수 없습니다."</string>
 | 
					    <string name="cant_get_sources">"소스 리스트를 얻을 수 없습니다."</string>
 | 
				
			||||||
    <string name="cant_create_source">"소스를 만들 수 없습니다."</string>
 | 
					    <string name="cant_create_source">"소스를 만들 수 없습니다."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Align left</string>
 | 
					    <string name="reader_text_align_left">Align left</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justify</string>
 | 
					    <string name="reader_text_align_justify">Justify</string>
 | 
				
			||||||
    <string name="settings_reader_font">Reader font</string>
 | 
					    <string name="settings_reader_font">Reader font</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"정보"</string>
 | 
					    <string name="action_about">"정보"</string>
 | 
				
			||||||
    <string name="marked_as_read">"항목 읽기"</string>
 | 
					    <string name="marked_as_read">"항목 읽기"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Item unread"</string>
 | 
					    <string name="marked_as_unread">"Item unread"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,7 @@
 | 
				
			|||||||
    <string name="wrong_infos">"Controleer de gegevens nogmaals."</string>
 | 
					    <string name="wrong_infos">"Controleer de gegevens nogmaals."</string>
 | 
				
			||||||
    <string name="all_posts_not_read">"Fout bij markeren als gelezen"</string>
 | 
					    <string name="all_posts_not_read">"Fout bij markeren als gelezen"</string>
 | 
				
			||||||
    <string name="all_posts_read">"Alle artikelen gemarkeerd als gelezen"</string>
 | 
					    <string name="all_posts_read">"Alle artikelen gemarkeerd als gelezen"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Login om bronnen toe te voegen"</string>
 | 
					    <string name="undo_string">"Ongedaan maken"</string>
 | 
				
			||||||
    <string name="cant_get_sources">"Kan de lijst met bronnen niet ophalen"</string>
 | 
					    <string name="cant_get_sources">"Kan de lijst met bronnen niet ophalen"</string>
 | 
				
			||||||
    <string name="cant_create_source">"Kan bron niet creëeren"</string>
 | 
					    <string name="cant_create_source">"Kan bron niet creëeren"</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -105,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Align left</string>
 | 
					    <string name="reader_text_align_left">Align left</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justify</string>
 | 
					    <string name="reader_text_align_justify">Justify</string>
 | 
				
			||||||
    <string name="settings_reader_font">Reader font</string>
 | 
					    <string name="settings_reader_font">Reader font</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -131,5 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"Over"</string>
 | 
					    <string name="action_about">"Over"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Artikel gelezen"</string>
 | 
					    <string name="marked_as_read">"Artikel gelezen"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Item unread"</string>
 | 
					    <string name="marked_as_unread">"Item unread"</string>
 | 
				
			||||||
    <string name="undo_string">"Ongedaan maken"</string>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"Nenhum post foi lido"</string>
 | 
					    <string name="all_posts_not_read">"Nenhum post foi lido"</string>
 | 
				
			||||||
    <string name="all_posts_read">"Todos os posts foram lidos"</string>
 | 
					    <string name="all_posts_read">"Todos os posts foram lidos"</string>
 | 
				
			||||||
    <string name="undo_string">"Desfazer"</string>
 | 
					    <string name="undo_string">"Desfazer"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Faça login para adicionar fontes."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"Não é possível obter a lista de fontes."</string>
 | 
					    <string name="cant_get_sources">"Não é possível obter a lista de fontes."</string>
 | 
				
			||||||
    <string name="cant_create_source">"Não é possível criar fonte."</string>
 | 
					    <string name="cant_create_source">"Não é possível criar fonte."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Align left</string>
 | 
					    <string name="reader_text_align_left">Align left</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justify</string>
 | 
					    <string name="reader_text_align_justify">Justify</string>
 | 
				
			||||||
    <string name="settings_reader_font">Reader font</string>
 | 
					    <string name="settings_reader_font">Reader font</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"Sobre"</string>
 | 
					    <string name="action_about">"Sobre"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Item lido"</string>
 | 
					    <string name="marked_as_read">"Item lido"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Item unread"</string>
 | 
					    <string name="marked_as_unread">"Item unread"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"Todas as postagens não foram lidas"</string>
 | 
					    <string name="all_posts_not_read">"Todas as postagens não foram lidas"</string>
 | 
				
			||||||
    <string name="all_posts_read">"Todas as postagens foram lidas"</string>
 | 
					    <string name="all_posts_read">"Todas as postagens foram lidas"</string>
 | 
				
			||||||
    <string name="undo_string">"Desfazer"</string>
 | 
					    <string name="undo_string">"Desfazer"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Logar para adicionar fontes."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"Não é possível obter a lista de fontes."</string>
 | 
					    <string name="cant_get_sources">"Não é possível obter a lista de fontes."</string>
 | 
				
			||||||
    <string name="cant_create_source">"Não é possível criar a fonte."</string>
 | 
					    <string name="cant_create_source">"Não é possível criar a fonte."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Align left</string>
 | 
					    <string name="reader_text_align_left">Align left</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justify</string>
 | 
					    <string name="reader_text_align_justify">Justify</string>
 | 
				
			||||||
    <string name="settings_reader_font">Reader font</string>
 | 
					    <string name="settings_reader_font">Reader font</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"Sobre"</string>
 | 
					    <string name="action_about">"Sobre"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Item lido"</string>
 | 
					    <string name="marked_as_read">"Item lido"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Item unread"</string>
 | 
					    <string name="marked_as_unread">"Item unread"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"All posts weren't read"</string>
 | 
					    <string name="all_posts_not_read">"All posts weren't read"</string>
 | 
				
			||||||
    <string name="all_posts_read">"All posts were read"</string>
 | 
					    <string name="all_posts_read">"All posts were read"</string>
 | 
				
			||||||
    <string name="undo_string">"Undo"</string>
 | 
					    <string name="undo_string">"Undo"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Log in to add sources."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"Can't get sources list."</string>
 | 
					    <string name="cant_get_sources">"Can't get sources list."</string>
 | 
				
			||||||
    <string name="cant_create_source">"Can't create source."</string>
 | 
					    <string name="cant_create_source">"Can't create source."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Align left</string>
 | 
					    <string name="reader_text_align_left">Align left</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justify</string>
 | 
					    <string name="reader_text_align_justify">Justify</string>
 | 
				
			||||||
    <string name="settings_reader_font">Reader font</string>
 | 
					    <string name="settings_reader_font">Reader font</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"මේ ගැන"</string>
 | 
					    <string name="action_about">"මේ ගැන"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Item read"</string>
 | 
					    <string name="marked_as_read">"Item read"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Item unread"</string>
 | 
					    <string name="marked_as_unread">"Item unread"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"Tüm mesajlar okunmadı"</string>
 | 
					    <string name="all_posts_not_read">"Tüm mesajlar okunmadı"</string>
 | 
				
			||||||
    <string name="all_posts_read">"Tüm mesajlar okundu"</string>
 | 
					    <string name="all_posts_read">"Tüm mesajlar okundu"</string>
 | 
				
			||||||
    <string name="undo_string">"Geri al"</string>
 | 
					    <string name="undo_string">"Geri al"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Kaynakları eklemek için giriş yapın."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"Kaynakları listesi alınamıyor."</string>
 | 
					    <string name="cant_get_sources">"Kaynakları listesi alınamıyor."</string>
 | 
				
			||||||
    <string name="cant_create_source">"Kaynak oluşturulamıyor."</string>
 | 
					    <string name="cant_create_source">"Kaynak oluşturulamıyor."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Align left</string>
 | 
					    <string name="reader_text_align_left">Align left</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justify</string>
 | 
					    <string name="reader_text_align_justify">Justify</string>
 | 
				
			||||||
    <string name="settings_reader_font">Reader font</string>
 | 
					    <string name="settings_reader_font">Reader font</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"Hakkında"</string>
 | 
					    <string name="action_about">"Hakkında"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Öğeleri oku"</string>
 | 
					    <string name="marked_as_read">"Öğeleri oku"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Item unread"</string>
 | 
					    <string name="marked_as_unread">"Item unread"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"所有帖子都未读"</string>
 | 
					    <string name="all_posts_not_read">"所有帖子都未读"</string>
 | 
				
			||||||
    <string name="all_posts_read">"所有帖子已读"</string>
 | 
					    <string name="all_posts_read">"所有帖子已读"</string>
 | 
				
			||||||
    <string name="undo_string">"撤销"</string>
 | 
					    <string name="undo_string">"撤销"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"登录以添加数据源。"</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"无法获取数据列表。"</string>
 | 
					    <string name="cant_get_sources">"无法获取数据列表。"</string>
 | 
				
			||||||
    <string name="cant_create_source">"无法创建源数据。"</string>
 | 
					    <string name="cant_create_source">"无法创建源数据。"</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"由于网络问题,无法获取 spouts 列表。"</string>
 | 
					    <string name="cant_get_spouts_no_network">"由于网络问题,无法获取 spouts 列表。"</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">左对齐</string>
 | 
					    <string name="reader_text_align_left">左对齐</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">左右对齐</string>
 | 
					    <string name="reader_text_align_justify">左右对齐</string>
 | 
				
			||||||
    <string name="settings_reader_font">阅读器字体</string>
 | 
					    <string name="settings_reader_font">阅读器字体</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">文章查看器中的静态底部栏</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">底部栏将始终显示</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">底部栏可以通过浮动按钮显示</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">删除源</string>
 | 
					    <string name="remove_source">删除源</string>
 | 
				
			||||||
    <string name="pref_theme_title">浅色/深色模式</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">深色模式</string>
 | 
					    <string name="mode_dark">深色模式</string>
 | 
				
			||||||
    <string name="mode_system">遵循系统设置</string>
 | 
					    <string name="mode_system">遵循系统设置</string>
 | 
				
			||||||
    <string name="mode_light">浅色模式</string>
 | 
					    <string name="mode_light">浅色模式</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"关于我们"</string>
 | 
					    <string name="action_about">"关于我们"</string>
 | 
				
			||||||
    <string name="marked_as_read">"已读"</string>
 | 
					    <string name="marked_as_read">"已读"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"未读条目"</string>
 | 
					    <string name="marked_as_unread">"未读条目"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"所有帖子都未读"</string>
 | 
					    <string name="all_posts_not_read">"所有帖子都未读"</string>
 | 
				
			||||||
    <string name="all_posts_read">"所有帖子已读"</string>
 | 
					    <string name="all_posts_read">"所有帖子已读"</string>
 | 
				
			||||||
    <string name="undo_string">"撤销"</string>
 | 
					    <string name="undo_string">"撤销"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"登录以添加数据源。"</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"无法获取数据列表。"</string>
 | 
					    <string name="cant_get_sources">"无法获取数据列表。"</string>
 | 
				
			||||||
    <string name="cant_create_source">"无法创建源数据。"</string>
 | 
					    <string name="cant_create_source">"无法创建源数据。"</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -106,11 +105,7 @@
 | 
				
			|||||||
    <string name="reader_text_align_left">Align left</string>
 | 
					    <string name="reader_text_align_left">Align left</string>
 | 
				
			||||||
    <string name="reader_text_align_justify">Justify</string>
 | 
					    <string name="reader_text_align_justify">Justify</string>
 | 
				
			||||||
    <string name="settings_reader_font">Reader font</string>
 | 
					    <string name="settings_reader_font">Reader font</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -132,4 +127,6 @@
 | 
				
			|||||||
    <string name="action_about">"关于我们"</string>
 | 
					    <string name="action_about">"关于我们"</string>
 | 
				
			||||||
    <string name="marked_as_read">"已读"</string>
 | 
					    <string name="marked_as_read">"已读"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"未讀項目"</string>
 | 
					    <string name="marked_as_unread">"未讀項目"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								androidApp/src/main/res/values/ids.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								androidApp/src/main/res/values/ids.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<resources>
 | 
				
			||||||
 | 
					    <item name="unread_action" type="id" />
 | 
				
			||||||
 | 
					    <item name="open_action" type="id" />
 | 
				
			||||||
 | 
					    <item name="share_action" type="id" />
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
@@ -23,7 +23,6 @@
 | 
				
			|||||||
    <string name="all_posts_not_read">"All posts weren't read"</string>
 | 
					    <string name="all_posts_not_read">"All posts weren't read"</string>
 | 
				
			||||||
    <string name="all_posts_read">"All posts were read"</string>
 | 
					    <string name="all_posts_read">"All posts were read"</string>
 | 
				
			||||||
    <string name="undo_string">"Undo"</string>
 | 
					    <string name="undo_string">"Undo"</string>
 | 
				
			||||||
    <string name="addStringNoUrl">"Log in to add sources."</string>
 | 
					 | 
				
			||||||
    <string name="cant_get_sources">"Can't get sources list."</string>
 | 
					    <string name="cant_get_sources">"Can't get sources list."</string>
 | 
				
			||||||
    <string name="cant_create_source">"Can't create source."</string>
 | 
					    <string name="cant_create_source">"Can't create source."</string>
 | 
				
			||||||
    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
					    <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
 | 
				
			||||||
@@ -108,11 +107,7 @@
 | 
				
			|||||||
    <string name="source_code_pro_font_id" translatable="false">source_code_pro_medium</string>
 | 
					    <string name="source_code_pro_font_id" translatable="false">source_code_pro_medium</string>
 | 
				
			||||||
    <string name="open_sans_font_id" translatable="false">open_sans</string>
 | 
					    <string name="open_sans_font_id" translatable="false">open_sans</string>
 | 
				
			||||||
    <string name="roboto_font_id" translatable="false">roboto</string>
 | 
					    <string name="roboto_font_id" translatable="false">roboto</string>
 | 
				
			||||||
    <string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_on">The bottom bar will always be displayed</string>
 | 
					 | 
				
			||||||
    <string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
 | 
					 | 
				
			||||||
    <string name="remove_source">Remove source</string>
 | 
					    <string name="remove_source">Remove source</string>
 | 
				
			||||||
    <string name="pref_theme_title">Light/Dark mode</string>
 | 
					 | 
				
			||||||
    <string name="mode_dark">Dark mode</string>
 | 
					    <string name="mode_dark">Dark mode</string>
 | 
				
			||||||
    <string name="mode_system">Follow the system setting</string>
 | 
					    <string name="mode_system">Follow the system setting</string>
 | 
				
			||||||
    <string name="mode_light">Light mode</string>
 | 
					    <string name="mode_light">Light mode</string>
 | 
				
			||||||
@@ -134,4 +129,6 @@
 | 
				
			|||||||
    <string name="action_about">"About"</string>
 | 
					    <string name="action_about">"About"</string>
 | 
				
			||||||
    <string name="marked_as_read">"Item read"</string>
 | 
					    <string name="marked_as_read">"Item read"</string>
 | 
				
			||||||
    <string name="marked_as_unread">"Item unread"</string>
 | 
					    <string name="marked_as_unread">"Item unread"</string>
 | 
				
			||||||
</resources>
 | 
					    <string name="confirm_delete_title">Confirm Deletion</string>
 | 
				
			||||||
 | 
					    <string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
 | 
				
			||||||
 | 
					</resources>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,14 +30,6 @@
 | 
				
			|||||||
        android:summaryOn="@string/pref_article_viewer_on"
 | 
					        android:summaryOn="@string/pref_article_viewer_on"
 | 
				
			||||||
        android:title="@string/pref_article_viewer_title"
 | 
					        android:title="@string/pref_article_viewer_title"
 | 
				
			||||||
        app:iconSpaceReserved="false"/>
 | 
					        app:iconSpaceReserved="false"/>
 | 
				
			||||||
    <SwitchPreference
 | 
					 | 
				
			||||||
        android:defaultValue="false"
 | 
					 | 
				
			||||||
        android:dependency="prefer_article_viewer"
 | 
					 | 
				
			||||||
        android:key="reader_static_bar"
 | 
					 | 
				
			||||||
        android:summaryOff="@string/reader_static_bar_off"
 | 
					 | 
				
			||||||
        android:summaryOn="@string/reader_static_bar_on"
 | 
					 | 
				
			||||||
        android:title="@string/reader_static_bar_title"
 | 
					 | 
				
			||||||
        app:iconSpaceReserved="false"/>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <PreferenceCategory
 | 
					    <PreferenceCategory
 | 
				
			||||||
        android:title="@string/pref_general_category_displaying">
 | 
					        android:title="@string/pref_general_category_displaying">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +0,0 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="utf-8"?>
 | 
					 | 
				
			||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
 | 
					 | 
				
			||||||
    xmlns:app="http://schemas.android.com/apk/res-auto">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <ListPreference
 | 
					 | 
				
			||||||
        android:defaultValue="0"
 | 
					 | 
				
			||||||
        android:entries="@array/ModeTitles"
 | 
					 | 
				
			||||||
        android:entryValues="@array/ModeValues"
 | 
					 | 
				
			||||||
        android:key="currentMode"
 | 
					 | 
				
			||||||
        app:iconSpaceReserved="false"
 | 
					 | 
				
			||||||
        android:title="@string/pref_theme_title"
 | 
					 | 
				
			||||||
        app:useSimpleSummaryProvider="false" />
 | 
					 | 
				
			||||||
</PreferenceScreen>
 | 
					 | 
				
			||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					@file:Suppress("ktlint")
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
 | 
					package bou.amine.apps.readerforselfossv2.android.tests.robolectric
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.view.Menu
 | 
					import android.view.Menu
 | 
				
			||||||
@@ -11,13 +13,18 @@ fun dialogMessage(): String {
 | 
				
			|||||||
    return latestDialog.findViewById<TextView>(android.R.id.message)?.text.toString()
 | 
					    return latestDialog.findViewById<TextView>(android.R.id.message)?.text.toString()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun Menu.assertClickable(@IdRes id: Int) {
 | 
					fun Menu.assertClickable(
 | 
				
			||||||
 | 
					    @IdRes id: Int,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
    this.assertVisible(id)
 | 
					    this.assertVisible(id)
 | 
				
			||||||
    val item = this.findItem(id)
 | 
					    val item = this.findItem(id)
 | 
				
			||||||
    assertTrue(item.isEnabled)
 | 
					    assertTrue(item.isEnabled)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun Menu.assertVisible(@IdRes id: Int) {
 | 
					fun Menu.assertVisible(
 | 
				
			||||||
 | 
					    @IdRes id: Int,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
    val item = this.findItem(id)
 | 
					    val item = this.findItem(id)
 | 
				
			||||||
    assertTrue(item.isVisible)
 | 
					    assertTrue(item.isVisible)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					@file:Suppress("ktlint")
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
 | 
					package bou.amine.apps.readerforselfossv2.android.tests.robolectric
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.widget.Button
 | 
					import android.widget.Button
 | 
				
			||||||
@@ -11,10 +13,8 @@ import org.junit.Test
 | 
				
			|||||||
import org.junit.runner.RunWith
 | 
					import org.junit.runner.RunWith
 | 
				
			||||||
import org.robolectric.Robolectric
 | 
					import org.robolectric.Robolectric
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@RunWith(RobotElectriqueRunner::class)
 | 
				
			||||||
@RunWith(RobotElectriqueRunnerclass::class)
 | 
					 | 
				
			||||||
class LoginActivityTest {
 | 
					class LoginActivityTest {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun login_shouldDisplay() {
 | 
					    fun login_shouldDisplay() {
 | 
				
			||||||
        Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
 | 
					        Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
 | 
				
			||||||
@@ -59,7 +59,8 @@ class LoginActivityTest {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /* @Test
 | 
					 */
 | 
				
			||||||
 | 
					/* @Test
 | 
				
			||||||
     fun connect() {
 | 
					     fun connect() {
 | 
				
			||||||
         Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
 | 
					         Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
 | 
				
			||||||
             controller.setup() // Moves the Activity to the RESUMED state
 | 
					             controller.setup() // Moves the Activity to the RESUMED state
 | 
				
			||||||
@@ -74,4 +75,7 @@ class LoginActivityTest {
 | 
				
			|||||||
             assertEquals(expectedIntent.component, actual.component)
 | 
					             assertEquals(expectedIntent.component, actual.component)
 | 
				
			||||||
         }
 | 
					         }
 | 
				
			||||||
     }*/
 | 
					     }*/
 | 
				
			||||||
}
 | 
					/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,13 @@
 | 
				
			|||||||
 | 
					@file:Suppress("ktlint")
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
 | 
					package bou.amine.apps.readerforselfossv2.android.tests.robolectric
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.robolectric.RobolectricTestRunner
 | 
					import org.robolectric.RobolectricTestRunner
 | 
				
			||||||
import org.robolectric.annotation.Config
 | 
					import org.robolectric.annotation.Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RobotElectriqueRunnerclass(testClass: Class<*>?) :
 | 
					class RobotElectriqueRunner(
 | 
				
			||||||
    RobolectricTestRunner(testClass) {
 | 
					    testClass: Class<*>?,
 | 
				
			||||||
 | 
					) : RobolectricTestRunner(testClass) {
 | 
				
			||||||
    override fun buildGlobalConfig(): Config {
 | 
					    override fun buildGlobalConfig(): Config = Config.Builder().setSdk(25, 30, 33).build()
 | 
				
			||||||
        return Config.Builder().setSdk(25, 30, 33).build()
 | 
					}
 | 
				
			||||||
    }
 | 
					*/
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					@file:Suppress("detekt:LargeClass")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
package bou.amine.apps.readerforselfossv2.tests.repository
 | 
					package bou.amine.apps.readerforselfossv2.tests.repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
 | 
					import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
 | 
				
			||||||
@@ -9,6 +11,7 @@ import bou.amine.apps.readerforselfossv2.model.SuccessResponse
 | 
				
			|||||||
import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
					import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
 | 
					import bou.amine.apps.readerforselfossv2.rest.SelfossApi
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
					import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
				
			||||||
 | 
					import bou.amine.apps.readerforselfossv2.service.ConnectivityService
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.ItemType
 | 
					import bou.amine.apps.readerforselfossv2.utils.ItemType
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.utils.toView
 | 
					import bou.amine.apps.readerforselfossv2.utils.toView
 | 
				
			||||||
import io.mockk.clearAllMocks
 | 
					import io.mockk.clearAllMocks
 | 
				
			||||||
@@ -22,7 +25,6 @@ import junit.framework.TestCase.assertFalse
 | 
				
			|||||||
import junit.framework.TestCase.assertNotSame
 | 
					import junit.framework.TestCase.assertNotSame
 | 
				
			||||||
import junit.framework.TestCase.assertSame
 | 
					import junit.framework.TestCase.assertSame
 | 
				
			||||||
import junit.framework.TestCase.assertTrue
 | 
					import junit.framework.TestCase.assertTrue
 | 
				
			||||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
					 | 
				
			||||||
import kotlinx.coroutines.runBlocking
 | 
					import kotlinx.coroutines.runBlocking
 | 
				
			||||||
import org.junit.Assert.assertNotEquals
 | 
					import org.junit.Assert.assertNotEquals
 | 
				
			||||||
import org.junit.Before
 | 
					import org.junit.Before
 | 
				
			||||||
@@ -42,23 +44,20 @@ private const val FEED_URL = "https://test.com/feed"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
private const val TAGS = "Test, New"
 | 
					private const val TAGS = "Test, New"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val NUMBER_ARTICLES = 100
 | 
				
			||||||
 | 
					private const val NUMBER_UNREAD = 50
 | 
				
			||||||
 | 
					private const val NUMBER_STARRED = 20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RepositoryTest {
 | 
					class RepositoryTest {
 | 
				
			||||||
    private val db = mockk<ReaderForSelfossDB>(relaxed = true)
 | 
					    private val db = mockk<ReaderForSelfossDB>(relaxed = true)
 | 
				
			||||||
    private val appSettingsService = mockk<AppSettingsService>()
 | 
					    private val appSettingsService = mockk<AppSettingsService>()
 | 
				
			||||||
    private val api = mockk<SelfossApi>()
 | 
					    private val api = mockk<SelfossApi>()
 | 
				
			||||||
 | 
					    private val connectivityService = mockk<ConnectivityService>()
 | 
				
			||||||
    private val NUMBER_ARTICLES = 100
 | 
					 | 
				
			||||||
    private val NUMBER_UNREAD = 50
 | 
					 | 
				
			||||||
    private val NUMBER_STARRED = 20
 | 
					 | 
				
			||||||
    private lateinit var repository: Repository
 | 
					    private lateinit var repository: Repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun initializeRepository(
 | 
					    private fun initializeRepository(isNetworkAvailable: Boolean = true) {
 | 
				
			||||||
        isConnectionAvailable: MutableStateFlow<Boolean> =
 | 
					        every { connectivityService.isNetworkAvailable() } returns isNetworkAvailable
 | 
				
			||||||
            MutableStateFlow(
 | 
					        repository = Repository(api, appSettingsService, connectivityService, db)
 | 
				
			||||||
                true,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
        repository = Repository(api, appSettingsService, isConnectionAvailable, db)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            repository.updateApiInformation()
 | 
					            repository.updateApiInformation()
 | 
				
			||||||
@@ -75,19 +74,20 @@ class RepositoryTest {
 | 
				
			|||||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
					        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        coEvery { api.apiInformation() } returns
 | 
					        coEvery { api.apiInformation() } returns
 | 
				
			||||||
                StatusAndData(
 | 
					            StatusAndData(
 | 
				
			||||||
                    success = true,
 | 
					                success = true,
 | 
				
			||||||
                    data = SelfossModel.ApiInformation(
 | 
					                data =
 | 
				
			||||||
 | 
					                    SelfossModel.ApiInformation(
 | 
				
			||||||
                        "2.19-ba1e8e3",
 | 
					                        "2.19-ba1e8e3",
 | 
				
			||||||
                        "4.0.0",
 | 
					                        "4.0.0",
 | 
				
			||||||
                        SelfossModel.ApiConfiguration(false, true)
 | 
					                        SelfossModel.ApiConfiguration(false, true),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
        coEvery { api.stats() } returns
 | 
					        coEvery { api.stats() } returns
 | 
				
			||||||
                StatusAndData(
 | 
					            StatusAndData(
 | 
				
			||||||
                    success = true,
 | 
					                success = true,
 | 
				
			||||||
                    data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED),
 | 
					                data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED),
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit
 | 
					        every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit
 | 
				
			||||||
        every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems()
 | 
					        every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems()
 | 
				
			||||||
@@ -107,7 +107,7 @@ class RepositoryTest {
 | 
				
			|||||||
    fun instantiate_repository_without_api_version() {
 | 
					    fun instantiate_repository_without_api_version() {
 | 
				
			||||||
        every { appSettingsService.getApiVersion() } returns -1
 | 
					        every { appSettingsService.getApiVersion() } returns -1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        coVerify(exactly = 0) { api.apiInformation() }
 | 
					        coVerify(exactly = 0) { api.apiInformation() }
 | 
				
			||||||
        coVerify(exactly = 0) { api.stats() }
 | 
					        coVerify(exactly = 0) { api.stats() }
 | 
				
			||||||
@@ -117,7 +117,7 @@ class RepositoryTest {
 | 
				
			|||||||
    fun get_api_4_date_with_api_1_version_stored() {
 | 
					    fun get_api_4_date_with_api_1_version_stored() {
 | 
				
			||||||
        every { appSettingsService.getApiVersion() } returns 1
 | 
					        every { appSettingsService.getApiVersion() } returns 1
 | 
				
			||||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
					        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
				
			||||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
					            StatusAndData(success = true, data = generateTestApiItem())
 | 
				
			||||||
        every { appSettingsService.updateApiVersion(any()) } returns Unit
 | 
					        every { appSettingsService.updateApiVersion(any()) } returns Unit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
@@ -133,14 +133,15 @@ class RepositoryTest {
 | 
				
			|||||||
    fun get_public_access() {
 | 
					    fun get_public_access() {
 | 
				
			||||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
					        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
				
			||||||
        coEvery { api.apiInformation() } returns
 | 
					        coEvery { api.apiInformation() } returns
 | 
				
			||||||
                StatusAndData(
 | 
					            StatusAndData(
 | 
				
			||||||
                    success = true,
 | 
					                success = true,
 | 
				
			||||||
                    data = SelfossModel.ApiInformation(
 | 
					                data =
 | 
				
			||||||
 | 
					                    SelfossModel.ApiInformation(
 | 
				
			||||||
                        "2.19-ba1e8e3",
 | 
					                        "2.19-ba1e8e3",
 | 
				
			||||||
                        "4.0.0",
 | 
					                        "4.0.0",
 | 
				
			||||||
                        SelfossModel.ApiConfiguration(true, true)
 | 
					                        SelfossModel.ApiConfiguration(true, true),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
        every { appSettingsService.getUserName() } returns ""
 | 
					        every { appSettingsService.getUserName() } returns ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
@@ -153,14 +154,15 @@ class RepositoryTest {
 | 
				
			|||||||
    fun get_public_access_username_not_empty() {
 | 
					    fun get_public_access_username_not_empty() {
 | 
				
			||||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
					        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
				
			||||||
        coEvery { api.apiInformation() } returns
 | 
					        coEvery { api.apiInformation() } returns
 | 
				
			||||||
                StatusAndData(
 | 
					            StatusAndData(
 | 
				
			||||||
                    success = true,
 | 
					                success = true,
 | 
				
			||||||
                    data = SelfossModel.ApiInformation(
 | 
					                data =
 | 
				
			||||||
 | 
					                    SelfossModel.ApiInformation(
 | 
				
			||||||
                        "2.19-ba1e8e3",
 | 
					                        "2.19-ba1e8e3",
 | 
				
			||||||
                        "4.0.0",
 | 
					                        "4.0.0",
 | 
				
			||||||
                        SelfossModel.ApiConfiguration(true, true)
 | 
					                        SelfossModel.ApiConfiguration(true, true),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
        every { appSettingsService.getUserName() } returns "username"
 | 
					        every { appSettingsService.getUserName() } returns "username"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
@@ -173,14 +175,15 @@ class RepositoryTest {
 | 
				
			|||||||
    fun get_public_access_no_auth() {
 | 
					    fun get_public_access_no_auth() {
 | 
				
			||||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
					        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
				
			||||||
        coEvery { api.apiInformation() } returns
 | 
					        coEvery { api.apiInformation() } returns
 | 
				
			||||||
                StatusAndData(
 | 
					            StatusAndData(
 | 
				
			||||||
                    success = true,
 | 
					                success = true,
 | 
				
			||||||
                    data = SelfossModel.ApiInformation(
 | 
					                data =
 | 
				
			||||||
 | 
					                    SelfossModel.ApiInformation(
 | 
				
			||||||
                        "2.19-ba1e8e3",
 | 
					                        "2.19-ba1e8e3",
 | 
				
			||||||
                        "4.0.0",
 | 
					                        "4.0.0",
 | 
				
			||||||
                        SelfossModel.ApiConfiguration(true, false)
 | 
					                        SelfossModel.ApiConfiguration(true, false),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
        every { appSettingsService.getUserName() } returns ""
 | 
					        every { appSettingsService.getUserName() } returns ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
@@ -193,14 +196,15 @@ class RepositoryTest {
 | 
				
			|||||||
    fun get_public_access_disabled() {
 | 
					    fun get_public_access_disabled() {
 | 
				
			||||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
					        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
				
			||||||
        coEvery { api.apiInformation() } returns
 | 
					        coEvery { api.apiInformation() } returns
 | 
				
			||||||
                StatusAndData(
 | 
					            StatusAndData(
 | 
				
			||||||
                    success = true,
 | 
					                success = true,
 | 
				
			||||||
                    data = SelfossModel.ApiInformation(
 | 
					                data =
 | 
				
			||||||
 | 
					                    SelfossModel.ApiInformation(
 | 
				
			||||||
                        "2.19-ba1e8e3",
 | 
					                        "2.19-ba1e8e3",
 | 
				
			||||||
                        "4.0.0",
 | 
					                        "4.0.0",
 | 
				
			||||||
                        SelfossModel.ApiConfiguration(false, true)
 | 
					                        SelfossModel.ApiConfiguration(false, true),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
        every { appSettingsService.getUserName() } returns ""
 | 
					        every { appSettingsService.getUserName() } returns ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
@@ -216,10 +220,10 @@ class RepositoryTest {
 | 
				
			|||||||
        val itemParameters = FakeItemParameters()
 | 
					        val itemParameters = FakeItemParameters()
 | 
				
			||||||
        itemParameters.datetime = "2021-04-23 11:45:32"
 | 
					        itemParameters.datetime = "2021-04-23 11:45:32"
 | 
				
			||||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
					        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
				
			||||||
                StatusAndData(
 | 
					            StatusAndData(
 | 
				
			||||||
                    success = true,
 | 
					                success = true,
 | 
				
			||||||
                    data = generateTestApiItem(itemParameters),
 | 
					                data = generateTestApiItem(itemParameters),
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
@@ -232,7 +236,7 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun get_newer_items() {
 | 
					    fun get_newer_items() {
 | 
				
			||||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
					        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
				
			||||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
					            StatusAndData(success = true, data = generateTestApiItem())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
@@ -247,7 +251,7 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun get_all_newer_items() {
 | 
					    fun get_all_newer_items() {
 | 
				
			||||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
					        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
				
			||||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
					            StatusAndData(success = true, data = generateTestApiItem())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        repository.displayedItems = ItemType.ALL
 | 
					        repository.displayedItems = ItemType.ALL
 | 
				
			||||||
@@ -263,7 +267,7 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun get_newer_starred_items() {
 | 
					    fun get_newer_starred_items() {
 | 
				
			||||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
					        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
				
			||||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
					            StatusAndData(success = true, data = generateTestApiItem())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        repository.displayedItems = ItemType.STARRED
 | 
					        repository.displayedItems = ItemType.STARRED
 | 
				
			||||||
@@ -280,7 +284,7 @@ class RepositoryTest {
 | 
				
			|||||||
    fun get_newer_items_without_connectivity() {
 | 
					    fun get_newer_items_without_connectivity() {
 | 
				
			||||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
					        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            repository.getNewerItems()
 | 
					            repository.getNewerItems()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -302,12 +306,12 @@ class RepositoryTest {
 | 
				
			|||||||
        coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
 | 
					        coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
 | 
				
			||||||
            itemParameter1,
 | 
					            itemParameter1,
 | 
				
			||||||
        ) +
 | 
					        ) +
 | 
				
			||||||
                generateTestDBItems(itemParameter2) +
 | 
					            generateTestDBItems(itemParameter2) +
 | 
				
			||||||
                generateTestDBItems(itemParameter3)
 | 
					            generateTestDBItems(itemParameter3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
					        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        repository.setTagFilter(SelfossModel.Tag("Test", "red", 3))
 | 
					        repository.setTagFilter(SelfossModel.Tag("Test", "red", 3))
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            repository.getNewerItems()
 | 
					            repository.getNewerItems()
 | 
				
			||||||
@@ -330,12 +334,12 @@ class RepositoryTest {
 | 
				
			|||||||
        coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
 | 
					        coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
 | 
				
			||||||
            itemParameter1,
 | 
					            itemParameter1,
 | 
				
			||||||
        ) +
 | 
					        ) +
 | 
				
			||||||
                generateTestDBItems(itemParameter2) +
 | 
					            generateTestDBItems(itemParameter2) +
 | 
				
			||||||
                generateTestDBItems(itemParameter3)
 | 
					            generateTestDBItems(itemParameter3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
					        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        repository.setSourceFilter(
 | 
					        repository.setSourceFilter(
 | 
				
			||||||
            SelfossModel.SourceDetail(
 | 
					            SelfossModel.SourceDetail(
 | 
				
			||||||
                1,
 | 
					                1,
 | 
				
			||||||
@@ -360,7 +364,7 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun get_older_items() {
 | 
					    fun get_older_items() {
 | 
				
			||||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
					        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
				
			||||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
					            StatusAndData(success = true, data = generateTestApiItem())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        repository.items = ArrayList(generateTestApiItem())
 | 
					        repository.items = ArrayList(generateTestApiItem())
 | 
				
			||||||
@@ -376,7 +380,7 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun get_all_older_items() {
 | 
					    fun get_all_older_items() {
 | 
				
			||||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
					        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
				
			||||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
					            StatusAndData(success = true, data = generateTestApiItem())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        repository.items = ArrayList(generateTestApiItem())
 | 
					        repository.items = ArrayList(generateTestApiItem())
 | 
				
			||||||
@@ -393,7 +397,7 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun get_older_starred_items() {
 | 
					    fun get_older_starred_items() {
 | 
				
			||||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
					        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
				
			||||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
					            StatusAndData(success = true, data = generateTestApiItem())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        repository.displayedItems = ItemType.STARRED
 | 
					        repository.displayedItems = ItemType.STARRED
 | 
				
			||||||
@@ -450,7 +454,7 @@ class RepositoryTest {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        var success: Boolean
 | 
					        var success: Boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            success = repository.reloadBadges()
 | 
					            success = repository.reloadBadges()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -470,7 +474,7 @@ class RepositoryTest {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        var success: Boolean
 | 
					        var success: Boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            success = repository.reloadBadges()
 | 
					            success = repository.reloadBadges()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -565,7 +569,7 @@ class RepositoryTest {
 | 
				
			|||||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
					        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
				
			||||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
					        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var testTags: List<SelfossModel.Tag>
 | 
					        var testTags: List<SelfossModel.Tag>
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            testTags = repository.getTags()
 | 
					            testTags = repository.getTags()
 | 
				
			||||||
@@ -583,7 +587,7 @@ class RepositoryTest {
 | 
				
			|||||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
					        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
				
			||||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
					        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var testTags: List<SelfossModel.Tag>
 | 
					        var testTags: List<SelfossModel.Tag>
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            testTags = repository.getTags()
 | 
					            testTags = repository.getTags()
 | 
				
			||||||
@@ -600,7 +604,7 @@ class RepositoryTest {
 | 
				
			|||||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
					        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
				
			||||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
					        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var testTags: List<SelfossModel.Tag>
 | 
					        var testTags: List<SelfossModel.Tag>
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            testTags = repository.getTags()
 | 
					            testTags = repository.getTags()
 | 
				
			||||||
@@ -618,7 +622,7 @@ class RepositoryTest {
 | 
				
			|||||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
					        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
				
			||||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
					        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var testTags: List<SelfossModel.Tag>
 | 
					        var testTags: List<SelfossModel.Tag>
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            testTags = repository.getTags()
 | 
					            testTags = repository.getTags()
 | 
				
			||||||
@@ -768,7 +772,7 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun get_sources_without_connection() {
 | 
					    fun get_sources_without_connection() {
 | 
				
			||||||
        val (_, sourcesDB) = prepareSources()
 | 
					        val (_, sourcesDB) = prepareSources()
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var testSources: List<SelfossModel.Source>
 | 
					        var testSources: List<SelfossModel.Source>
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            testSources = repository.getSourcesDetails()
 | 
					            testSources = repository.getSourcesDetails()
 | 
				
			||||||
@@ -785,7 +789,7 @@ class RepositoryTest {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
					        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
				
			||||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
					        every { appSettingsService.isUpdateSourcesEnabled() } returns true
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var testSources: List<SelfossModel.Source>
 | 
					        var testSources: List<SelfossModel.Source>
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            testSources = repository.getSourcesDetails()
 | 
					            testSources = repository.getSourcesDetails()
 | 
				
			||||||
@@ -802,7 +806,7 @@ class RepositoryTest {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
					        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
				
			||||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
					        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var testSources: List<SelfossModel.Source>
 | 
					        var testSources: List<SelfossModel.Source>
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            testSources = repository.getSourcesDetails()
 | 
					            testSources = repository.getSourcesDetails()
 | 
				
			||||||
@@ -819,7 +823,7 @@ class RepositoryTest {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
					        every { appSettingsService.isItemCachingEnabled() } returns false
 | 
				
			||||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
					        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var testSources: List<SelfossModel.Source>
 | 
					        var testSources: List<SelfossModel.Source>
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            testSources = repository.getSourcesDetails()
 | 
					            testSources = repository.getSourcesDetails()
 | 
				
			||||||
@@ -833,7 +837,7 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun create_source() {
 | 
					    fun create_source() {
 | 
				
			||||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
					        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
				
			||||||
                SuccessResponse(true)
 | 
					            SuccessResponse(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        var response: Boolean
 | 
					        var response: Boolean
 | 
				
			||||||
@@ -861,7 +865,7 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun create_source_but_response_fails() {
 | 
					    fun create_source_but_response_fails() {
 | 
				
			||||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
					        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
				
			||||||
                SuccessResponse(false)
 | 
					            SuccessResponse(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        var response: Boolean
 | 
					        var response: Boolean
 | 
				
			||||||
@@ -889,9 +893,9 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun create_source_without_connection() {
 | 
					    fun create_source_without_connection() {
 | 
				
			||||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
					        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
				
			||||||
                SuccessResponse(true)
 | 
					            SuccessResponse(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var response: Boolean
 | 
					        var response: Boolean
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            response =
 | 
					            response =
 | 
				
			||||||
@@ -948,7 +952,7 @@ class RepositoryTest {
 | 
				
			|||||||
    fun delete_source_without_connection() {
 | 
					    fun delete_source_without_connection() {
 | 
				
			||||||
        coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
 | 
					        coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var response: Boolean
 | 
					        var response: Boolean
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            response = repository.deleteSource(5, "src")
 | 
					            response = repository.deleteSource(5, "src")
 | 
				
			||||||
@@ -962,10 +966,10 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun update_remote() {
 | 
					    fun update_remote() {
 | 
				
			||||||
        coEvery { api.update() } returns
 | 
					        coEvery { api.update() } returns
 | 
				
			||||||
                StatusAndData(
 | 
					            StatusAndData(
 | 
				
			||||||
                    success = true,
 | 
					                success = true,
 | 
				
			||||||
                    data = "finished",
 | 
					                data = "finished",
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        var response: Boolean
 | 
					        var response: Boolean
 | 
				
			||||||
@@ -980,10 +984,10 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun update_remote_but_response_fails() {
 | 
					    fun update_remote_but_response_fails() {
 | 
				
			||||||
        coEvery { api.update() } returns
 | 
					        coEvery { api.update() } returns
 | 
				
			||||||
                StatusAndData(
 | 
					            StatusAndData(
 | 
				
			||||||
                    success = false,
 | 
					                success = false,
 | 
				
			||||||
                    data = "unallowed access",
 | 
					                data = "unallowed access",
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        var response: Boolean
 | 
					        var response: Boolean
 | 
				
			||||||
@@ -998,10 +1002,10 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun update_remote_with_unallowed_access() {
 | 
					    fun update_remote_with_unallowed_access() {
 | 
				
			||||||
        coEvery { api.update() } returns
 | 
					        coEvery { api.update() } returns
 | 
				
			||||||
                StatusAndData(
 | 
					            StatusAndData(
 | 
				
			||||||
                    success = true,
 | 
					                success = true,
 | 
				
			||||||
                    data = "unallowed access",
 | 
					                data = "unallowed access",
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        var response: Boolean
 | 
					        var response: Boolean
 | 
				
			||||||
@@ -1016,12 +1020,12 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun update_remote_without_connection() {
 | 
					    fun update_remote_without_connection() {
 | 
				
			||||||
        coEvery { api.update() } returns
 | 
					        coEvery { api.update() } returns
 | 
				
			||||||
                StatusAndData(
 | 
					            StatusAndData(
 | 
				
			||||||
                    success = true,
 | 
					                success = true,
 | 
				
			||||||
                    data = "undocumented...",
 | 
					                data = "undocumented...",
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var response: Boolean
 | 
					        var response: Boolean
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            response = repository.updateRemote()
 | 
					            response = repository.updateRemote()
 | 
				
			||||||
@@ -1063,7 +1067,7 @@ class RepositoryTest {
 | 
				
			|||||||
    fun login_but_without_connection() {
 | 
					    fun login_but_without_connection() {
 | 
				
			||||||
        coEvery { api.login() } returns SuccessResponse(success = true)
 | 
					        coEvery { api.login() } returns SuccessResponse(success = true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        var response: Boolean
 | 
					        var response: Boolean
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            response = repository.login()
 | 
					            response = repository.login()
 | 
				
			||||||
@@ -1109,11 +1113,11 @@ class RepositoryTest {
 | 
				
			|||||||
                any(),
 | 
					                any(),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        } returnsMany
 | 
					        } returnsMany
 | 
				
			||||||
                listOf(
 | 
					            listOf(
 | 
				
			||||||
                    StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
					                StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
				
			||||||
                    StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
 | 
					                StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
 | 
				
			||||||
                    StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
					                StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
				
			||||||
                )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        prepareSearch()
 | 
					        prepareSearch()
 | 
				
			||||||
@@ -1127,7 +1131,7 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun cache_items_but_response_fails() {
 | 
					    fun cache_items_but_response_fails() {
 | 
				
			||||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
					        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
				
			||||||
                StatusAndData(success = false, data = generateTestApiItem())
 | 
					            StatusAndData(success = false, data = generateTestApiItem())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository()
 | 
					        initializeRepository()
 | 
				
			||||||
        prepareSearch()
 | 
					        prepareSearch()
 | 
				
			||||||
@@ -1141,9 +1145,9 @@ class RepositoryTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun cache_items_without_connection() {
 | 
					    fun cache_items_without_connection() {
 | 
				
			||||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
					        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
				
			||||||
                StatusAndData(success = false, data = generateTestApiItem())
 | 
					            StatusAndData(success = false, data = generateTestApiItem())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializeRepository(MutableStateFlow(false))
 | 
					        initializeRepository(false)
 | 
				
			||||||
        prepareSearch()
 | 
					        prepareSearch()
 | 
				
			||||||
        runBlocking {
 | 
					        runBlocking {
 | 
				
			||||||
            repository.tryToCacheItemsAndGetNewOnes()
 | 
					            repository.tryToCacheItemsAndGetNewOnes()
 | 
				
			||||||
@@ -1168,4 +1172,4 @@ class RepositoryTest {
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        repository.searchFilter = "search"
 | 
					        repository.searchFilter = "search"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,8 @@ package bou.amine.apps.readerforselfossv2.tests.repository
 | 
				
			|||||||
import bou.amine.apps.readerforselfossv2.dao.ITEM
 | 
					import bou.amine.apps.readerforselfossv2.dao.ITEM
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
					import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> {
 | 
					fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> =
 | 
				
			||||||
    return listOf(
 | 
					    listOf(
 | 
				
			||||||
        ITEM(
 | 
					        ITEM(
 | 
				
			||||||
            id = item.id,
 | 
					            id = item.id,
 | 
				
			||||||
            datetime = item.datetime,
 | 
					            datetime = item.datetime,
 | 
				
			||||||
@@ -20,10 +20,9 @@ fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<I
 | 
				
			|||||||
            author = item.author,
 | 
					            author = item.author,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<SelfossModel.Item> {
 | 
					fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<SelfossModel.Item> =
 | 
				
			||||||
    return listOf(
 | 
					    listOf(
 | 
				
			||||||
        SelfossModel.Item(
 | 
					        SelfossModel.Item(
 | 
				
			||||||
            id = item.id.toInt(),
 | 
					            id = item.id.toInt(),
 | 
				
			||||||
            datetime = item.datetime,
 | 
					            datetime = item.datetime,
 | 
				
			||||||
@@ -39,7 +38,6 @@ fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<S
 | 
				
			|||||||
            author = item.author,
 | 
					            author = item.author,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FakeItemParameters {
 | 
					class FakeItemParameters {
 | 
				
			||||||
    var id = "20"
 | 
					    var id = "20"
 | 
				
			||||||
@@ -56,4 +54,4 @@ class FakeItemParameters {
 | 
				
			|||||||
    var sourcetitle = "La Chimica e la Società"
 | 
					    var sourcetitle = "La Chimica e la Società"
 | 
				
			||||||
    var tags = "Chimica, Testing"
 | 
					    var tags = "Chimica, Testing"
 | 
				
			||||||
    var author = "Someone important"
 | 
					    var author = "Someone important"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,7 @@
 | 
				
			|||||||
buildscript {
 | 
					 | 
				
			||||||
    dependencies {
 | 
					 | 
				
			||||||
        // SqlDelight
 | 
					 | 
				
			||||||
        classpath("com.squareup.sqldelight:gradle-plugin:1.5.5")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
plugins {
 | 
					plugins {
 | 
				
			||||||
    //trick: for the same plugin versions in all sub-modules
 | 
					    // trick: for the same plugin versions in all sub-modules
 | 
				
			||||||
    id("com.android.application").version("8.7.3").apply(false)
 | 
					    id("com.android.application").version("8.8.1").apply(false)
 | 
				
			||||||
    id("com.android.library").version("8.7.3").apply(false)
 | 
					    id("com.android.library").version("8.8.1").apply(false)
 | 
				
			||||||
    id("org.jetbrains.kotlin.android").version("2.1.0").apply(false)
 | 
					    id("org.jetbrains.kotlin.android").version("2.1.0").apply(false)
 | 
				
			||||||
    kotlin("multiplatform").version("2.1.0").apply(false)
 | 
					    kotlin("multiplatform").version("2.1.0").apply(false)
 | 
				
			||||||
    id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
 | 
					    id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
 | 
				
			||||||
@@ -23,7 +16,6 @@ allprojects {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
tasks.register("clean", Delete::class) {
 | 
					tasks.register("clean", Delete::class) {
 | 
				
			||||||
    delete(layout.buildDirectory)
 | 
					    delete(layout.buildDirectory)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -31,4 +23,4 @@ tasks.register("clean", Delete::class) {
 | 
				
			|||||||
dependencies {
 | 
					dependencies {
 | 
				
			||||||
    kover(project(":shared"))
 | 
					    kover(project(":shared"))
 | 
				
			||||||
    kover(project(":androidApp"))
 | 
					    kover(project(":androidApp"))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										786
									
								
								detekt.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										786
									
								
								detekt.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,786 @@
 | 
				
			|||||||
 | 
					build:
 | 
				
			||||||
 | 
					  maxIssues: 0
 | 
				
			||||||
 | 
					  excludeCorrectable: false
 | 
				
			||||||
 | 
					  weights:
 | 
				
			||||||
 | 
					  # complexity: 2
 | 
				
			||||||
 | 
					  # LongParameterList: 1
 | 
				
			||||||
 | 
					  # style: 1
 | 
				
			||||||
 | 
					  # comments: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					config:
 | 
				
			||||||
 | 
					  validation: true
 | 
				
			||||||
 | 
					  warningsAsErrors: false
 | 
				
			||||||
 | 
					  checkExhaustiveness: false
 | 
				
			||||||
 | 
					  # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
 | 
				
			||||||
 | 
					  excludes: ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					processors:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  exclude:
 | 
				
			||||||
 | 
					    - 'DetektProgressListener'
 | 
				
			||||||
 | 
					  # - 'KtFileCountProcessor'
 | 
				
			||||||
 | 
					  # - 'PackageCountProcessor'
 | 
				
			||||||
 | 
					  # - 'ClassCountProcessor'
 | 
				
			||||||
 | 
					  # - 'FunctionCountProcessor'
 | 
				
			||||||
 | 
					  # - 'PropertyCountProcessor'
 | 
				
			||||||
 | 
					  # - 'ProjectComplexityProcessor'
 | 
				
			||||||
 | 
					  # - 'ProjectCognitiveComplexityProcessor'
 | 
				
			||||||
 | 
					  # - 'ProjectLLOCProcessor'
 | 
				
			||||||
 | 
					  # - 'ProjectCLOCProcessor'
 | 
				
			||||||
 | 
					  # - 'ProjectLOCProcessor'
 | 
				
			||||||
 | 
					  # - 'ProjectSLOCProcessor'
 | 
				
			||||||
 | 
					  # - 'LicenseHeaderLoaderExtension'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					console-reports:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  exclude:
 | 
				
			||||||
 | 
					    - 'ProjectStatisticsReport'
 | 
				
			||||||
 | 
					    - 'ComplexityReport'
 | 
				
			||||||
 | 
					    - 'NotificationReport'
 | 
				
			||||||
 | 
					    - 'FindingsReport'
 | 
				
			||||||
 | 
					    - 'FileBasedFindingsReport'
 | 
				
			||||||
 | 
					  #  - 'LiteFindingsReport'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					output-reports:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  exclude:
 | 
				
			||||||
 | 
					  # - 'TxtOutputReport'
 | 
				
			||||||
 | 
					  # - 'XmlOutputReport'
 | 
				
			||||||
 | 
					  # - 'HtmlOutputReport'
 | 
				
			||||||
 | 
					  # - 'MdOutputReport'
 | 
				
			||||||
 | 
					  # - 'SarifOutputReport'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					comments:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  AbsentOrWrongFileLicense:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    licenseTemplateFile: 'license.template'
 | 
				
			||||||
 | 
					    licenseTemplateIsRegex: false
 | 
				
			||||||
 | 
					  CommentOverPrivateFunction:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  CommentOverPrivateProperty:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  DeprecatedBlockTag:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  EndOfSentenceFormat:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
 | 
				
			||||||
 | 
					  KDocReferencesNonPublicProperty:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					  OutdatedDocumentation:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    matchTypeParameters: true
 | 
				
			||||||
 | 
					    matchDeclarationsOrder: true
 | 
				
			||||||
 | 
					    allowParamOnConstructorProperties: false
 | 
				
			||||||
 | 
					  UndocumentedPublicClass:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					    searchInNestedClass: true
 | 
				
			||||||
 | 
					    searchInInnerClass: true
 | 
				
			||||||
 | 
					    searchInInnerObject: true
 | 
				
			||||||
 | 
					    searchInInnerInterface: true
 | 
				
			||||||
 | 
					    searchInProtectedClass: false
 | 
				
			||||||
 | 
					  UndocumentedPublicFunction:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					    searchProtectedFunction: false
 | 
				
			||||||
 | 
					  UndocumentedPublicProperty:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					    searchProtectedProperty: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					complexity:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  CognitiveComplexMethod:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    threshold: 15
 | 
				
			||||||
 | 
					  ComplexCondition:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    threshold: 4
 | 
				
			||||||
 | 
					  ComplexInterface:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    threshold: 10
 | 
				
			||||||
 | 
					    includeStaticDeclarations: false
 | 
				
			||||||
 | 
					    includePrivateDeclarations: false
 | 
				
			||||||
 | 
					    ignoreOverloaded: false
 | 
				
			||||||
 | 
					  CyclomaticComplexMethod:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    threshold: 15
 | 
				
			||||||
 | 
					    ignoreSingleWhenExpression: false
 | 
				
			||||||
 | 
					    ignoreSimpleWhenEntries: false
 | 
				
			||||||
 | 
					    ignoreNestingFunctions: false
 | 
				
			||||||
 | 
					    nestingFunctions:
 | 
				
			||||||
 | 
					      - 'also'
 | 
				
			||||||
 | 
					      - 'apply'
 | 
				
			||||||
 | 
					      - 'forEach'
 | 
				
			||||||
 | 
					      - 'isNotNull'
 | 
				
			||||||
 | 
					      - 'ifNull'
 | 
				
			||||||
 | 
					      - 'let'
 | 
				
			||||||
 | 
					      - 'run'
 | 
				
			||||||
 | 
					      - 'use'
 | 
				
			||||||
 | 
					      - 'with'
 | 
				
			||||||
 | 
					  LabeledExpression:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    ignoredLabels: [ ]
 | 
				
			||||||
 | 
					  LargeClass:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    threshold: 600
 | 
				
			||||||
 | 
					  LongMethod:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    threshold: 60
 | 
				
			||||||
 | 
					  LongParameterList:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    functionThreshold: 6
 | 
				
			||||||
 | 
					    constructorThreshold: 7
 | 
				
			||||||
 | 
					    ignoreDefaultParameters: false
 | 
				
			||||||
 | 
					    ignoreDataClasses: true
 | 
				
			||||||
 | 
					    ignoreAnnotatedParameter: [ ]
 | 
				
			||||||
 | 
					  MethodOverloading:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    threshold: 6
 | 
				
			||||||
 | 
					  NamedArguments:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    threshold: 3
 | 
				
			||||||
 | 
					    ignoreArgumentsMatchingNames: false
 | 
				
			||||||
 | 
					  NestedBlockDepth:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    threshold: 4
 | 
				
			||||||
 | 
					  NestedScopeFunctions:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    threshold: 1
 | 
				
			||||||
 | 
					    functions:
 | 
				
			||||||
 | 
					      - 'kotlin.apply'
 | 
				
			||||||
 | 
					      - 'kotlin.run'
 | 
				
			||||||
 | 
					      - 'kotlin.with'
 | 
				
			||||||
 | 
					      - 'kotlin.let'
 | 
				
			||||||
 | 
					      - 'kotlin.also'
 | 
				
			||||||
 | 
					  ReplaceSafeCallChainWithRun:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  StringLiteralDuplication:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					    threshold: 3
 | 
				
			||||||
 | 
					    ignoreAnnotation: true
 | 
				
			||||||
 | 
					    excludeStringsWithLessThan5Characters: true
 | 
				
			||||||
 | 
					    ignoreStringsRegex: '$^'
 | 
				
			||||||
 | 
					  TooManyFunctions:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/android/*Activity.kt', '**/fragments/*Fragment.kt' ]
 | 
				
			||||||
 | 
					    thresholdInFiles: 11
 | 
				
			||||||
 | 
					    thresholdInClasses: 11
 | 
				
			||||||
 | 
					    thresholdInInterfaces: 11
 | 
				
			||||||
 | 
					    thresholdInObjects: 11
 | 
				
			||||||
 | 
					    thresholdInEnums: 11
 | 
				
			||||||
 | 
					    ignoreDeprecated: false
 | 
				
			||||||
 | 
					    ignorePrivate: false
 | 
				
			||||||
 | 
					    ignoreOverridden: false
 | 
				
			||||||
 | 
					    ignoreAnnotatedFunctions: [ ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					coroutines:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  GlobalCoroutineUsage:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  InjectDispatcher:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    dispatcherNames:
 | 
				
			||||||
 | 
					      - 'IO'
 | 
				
			||||||
 | 
					      - 'Default'
 | 
				
			||||||
 | 
					      - 'Unconfined'
 | 
				
			||||||
 | 
					  RedundantSuspendModifier:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  SleepInsteadOfDelay:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  SuspendFunSwallowedCancellation:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  SuspendFunWithCoroutineScopeReceiver:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  SuspendFunWithFlowReturnType:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					empty-blocks:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  EmptyCatchBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    allowedExceptionNameRegex: '_|(ignore|expected).*'
 | 
				
			||||||
 | 
					  EmptyClassBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptyDefaultConstructor:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptyDoWhileBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptyElseBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptyFinallyBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptyForBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptyFunctionBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    ignoreOverridden: false
 | 
				
			||||||
 | 
					  EmptyIfBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptyInitBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptyKtFile:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptySecondaryConstructor:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptyTryBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptyWhenBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EmptyWhileBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exceptions:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  ExceptionRaisedInUnexpectedLocation:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    methodNames:
 | 
				
			||||||
 | 
					      - 'equals'
 | 
				
			||||||
 | 
					      - 'finalize'
 | 
				
			||||||
 | 
					      - 'hashCode'
 | 
				
			||||||
 | 
					      - 'toString'
 | 
				
			||||||
 | 
					  InstanceOfCheckForException:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					  NotImplementedDeclaration:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  ObjectExtendsThrowable:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  PrintStackTrace:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  RethrowCaughtException:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  ReturnFromFinally:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    ignoreLabeled: false
 | 
				
			||||||
 | 
					  SwallowedException:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    ignoredExceptionTypes:
 | 
				
			||||||
 | 
					      - 'InterruptedException'
 | 
				
			||||||
 | 
					      - 'MalformedURLException'
 | 
				
			||||||
 | 
					      - 'NumberFormatException'
 | 
				
			||||||
 | 
					      - 'ParseException'
 | 
				
			||||||
 | 
					    allowedExceptionNameRegex: '_|(ignore|expected).*'
 | 
				
			||||||
 | 
					  ThrowingExceptionFromFinally:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  ThrowingExceptionInMain:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  ThrowingExceptionsWithoutMessageOrCause:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					    exceptions:
 | 
				
			||||||
 | 
					      - 'ArrayIndexOutOfBoundsException'
 | 
				
			||||||
 | 
					      - 'Exception'
 | 
				
			||||||
 | 
					      - 'IllegalArgumentException'
 | 
				
			||||||
 | 
					      - 'IllegalMonitorStateException'
 | 
				
			||||||
 | 
					      - 'IllegalStateException'
 | 
				
			||||||
 | 
					      - 'IndexOutOfBoundsException'
 | 
				
			||||||
 | 
					      - 'NullPointerException'
 | 
				
			||||||
 | 
					      - 'RuntimeException'
 | 
				
			||||||
 | 
					      - 'Throwable'
 | 
				
			||||||
 | 
					  ThrowingNewInstanceOfSameException:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  TooGenericExceptionCaught:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					    exceptionNames:
 | 
				
			||||||
 | 
					      - 'ArrayIndexOutOfBoundsException'
 | 
				
			||||||
 | 
					      - 'Error'
 | 
				
			||||||
 | 
					      - 'Exception'
 | 
				
			||||||
 | 
					      - 'IllegalMonitorStateException'
 | 
				
			||||||
 | 
					      - 'IndexOutOfBoundsException'
 | 
				
			||||||
 | 
					      - 'NullPointerException'
 | 
				
			||||||
 | 
					      - 'RuntimeException'
 | 
				
			||||||
 | 
					      - 'Throwable'
 | 
				
			||||||
 | 
					    allowedExceptionNameRegex: '_|(ignore|expected).*'
 | 
				
			||||||
 | 
					  TooGenericExceptionThrown:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    exceptionNames:
 | 
				
			||||||
 | 
					      - 'Error'
 | 
				
			||||||
 | 
					      - 'Exception'
 | 
				
			||||||
 | 
					      - 'RuntimeException'
 | 
				
			||||||
 | 
					      - 'Throwable'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					naming:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  BooleanPropertyNaming:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    allowedPattern: '^(is|has|are)'
 | 
				
			||||||
 | 
					  ClassNaming:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    classPattern: '[A-Z][a-zA-Z0-9]*'
 | 
				
			||||||
 | 
					  ConstructorParameterNaming:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    parameterPattern: '[a-z][A-Za-z0-9]*'
 | 
				
			||||||
 | 
					    privateParameterPattern: '[a-z][A-Za-z0-9]*'
 | 
				
			||||||
 | 
					    excludeClassPattern: '$^'
 | 
				
			||||||
 | 
					  EnumNaming:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
 | 
				
			||||||
 | 
					  ForbiddenClassName:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    forbiddenName: [ ]
 | 
				
			||||||
 | 
					  FunctionMaxLength:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    maximumFunctionNameLength: 30
 | 
				
			||||||
 | 
					  FunctionMinLength:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    minimumFunctionNameLength: 3
 | 
				
			||||||
 | 
					  FunctionNaming:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					    functionPattern: '[a-z][a-zA-Z0-9]*'
 | 
				
			||||||
 | 
					    excludeClassPattern: '$^'
 | 
				
			||||||
 | 
					  FunctionParameterNaming:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    parameterPattern: '[a-z][A-Za-z0-9]*'
 | 
				
			||||||
 | 
					    excludeClassPattern: '$^'
 | 
				
			||||||
 | 
					  InvalidPackageDeclaration:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    rootPackage: ''
 | 
				
			||||||
 | 
					    requireRootInDeclaration: false
 | 
				
			||||||
 | 
					  LambdaParameterNaming:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    parameterPattern: '[a-z][A-Za-z0-9]*|_'
 | 
				
			||||||
 | 
					  MatchingDeclarationName:
 | 
				
			||||||
 | 
					    active: false # done in ktlint
 | 
				
			||||||
 | 
					    mustBeFirst: true
 | 
				
			||||||
 | 
					  MemberNameEqualsClassName:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    ignoreOverridden: true
 | 
				
			||||||
 | 
					  NoNameShadowing:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  NonBooleanPropertyPrefixedWithIs:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  ObjectPropertyNaming:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    constantPattern: '[A-Za-z][_A-Za-z0-9]*'
 | 
				
			||||||
 | 
					    propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
 | 
				
			||||||
 | 
					    privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
 | 
				
			||||||
 | 
					  PackageNaming:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
 | 
				
			||||||
 | 
					  TopLevelPropertyNaming:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    constantPattern: '[A-Z][_A-Z0-9]*'
 | 
				
			||||||
 | 
					    propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
 | 
				
			||||||
 | 
					    privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
 | 
				
			||||||
 | 
					  VariableMaxLength:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    maximumVariableNameLength: 64
 | 
				
			||||||
 | 
					  VariableMinLength:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    minimumVariableNameLength: 1
 | 
				
			||||||
 | 
					  VariableNaming:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    variablePattern: '[a-z][A-Za-z0-9]*'
 | 
				
			||||||
 | 
					    privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
 | 
				
			||||||
 | 
					    excludeClassPattern: '$^'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					performance:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  ArrayPrimitive:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  CouldBeSequence:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    threshold: 3
 | 
				
			||||||
 | 
					  ForEachOnRange:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					  SpreadOperator:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					  UnnecessaryPartOfBinaryExpression:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UnnecessaryTemporaryInstantiation:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					potential-bugs:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  AvoidReferentialEquality:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    forbiddenTypePatterns:
 | 
				
			||||||
 | 
					      - 'kotlin.String'
 | 
				
			||||||
 | 
					  CastNullableToNonNullableType:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  CastToNullableType:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  Deprecation:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  DontDowncastCollectionTypes:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  DoubleMutabilityForCollection:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    mutableTypes:
 | 
				
			||||||
 | 
					      - 'kotlin.collections.MutableList'
 | 
				
			||||||
 | 
					      - 'kotlin.collections.MutableMap'
 | 
				
			||||||
 | 
					      - 'kotlin.collections.MutableSet'
 | 
				
			||||||
 | 
					      - 'java.util.ArrayList'
 | 
				
			||||||
 | 
					      - 'java.util.LinkedHashSet'
 | 
				
			||||||
 | 
					      - 'java.util.HashSet'
 | 
				
			||||||
 | 
					      - 'java.util.LinkedHashMap'
 | 
				
			||||||
 | 
					      - 'java.util.HashMap'
 | 
				
			||||||
 | 
					  ElseCaseInsteadOfExhaustiveWhen:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    ignoredSubjectTypes: [ ]
 | 
				
			||||||
 | 
					  EqualsAlwaysReturnsTrueOrFalse:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EqualsWithHashCodeExist:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  ExitOutsideMain:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  ExplicitGarbageCollectionCall:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  HasPlatformType:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  IgnoredReturnValue:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    restrictToConfig: true
 | 
				
			||||||
 | 
					    returnValueAnnotations:
 | 
				
			||||||
 | 
					      - 'CheckResult'
 | 
				
			||||||
 | 
					      - '*.CheckResult'
 | 
				
			||||||
 | 
					      - 'CheckReturnValue'
 | 
				
			||||||
 | 
					      - '*.CheckReturnValue'
 | 
				
			||||||
 | 
					    ignoreReturnValueAnnotations:
 | 
				
			||||||
 | 
					      - 'CanIgnoreReturnValue'
 | 
				
			||||||
 | 
					      - '*.CanIgnoreReturnValue'
 | 
				
			||||||
 | 
					    returnValueTypes:
 | 
				
			||||||
 | 
					      - 'kotlin.sequences.Sequence'
 | 
				
			||||||
 | 
					      - 'kotlinx.coroutines.flow.*Flow'
 | 
				
			||||||
 | 
					      - 'java.util.stream.*Stream'
 | 
				
			||||||
 | 
					    ignoreFunctionCall: [ ]
 | 
				
			||||||
 | 
					  ImplicitDefaultLocale:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  ImplicitUnitReturnType:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    allowExplicitReturnType: true
 | 
				
			||||||
 | 
					  InvalidRange:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  IteratorHasNextCallsNextMethod:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  IteratorNotThrowingNoSuchElementException:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  LateinitUsage:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					    ignoreOnClassesPattern: ''
 | 
				
			||||||
 | 
					  MapGetWithNotNullAssertionOperator:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  MissingPackageDeclaration:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    excludes: [ '**/*.kts' ]
 | 
				
			||||||
 | 
					  NullCheckOnMutableProperty:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  NullableToStringCall:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  PropertyUsedBeforeDeclaration:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UnconditionalJumpStatementInLoop:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UnnecessaryNotNullCheck:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UnnecessaryNotNullOperator:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UnnecessarySafeCall:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UnreachableCatchBlock:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UnreachableCode:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UnsafeCallOnNullableType:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
				
			||||||
 | 
					  UnsafeCast:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UnusedUnaryOperator:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UselessPostfixExpression:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  WrongEqualsTypeParameter:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					style:
 | 
				
			||||||
 | 
					  active: true
 | 
				
			||||||
 | 
					  AlsoCouldBeApply:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  BracesOnIfStatements:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    singleLine: 'never'
 | 
				
			||||||
 | 
					    multiLine: 'always'
 | 
				
			||||||
 | 
					  BracesOnWhenStatements:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    singleLine: 'necessary'
 | 
				
			||||||
 | 
					    multiLine: 'consistent'
 | 
				
			||||||
 | 
					  CanBeNonNullable:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  CascadingCallWrapping:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    includeElvis: true
 | 
				
			||||||
 | 
					  ClassOrdering:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  CollapsibleIfStatements:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  DataClassContainsFunctions:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    conversionFunctionPrefix:
 | 
				
			||||||
 | 
					      - 'to'
 | 
				
			||||||
 | 
					    allowOperators: false
 | 
				
			||||||
 | 
					  DataClassShouldBeImmutable:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  DestructuringDeclarationWithTooManyEntries:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    maxDestructuringEntries: 3
 | 
				
			||||||
 | 
					  DoubleNegativeLambda:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    negativeFunctions:
 | 
				
			||||||
 | 
					      - reason: 'Use `takeIf` instead.'
 | 
				
			||||||
 | 
					        value: 'takeUnless'
 | 
				
			||||||
 | 
					      - reason: 'Use `all` instead.'
 | 
				
			||||||
 | 
					        value: 'none'
 | 
				
			||||||
 | 
					    negativeFunctionNameParts:
 | 
				
			||||||
 | 
					      - 'not'
 | 
				
			||||||
 | 
					      - 'non'
 | 
				
			||||||
 | 
					  EqualsNullCall:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  EqualsOnSignatureLine:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  ExplicitCollectionElementAccessMethod:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  ExplicitItLambdaParameter:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  ExpressionBodySyntax:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    includeLineWrapping: false
 | 
				
			||||||
 | 
					  ForbiddenAnnotation:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    annotations:
 | 
				
			||||||
 | 
					      - reason: 'it is a java annotation. Use `Suppress` instead.'
 | 
				
			||||||
 | 
					        value: 'java.lang.SuppressWarnings'
 | 
				
			||||||
 | 
					      - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.'
 | 
				
			||||||
 | 
					        value: 'java.lang.Deprecated'
 | 
				
			||||||
 | 
					      - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.'
 | 
				
			||||||
 | 
					        value: 'java.lang.annotation.Documented'
 | 
				
			||||||
 | 
					      - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.'
 | 
				
			||||||
 | 
					        value: 'java.lang.annotation.Target'
 | 
				
			||||||
 | 
					      - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.'
 | 
				
			||||||
 | 
					        value: 'java.lang.annotation.Retention'
 | 
				
			||||||
 | 
					      - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.'
 | 
				
			||||||
 | 
					        value: 'java.lang.annotation.Repeatable'
 | 
				
			||||||
 | 
					      - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265'
 | 
				
			||||||
 | 
					        value: 'java.lang.annotation.Inherited'
 | 
				
			||||||
 | 
					  ForbiddenComment:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    comments:
 | 
				
			||||||
 | 
					      - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
 | 
				
			||||||
 | 
					        value: 'FIXME:'
 | 
				
			||||||
 | 
					      - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
 | 
				
			||||||
 | 
					        value: 'STOPSHIP:'
 | 
				
			||||||
 | 
					      - reason: 'Forbidden TODO todo marker in comment, please do the changes.'
 | 
				
			||||||
 | 
					        value: 'TODO:'
 | 
				
			||||||
 | 
					    allowedPatterns: ''
 | 
				
			||||||
 | 
					  ForbiddenImport:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    imports: [ ]
 | 
				
			||||||
 | 
					    forbiddenPatterns: ''
 | 
				
			||||||
 | 
					  ForbiddenMethodCall:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    methods:
 | 
				
			||||||
 | 
					      - reason: 'print does not allow you to configure the output stream. Use a logger instead.'
 | 
				
			||||||
 | 
					        value: 'kotlin.io.print'
 | 
				
			||||||
 | 
					      - reason: 'println does not allow you to configure the output stream. Use a logger instead.'
 | 
				
			||||||
 | 
					        value: 'kotlin.io.println'
 | 
				
			||||||
 | 
					  ForbiddenSuppress:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    rules: [ ]
 | 
				
			||||||
 | 
					  ForbiddenVoid:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    ignoreOverridden: false
 | 
				
			||||||
 | 
					    ignoreUsageInGenerics: false
 | 
				
			||||||
 | 
					  FunctionOnlyReturningConstant:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    ignoreOverridableFunction: true
 | 
				
			||||||
 | 
					    ignoreActualFunction: true
 | 
				
			||||||
 | 
					    excludedFunctions: [ ]
 | 
				
			||||||
 | 
					  LoopWithTooManyJumpStatements:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    maxJumpCount: 1
 | 
				
			||||||
 | 
					  MagicNumber:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ]
 | 
				
			||||||
 | 
					    ignoreNumbers:
 | 
				
			||||||
 | 
					      - '-1'
 | 
				
			||||||
 | 
					      - '0'
 | 
				
			||||||
 | 
					      - '1'
 | 
				
			||||||
 | 
					      - '2'
 | 
				
			||||||
 | 
					    ignoreHashCodeFunction: true
 | 
				
			||||||
 | 
					    ignorePropertyDeclaration: false
 | 
				
			||||||
 | 
					    ignoreLocalVariableDeclaration: false
 | 
				
			||||||
 | 
					    ignoreConstantDeclaration: true
 | 
				
			||||||
 | 
					    ignoreCompanionObjectPropertyDeclaration: true
 | 
				
			||||||
 | 
					    ignoreAnnotation: false
 | 
				
			||||||
 | 
					    ignoreNamedArgument: true
 | 
				
			||||||
 | 
					    ignoreEnums: false
 | 
				
			||||||
 | 
					    ignoreRanges: false
 | 
				
			||||||
 | 
					    ignoreExtensionFunctions: true
 | 
				
			||||||
 | 
					  MandatoryBracesLoops:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  MaxChainedCallsOnSameLine:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    maxChainedCalls: 5
 | 
				
			||||||
 | 
					  MaxLineLength:
 | 
				
			||||||
 | 
					    active: false # done in ktlint
 | 
				
			||||||
 | 
					    maxLineLength: 140 # default is 120. 140 to match ktlint
 | 
				
			||||||
 | 
					    excludePackageStatements: true
 | 
				
			||||||
 | 
					    excludeImportStatements: true
 | 
				
			||||||
 | 
					    excludeCommentStatements: false
 | 
				
			||||||
 | 
					    excludeRawStrings: true
 | 
				
			||||||
 | 
					  MayBeConst:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  ModifierOrder:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  MultilineLambdaItParameter:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  MultilineRawStringIndentation:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    indentSize: 4
 | 
				
			||||||
 | 
					    trimmingMethods:
 | 
				
			||||||
 | 
					      - 'trimIndent'
 | 
				
			||||||
 | 
					      - 'trimMargin'
 | 
				
			||||||
 | 
					  NestedClassesVisibility:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  NewLineAtEndOfFile:
 | 
				
			||||||
 | 
					    active: false # done in ktlint
 | 
				
			||||||
 | 
					  NoTabs:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  NullableBooleanCheck:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  ObjectLiteralToLambda:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  OptionalAbstractKeyword:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  OptionalUnit:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  PreferToOverPairSyntax:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  ProtectedMemberInFinalClass:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  RedundantExplicitType:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  RedundantHigherOrderMapUsage:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  RedundantVisibilityModifierRule:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  ReturnCount:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    max: 2
 | 
				
			||||||
 | 
					    excludedFunctions:
 | 
				
			||||||
 | 
					      - 'equals'
 | 
				
			||||||
 | 
					    excludeLabeled: false
 | 
				
			||||||
 | 
					    excludeReturnFromLambda: true
 | 
				
			||||||
 | 
					    excludeGuardClauses: false
 | 
				
			||||||
 | 
					  SafeCast:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  SerialVersionUIDInSerializableClass:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  SpacingBetweenPackageAndImports:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  StringShouldBeRawString:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    maxEscapedCharacterCount: 2
 | 
				
			||||||
 | 
					    ignoredCharacters: [ ]
 | 
				
			||||||
 | 
					  ThrowsCount:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    max: 2
 | 
				
			||||||
 | 
					    excludeGuardClauses: false
 | 
				
			||||||
 | 
					  TrailingWhitespace:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  TrimMultilineRawString:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    trimmingMethods:
 | 
				
			||||||
 | 
					      - 'trimIndent'
 | 
				
			||||||
 | 
					      - 'trimMargin'
 | 
				
			||||||
 | 
					  UnderscoresInNumericLiterals:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    acceptableLength: 4
 | 
				
			||||||
 | 
					    allowNonStandardGrouping: false
 | 
				
			||||||
 | 
					  UnnecessaryAbstractClass:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UnnecessaryAnnotationUseSiteTarget:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UnnecessaryApply:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UnnecessaryBackticks:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UnnecessaryBracesAroundTrailingLambda:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UnnecessaryFilter:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UnnecessaryInheritance:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UnnecessaryInnerClass:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UnnecessaryLet:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UnnecessaryParentheses:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    allowForUnclearPrecedence: false
 | 
				
			||||||
 | 
					  UntilInsteadOfRangeTo:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UnusedImports:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UnusedParameter:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    allowedNames: 'ignored|expected'
 | 
				
			||||||
 | 
					  UnusedPrivateClass:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UnusedPrivateMember:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    allowedNames: ''
 | 
				
			||||||
 | 
					  UnusedPrivateProperty:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    allowedNames: '_|ignored|expected|serialVersionUID'
 | 
				
			||||||
 | 
					    excludes: [ '**/build.gradle.kts' ]
 | 
				
			||||||
 | 
					  UseAnyOrNoneInsteadOfFind:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UseArrayLiteralsInAnnotations:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UseCheckNotNull:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UseCheckOrError:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UseDataClass:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    allowVars: false
 | 
				
			||||||
 | 
					  UseEmptyCounterpart:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UseIfEmptyOrIfBlank:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UseIfInsteadOfWhen:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					    ignoreWhenContainingVariableDeclaration: false
 | 
				
			||||||
 | 
					  UseIsNullOrEmpty:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UseLet:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UseOrEmpty:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UseRequire:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UseRequireNotNull:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UseSumOfInsteadOfFlatMapSize:
 | 
				
			||||||
 | 
					    active: false
 | 
				
			||||||
 | 
					  UselessCallOnNotNull:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  UtilityClassWithPublicConstructor:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					  VarCouldBeVal:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    ignoreLateinitVar: false
 | 
				
			||||||
 | 
					  WildcardImport:
 | 
				
			||||||
 | 
					    active: true
 | 
				
			||||||
 | 
					    excludeImports:
 | 
				
			||||||
 | 
					      - 'java.util.*'
 | 
				
			||||||
							
								
								
									
										16
									
								
								fastlane/metadata/android/en-US/changelogs/v124123651.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								fastlane/metadata/android/en-US/changelogs/v124123651.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					**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
 | 
				
			||||||
							
								
								
									
										14
									
								
								fastlane/metadata/android/en-US/changelogs/v125010031.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								fastlane/metadata/android/en-US/changelogs/v125010031.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					**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
 | 
				
			||||||
@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					**v125010111**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Debug trying to fix context issues. (#174)
 | 
				
			||||||
 | 
					- Changelog for v125010031
 | 
				
			||||||
@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					**v125010131**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- fix: reload the adapter when it's needed. Fixes #128. (#176)
 | 
				
			||||||
 | 
					- feat: basic auth and images loading. Fixes #172. (#175)
 | 
				
			||||||
 | 
					- Changelog for v125010111
 | 
				
			||||||
@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					**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
 | 
				
			||||||
@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					**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
 | 
				
			||||||
@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					**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.
 | 
				
			||||||
@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					**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
 | 
				
			||||||
@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					**v125020581**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- fix: url can be empty ?
 | 
				
			||||||
 | 
					- Changelog for v125020471
 | 
				
			||||||
							
								
								
									
										12
									
								
								fastlane/metadata/android/en-US/changelogs/v125030681.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								fastlane/metadata/android/en-US/changelogs/v125030681.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					**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
 | 
				
			||||||
@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					**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
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 24 KiB  | 
@@ -18,12 +18,13 @@ kotlin.code.style=official
 | 
				
			|||||||
#Android
 | 
					#Android
 | 
				
			||||||
android.useAndroidX=true
 | 
					android.useAndroidX=true
 | 
				
			||||||
#android.nonTransitiveRClass=true
 | 
					#android.nonTransitiveRClass=true
 | 
				
			||||||
android.enableJetifier=true
 | 
					android.enableJetifier=false
 | 
				
			||||||
android.nonTransitiveRClass=false
 | 
					android.nonTransitiveRClass=true
 | 
				
			||||||
#MPP
 | 
					#MPP
 | 
				
			||||||
kotlin.mpp.enableCInteropCommonization=true
 | 
					kotlin.mpp.enableCInteropCommonization=true
 | 
				
			||||||
org.gradle.parallel=true
 | 
					org.gradle.parallel=true
 | 
				
			||||||
org.gradle.caching=true
 | 
					org.gradle.caching=true
 | 
				
			||||||
ignoreGitVersion=false
 | 
					ignoreGitVersion=false
 | 
				
			||||||
kotlin.native.cacheKind.iosX64=none
 | 
					kotlin.native.cacheKind.iosX64=none
 | 
				
			||||||
org.gradle.configureondemand=true
 | 
					org.gradle.configureondemand=true
 | 
				
			||||||
 | 
					kotlin.jvm.target.validation.mode=IGNORE
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							@@ -1,6 +1,6 @@
 | 
				
			|||||||
#Mon Nov 25 22:48:24 CET 2024
 | 
					#Sun Feb 09 14:44:52 CET 2025
 | 
				
			||||||
distributionBase=GRADLE_USER_HOME
 | 
					distributionBase=GRADLE_USER_HOME
 | 
				
			||||||
distributionPath=wrapper/dists
 | 
					distributionPath=wrapper/dists
 | 
				
			||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
 | 
					distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
 | 
				
			||||||
zipStoreBase=GRADLE_USER_HOME
 | 
					zipStoreBase=GRADLE_USER_HOME
 | 
				
			||||||
zipStorePath=wrapper/dists
 | 
					zipStorePath=wrapper/dists
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,17 @@
 | 
				
			|||||||
val ktorVersion = "2.3.2"
 | 
					val ktorVersion = "3.0.3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object SqlDelight {
 | 
					object SqlDelight {
 | 
				
			||||||
    const val runtime = "com.squareup.sqldelight:runtime:1.5.4"
 | 
					    const val runtime = "app.cash.sqldelight:runtime:2.0.2"
 | 
				
			||||||
    const val android = "com.squareup.sqldelight:android-driver:1.5.4"
 | 
					    const val android = "app.cash.sqldelight:android-driver:2.0.2"
 | 
				
			||||||
    const val native = "com.squareup.sqldelight:native-driver:1.5.4"
 | 
					    const val native = "app.cash.sqldelight:native-driver:2.0.2"
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
plugins {
 | 
					plugins {
 | 
				
			||||||
    kotlin("multiplatform")
 | 
					    kotlin("multiplatform")
 | 
				
			||||||
    id("com.android.library")
 | 
					    id("com.android.library")
 | 
				
			||||||
    id("com.squareup.sqldelight")
 | 
					 | 
				
			||||||
    kotlin("plugin.serialization") version "1.9.0"
 | 
					    kotlin("plugin.serialization") version "1.9.0"
 | 
				
			||||||
    id("org.jetbrains.kotlinx.kover")
 | 
					    id("org.jetbrains.kotlinx.kover")
 | 
				
			||||||
 | 
					    id("app.cash.sqldelight") version "2.0.2"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
kotlin {
 | 
					kotlin {
 | 
				
			||||||
@@ -37,17 +36,17 @@ kotlin {
 | 
				
			|||||||
                implementation("io.ktor:ktor-client-logging:$ktorVersion")
 | 
					                implementation("io.ktor:ktor-client-logging:$ktorVersion")
 | 
				
			||||||
                implementation("io.ktor:ktor-client-auth:$ktorVersion")
 | 
					                implementation("io.ktor:ktor-client-auth:$ktorVersion")
 | 
				
			||||||
                implementation("io.ktor:ktor-client-cio:$ktorVersion")
 | 
					                implementation("io.ktor:ktor-client-cio:$ktorVersion")
 | 
				
			||||||
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
 | 
					                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                implementation("org.jsoup:jsoup:1.15.4")
 | 
					                implementation("org.jsoup:jsoup:1.15.4")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                //Dependency Injection
 | 
					                // Dependency Injection
 | 
				
			||||||
                implementation("org.kodein.di:kodein-di:7.14.0")
 | 
					                implementation("org.kodein.di:kodein-di:7.14.0")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                //Settings
 | 
					                // Settings
 | 
				
			||||||
                implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC")
 | 
					                implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                //Logging
 | 
					                // Logging
 | 
				
			||||||
                implementation("io.github.aakira:napier:2.6.1")
 | 
					                implementation("io.github.aakira:napier:2.6.1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Sql
 | 
					                // Sql
 | 
				
			||||||
@@ -55,6 +54,10 @@ kotlin {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                // Sql
 | 
					                // Sql
 | 
				
			||||||
                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
 | 
					                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Connectivity
 | 
				
			||||||
 | 
					                implementation("dev.jordond.connectivity:connectivity-core:1.2.0")
 | 
				
			||||||
 | 
					                implementation("dev.jordond.connectivity:connectivity-device:1.2.0")
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        val commonTest by getting {
 | 
					        val commonTest by getting {
 | 
				
			||||||
@@ -66,7 +69,6 @@ kotlin {
 | 
				
			|||||||
        val androidMain by getting {
 | 
					        val androidMain by getting {
 | 
				
			||||||
            dependencies {
 | 
					            dependencies {
 | 
				
			||||||
                implementation("com.squareup.okhttp3:okhttp:4.11.0")
 | 
					                implementation("com.squareup.okhttp3:okhttp:4.11.0")
 | 
				
			||||||
                implementation("io.ktor:ktor-client-okhttp:2.2.4")
 | 
					 | 
				
			||||||
                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
 | 
					                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Sql
 | 
					                // Sql
 | 
				
			||||||
@@ -86,7 +88,6 @@ kotlin {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            dependencies {
 | 
					            dependencies {
 | 
				
			||||||
                implementation(SqlDelight.native)
 | 
					                implementation(SqlDelight.native)
 | 
				
			||||||
                implementation("io.ktor:ktor-client-ios:2.1.1")
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        val iosX64Test by getting
 | 
					        val iosX64Test by getting
 | 
				
			||||||
@@ -110,11 +111,10 @@ android {
 | 
				
			|||||||
    namespace = "bou.amine.apps.readerforselfossv2"
 | 
					    namespace = "bou.amine.apps.readerforselfossv2"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
sqldelight {
 | 
					sqldelight {
 | 
				
			||||||
    database("ReaderForSelfossDB") {
 | 
					    databases {
 | 
				
			||||||
        packageName = "bou.amine.apps.readerforselfossv2.dao"
 | 
					        create("ReaderForSelfossDB") {
 | 
				
			||||||
        sourceFolders = listOf("sqldelight")
 | 
					            packageName.set("bou.amine.apps.readerforselfossv2.dao")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +0,0 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.dao
 | 
					 | 
				
			||||||
import android.content.Context
 | 
					 | 
				
			||||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
 | 
					 | 
				
			||||||
import com.squareup.sqldelight.db.SqlDriver
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
actual class DriverFactory(private val context: Context) {
 | 
					 | 
				
			||||||
    actual fun createDriver(): SqlDriver {
 | 
					 | 
				
			||||||
        return AndroidSqliteDriver(ReaderForSelfossDB.Schema, context, "ReaderForSelfossV2-android.db")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					package bou.amine.apps.readerforselfossv2.dao
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Context
 | 
				
			||||||
 | 
					import app.cash.sqldelight.db.SqlDriver
 | 
				
			||||||
 | 
					import app.cash.sqldelight.driver.android.AndroidSqliteDriver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					actual class DriverFactory(
 | 
				
			||||||
 | 
					    private val context: Context,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    actual fun createDriver(): SqlDriver =
 | 
				
			||||||
 | 
					        AndroidSqliteDriver(
 | 
				
			||||||
 | 
					            ReaderForSelfossDB.Schema,
 | 
				
			||||||
 | 
					            context,
 | 
				
			||||||
 | 
					            "ReaderForSelfossV2-android.db",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -8,16 +8,20 @@ class NaiveTrustManager : X509TrustManager {
 | 
				
			|||||||
    override fun checkClientTrusted(
 | 
					    override fun checkClientTrusted(
 | 
				
			||||||
        chain: Array<out X509Certificate>?,
 | 
					        chain: Array<out X509Certificate>?,
 | 
				
			||||||
        authType: String?,
 | 
					        authType: String?,
 | 
				
			||||||
    ) {}
 | 
					    ) {
 | 
				
			||||||
 | 
					        // Nothing
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun checkServerTrusted(
 | 
					    override fun checkServerTrusted(
 | 
				
			||||||
        chain: Array<out X509Certificate>?,
 | 
					        chain: Array<out X509Certificate>?,
 | 
				
			||||||
        authType: String?,
 | 
					        authType: String?,
 | 
				
			||||||
    ) {}
 | 
					    ) {
 | 
				
			||||||
 | 
					        // Nothing
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
 | 
					    override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
 | 
					actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
 | 
				
			||||||
    config.https.trustManager = NaiveTrustManager()
 | 
					    config.https.trustManager = NaiveTrustManager()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.utils
 | 
					package bou.amine.apps.readerforselfossv2.utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.text.format.DateUtils
 | 
					import android.text.format.DateUtils
 | 
				
			||||||
import io.github.aakira.napier.Napier
 | 
					import kotlinx.datetime.Clock
 | 
				
			||||||
import kotlinx.datetime.*
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Suppress("detekt:UtilityClassWithPublicConstructor")
 | 
				
			||||||
actual class DateUtils {
 | 
					actual class DateUtils {
 | 
				
			||||||
    actual companion object {
 | 
					    actual companion object {
 | 
				
			||||||
        actual fun parseRelativeDate(dateString: String): String {
 | 
					        actual fun parseRelativeDate(dateString: String): String {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,46 +4,36 @@ import android.net.Uri
 | 
				
			|||||||
import android.text.Html
 | 
					import android.text.Html
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
					import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
				
			||||||
import org.jsoup.Jsoup
 | 
					import org.jsoup.Jsoup
 | 
				
			||||||
import java.util.*
 | 
					import java.util.Locale
 | 
				
			||||||
 | 
					
 | 
				
			||||||
actual fun String.getHtmlDecoded(): String {
 | 
					actual fun String.getHtmlDecoded(): String = Html.fromHtml(this).toString()
 | 
				
			||||||
    return Html.fromHtml(this).toString()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
actual fun SelfossModel.Item.getIcon(baseUrl: String): String {
 | 
					actual fun SelfossModel.Item.getIcon(baseUrl: String): String = constructUrl(baseUrl, "favicons", icon)
 | 
				
			||||||
    return constructUrl(baseUrl, "favicons", icon)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
 | 
					actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String = constructUrl(baseUrl, "thumbnails", thumbnail)
 | 
				
			||||||
    return constructUrl(baseUrl, "thumbnails", thumbnail)
 | 
					
 | 
				
			||||||
}
 | 
					val IMAGE_EXTENSION_REGEXP = """\.(jpg|jpeg|png|webp)""".toRegex()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
actual fun SelfossModel.Item.getImages(): ArrayList<String> {
 | 
					actual fun SelfossModel.Item.getImages(): ArrayList<String> {
 | 
				
			||||||
    val allImages = ArrayList<String>()
 | 
					    val allImages = ArrayList<String>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (image in Jsoup.parse(content).getElementsByTag("img")) {
 | 
					    for (image in Jsoup.parse(content).getElementsByTag("img")) {
 | 
				
			||||||
        val url = image.attr("src")
 | 
					        val url = image.attr("src")
 | 
				
			||||||
        if (url.lowercase(Locale.US).contains(".jpg") ||
 | 
					        if (IMAGE_EXTENSION_REGEXP.containsMatchIn(url.lowercase(Locale.US))) {
 | 
				
			||||||
            url.lowercase(Locale.US).contains(".jpeg") ||
 | 
					 | 
				
			||||||
            url.lowercase(Locale.US).contains(".png") ||
 | 
					 | 
				
			||||||
            url.lowercase(Locale.US).contains(".webp")
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
            allImages.add(url)
 | 
					            allImages.add(url)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return allImages
 | 
					    return allImages
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
 | 
					actual fun SelfossModel.Source.getIcon(baseUrl: String): String = constructUrl(baseUrl, "favicons", icon)
 | 
				
			||||||
    return constructUrl(baseUrl, "favicons", icon)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
actual fun constructUrl(
 | 
					actual fun constructUrl(
 | 
				
			||||||
    baseUrl: String,
 | 
					    baseUrl: String,
 | 
				
			||||||
    path: String,
 | 
					    path: String,
 | 
				
			||||||
    file: String?,
 | 
					    file: String?,
 | 
				
			||||||
): String {
 | 
					): String =
 | 
				
			||||||
    return if (file == null || file == "null" || file.isEmpty()) {
 | 
					    if (file == null || file == "null" || file.isEmpty()) {
 | 
				
			||||||
        ""
 | 
					        ""
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
 | 
					        val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
 | 
				
			||||||
@@ -51,4 +41,3 @@ actual fun constructUrl(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        baseUriBuilder.toString()
 | 
					        baseUriBuilder.toString()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.dao
 | 
					package bou.amine.apps.readerforselfossv2.dao
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import com.squareup.sqldelight.db.SqlDriver
 | 
					import app.cash.sqldelight.db.SqlDriver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
expect class DriverFactory {
 | 
					expect class DriverFactory {
 | 
				
			||||||
    fun createDriver(): SqlDriver
 | 
					    fun createDriver(): SqlDriver
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
package bou.amine.apps.readerforselfossv2.DI
 | 
					package bou.amine.apps.readerforselfossv2.di
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
 | 
					import bou.amine.apps.readerforselfossv2.rest.MercuryApi
 | 
				
			||||||
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
 | 
					import bou.amine.apps.readerforselfossv2.rest.SelfossApi
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user