Compare commits

..

77 Commits

Author SHA1 Message Date
c87abec0b9 debug: Debug null context.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-05-12 20:31:40 +02:00
0aba41d8bf Changelog for v123051301 [CI SKIP] 2023-05-10 19:36:31 +00:00
2a2d1047b4 feat: Basic auth from url. Fixes #142 (#143)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Co-authored-by: aminecmi <aminecmi@gmail.com>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/143
2023-05-10 19:19:11 +00:00
66ef1ccf32 debug: Debug index out of bound exception.
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-10 20:50:13 +02:00
677ede5bc7 Changelog for v123051211 [CI SKIP] 2023-05-01 18:26:12 +00:00
996a7ed22c fix: Sometimes url isn't even defined.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-04-30 18:41:15 +02:00
85208c4e5a Changelog for v123041021 [CI SKIP] 2023-04-12 19:01:21 +00:00
5cfec50cba fix: 'Enable Core Library Desugaring to support older Android versions' (#138) from davidoskky/ReaderForSelfoss-multiplatform:desugaring into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/138
2023-04-12 18:26:37 +00:00
76ad71e1dc Enable Core Library Desugaring to support older Android versions
Some checks failed
continuous-integration/drone/pr Build is failing
2023-04-12 16:29:47 +02:00
0277fb507c Changelog for v123030851 [CI SKIP] 2023-03-26 18:21:43 +00:00
8d7d3174aa chore: replace textDrawable library (#136)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
## Types of changes

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

This closes issue #120

Removed the dependency `com.amulyakhare.textdrawable` and slightly simplified the logic required to set circular images.

Co-authored-by: davidoskky <davidoskky@yahoo.it>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/136
Co-authored-by: davidoskky <davidoskky@hidden.hidden>
Co-committed-by: davidoskky <davidoskky@hidden.hidden>
2023-03-26 11:12:01 +00:00
00eb3333fe refactor: Remove slow login check. Closes #135.
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-25 20:28:30 +01:00
629ca01d99 ci: send the mapping file after a release.
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-17 10:10:38 +01:00
c2d8681ce8 Changelog for v123030751 [CI SKIP] 2023-03-16 19:41:34 +00:00
08f79cb148 debug: added a lot to pinpoint the url issue.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-03-16 20:26:36 +01:00
e21906e70d feat: Use /sources/stats in the home (#133)
All checks were successful
continuous-integration/drone/push Build is passing
## Types of changes

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

This is implements feature #131 and it will allow implementing #132
With this, public mode functions perfectly and also has source filtering.

Co-authored-by: davidoskky <davidoskky@yahoo.it>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/133
Co-authored-by: davidoskky <davidoskky@hidden.hidden>
Co-committed-by: davidoskky <davidoskky@hidden.hidden>
2023-03-13 16:26:54 +00:00
9d2cc32bc9 Changelog for v123030681 [CI SKIP] 2023-03-09 20:27:08 +00:00
d9d057c8dc fix: Unread and starred can be null.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-03-09 20:46:09 +01:00
1f3fa0c4a6 Fixed version number issue.
Some checks failed
continuous-integration/drone/push Build is failing
2023-03-05 21:02:03 +01:00
dea3def385 Changelog for v123030621 [CI SKIP] 2023-03-03 20:07:52 +00:00
f72ef2f5d4 fix: url required issue.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-03-03 20:39:33 +01:00
f28cb759df fix: Canvas reused issue. 2023-03-03 20:39:20 +01:00
b9d69c3e64 Changelog for v123020572 [CI SKIP] 2023-02-26 19:13:41 +00:00
c2a1c9eaac fix: requirecontext issues ?
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-26 16:05:26 +01:00
bf37209a15 debug: activity not found exception. 2023-02-26 15:40:58 +01:00
2c558fe6fd Changelog for v123020571 [CI SKIP] 2023-02-26 07:49:16 +00:00
ad88011454 chore: remove errors logging.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-25 22:57:59 +01:00
559c17bc1d fix: quickfix for url param not provided for some sources. 2023-02-25 22:46:04 +01:00
ab9c46f0eb Update 'CHANGELOG.md'
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-21 15:00:37 +00:00
aa799d2ca8 Changelog for v123020523 [CI SKIP] 2023-02-21 14:49:14 +00:00
177c978474 fix: Git changelog.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-21 14:05:32 +00:00
39b9991413 Changelog for v123020522 [CI SKIP] 2023-02-21 13:25:11 +00:00
b303f110f1 fix: still fixing changelog versions.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-21 12:59:38 +00:00
f851941a6a Changelog for v123020521 [CI SKIP] 2023-02-21 11:31:18 +00:00
a313552976 fix: change changelog version on release.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-21 11:20:43 +00:00
6ac97ed3fe Changelog for v123020491 [CI SKIP] 2023-02-20 09:39:40 +00:00
d583b937b7 fix: Fixed acra bug reporting.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-20 10:29:00 +01:00
15b9a2d935 Changelog for v123010301 [CI SKIP] 2023-02-18 20:01:03 +00:00
5a8ce15961 Chore: acra config.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-02-18 20:38:06 +01:00
e1c64cef46 Changelog for v123010281 [CI SKIP] 2023-01-30 19:55:23 +00:00
ee064f3cb4 improvement: Improve right to left support (#130)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Co-authored-by: davidoskky <davidoskky@hidden.hidden>
Co-committed-by: davidoskky <davidoskky@hidden.hidden>
2023-01-29 12:55:28 +00:00
3e46e2ff29 Changelog for v123010261 [CI SKIP] 2023-01-28 10:57:06 +00:00
f28e702549 feat: Handle public instances (#126)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Co-authored-by: davidoskky <davidoskky@hidden.hidden>
Co-committed-by: davidoskky <davidoskky@hidden.hidden>
2023-01-28 10:25:28 +00:00
fc31a4399c ci: Pull request should trigger ci.
Some checks are pending
continuous-integration/drone/push Build is running
2023-01-28 11:23:00 +01:00
9b23053b66 fix: Complete the disconnection before redirecting to the login screen
Some checks are pending
continuous-integration/drone/push Build is running
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/129
2023-01-28 11:20:19 +01:00
389a04d250 Complete the disconnection before redirecting to the login screen 2023-01-27 14:21:33 +01:00
40e1d1478b Changelog for v123010241 [CI SKIP] 2023-01-26 10:55:43 +00:00
2154ff3c33 Merge pull request 'feat: swipe down to close images' (#122) from davidoskky/ReaderForSelfoss-multiplatform:swipe_down into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/122
2023-01-26 10:42:15 +00:00
2245565f95 Remove unnecessary definition 2023-01-25 21:25:32 +01:00
014858f06b Remove unused import 2023-01-25 20:40:20 +01:00
3f1f86a78e Adjust the image closing animation 2023-01-25 10:34:03 +01:00
a549169a7c Add a dark hue to the underlying article when swiping to close images 2023-01-25 10:27:38 +01:00
be7cae365a Rename activity style to avoid interferences 2023-01-25 02:09:41 +01:00
cef3b2e593 Adapt the style of the image activity to the rest of the application 2023-01-25 01:54:35 +01:00
ae927ebc57 Resolve issues when swiping down to close images 2023-01-25 00:46:43 +01:00
90532cf501 Changelog for v123010041 [CI SKIP] 2023-01-24 12:41:52 +00:00
ab0678d61e Merge pull request 'scroll-tag-filters' (#124) from scroll-tag-filters into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/124
2023-01-23 21:42:42 +00:00
a1b7d22d26 fix: added POST_NOTIFICATIONS to fix notifications issues.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-23 22:32:52 +01:00
29eae4b1f6 fix: scrollable filter sheet.
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-23 21:13:10 +01:00
f5bbc63481 enhancement: Ellipsize chips text. 2023-01-23 21:12:56 +01:00
ddc72d85b0 Close the image fragment only if the image has been dragged down 2023-01-21 16:37:25 +01:00
68bbf5b2d3 Animate swipe down to close images 2023-01-20 16:36:52 +01:00
95e76a55da Cleaning.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-12 22:01:36 +01:00
2b6659f4ec Swipe down to close images 2023-01-11 22:28:14 +01:00
e0c118a73e Changelog for v122123641 [CI SKIP] 2023-01-04 19:27:56 +00:00
4e61b2aed6 feat: Disable the failing source in the filter sheet.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2023-01-03 21:35:21 +01:00
ba2758c0a3 feat: Display the source error in the sources list. 2023-01-03 21:28:40 +01:00
c718b966a1 Changelog for v122123631 [CI SKIP] 2022-12-30 19:24:48 +00:00
99438e142f build: Added back maven repos (see 1fb9d60dc5 (note_1223925153))
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-30 20:10:08 +01:00
4d8076c3cf build: Added back maven repos (see 1fb9d60dc5 (note_1223925153))
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-29 21:34:13 +01:00
db75c5b74a debug: trying to resolve Canvas: trying to use a recycled bitmap.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-29 20:40:39 +01:00
966a082147 fix: NPE may be caused by the binding or the title that was null.
Some checks are pending
continuous-integration/drone/push Build is running
2022-12-29 20:35:03 +01:00
cd20a5ec29 chore: Skip drone pipeline on changelog push.
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-12-29 14:11:29 +01:00
cc4c1c9201 Changelog for v122123621
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-12-29 13:08:23 +00:00
ff021d572c fix: Automatic CHANGELOG generation.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-29 13:11:23 +01:00
89992967be Merge pull request 'Sources Upsert' (#119) from sources-edit into master
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/119
2022-12-28 21:31:49 +00:00
3c68bde62b Source update screen.
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-28 22:19:11 +01:00
49 changed files with 2008 additions and 1135 deletions

View File

@ -35,6 +35,7 @@ steps:
trigger:
event:
- push
- pull_request
---
kind: pipeline
@ -46,13 +47,15 @@ steps:
image: ubuntu:latest
commands:
- apt-get update && apt-get install -y git
- VER=$(git describe --tags --abbrev=0)
- CHANGELOG=$(git log $VER..HEAD --pretty="- %s")
- echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md
- git add CHANGELOG.md
- git commit -m "Changelog for $VER"
- git fetch --tags -p
- PREV=$(git describe --tags --abbrev=0)
- ./build.sh --publish --from-ci
- git remote add pushing https://$GITEA_USR:$GITEA_PASS@gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform.git
- VER=$(git describe --tags --abbrev=0)
- CHANGELOG=$(git log $PREV..HEAD --pretty="- %s")
- echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md
- git add CHANGELOG.md
- git commit -m "Changelog for $VER [CI SKIP]"
- git push pushing master
- git push pushing --tags
environment:
@ -134,6 +137,27 @@ steps:
from_secret: giteaAPI
base_url: https://gitea.amine-louveau.fr
files: signed.apk
- name: notify
image: drillster/drone-email
failure: ignore
settings:
host:
from_secret: smtpHOST
port:
from_secret: smtpPORT
username:
from_secret: smtpUSERNAME
password:
from_secret: smtpPASSWORD
from:
from_secret: smtpFROM
subject: Mapping file
recipients:
from_secret: smtpTO
recipients_only: true
skip_verify: true
attachment: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
trigger:
event:
- tag

View File

@ -1,3 +1,162 @@
**v123051301**
- feat: Basic auth from url. Fixes #142 (#143)
- debug: Debug index out of bound exception.
- Changelog for v123051211 [CI SKIP]
--------------------------------------------------------------------
**v123051211**
- fix: Sometimes url isn't even defined.
- Changelog for v123041021 [CI SKIP]
--------------------------------------------------------------------
**v123041021**
- fix: 'Enable Core Library Desugaring to support older Android versions' (#138) from davidoskky/ReaderForSelfoss-multiplatform:desugaring into master
- Enable Core Library Desugaring to support older Android versions
- Changelog for v123030851 [CI SKIP]
--------------------------------------------------------------------
**v123030851**
- chore: replace textDrawable library (#136)
- refactor: Remove slow login check. Closes #135.
- ci: send the mapping file after a release.
- Changelog for v123030751 [CI SKIP]
--------------------------------------------------------------------
**v123030751**
- debug: added a lot to pinpoint the url issue.
- feat: Use /sources/stats in the home (#133)
- Changelog for v123030681 [CI SKIP]
--------------------------------------------------------------------
**v123030681**
- fix: Unread and starred can be null.
- Fixed version number issue.
- Changelog for v123030621 [CI SKIP]
--------------------------------------------------------------------
**v123030621**
- fix: url required issue.
- fix: Canvas reused issue.
- Changelog for v123020572 [CI SKIP]
--------------------------------------------------------------------
**v123020572**
- fix: requirecontext issues ?
- debug: activity not found exception.
- Changelog for v123020571 [CI SKIP]
--------------------------------------------------------------------
**v123020571**
- chore: remove errors logging.
- fix: quickfix for url param not provided for some sources.
- Update 'CHANGELOG.md'
- Changelog for v123020523 [CI SKIP]
--------------------------------------------------------------------
**v123020523**
- fix: Git changelog.
--------------------------------------------------------------------
**v123020491**
- fix: Fixed acra bug reporting.
--------------------------------------------------------------------
**v123010301**
- Chore: acra config.
--------------------------------------------------------------------
**v123010281**
- improvement: Improve right to left support (#130) Co-authored-by: davidoskky <davidoskky@hidden.hidden> Co-committed-by: davidoskky <davidoskky@hidden.hidden>
--------------------------------------------------------------------
**v123010261**
- feat: Handle public instances (#126) Co-authored-by: davidoskky <davidoskky@hidden.hidden> Co-committed-by: davidoskky <davidoskky@hidden.hidden>
- ci: Pull request should trigger ci.
- fix: Complete the disconnection before redirecting to the login screen
- Complete the disconnection before redirecting to the login screen
--------------------------------------------------------------------
**v123010241**
- Merge pull request 'feat: swipe down to close images' (#122) from davidoskky/ReaderForSelfoss-multiplatform:swipe_down into master
- Remove unnecessary definition
- Remove unused import
- Adjust the image closing animation
- Add a dark hue to the underlying article when swiping to close images
- Rename activity style to avoid interferences
- Adapt the style of the image activity to the rest of the application
- Resolve issues when swiping down to close images
- Close the image fragment only if the image has been dragged down
- Animate swipe down to close images
- Swipe down to close images
--------------------------------------------------------------------
**v123010041**
- Merge pull request 'scroll-tag-filters' (#124) from scroll-tag-filters into master
- fix: added POST_NOTIFICATIONS to fix notifications issues.
- fix: scrollable filter sheet.
- enhancement: Ellipsize chips text.
- Cleaning.
--------------------------------------------------------------------
**v122123641**
- feat: Disable the failing source in the filter sheet.
- feat: Display the source error in the sources list.
--------------------------------------------------------------------
**v122123631**
- build: Added back maven repos (see https://gitlab.com/fdroid/fdroiddata/-/commit/1fb9d60dc58511abc2bb4eb321977922a0682c8b#note_1223925153)
- build: Added back maven repos (see https://gitlab.com/fdroid/fdroiddata/-/commit/1fb9d60dc58511abc2bb4eb321977922a0682c8b#note_1223925153)
- debug: trying to resolve `Canvas: trying to use a recycled bitmap`.
- fix: NPE may be caused by the binding or the title that was null.
- chore: Skip drone pipeline on changelog push.
--------------------------------------------------------------------
**v122123621**
- fix: Automatic CHANGELOG generation.
- Merge pull request 'Sources Upsert' (#119) from sources-edit into master
- Source update screen.
- Sources menu.
- chore: Automatic CHANGELOG generation.
--------------------------------------------------------------------
# V2/Multiplatform rewrite
**v1**

View File

@ -28,7 +28,7 @@ fun gitVersion(): String {
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
process = if (maybeTagOfCurrentCommit.isEmpty()) {
println("No tag on current commit. Will take the latest one.")
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1")
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
} else {
println("Tag found on current commit")
execWithOutput("git -C ../ describe --contains HEAD")
@ -56,6 +56,7 @@ fun versionNameFromGit(): String {
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
// Flag to enable support for the new language APIs
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
@ -66,7 +67,7 @@ android {
jvmTarget = "11"
}
compileSdk = 33
buildToolsVersion = "31.0.0"
buildToolsVersion = "33.0.0"
buildFeatures {
viewBinding = true
}
@ -112,10 +113,11 @@ android {
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
implementation(project(":shared"))
implementation("com.google.android.material:material:1.5.0")
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
implementation("androidx.preference:preference-ktx:1.1.1")
@ -131,7 +133,7 @@ dependencies {
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.annotation:annotation:1.3.0")
implementation("androidx.work:work-runtime-ktx:2.7.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("org.jsoup:jsoup:1.14.3")
//multidex
@ -143,11 +145,10 @@ dependencies {
// Material-ish things
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1")
// glide
kapt("com.github.bumptech.glide:compiler:4.14.2")
implementation("com.github.bumptech.glide:okhttp3-integration:4.14.2")
kapt("com.github.bumptech.glide:compiler:4.15.0")
implementation("com.github.bumptech.glide:okhttp3-integration:4.15.0")
// Themes
implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
@ -175,7 +176,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
// Network information
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
// SQLDELIGHT
implementation("com.squareup.sqldelight:android-driver:1.5.4")

View File

@ -2,6 +2,7 @@
<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.ACCESS_NETWORK_STATE" />
@ -69,7 +70,8 @@
android:name=".ReaderActivity">
</activity>
<activity
android:name=".ImageActivity">
android:name=".ImageActivity"
android:theme="@style/Theme.AppCompat.ImageActivity">
</activity>
<meta-data android:name="android.webkit.WebView.MetricsOptOut"

View File

@ -36,6 +36,7 @@ import com.ashokvarma.bottomnavigation.TextBadgeItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
@ -114,10 +115,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
}
val swipeDirs = if (appSettingsService.getPublicAccess()) {
0
} else {
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
}
val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback(
0,
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
swipeDirs
) {
override fun getSwipeDirs(
recyclerView: RecyclerView,
@ -366,7 +373,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
fun fetchOnEmptyList() {
binding.recyclerView.doOnNextLayout {
// TODO: do if last element (or is empty ?)
getElementsAccordingToTab(true)
}
}
@ -511,6 +517,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.home_menu, menu)
if (appSettingsService.getPublicAccess()) {
menu.removeItem(R.id.readAll)
menu.removeItem(R.id.action_sources)
}
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.getActionView() as SearchView
@ -539,11 +549,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
R.id.refresh -> {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
// TODO: Use Dispatchers.IO
CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote()
if (updatedRemote) {
// TODO: Send toast messages from the repository
Toast.makeText(
this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG
@ -591,12 +599,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
return true
}
R.id.action_disconnect -> {
CoroutineScope(Dispatchers.Main).launch {
runBlocking {
repository.logout()
}
this@HomeActivity.finish()
val intent = Intent(this, LoginActivity::class.java)
this.startActivity(intent)
finish()
return true
}
R.id.action_settings -> {

View File

@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.android
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
@ -23,7 +24,6 @@ class ImageActivity : AppCompatActivity() {
setContentView(view)
setSupportActionBar(binding.toolBar)
supportActionBar?.setDisplayShowTitleEnabled(false)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
allImages = intent.getStringArrayListExtra("allImages") as ArrayList<String>
@ -31,12 +31,35 @@ class ImageActivity : AppCompatActivity() {
binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(position, false)
val transitionListener = object : MotionLayout.TransitionListener {
override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {
// Nothing
}
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float
) {
// Nothing
}
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
if (motionLayout?.currentState == binding.root.endState) {
onBackPressedDispatcher.onBackPressed()
overridePendingTransition(0, 0)
}
}
override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {
// Nothing
}
}
binding.root.setTransitionListener(transitionListener)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
onBackPressedDispatcher.onBackPressed()
return true
}
}

View File

@ -27,7 +27,6 @@ import org.acra.ACRA
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import java.security.MessageDigest
class LoginActivity : AppCompatActivity(), DIAware {
@ -58,29 +57,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
if (appSettingsService.getBaseUrl().isNotEmpty()) {
showProgress(true)
// This should be reverted when "old" users connected with a non-selfoss rss
// are handled. Revert to "simple" way.
CoroutineScope(Dispatchers.Main).launch {
try {
val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance()
if (!errorFetching && !displaySelfossOnly) {
goToMain()
} else {
showProgress(false)
if (displaySelfossOnly) {
Toast.makeText(
applicationContext,
R.string.application_selfoss_only,
Toast.LENGTH_LONG
).show()
}
repository.logout()
}
} catch (e: Throwable) {
repository.logout()
showProgress(false)
}
}
goToMain()
}
handleActions()
@ -129,7 +106,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
private fun goToMain() {
CoroutineScope(Dispatchers.Main).launch {
repository.updateApiVersion()
repository.updateApiInformation()
ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString())
}
val intent = Intent(this, HomeActivity::class.java)
@ -157,13 +134,64 @@ class LoginActivity : AppCompatActivity(), DIAware {
val login = binding.loginView.text.toString().trim()
val password = binding.passwordView.text.toString().trim()
var cancel = false
var focusView: View? = null
failInvalidUrl(url)
failLoginDetails(password, login)
showProgress(true)
repository.refreshLoginInformation(url, login, password)
CoroutineScope(Dispatchers.Main).launch {
val result = repository.login()
if (result) {
val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance()
if (!errorFetching && !displaySelfossOnly) {
goToMain()
} else {
if (displaySelfossOnly) {
Toast.makeText(
applicationContext,
R.string.application_selfoss_only,
Toast.LENGTH_LONG
).show()
}
preferenceError()
}
} else {
preferenceError()
}
showProgress(false)
}
}
private fun failLoginDetails(
password: String,
login: String
) {
var lastFocusedView: View? = null
var cancel = false
if (isWithLogin) {
if (TextUtils.isEmpty(password)) {
binding.passwordView.error = getString(R.string.error_invalid_password)
lastFocusedView = binding.passwordView
cancel = true
}
if (TextUtils.isEmpty(login)) {
binding.loginView.error = getString(R.string.error_field_required)
lastFocusedView = binding.loginView
cancel = true
}
}
maybeCancelAndFocusView(cancel, lastFocusedView)
}
private fun failInvalidUrl(url: String) {
val focusView = binding.urlView
var cancel = false
if (url.isBaseUrlInvalid()) {
binding.urlView.error = getString(R.string.login_url_problem)
focusView = binding.urlView
cancel = true
binding.urlView.error = getString(R.string.login_url_problem)
inValidCount++
if (inValidCount == 3) {
val alertDialog = AlertDialog.Builder(this).create()
@ -177,49 +205,12 @@ class LoginActivity : AppCompatActivity(), DIAware {
inValidCount = 0
}
}
maybeCancelAndFocusView(cancel, focusView)
}
if (isWithLogin) {
if (TextUtils.isEmpty(password)) {
binding.passwordView.error = getString(R.string.error_invalid_password)
focusView = binding.passwordView
cancel = true
}
if (TextUtils.isEmpty(login)) {
binding.loginView.error = getString(R.string.error_field_required)
focusView = binding.loginView
cancel = true
}
}
private fun maybeCancelAndFocusView(cancel: Boolean, focusView: View?) {
if (cancel) {
focusView?.requestFocus()
} else {
showProgress(true)
repository.refreshLoginInformation(url, login, password)
CoroutineScope(Dispatchers.Main).launch {
val result = repository.login()
if (result) {
val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance()
if (!errorFetching && !displaySelfossOnly) {
goToMain()
} else {
if (displaySelfossOnly) {
Toast.makeText(
applicationContext,
R.string.application_selfoss_only,
Toast.LENGTH_LONG
).show()
}
preferenceError()
}
} else {
preferenceError()
}
showProgress(false)
}
}
}

View File

@ -34,10 +34,18 @@ import org.kodein.di.*
class MyApp : MultiDexApplication(), DIAware {
override val di by DI.lazy {
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) }
import(networkModule)
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) }
bind<Repository>() with singleton {
Repository(
instance(),
instance(),
isConnectionAvailable,
instance()
)
}
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
}
@ -59,7 +67,12 @@ class MyApp : MultiDexApplication(), DIAware {
handleNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository))
ProcessLifecycleOwner.get().lifecycle.addObserver(
AppLifeCycleObserver(
connectivityStatus,
repository
)
)
CoroutineScope(Dispatchers.Main).launch {
viewModel.networkAvailableProvider.collect { networkAvailable ->
@ -88,22 +101,38 @@ class MyApp : MultiDexApplication(), DIAware {
initAcra {
reportFormat = StringFormat.JSON
reportContent = listOf(
ReportField.REPORT_ID, ReportField.INSTALLATION_ID,
ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME,
ReportField.BUILD, ReportField.ANDROID_VERSION, ReportField.BRAND, ReportField.PHONE_MODEL,
ReportField.AVAILABLE_MEM_SIZE, ReportField.TOTAL_MEM_SIZE,
ReportField.STACK_TRACE, ReportField.APPLICATION_LOG, ReportField.LOGCAT,
ReportField.INITIAL_CONFIGURATION, ReportField.CRASH_CONFIGURATION, ReportField.IS_SILENT,
ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA)
ReportField.REPORT_ID,
ReportField.INSTALLATION_ID,
ReportField.APP_VERSION_CODE,
ReportField.APP_VERSION_NAME,
ReportField.BUILD,
ReportField.ANDROID_VERSION,
ReportField.BRAND,
ReportField.PHONE_MODEL,
ReportField.AVAILABLE_MEM_SIZE,
ReportField.TOTAL_MEM_SIZE,
ReportField.STACK_TRACE,
ReportField.APPLICATION_LOG,
ReportField.LOGCAT,
ReportField.INITIAL_CONFIGURATION,
ReportField.CRASH_CONFIGURATION,
ReportField.IS_SILENT,
ReportField.USER_APP_START_DATE,
ReportField.USER_COMMENT,
ReportField.USER_CRASH_DATE,
ReportField.USER_EMAIL,
ReportField.CUSTOM_DATA
)
toast {
//required
text = getString(R.string.crash_toast_text)
length = Toast.LENGTH_SHORT
}
httpSender {
uri = "https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/
basicAuthLogin = "LMTlLZuazADohTCm"
basicAuthPassword = "he6ghHp83F0PYPfh"
uri =
"https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/
basicAuthLogin = "qMEscjj89Gwt6cPR"
basicAuthPassword = "Yo58QFlGzFaWlBzP"
httpMethod = HttpSender.Method.POST
}
}
@ -119,7 +148,11 @@ class MyApp : MultiDexApplication(), DIAware {
val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = NotificationChannel(AppSettingsService.newItemsChannelId, newItemsChannelname, newItemsChannelimportance)
val newItemsChannelmChannel = NotificationChannel(
AppSettingsService.newItemsChannelId,
newItemsChannelname,
newItemsChannelimportance
)
notificationManager.createNotificationChannel(mChannel)
notificationManager.createNotificationChannel(newItemsChannelmChannel)
@ -133,13 +166,17 @@ class MyApp : MultiDexApplication(), DIAware {
if (e is NoClassDefFoundError && e.stackTrace.asList().any {
it.toString().contains("android.view.ViewDebug")
}) {
// Nothing
} else {
oldHandler.uncaughtException(thread, e)
}
}
}
class AppLifeCycleObserver(val connectivityStatus: ConnectivityStatus, val repository: Repository) : DefaultLifecycleObserver {
class AppLifeCycleObserver(
val connectivityStatus: ConnectivityStatus,
val repository: Repository
) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)

View File

@ -71,7 +71,12 @@ class ReaderActivity : AppCompatActivity(), DIAware {
finish()
}
readItem(allItems[currentItem])
try {
readItem(allItems[currentItem])
} catch (e: IndexOutOfBoundsException) {
e.sendSilentlyWithAcraWithName("out of bound > size = ${allItems.size} currentItem = $currentItem")
finish()
}
binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(currentItem, false)
@ -84,7 +89,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
}
private fun readItem(item: SelfossModel.Item) {
if (appSettingsService.isMarkOnScrollEnabled()) {
if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess()) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(item)
}
@ -137,28 +142,34 @@ class ReaderActivity : AppCompatActivity(), DIAware {
inflater.inflate(R.menu.reader_menu, menu)
toolbarMenu = menu
if (allItems.isNotEmpty() && allItems[currentItem].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
alignmentMenu()
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if (allItems[position].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
readItem(allItems[position])
}
if (appSettingsService.getPublicAccess()) {
menu.removeItem(R.id.star)
} else {
if (allItems.isNotEmpty() && allItems[currentItem].starred) {
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
}
@ -177,20 +188,18 @@ class ReaderActivity : AppCompatActivity(), DIAware {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
onBackPressedDispatcher.onBackPressed()
return true
}
R.id.star -> {
if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(allItems[binding.pager.currentItem])
// TODO: Handle failure
}
afterUnsave()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(allItems[binding.pager.currentItem])
// TODO: Handle failure
}
afterSave()
}

View File

@ -49,13 +49,13 @@ class SourcesActivity : AppCompatActivity(), DIAware {
super.onResume()
val mLayoutManager = LinearLayoutManager(this)
var items: ArrayList<SelfossModel.Source>
var items: ArrayList<SelfossModel.SourceDetail>
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources()
val response = repository.getSourcesDetails()
if (response.isNotEmpty()) {
items = response
val mAdapter = SourcesListAdapter(

View File

@ -24,7 +24,7 @@ import org.kodein.di.instance
class UpsertSourceActivity : AppCompatActivity(), DIAware {
private var existingSource: SelfossModel.Source? = null
private var existingSource: SelfossModel.SourceDetail? = null
private var mSpoutsValue: String? = null
private lateinit var binding: ActivityUpsertSourceBinding
@ -68,7 +68,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
private fun initFields(items: Map<String, SelfossModel.Spout>) {
binding.nameInput.setText(existingSource!!.title)
binding.tags.setText(existingSource!!.tags.joinToString(", "))
binding.tags.setText(existingSource!!.tags?.joinToString(", "))
binding.sourceUri.setText(existingSource!!.params?.url)
binding.spoutsSpinner.setSelection(items.keys.indexOf(existingSource!!.spout))
binding.progress.visibility = View.GONE

View File

@ -9,10 +9,9 @@ import android.widget.ImageView.ScaleType
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
@ -22,8 +21,6 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -38,7 +35,6 @@ class ItemCardAdapter(
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL
private val imageMaxHeight: Int =
c.resources.getDimension(R.dimen.card_image_max_height).toInt()
@ -56,6 +52,10 @@ class ItemCardAdapter(
val itm = items[position]
binding.favButton.isSelected = itm.starred
if (appSettingsService.getPublicAccess()) {
binding.favButton.visibility = View.GONE
}
binding.title.text = itm.title.getHtmlDecoded()
binding.title.setOnTouchListener(LinkOnTouchListener())
@ -79,16 +79,9 @@ class ItemCardAdapter(
}
if (itm.getIcon(repository.baseUrl).isEmpty()) {
val color = generator.getColor(itm.title.getHtmlDecoded())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
binding.sourceImage.setImageDrawable(drawable)
binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else {
c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
}
}
}

View File

@ -7,10 +7,8 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
@ -18,8 +16,6 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import org.kodein.di.DI
import org.kodein.di.android.closestDI
import org.kodein.di.instance
@ -29,7 +25,6 @@ class ItemListAdapter(
override var items: ArrayList<SelfossModel.Item>,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val generator: ColorGenerator = ColorGenerator.MATERIAL
private val c: Context = app.baseContext
override val di: DI by closestDI(app)
@ -56,20 +51,12 @@ class ItemListAdapter(
if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
if (itm.getIcon(repository.baseUrl).isEmpty()) {
val color = generator.getColor(itm.title.getHtmlDecoded())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
binding.itemImage.setImageDrawable(drawable)
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else {
c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
}
} else {
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage)
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage)
}
}
}

View File

@ -4,22 +4,20 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -30,10 +28,9 @@ import org.kodein.di.instance
class SourcesListAdapter(
private val app: Activity,
private val items: ArrayList<SelfossModel.Source>
private val items: ArrayList<SelfossModel.SourceDetail>
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL
private lateinit var binding: SourceListItemBinding
override val di: DI by closestDI(app)
@ -48,16 +45,16 @@ class SourcesListAdapter(
val itm = items[position]
if (itm.getIcon(repository.baseUrl).isEmpty()) {
val color = generator.getColor(itm.title.getHtmlDecoded())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
binding.itemImage.setImageDrawable(drawable)
binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
} else {
c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
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()

View File

@ -26,85 +26,89 @@ import org.kodein.di.instance
import java.util.*
import kotlin.concurrent.schedule
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params), DIAware {
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params),
DIAware {
override val di by lazy { (applicationContext as MyApp).di }
private val repository : Repository by instance()
private val appSettingsService : AppSettingsService by instance()
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
override fun doWork(): Result {
if (appSettingsService.isPeriodicRefreshEnabled() && isNetworkAccessible(context)) {
override fun doWork(): Result {
if (appSettingsService.isPeriodicRefreshEnabled() && isNetworkAccessible(context)) {
CoroutineScope(Dispatchers.IO).launch {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
CoroutineScope(Dispatchers.IO).launch {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification =
NotificationCompat.Builder(applicationContext, AppSettingsService.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(AppSettingsService.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
val notification =
NotificationCompat.Builder(applicationContext, AppSettingsService.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(AppSettingsService.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build())
notificationManager.notify(1, notification.build())
repository.handleDBActions()
repository.handleDBActions()
val apiItems = repository.tryToCacheItemsAndGetNewOnes()
if (appSettingsService.isNotifyNewItemsEnabled()) {
launch {
handleNewItemsNotification(apiItems, notificationManager)
val apiItems = repository.tryToCacheItemsAndGetNewOnes()
if (appSettingsService.isNotifyNewItemsEnabled()) {
launch {
handleNewItemsNotification(apiItems, notificationManager)
}
}
apiItems.map { it.preloadImages(context) }
}
apiItems.map { it.preloadImages(context) }
}
return Result.success()
}
return Result.success()
}
private fun handleNewItemsNotification(
newItems: List<SelfossModel.Item>?,
notificationManager: NotificationManager
) {
// TODO: Check if this coroutine is actually required
CoroutineScope(Dispatchers.IO).launch {
val apiItems = newItems.orEmpty()
val apiItems = newItems.orEmpty()
val newSize = apiItems.filter { it.unread }.size
if (newSize > 0) {
val newSize = apiItems.filter { it.unread }.size
if (newSize > 0) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, pflags)
val newItemsNotification =
NotificationCompat.Builder(applicationContext, AppSettingsService.newItemsChannelId)
.setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(
context.getString(
R.string.new_items_notification_text,
newSize
)
)
.setPriority(PRIORITY_DEFAULT)
.setChannelId(AppSettingsService.newItemsChannelId)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
Timer("", false).schedule(4000) {
notificationManager.notify(2, newItemsNotification.build())
}
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
val pendingIntent: PendingIntent =
PendingIntent.getActivity(context, 0, intent, pflags)
val newItemsNotification =
NotificationCompat.Builder(
applicationContext,
AppSettingsService.newItemsChannelId
)
.setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(
context.getString(
R.string.new_items_notification_text,
newSize
)
)
.setPriority(PRIORITY_DEFAULT)
.setChannelId(AppSettingsService.newItemsChannelId)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
Timer("", false).schedule(4000) {
notificationManager.notify(2, newItemsNotification.build())
}
}
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}

View File

@ -1,5 +1,6 @@
package bou.amine.apps.readerforselfossv2.android.fragments
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.TypedArray
@ -28,6 +29,7 @@ import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.MercuryModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
@ -55,6 +57,8 @@ import java.util.*
import java.util.concurrent.ExecutionException
private const val IMAGE_JPG = "image/jpg"
class ArticleFragment : Fragment(), DIAware {
private var fontSize: Int = 16
private lateinit var item: SelfossModel.Item
@ -63,13 +67,12 @@ class ArticleFragment : Fragment(), DIAware {
private lateinit var contentSource: String
private lateinit var contentImage: String
private lateinit var contentTitle: String
private lateinit var allImages : ArrayList<String>
private lateinit var allImages: ArrayList<String>
private lateinit var fab: FloatingActionButton
private lateinit var textAlignment: String
private var _binding: FragmentArticleBinding? = null
private val binding get() = _binding
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 appSettingsService: AppSettingsService by instance()
@ -78,7 +81,7 @@ class ArticleFragment : Fragment(), DIAware {
private var font = ""
private var staticBar = false
private val mercuryApi : MercuryApi by instance()
private val mercuryApi: MercuryApi by instance()
override fun onCreate(savedInstanceState: Bundle?) {
@ -95,7 +98,7 @@ class ArticleFragment : Fragment(), DIAware {
savedInstanceState: Bundle?
): View {
try {
_binding = FragmentArticleBinding.inflate(inflater, container, false)
binding = FragmentArticleBinding.inflate(inflater, container, false)
url = item.getLinkDecoded()
contentText = item.content
@ -110,89 +113,27 @@ class ArticleFragment : Fragment(), DIAware {
refreshAlignment()
fab = binding!!.fab
fab = binding.fab
fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
fab.rippleColor = resources.getColor(R.color.colorAccentDark)
val floatingToolbar: FloatingToolbar = binding!!.floatingToolbar
floatingToolbar.attachFab(fab)
floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent))
floatingToolbar.setClickListener(
object : FloatingToolbar.ItemClickListener {
override fun onItemClick(item: MenuItem) {
when (item.itemId) {
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
R.id.unread_action -> if (context != null) {
if (this@ArticleFragment.item.unread) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = false
Toast.makeText(
context,
R.string.marked_as_read,
Toast.LENGTH_LONG
).show()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = true
Toast.makeText(
context,
R.string.marked_as_unread,
Toast.LENGTH_LONG
).show()
}
}
else -> Unit
}
}
override fun onItemLongClick(item: MenuItem?) {
}
}
)
val floatingToolbar: FloatingToolbar = handleFloatingToolbar()
if (staticBar) {
fab.hide()
floatingToolbar.show()
}
binding!!.source.text = contentSource
binding.source.text = contentSource
if (typeface != null) {
binding!!.source.typeface = typeface
binding.source.typeface = typeface
}
if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury()
} else {
binding!!.titleView.text = contentTitle
if (typeface != null) {
binding!!.titleView.typeface = typeface
}
handleContent()
htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding!!.imageView.visibility = View.VISIBLE
Glide
.with(requireContext())
.asBitmap()
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding!!.imageView)
} else {
binding!!.imageView.visibility = View.GONE
}
}
binding!!.nestedScrollView.setOnScrollChangeListener(
binding.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
floatingToolbar.hide()
@ -209,24 +150,99 @@ class ArticleFragment : Fragment(), DIAware {
} catch (e: InflateException) {
e.sendSilentlyWithAcraWithName("webview not available")
AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton(android.R.string.ok
) { _, _ ->
appSettingsService.disableArticleViewer()
requireActivity().finish()
}
.create()
.show()
if (context != null) {
AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton(
android.R.string.ok
) { _, _ ->
appSettingsService.disableArticleViewer()
requireActivity().finish()
}
.create()
.show()
}
}
return binding!!.root
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
private fun handleContent() {
if (contentText.isEmptyOrNullOrNullString()) {
if (repository.isNetworkAvailable()) {
getContentFromMercury()
}
} else {
binding.titleView.text = contentTitle
if (typeface != null) {
binding.titleView.typeface = typeface
}
htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding.imageView.visibility = View.VISIBLE
Glide
.with(requireContext())
.asBitmap()
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else {
binding.imageView.visibility = View.GONE
}
}
}
private fun handleFloatingToolbar(): FloatingToolbar {
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
if (appSettingsService.getPublicAccess()) {
floatingToolbar.setMenu(R.menu.reader_toolbar_no_read)
}
floatingToolbar.attachFab(fab)
floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent))
floatingToolbar.setClickListener(
object : FloatingToolbar.ItemClickListener {
override fun onItemClick(item: MenuItem) {
when (item.itemId) {
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
R.id.unread_action -> if (context != null) {
if (this@ArticleFragment.item.unread) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = false
Toast.makeText(
context,
R.string.marked_as_read,
Toast.LENGTH_LONG
).show()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = true
Toast.makeText(
context,
R.string.marked_as_unread,
Toast.LENGTH_LONG
).show()
}
}
else -> Unit
}
}
override fun onItemLongClick(item: MenuItem?) {
// We do nothing
}
}
)
return floatingToolbar
}
private fun refreshAlignment() {
@ -238,181 +254,195 @@ class ArticleFragment : Fragment(), DIAware {
}
private fun getContentFromMercury() {
if (repository.isNetworkAvailable()) {
binding!!.progressBar.visibility = View.VISIBLE
binding.progressBar.visibility = View.VISIBLE
CoroutineScope(Dispatchers.Main).launch {
try {
val response = mercuryApi.query(url)
if (response.success && response.data != null && !response.data?.content.isNullOrEmpty()) {
binding!!.titleView.text = response.data!!.title.orEmpty()
try {
if (typeface != null) {
binding!!.titleView.typeface = typeface
}
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > typeface")
}
try {
// Note: Mercury may return relative urls... If it does the url val will not be changed.
URL(response.data!!.url)
url = response.data!!.url
} catch (e: MalformedURLException) {
// Mercury returned a relative url
e.sendSilentlyWithAcraWithName("getContentFromMercury > malformedurlexception")
}
try {
contentText = response.data!!.content.orEmpty()
htmlToWebview()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > contenttext or html")
}
if (!response.data?.lead_image_url.isNullOrEmpty() && context != null) {
try {
binding!!.imageView.visibility = View.VISIBLE
try {
Glide
.with(requireContext())
.asBitmap()
.load(
response.data!!.lead_image_url.orEmpty()
)
.apply(RequestOptions.fitCenterTransform())
.into(binding!!.imageView)
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > glide lead image")
}
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > outside glide lead image")
}
} else {
binding!!.imageView.visibility = View.GONE
}
try {
binding!!.nestedScrollView.scrollTo(0, 0)
binding!!.progressBar.visibility = View.GONE
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > scrollview")
}
} else {
openInBrowserAfterFailing()
}
} catch (e: SocketTimeoutException) {
openInBrowserAfterFailing()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > whole thing")
CoroutineScope(Dispatchers.Main).launch {
try {
val response = mercuryApi.query(url)
if (response.success && response.data != null) {
handleMercuryData(response.data!!)
} else {
openInBrowserAfterFailing()
}
} catch (e: SocketTimeoutException) {
openInBrowserAfterFailing()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > $url")
openInBrowserAfterFailing()
}
}
}
private fun htmlToWebview() {
private fun handleMercuryData(data: MercuryModel.ParsedContent) {
if (data.error == true || data.failed == true) {
openInBrowserAfterFailing()
} else {
binding.titleView.text = data.title.orEmpty()
if (typeface != null) {
binding.titleView.typeface = typeface
}
URL(data.url)
url = data.url!!
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
contentText = data.content.orEmpty()
htmlToWebview()
handleLeadImage(data?.lead_image_url)
binding!!.webcontent.settings.standardFontFamily = a.getString(0)
binding!!.webcontent.visibility = View.VISIBLE
binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE
}
}
val colorOnSurface = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
private fun handleLeadImage(lead_image_url: String?) {
if (!lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE
Glide
.with(requireContext())
.asBitmap()
.load(
lead_image_url
)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else {
binding.imageView.visibility = View.GONE
}
}
val colorSurface = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
binding!!.webcontent.settings.useWideViewPort = true
binding!!.webcontent.settings.loadWithOverviewMode = true
binding!!.webcontent.settings.javaScriptEnabled = false
binding!!.webcontent.webViewClient = object : WebViewClient() {
private fun handleImageLoading() {
binding.webcontent.webViewClient = object : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
if (binding!!.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean {
return if (context != null && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
try {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (e: ActivityNotFoundException) {
e.sendSilentlyWithAcraWithName("activityNotFound > $url")
}
true
} else {
false
}
return true
}
@Deprecated("Deprecated in Java")
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US).contains(".jpeg")) {
if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US)
.contains(".jpeg")
) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG))
} catch ( e : ExecutionException) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg > $url")
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)
)
} catch (e: ExecutionException) {
// Do nothing
}
}
else if (url.lowercase(Locale.US).contains(".png")) {
} 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) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > png > $url")
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")) {
} 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) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > webp > $url")
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)
}
}
}
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick()
}
})
private fun htmlToWebview() {
if (context != null) {
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
binding!!.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)}
binding!!.webcontent.settings.layoutAlgorithm =
binding.webcontent.settings.standardFontFamily = a.getString(0)
binding.webcontent.visibility = View.VISIBLE
val colorOnSurface = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
val colorSurface = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false
handleImageLoading()
val gestureDetector =
GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick()
}
})
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
binding.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
var baseUrl: String? = null
var baseUrl: String? = null
try {
val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) {
e.sendSilentlyWithAcraWithName("htmlToWebview > item url")
}
try {
val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) {
e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
}
val fontName = when (font) {
getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto"
getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
else -> ""
}
val fontName = when (font) {
getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto"
getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
else -> ""
}
val fontLinkAndStyle = if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${fontName.replace(" ", "+")}" rel="stylesheet">
val fontLinkAndStyle = if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${
fontName.replace(
" ",
"+"
)
}" rel="stylesheet">
|<style>
| * {
| font-family: '$fontName';
| }
|</style>
""".trimMargin()
} else {
""
}
} else {
""
}
binding!!.webcontent.loadDataWithBaseURL(
baseUrl,
"""<html>
binding.webcontent.loadDataWithBaseURL(
baseUrl,
"""<html>
|<head>
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <style>
@ -423,7 +453,12 @@ class ArticleFragment : Fragment(), DIAware {
| max-width: 100%;
| }
| a {
| color: ${String.format("#%06X", 0xFFFFFF and resources.getColor(R.color.colorAccent))} !important;
| color: ${
String.format(
"#%06X",
0xFFFFFF and resources.getColor(R.color.colorAccent)
)
} !important;
| }
| *:not(a) {
| color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)};
@ -434,11 +469,26 @@ class ArticleFragment : Fragment(), DIAware {
| word-break: break-word;
| overflow:hidden;
| line-height: 1.5em;
| background-color: ${String.format("#%06X", 0xFFFFFF and colorSurface.data)};
| background-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data
)
};
| }
| body, html {
| background-color: ${String.format("#%06X", 0xFFFFFF and colorSurface.data)} !important;
| border-color: ${String.format("#%06X", 0xFFFFFF and colorSurface.data)} !important;
| background-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data
)
} !important;
| border-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data
)
} !important;
| padding: 0 !important;
| margin: 0 !important;
| }
@ -448,7 +498,12 @@ class ArticleFragment : Fragment(), DIAware {
| pre, code {
| white-space: pre-wrap;
| width:100%;
| background-color: ${String.format("#%06X", 0xFFFFFF and colorSurface.data)};
| background-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data
)
};
| }
| </style>
| $fontLinkAndStyle
@ -456,32 +511,38 @@ class ArticleFragment : Fragment(), DIAware {
|<body>
| $contentText
|</body>""".trimMargin(),
"text/html",
"utf-8",
null
)
"text/html",
"utf-8",
null
)
}
}
fun scrollDown() {
val height = binding!!.nestedScrollView.measuredHeight
binding!!.nestedScrollView.smoothScrollBy(0, height/2)
val height = binding.nestedScrollView.measuredHeight
binding.nestedScrollView.smoothScrollBy(0, height / 2)
}
fun scrollUp() {
val height = binding!!.nestedScrollView.measuredHeight
binding!!.nestedScrollView.smoothScrollBy(0, -height/2)
val height = binding.nestedScrollView.measuredHeight
binding.nestedScrollView.smoothScrollBy(0, -height / 2)
}
private fun openInBrowserAfterFailing() {
binding!!.progressBar.visibility = View.GONE
requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
binding.progressBar.visibility = View.GONE
if (context != null) {
requireContext().openInBrowserAsNewTask(this@ArticleFragment.item)
} else {
Exception("openInBrowserAfterFailing context is null").sendSilentlyWithAcraWithName("openInBrowserAfterFailing > $context")
}
}
companion object {
private const val ARG_ITEMS = "items"
fun newInstance(
item: SelfossModel.Item
item: SelfossModel.Item
): ArticleFragment {
val fragment = ArticleFragment()
val args = Bundle()
@ -492,10 +553,11 @@ class ArticleFragment : Fragment(), DIAware {
}
fun performClick(): Boolean {
if (binding!!.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding!!.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) {
val position : Int = allImages.indexOf(binding!!.webcontent.hitTestResult.extra)
val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
val intent = Intent(activity, ImageActivity::class.java)
intent.putExtra("allImages", allImages)

View File

@ -4,7 +4,9 @@ import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
@ -12,15 +14,14 @@ import android.view.View.VISIBLE
import android.view.ViewGroup
import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.target.ViewTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.chip.Chip
import kotlinx.coroutines.CoroutineScope
@ -34,6 +35,7 @@ import org.kodein.di.instance
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
private lateinit var binding: FilterFragmentBinding
override val di: DI by closestDI()
private val repository: Repository by instance()
@ -44,8 +46,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding =
bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding.inflate(
binding =
FilterFragmentBinding.inflate(
inflater,
container,
false
@ -53,112 +55,14 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
val context: Context? = context
val tagGroup = binding.tagsGroup
val sourceGroup = binding.sourcesGroup
if (context == null) {
dismiss()
Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} else {
CoroutineScope(Dispatchers.Main).launch {
val tags = repository.getTags()
tags.forEach { tag ->
val c = Chip(context)
c.text = tag.tag
val gd = GradientDrawable()
val gdColor = try {
Color.parseColor(tag.color)
} catch (e: IllegalArgumentException) {
e.sendSilentlyWithAcraWithName("color issue " + tag.color)
resources.getColor(R.color.colorPrimary)
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
c.chipIcon = gd
c.setOnCloseIconClickListener {
(it as Chip).isCloseIconVisible = false
selectedChip = null
repository.setTagFilter(null)
}
c.setOnClickListener {
if (selectedChip != null) {
selectedChip!!.isCloseIconVisible = false
}
(it as Chip).isCloseIconVisible = true
selectedChip = it
repository.setTagFilter(tag)
repository.setSourceFilter(null)
}
if (repository.tagFilter.value?.equals(tag) == true) {
c.isCloseIconVisible = true
selectedChip = c
}
tagGroup.addView(c)
}
repository.getSources().forEach { source ->
val c = Chip(context)
Glide.with(context)
.load(source.getIcon(repository.baseUrl))
.listener(object : RequestListener<Drawable?> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable?>?,
isFirstResource: Boolean
): Boolean {
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable?>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
c.chipIcon = resource
return false
}
}).preload()
c.text = source.title.getHtmlDecoded()
c.setOnCloseIconClickListener {
(it as Chip).isCloseIconVisible = false
selectedChip = null
repository.setSourceFilter(null)
}
c.setOnClickListener {
if (selectedChip != null) {
selectedChip!!.isCloseIconVisible = false
}
(it as Chip).isCloseIconVisible = true
selectedChip = it
repository.setSourceFilter(source)
repository.setTagFilter(null)
}
if (repository.sourceFilter.value?.equals(source) == true) {
c.isCloseIconVisible = true
selectedChip = c
}
sourceGroup.addView(c)
}
handleTagChips(context)
handleSourceChips(context)
binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE
@ -173,6 +77,121 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
return binding.root
}
private suspend fun handleSourceChips(
context: Context
) {
val sourceGroup = binding.sourcesGroup
repository.getSourcesDetailsOrStats().forEach { source ->
val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END
Glide.with(context)
.load(source.getIcon(repository.baseUrl))
.into(object : ViewTarget<Chip?, Drawable?>(c) {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable?>?
) {
try {
c.chipIcon = resource
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("sources > onResourceReady")
}
}
})
c.text = source.title.getHtmlDecoded()
c.setOnCloseIconClickListener {
(it as Chip).isCloseIconVisible = false
selectedChip = null
repository.setSourceFilter(null)
}
c.setOnClickListener {
if (selectedChip != null) {
selectedChip!!.isCloseIconVisible = false
}
(it as Chip).isCloseIconVisible = true
selectedChip = it
repository.setSourceFilter(source)
repository.setTagFilter(null)
}
if (repository.sourceFilter.value?.equals(source) == true) {
c.isCloseIconVisible = true
selectedChip = c
}
c.isEnabled = source.error.isNullOrBlank()
if (!source.error.isNullOrBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
c.tooltipText = source.error
}
sourceGroup.addView(c)
}
}
private suspend fun handleTagChips(
context: Context,
) {
val tagGroup = binding.tagsGroup
val tags = repository.getTags()
tags.forEach { tag ->
val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END
c.text = tag.tag
try {
val gd = GradientDrawable()
val gdColor = try {
Color.parseColor(tag.color)
} catch (e: IllegalArgumentException) {
e.sendSilentlyWithAcraWithName("color issue " + tag.color)
resources.getColor(R.color.colorPrimary)
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
c.chipIcon = gd
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
}
c.setOnCloseIconClickListener {
(it as Chip).isCloseIconVisible = false
selectedChip = null
repository.setTagFilter(null)
}
c.setOnClickListener {
if (selectedChip != null) {
selectedChip!!.isCloseIconVisible = false
}
(it as Chip).isCloseIconVisible = true
selectedChip = it
repository.setTagFilter(tag)
repository.setSourceFilter(null)
}
if (repository.tagFilter.value?.equals(tag) == true) {
c.isCloseIconVisible = true
selectedChip = c
}
tagGroup.addView(c)
}
}
companion object {
const val TAG = "FilterModalBottomSheet"
}

View File

@ -8,7 +8,6 @@ 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
import org.acra.ktx.sendSilentlyWithAcra
fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages()

View File

@ -21,7 +21,6 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.mikepenz.aboutlibraries.LibsBuilder
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
private const val TITLE_TAG = "settingsActivityTitle"
@ -146,8 +145,12 @@ class SettingsActivity : AppCompatActivity(),
fontSize?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.addTextChangedListener { object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
// We do nothing
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
// We do nothing
}
override fun afterTextChanged(editable: Editable) {
try {
editText.textSize = editable.toString().toInt().toFloat()

View File

@ -0,0 +1,62 @@
package bou.amine.apps.readerforselfossv2.android.utils
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.RelativeLayout
import android.widget.TextView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
import com.google.android.material.imageview.ShapeableImageView
import kotlin.math.abs
class CircleImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
val view: View
val imageView: ShapeableImageView
val textView: TextView
private val colorScheme = listOf(
-0x1a8c8d,
-0xf9d6e,
-0x459738,
-0x6a8a33,
-0x867935,
-0x9b4a0a,
-0xb03c09,
-0xb22f1f,
-0xb24954,
-0x7e387c,
-0x512a7f,
-0x759b,
-0x2b1ea9,
-0x2ab1,
-0x48b3,
-0x5e7781,
-0x6f5b52
)
init {
view = LayoutInflater.from(context).inflate(R.layout.circle_image_view, this, true)
imageView = view.findViewById(R.id.circleImage)
textView = view.findViewById(R.id.circleText)
}
fun setBackgroundAndText(text: String) {
val circleDrawable = GradientDrawable()
val color = colorFromIdentifier(text)
circleDrawable.setColor(color)
imageView.setImageDrawable(circleDrawable)
textView.text = text.toTextDrawableString()
}
private fun colorFromIdentifier(key: String): Int {
return colorScheme[abs(key.hashCode()) % colorScheme.size]
}
}

View File

@ -3,10 +3,9 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide
import android.content.Context
import android.graphics.Bitmap
import android.widget.ImageView
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.BitmapImageViewTarget
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
@ -18,21 +17,13 @@ fun Context.bitmapCenterCrop(url: String, iv: ImageView) =
.apply(RequestOptions.centerCropTransform())
.into(iv)
fun Context.circularBitmapDrawable(url: String, iv: ImageView) =
fun Context.circularDrawable(url: String, view: CircleImageView) {
view.textView.text =""
Glide.with(this)
.asBitmap()
.load(url)
.apply(RequestOptions.centerCropTransform())
.into(object : BitmapImageViewTarget(iv) {
override fun setResource(resource: Bitmap?) {
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(
resources,
resource
)
circularBitmapDrawable.isCircular = true
iv.setImageDrawable(circularBitmapDrawable)
}
})
.into(view.imageView)
}
fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream {
val byteArrayOutputStream = ByteArrayOutputStream()

View File

@ -1,13 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawerContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="bou.amine.apps.readerforselfossv2.android.HomeActivity"
android:fitsSystemWindows="true"
xmlns:app="http://schemas.android.com/apk/res-auto">
tools:context="bou.amine.apps.readerforselfossv2.android.HomeActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordLayout"
@ -28,12 +27,14 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar app:popupTheme="?attr/toolbarPopupTheme" android:theme="@style/ToolBarStyle"
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme"
/>
/>
</com.google.android.material.appbar.AppBarLayout>
@ -45,19 +46,19 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/windowBackground">
android:background="?android:attr/windowBackground"
android:orientation="vertical">
<TextView
android:id="@+id/emptyText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:gravity="center_horizontal"
android:paddingTop="100dp"
android:text="@string/nothing_here"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:background="@android:color/transparent"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
@ -69,7 +70,7 @@
android:paddingBottom="60dp"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/list_item"/>
tools:listitem="@layout/list_item" />
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@ -77,6 +78,7 @@
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.ashokvarma.bottomnavigation.BottomNavigationBar
android:id="@+id/bottomBar"
android:layout_width="match_parent"

View File

@ -1,33 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
app:layoutDescription="@xml/image_close_scene">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
<androidx.appcompat.widget.Toolbar android:theme="@style/ToolBarStyle"
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
/>
/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appBarLayout" />
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_constraintTop_toBottomOf="@+id/appBarLayout">
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.motion.widget.MotionLayout>

View File

@ -1,31 +1,30 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context="bou.amine.apps.readerforselfossv2.android.LoginActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar app:popupTheme="?attr/toolbarPopupTheme" android:theme="@style/ToolBarStyle"
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
/>
android:theme="@style/ToolBarStyle"
app:popupTheme="?attr/toolbarPopupTheme" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
android:padding="@dimen/activity_horizontal_margin">
<!-- Login progress -->
<ProgressBar
android:id="@+id/loginProgress"
@ -33,67 +32,65 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"/>
android:visibility="gone" />
<ScrollView
<LinearLayout
android:id="@+id/loginForm"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
<EditText
android:id="@+id/urlView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:hint="@string/prompt_url"
android:imeOptions="actionUnspecified"
android:importantForAutofill="no"
android:inputType="textUri"
android:maxLines="1"
android:minHeight="48dp" />
<EditText
android:id="@+id/urlView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/prompt_url"
android:imeOptions="actionUnspecified"
android:importantForAutofill="no"
android:inputType="textUri"
android:maxLines="1" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/withLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/withLoginSwitch"
android:textAlignment="viewStart" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:text="@string/withLoginSwitch"
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/withLogin"
android:layout_weight="1"/>
<EditText
android:id="@+id/loginView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="username"
android:hint="@string/prompt_login"
android:inputType="text"
android:maxLines="1"
android:minHeight="48dp"
android:visibility="gone" />
<EditText
android:id="@+id/loginView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="username"
android:hint="@string/prompt_login"
android:inputType="text"
android:maxLines="1"
android:visibility="gone" />
<EditText
android:id="@+id/passwordView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="password"
android:hint="@string/prompt_password"
android:inputType="textPassword"
android:maxLines="1"
android:minHeight="48dp"
android:visibility="gone" />
<EditText
android:id="@+id/passwordView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="password"
android:hint="@string/prompt_password"
android:inputType="textPassword"
android:maxLines="1"
android:visibility="gone" />
<Button
android:id="@+id/signInButton"
style="?android:textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@string/action_sign_in"
android:textStyle="bold" />
<Button
android:id="@+id/signInButton"
style="?android:textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@string/action_sign_in"
android:textStyle="bold" />
</LinearLayout>
</ScrollView>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -24,7 +24,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/source_list_item">
</androidx.recyclerview.widget.RecyclerView>
<com.google.android.material.floatingactionbutton.FloatingActionButton

View File

@ -17,100 +17,83 @@
<androidx.appcompat.widget.Toolbar app:popupTheme="?attr/toolbarPopupTheme" android:theme="@style/ToolBarStyle"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
/>
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:layout_height="match_parent"
android:layout_width="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/formContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:visibility="gone"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintVertical_bias="0.0">
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<EditText
android:id="@+id/nameInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginTop="16dp"
android:autofillHints="false"
android:ems="10"
android:hint="@string/add_source_hint_name"
android:inputType="text"
android:textColorHint="?android:textColorPrimary"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/sourceUri"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:ems="10"
android:id="@+id/sourceUri"
android:hint="@string/add_source_hint_url"
android:textColorHint="?android:textColorPrimary"
android:minHeight="48dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/nameInput"
app:layout_constraintRight_toRightOf="parent"
android:autofillHints="false"
android:hint="@string/add_source_hint_url"
android:inputType="textUri"
android:textColorHint="?android:textColorPrimary"
app:layout_constraintLeft_toLeftOf="parent"
android:autofillHints="false" />
app:layout_constraintTop_toBottomOf="@+id/nameInput" />
<EditText
android:id="@+id/tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:id="@+id/tags"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:minHeight="48dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/sourceUri"
android:autofillHints="false"
android:hint="@string/add_source_hint_tags"
android:textColorHint="?android:textColorPrimary"
android:inputType="text"
android:autofillHints="false" />
android:textColorHint="?android:textColorPrimary"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sourceUri" />
<Spinner
android:layout_width="match_parent"
android:id="@+id/spoutsSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/tags"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_height="40dp"/>
app:layout_constraintTop_toBottomOf="@+id/tags" />
<Button
android:text="@string/add_source_save"
android:id="@+id/saveBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/saveBtn"
android:elevation="5dp"
android:textColor="?android:textColorPrimary"
android:layout_marginEnd="16dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/spoutsSpinner"
android:elevation="5dp"
android:text="@string/add_source_save"
android:textColor="?android:textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="16dp"
app:layout_constraintVertical_bias="0.0"/>
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/spoutsSpinner" />
</androidx.constraintlayout.widget.ConstraintLayout>
@ -119,8 +102,6 @@
style="?android:attr/progressBarStyleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"

View File

@ -1,18 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
app:layout_constraintHorizontal_bias="0.62"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
card_view:cardElevation="2dp"
card_view:cardUseCompatPadding="true"
@ -28,8 +24,8 @@
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:cropToPadding="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/background_splash"
card_view:layout_constraintBottom_toTopOf="@+id/constraintLayout" />
@ -39,18 +35,17 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/itemImage">
<ImageView
<bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
android:id="@+id/sourceImage"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/background_splash" />
@ -58,70 +53,58 @@
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:gravity="start"
android:layout_margin="8dp"
android:textAlignment="viewStart"
android:textStyle="bold"
android:textColor="?android:textColorPrimary"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toRightOf="@+id/sourceImage"
app:layout_constraintRight_toRightOf="parent"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/sourceImage"
app:layout_constraintTop_toTopOf="@+id/sourceImage"
tools:text="Titre" />
<TextView
android:id="@+id/sourceTitleAndDate"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="start"
android:textAlignment="viewStart"
android:textSize="14sp"
android:textColor="?android:textColorPrimary"
app:layout_constraintLeft_toLeftOf="@+id/title"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="Google Actualité Il y a 5h" />
<RelativeLayout
android:layout_width="0dp"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sourceTitleAndDate">
<ImageButton
android:id="@+id/favButton"
android:id="@+id/browserBtn"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:adjustViewBounds="true"
android:background="@android:color/transparent"
android:contentDescription="@string/reader_action_open"
android:elevation="5dp"
android:padding="4dp"
android:scaleType="centerCrop"
app:srcCompat="@drawable/ic_menu_heart_60dp"
app:tint="@color/ic_menu_heart_color" />
app:srcCompat="@drawable/ic_open_in_browser_black_24dp"
app:tint="?android:attr/textColorPrimary" />
<ImageButton
android:id="@+id/shareBtn"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_toLeftOf="@+id/favButton"
android:layout_toStartOf="@+id/favButton"
android:layout_marginStart="16dp"
android:adjustViewBounds="true"
android:background="@android:color/transparent"
android:contentDescription="@string/share"
android:elevation="5dp"
android:padding="4dp"
android:scaleType="centerCrop"
@ -129,23 +112,21 @@
app:tint="?android:attr/textColorPrimary" />
<ImageButton
android:id="@+id/browserBtn"
android:id="@+id/favButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_toLeftOf="@+id/shareBtn"
android:layout_toStartOf="@+id/shareBtn"
android:layout_marginStart="16dp"
android:adjustViewBounds="true"
android:background="@android:color/transparent"
android:contentDescription="@string/add_to_favs_reader"
android:elevation="5dp"
android:padding="4dp"
android:scaleType="centerCrop"
app:srcCompat="@drawable/ic_open_in_browser_black_24dp"
app:tint="?android:attr/textColorPrimary" />
app:srcCompat="@drawable/ic_menu_heart_60dp"
app:tint="@color/ic_menu_heart_color" />
</RelativeLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/circleImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/circleImageView"
app:srcCompat="@drawable/background_splash" />
<TextView
android:id="@+id/circleText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="none"
android:gravity="center"
android:singleLine="true"
android:textColor="@color/white"
android:textIsSelectable="false"
android:textSize="20sp"
android:typeface="normal" />
</RelativeLayout>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -17,73 +16,81 @@
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/filterView"
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible">
android:fillViewport="true">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:clickable="true"
app:backgroundTint="@color/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/colorAccentDark"
app:srcCompat="@drawable/ic_menu_search_white_24dp" />
<TextView
android:id="@+id/filterTagsTitle"
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:text="@string/filter_item_tags"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/tagsGroup"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/filterView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filterTagsTitle"
app:singleSelection="true">
android:visibility="gone"
tools:visibility="visible">
</com.google.android.material.chip.ChipGroup>
<TextView
android:id="@+id/filterTagsTitle"
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:text="@string/filter_item_tags"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/filterSourcesTitle"
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:text="@string/filter_item_sources"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tagsGroup" />
<TextView
android:id="@+id/filterSourcesTitle"
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:text="@string/filter_item_sources"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tagsGroup" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/sourcesGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filterSourcesTitle">
<com.google.android.material.chip.ChipGroup
android:id="@+id/tagsGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filterTagsTitle"
app:singleSelection="true">
</com.google.android.material.chip.ChipGroup>
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.chip.ChipGroup
android:id="@+id/sourcesGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filterSourcesTitle">
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:clickable="true"
android:contentDescription="@string/menu_home_search"
android:focusable="true"
app:backgroundTint="@color/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/colorAccentDark"
app:srcCompat="@drawable/ic_menu_search_white_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,5 +1,4 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -22,10 +21,22 @@
android:layout_width="0dp"
android:layout_height="200dp"
android:scaleType="centerCrop"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/titleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<TextView
android:id="@+id/source"
@ -36,40 +47,23 @@
android:layout_marginRight="16dp"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleView" />
<TextView
android:id="@+id/titleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:textStyle="bold"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<WebView
android:id="@+id/webcontent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="24dp"
android:layout_marginRight="16dp"
android:background="?attr/webviewBackground"
android:paddingBottom="48dp"
android:textColorLink="?attr/colorAccent"
android:visibility="gone"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="24dp"
android:paddingBottom="48dp"
android:background="?attr/webviewBackground"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/source"
tools:visibility="visible" />
@ -80,10 +74,10 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom|end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_gravity="end|bottom|right">
app:layout_constraintStart_toStartOf="parent">
<com.github.rubensousa.floatingtoolbar.FloatingToolbar
android:id="@+id/floatingToolbar"
@ -96,12 +90,11 @@
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom|right"
android:layout_marginBottom="16dp"
android:layout_gravity="end|bottom"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingBottom="@dimen/activity_vertical_margin"
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"
@ -112,11 +105,11 @@
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:animateLayoutChanges="true"
android:alpha="0.8"
android:animateLayoutChanges="true"
android:background="@color/black"
android:clickable="false">
android:clickable="false"
android:visibility="gone">
<ProgressBar
style="?android:attr/progressBarStyleLarge"

View File

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/photoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:adjustViewBounds="true"
android:background="@drawable/checkerboard"
app:srcCompat="@android:drawable/screen_background_dark" />
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/photoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:adjustViewBounds="true"
android:background="@drawable/checkerboard"
app:srcCompat="@android:drawable/screen_background_dark" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,17 +3,16 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="88dp">
android:layout_height="wrap_content">
<ImageView
<bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
android:id="@+id/itemImage"
android:layout_width="46dp"
android:layout_height="46dp"
android:layout_marginStart="8dp"
android:layout_marginTop="21dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginLeft="8dp" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
@ -24,39 +23,30 @@
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:fontFamily="sans-serif"
android:gravity="start"
android:maxLines="3"
android:textAlignment="viewStart"
android:textAllCaps="false"
android:textColor="?android:textColorPrimary"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:textColorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/itemImage"
app:layout_constraintTop_toTopOf="parent"
tools:text="Titre"
android:layout_marginLeft="8dp"
android:layout_marginRight="16dp" />
tools:text="Titre" />
<TextView
android:id="@+id/sourceTitleAndDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="66dp"
android:layout_marginEnd="16dp"
android:gravity="start"
android:maxLines="1"
android:textAlignment="viewStart"
android:textSize="14sp"
android:textColor="?android:textColorPrimary"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/itemImage"
app:layout_constraintTop_toTopOf="parent"
tools:text="Google Actualité Il y a 5h"
android:layout_marginLeft="8dp"
android:layout_marginRight="16dp" />
app:layout_constraintTop_toBottomOf="@+id/itemImage"
tools:text="Google Actualité Il y a 5h" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,48 +3,74 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/itemImage"
android:layout_width="36dp"
android:layout_height="36dp"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/sourceTitle"
android:layout_width="0dp"
android:layout_height="17dp"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:gravity="start"
android:maxLines="1"
android:textAlignment="textStart"
android:textSize="13sp"
android:textColor="?android:textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/deleteBtn"
app:layout_constraintStart_toEndOf="@+id/itemImage"
app:layout_constraintTop_toTopOf="parent"
tools:text="source title" />
<Button
android:id="@+id/deleteBtn"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_width="48dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/ic_remove_circle_outline_black_24dp"
android:backgroundTint="?android:textColorSecondary"
android:elevation="4dp"
android:contentDescription="@string/remove_source"
android:elevation="4dp"
app:iconSize="34dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
android:id="@+id/itemImage"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/sourceTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewStart"
android:textColor="?android:textColorPrimary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/errorText"
app:layout_constraintEnd_toStartOf="@+id/deleteBtn"
app:layout_constraintStart_toEndOf="@+id/itemImage"
app:layout_constraintTop_toTopOf="parent"
tools:text="Source title" />
<TextView
android:id="@+id/errorText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10sp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="@color/red"
android:textStyle="italic"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/deleteBtn"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/itemImage"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,16 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/open_action"
android:icon="@drawable/ic_open_in_browser_white_24dp"
android:title="@string/reader_action_open"
app:showAsAction="ifRoom" />
<item
android:id="@+id/share_action"
android:icon="@drawable/ic_share_white_24dp"
android:title="@string/reader_action_share"
app:showAsAction="ifRoom" />
</menu>

View File

@ -12,4 +12,5 @@
<color name="refresh_progress_2">@color/colorAccent</color>
<color name="refresh_progress_3">@color/pink</color>
<color name="dark">#FF282828</color>
<color name="transparent_dark_background">#33000000</color>
</resources>

View File

@ -26,4 +26,16 @@
<item name="android:textColorSecondary">@color/white</item>
<item name="actionMenuTextColor">@color/white</item>
</style>
<style name="Theme.AppCompat.ImageActivity" parent="NoBar">
<item name="android:windowBackground">@color/transparent_dark_background</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowIsTranslucent">true</item>
</style>
<style name="circleImageView" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item>
</style>
</resources>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto"
android:id="@+id/motionLayout">
<Transition
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end"
motion:duration="500"
motion:motionInterpolator="linear">
<OnSwipe
motion:touchAnchorId="@+id/scrollView"
motion:touchAnchorSide="top"
motion:onTouchUp="autoCompleteToStart" />
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@id/scrollView"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintTop_toBottomOf="@+id/appBarLayout"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent">
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/scrollView"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginBottom="0dp"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent">
</Constraint>
</ConstraintSet>
</MotionScene>

View File

@ -18,6 +18,20 @@ import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Test
private const val BASE_URL = "https://test.com/selfoss/"
private const val USERNAME = "username"
private const val SPOUT = "spouts\\rss\\fulltextrss"
private const val IMAGE_URL = "b3aa8a664d08eb15d6ff1db2fa83e0d9.png"
private const val IMAGE_URL_2 = "d8c92cdb1ef119ea85c4b9205c879ca7.png"
private const val FEED_URL = "https://test.com/feed"
private const val TAGS = "Test, New"
class RepositoryTest {
private val db = mockk<ReaderForSelfossDB>(relaxed = true)
private val appSettingsService = mockk<AppSettingsService>()
@ -37,7 +51,7 @@ class RepositoryTest {
repository = Repository(api, appSettingsService, isConnectionAvailable, db)
runBlocking {
repository.updateApiVersion()
repository.updateApiInformation()
}
}
@ -45,13 +59,14 @@ class RepositoryTest {
fun setup() {
clearAllMocks()
every { appSettingsService.getApiVersion() } returns 4
every { appSettingsService.getBaseUrl() } returns "https://test.com/selfoss/"
every { appSettingsService.getBaseUrl() } returns BASE_URL
every { appSettingsService.getUserName() } returns USERNAME
every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.version() } returns StatusAndData(
coEvery { api.apiInformation() } returns StatusAndData(
success = true,
data = SelfossModel.ApiVersion("2.19-ba1e8e3", "4.0.0")
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true))
)
coEvery { api.stats() } returns StatusAndData(
success = true,
@ -69,7 +84,7 @@ class RepositoryTest {
fun instantiate_repository() {
initializeRepository()
coVerify(exactly = 1) { api.version() }
coVerify(exactly = 1) { api.apiInformation() }
}
@Test
@ -78,7 +93,7 @@ class RepositoryTest {
initializeRepository(MutableStateFlow(false))
coVerify(exactly = 0) { api.version() }
coVerify(exactly = 0) { api.apiInformation() }
coVerify(exactly = 0) { api.stats() }
}
@ -98,10 +113,70 @@ class RepositoryTest {
verify(exactly = 1) { appSettingsService.updateApiVersion(4) }
}
@Test
fun get_public_access() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true))
)
every { appSettingsService.getUserName() } returns ""
initializeRepository()
coVerify(exactly = 1) { api.apiInformation() }
coVerify(exactly = 1) { appSettingsService.updatePublicAccess(true) }
}
@Test
fun get_public_access_username_not_empty() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true))
)
every { appSettingsService.getUserName() } returns "username"
initializeRepository()
coVerify(exactly = 1) { api.apiInformation() }
coVerify(exactly = 0) { appSettingsService.updatePublicAccess(true) }
}
@Test
fun get_public_access_no_auth() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, false))
)
every { appSettingsService.getUserName() } returns ""
initializeRepository()
coVerify(exactly = 1) { api.apiInformation() }
coVerify(exactly = 0) { appSettingsService.updatePublicAccess(true) }
}
@Test
fun get_public_access_disabled() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true))
)
every { appSettingsService.getUserName() } returns ""
initializeRepository()
coVerify(exactly = 1) { api.apiInformation() }
coVerify(exactly = 0) { appSettingsService.updatePublicAccess(true) }
}
@Test
fun get_api_1_date_with_api_4_version_stored() {
every { appSettingsService.getApiVersion() } returns 4
coEvery { api.version() } returns StatusAndData(success = false, null)
coEvery { api.apiInformation() } returns StatusAndData(success = false, null)
val itemParameters = FakeItemParameters()
itemParameters.datetime = "2021-04-23 11:45:32"
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
@ -225,13 +300,14 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(MutableStateFlow(false))
repository.setSourceFilter(SelfossModel.Source(
repository.setSourceFilter(SelfossModel.SourceDetail(
1,
"Test",
null,
listOf("tags"),
"spouts\\rss\\fulltextrss",
SPOUT,
"",
"b3aa8a664d08eb15d6ff1db2fa83e0d9.png",
IMAGE_URL,
SelfossModel.SourceParams("url")
))
runBlocking {
@ -534,34 +610,36 @@ class RepositoryTest {
fun get_sources() {
val (sources, sourcesDB) = prepareSources()
initializeRepository()
var testSources: List<SelfossModel.Source>?
var testSources: List<SelfossModel.Source>
runBlocking {
testSources = repository.getSources()
testSources = repository.getSourcesDetails()
}
assertSame(sources, testSources)
assertEquals(sources, testSources)
assertNotEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 1) { api.sources() }
coVerify(exactly = 1) { api.sourcesDetailed() }
}
private fun prepareSources(): Pair<ArrayList<SelfossModel.Source>, List<SOURCE>> {
private fun prepareSources(): Pair<ArrayList<SelfossModel.SourceDetail>, List<SOURCE>> {
val sources = arrayListOf(
SelfossModel.Source(
SelfossModel.SourceDetail(
1,
"First source",
null,
listOf("Test", "second"),
"spouts\\rss\\fulltextrss",
SPOUT,
"",
"d8c92cdb1ef119ea85c4b9205c879ca7.png",
IMAGE_URL_2,
SelfossModel.SourceParams("url")
),
SelfossModel.Source(
SelfossModel.SourceDetail(
2,
"Second source",
null,
listOf("second"),
"spouts\\rss\\fulltextrss",
SPOUT,
"",
"b3aa8a664d08eb15d6ff1db2fa83e0d9.png",
IMAGE_URL,
SelfossModel.SourceParams("url")
)
)
@ -570,21 +648,23 @@ class RepositoryTest {
"1",
"First DB source",
"Test,second",
"spouts\\rss\\fulltextrss",
SPOUT,
"",
"d8c92cdb1ef119ea85c4b9205c879ca7.png"
IMAGE_URL_2,
"url"
),
SOURCE(
"2",
"Second source",
"second",
"spouts\\rss\\fulltextrss",
SPOUT,
"",
"b3aa8a664d08eb15d6ff1db2fa83e0d9.png"
IMAGE_URL,
"url"
)
)
coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
return Pair(sources, sourcesDB)
}
@ -598,13 +678,13 @@ class RepositoryTest {
initializeRepository()
var testSources: List<SelfossModel.Source>?
runBlocking {
testSources = repository.getSources()
testSources = repository.getSourcesDetails()
// Sources will be fetched from the database on the second call, thus testSources != sources
testSources = repository.getSources()
testSources = repository.getSourcesDetails()
}
coVerify(exactly = 1) { api.sources() }
assertNotSame(sources, testSources)
coVerify(exactly = 1) { api.sourcesDetailed() }
assertNotEquals(sources, testSources)
assertEquals(sourcesDB.map { it.toView() }, testSources)
verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
}
@ -616,13 +696,13 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns false
initializeRepository()
var testSources: List<SelfossModel.Source>?
var testSources: List<SelfossModel.Source>
runBlocking {
testSources = repository.getSources()
testSources = repository.getSourcesDetails()
}
assertSame(sources, testSources)
coVerify(exactly = 1) { api.sources() }
assertEquals(sources, testSources)
coVerify(exactly = 1) { api.sourcesDetailed() }
verify(exactly = 0) { db.sourcesQueries }
}
@ -633,13 +713,13 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns false
initializeRepository()
var testSources: List<SelfossModel.Source>?
var testSources: List<SelfossModel.Source>
runBlocking {
testSources = repository.getSources()
testSources = repository.getSourcesDetails()
}
assertSame(sources, testSources)
coVerify(exactly = 1) { api.sources() }
assertEquals(sources, testSources)
coVerify(exactly = 1) { api.sourcesDetailed() }
verify(atLeast = 1) { db.sourcesQueries }
}
@ -647,13 +727,13 @@ class RepositoryTest {
fun get_sources_without_connection() {
val (_, sourcesDB) = prepareSources()
initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>?
var testSources: List<SelfossModel.Source>
runBlocking {
testSources = repository.getSources()
testSources = repository.getSourcesDetails()
}
assertEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 0) { api.sources() }
coVerify(exactly = 0) { api.sourcesDetailed() }
verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
}
@ -664,13 +744,13 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns true
initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>?
var testSources: List<SelfossModel.Source>
runBlocking {
testSources = repository.getSources()
testSources = repository.getSourcesDetails()
}
assertEquals(emptyList<SelfossModel.Source>(), testSources)
coVerify(exactly = 0) { api.sources() }
coVerify(exactly = 0) { api.sourcesDetailed() }
verify(exactly = 0) { db.sourcesQueries.sources().executeAsList() }
}
@ -681,13 +761,13 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true
every { appSettingsService.isUpdateSourcesEnabled() } returns false
initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>?
var testSources: List<SelfossModel.Source>
runBlocking {
testSources = repository.getSources()
testSources = repository.getSourcesDetails()
}
assertEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 0) { api.sources() }
coVerify(exactly = 0) { api.sourcesDetailed() }
verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
}
@ -698,13 +778,13 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false
initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source>?
var testSources: List<SelfossModel.Source>
runBlocking {
testSources = repository.getSources()
testSources = repository.getSourcesDetails()
}
assertEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 0) { api.sources() }
coVerify(exactly = 0) { api.sourcesDetailed() }
verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
}
@ -718,9 +798,9 @@ class RepositoryTest {
runBlocking {
response = repository.createSource(
"test",
"https://test.com/feed",
"spouts\\rss\\fulltextrss",
"Test, New",
FEED_URL,
SPOUT,
TAGS,
)
}
@ -745,9 +825,9 @@ class RepositoryTest {
runBlocking {
response = repository.createSource(
"test",
"https://test.com/feed",
"spouts\\rss\\fulltextrss",
"Test, New"
FEED_URL,
SPOUT,
TAGS
)
}
@ -772,9 +852,9 @@ class RepositoryTest {
runBlocking {
response = repository.createSource(
"test",
"https://test.com/feed",
"spouts\\rss\\fulltextrss",
"Test, New"
FEED_URL,
SPOUT,
TAGS
)
}
@ -950,12 +1030,12 @@ class RepositoryTest {
coEvery { appSettingsService.refreshLoginInformation(any(), any(), any()) } returns Unit
initializeRepository()
repository.refreshLoginInformation("https://test.com/selfoss/", "login", "password")
repository.refreshLoginInformation(BASE_URL, "login", "password")
coVerify(exactly = 1) { api.refreshLoginInformation() }
coVerify(exactly = 1) {
appSettingsService.refreshLoginInformation(
"https://test.com/selfoss/",
BASE_URL,
"login",
"password"
)
@ -1025,13 +1105,14 @@ class RepositoryTest {
private fun prepareSearch() {
repository.setTagFilter(SelfossModel.Tag("Tag", "read", 0))
repository.setSourceFilter(
SelfossModel.Source(
SelfossModel.SourceDetail(
1,
"First source",
5,
listOf("Test", "second"),
"spouts\\rss\\fulltextrss",
SPOUT,
"",
"d8c92cdb1ef119ea85c4b9205c879ca7.png",
IMAGE_URL_2,
SelfossModel.SourceParams("url")
)
)

View File

@ -7,8 +7,8 @@ buildscript {
plugins {
//trick: for the same plugin versions in all sub-modules
id("com.android.application").version("7.3.1").apply(false)
id("com.android.library").version("7.3.1").apply(false)
id("com.android.application").version("7.4.0").apply(false)
id("com.android.library").version("7.4.0").apply(false)
kotlin("android").version("1.7.20").apply(false)
kotlin("multiplatform").version("1.7.20").apply(false)
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
@ -17,12 +17,11 @@ plugins {
allprojects {
repositories {
maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
// IMPORTANT : Add back when new library added
// google()
// mavenCentral()
// jcenter()
// maven { url = uri("https://www.jitpack.io") }
// maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
google()
mavenCentral()
jcenter()
maven { url = uri("https://www.jitpack.io") }
}
}

View File

@ -1,6 +1,6 @@
#Wed Feb 09 17:05:19 CET 2022
#Mon Jan 23 20:47:46 CET 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

@ -2,20 +2,18 @@ val pushCache: String by settings
pluginManagement {
repositories {
maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
// IMPORTANT : Add back when new plugin added
// google()
// gradlePluginPortal()
// mavenCentral()
// maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
google()
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
// IMPORTANT : Add back when new library added
// google()
// mavenCentral()
// maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
google()
mavenCentral()
}
}

View File

@ -9,7 +9,6 @@ import org.kodein.di.instance
import org.kodein.di.singleton
val networkModule by DI.Module {
bind<AppSettingsService>() with singleton { AppSettingsService() }
bind<SelfossApi>() with singleton { SelfossApi(instance()) }
bind<MercuryApi>() with singleton { MercuryApi() }
}

View File

@ -6,9 +6,12 @@ class MercuryModel {
@Serializable
class ParsedContent(
val title: String,
val title: String?,
val content: String?,
val lead_image_url: String?,
val url: String
val lead_image_url: String?, // NOSONAR
val url: String?,
val error: Boolean?,
val message: String?,
val failed: Boolean?
)
}

View File

@ -24,8 +24,8 @@ class SelfossModel {
@Serializable
class Stats(
val total: Int,
val unread: Int,
val starred: Int
val unread: Int?,
val starred: Int?
)
@Serializable
@ -35,33 +35,67 @@ class SelfossModel {
)
@Serializable
data class ApiVersion(
data class ApiInformation(
val version: String?,
val apiversion: String?
val apiversion: String?,
val configuration: ApiConfiguration?
) {
fun getApiMajorVersion() : Int {
fun getApiMajorVersion(): Int {
var versionNumber = 0
if (apiversion != null) {
versionNumber = apiversion.substringBefore(".").toInt()
}
return versionNumber
}
fun getApiConfiguration() = configuration ?: ApiConfiguration(null, null)
}
@Serializable
data class Source(
val id: Int,
val title: String,
data class ApiConfiguration(
@Serializable(with = BooleanSerializer::class)
val publicMode: Boolean?,
@Serializable(with = BooleanSerializer::class)
val authEnabled: Boolean?
) {
fun isAuthEnabled() = authEnabled ?: true
fun isPublicModeEnabled() = publicMode ?: false
}
interface Source {
val id: Int
var title: String
var unread: Int?
var error: String?
var icon: String?
}
@Serializable
data class SourceStats(
override val id: Int,
override var title: String,
override var unread: Int?,
override var error: String? = null,
override var icon: String? = null
) : Source
@Serializable
data class SourceDetail(
override val id: Int,
override var title: String,
override var unread: Int? = null,
@Serializable(with = TagsListSerializer::class)
val tags: List<String>,
val spout: String,
val error: String,
val icon: String?,
val params: SourceParams?
)
var tags: List<String>?,
var spout: String?,
override var error: String?,
override var icon: String?,
var params: SourceParams?
) : Source
@Serializable
data class SourceParams(
val url: String
val url: String? = null
)
@Serializable
data class Item(
@ -81,19 +115,13 @@ class SelfossModel {
val tags: List<String>,
val author: String?
) {
// TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String {
var stringUrl: String
stringUrl =
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
link.substringAfter("&amp;url=")
} else {
this.link.replace("&amp;", "&")
}
} else {
this.link.replace("&amp;", "&")
}
stringUrl = if (link.contains("//news.google.com/news/") && link.contains("&amp;url=")) {
link.substringAfter("&amp;url=")
} else {
this.link.replace("&amp;", "&")
}
// handle :443 => https
if (stringUrl.contains(":443")) {

View File

@ -44,16 +44,14 @@ class Repository(
private val _badgeStarred = MutableStateFlow(0)
val badgeStarred = _badgeStarred.asStateFlow()
private var fetchedSources = false
private var fetchedTags = false
private var fetchedSources = false
private var _readerItems = ArrayList<SelfossModel.Item>()
private var _selectedSource: SelfossModel.Source? = null
private var _selectedSource: SelfossModel.SourceDetail? = null
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// TODO: Use the updatedSince parameter
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
var fromDB = false
if (isNetworkAvailable()) {
fetchedItems = api.getItems(
displayedItems.type,
@ -63,31 +61,27 @@ class Repository(
searchFilter,
null
)
} else {
if (appSettingsService.isItemCachingEnabled()) {
fromDB = true
var dbItems = getDBItems().filter {
displayedItems == ItemType.ALL ||
(it.unread && displayedItems == ItemType.UNREAD) ||
(it.starred && displayedItems == ItemType.STARRED)
}
if (tagFilter.value != null) {
dbItems = dbItems.filter { it.tags.split(',').contains(tagFilter.value!!.tag) }
}
if (sourceFilter.value != null) {
dbItems = dbItems.filter { it.sourcetitle == sourceFilter.value!!.title }
}
fetchedItems = StatusAndData.succes(
dbItems.map { it.toView() }
)
} else if (appSettingsService.isItemCachingEnabled()) {
var dbItems = getDBItems().filter {
displayedItems == ItemType.ALL ||
(it.unread && displayedItems == ItemType.UNREAD) ||
(it.starred && displayedItems == ItemType.STARRED)
}
if (tagFilter.value != null) {
dbItems = dbItems.filter { it.tags.split(',').contains(tagFilter.value!!.tag) }
}
if (sourceFilter.value != null) {
dbItems = dbItems.filter { it.sourcetitle == sourceFilter.value!!.title }
}
val itemsList = ArrayList(dbItems.map { it.toView() })
itemsList.sortByDescending { DateUtils.parseDate(it.datetime) }
fetchedItems = StatusAndData.succes(
itemsList
)
}
if (fetchedItems.success && fetchedItems.data != null) {
items = ArrayList(fetchedItems.data!!)
if (fromDB) {
items.sortByDescending { DateUtils.parseDate(it.datetime) }
}
}
return items
}
@ -138,9 +132,9 @@ class Repository(
if (isNetworkAvailable()) {
val response = api.stats()
if (response.success && response.data != null) {
_badgeUnread.value = response.data.unread
_badgeUnread.value = response.data.unread ?: 0
_badgeAll.value = response.data.total
_badgeStarred.value = response.data.starred
_badgeStarred.value = response.data.starred ?: 0
success = true
}
} else if (appSettingsService.isItemCachingEnabled()) {
@ -173,40 +167,61 @@ class Repository(
}
}
// TODO: Add tests
suspend fun getSpouts(): Map<String, SelfossModel.Spout> {
return if (isNetworkAvailable()) {
val spouts = api.spouts()
if (spouts.success && spouts.data != null) {
spouts.data
} else {
emptyMap() // TODO: do something here
emptyMap()
}
} else {
throw NetworkUnavailableException()
}
}
suspend fun getSources(): ArrayList<SelfossModel.Source> {
suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> {
var sources = ArrayList<SelfossModel.Source>()
val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
return if (isNetworkAvailable() && !fetchedSources) {
val apiSources = api.sources()
if (apiSources.success && apiSources.data != null && isDatabaseEnabled) {
resetDBSourcesWithData(apiSources.data)
if (!appSettingsService.isUpdateSourcesEnabled()) {
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
if (shouldFetch && isNetworkAvailable()) {
if (appSettingsService.getPublicAccess()) {
val apiSources = api.sourcesStats()
if (apiSources.success && apiSources.data != null) {
fetchedSources = true
sources = apiSources.data as ArrayList<SelfossModel.Source>
}
} else {
sources = getSourcesDetails() as ArrayList<SelfossModel.Source>
}
apiSources.data ?: ArrayList()
} else if (isDatabaseEnabled) {
ArrayList(getDBSources().map { it.toView() })
} else {
ArrayList()
sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.Source>
}
return sources
}
suspend fun getSourcesDetails(): ArrayList<SelfossModel.SourceDetail> {
var sources = ArrayList<SelfossModel.SourceDetail>()
val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
if (shouldFetch && isNetworkAvailable()) {
val apiSources = api.sourcesDetailed()
if (apiSources.success && apiSources.data != null) {
fetchedSources = true
sources = apiSources.data
if (isDatabaseEnabled) {
resetDBSourcesWithData(sources)
}
}
} else if (isDatabaseEnabled) {
sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.SourceDetail>
}
return sources
}
// TODO: Add tests
suspend fun markAsRead(item: SelfossModel.Item): Boolean {
val success = markAsReadById(item.id)
@ -225,7 +240,6 @@ class Repository(
}
}
// TODO: Add tests
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
val success = unmarkAsReadById(item.id)
@ -244,7 +258,6 @@ class Repository(
}
}
// TODO: Add tests
suspend fun starr(item: SelfossModel.Item): Boolean {
val success = starrById(item.id)
@ -263,7 +276,6 @@ class Repository(
}
}
// TODO: Add tests
suspend fun unstarr(item: SelfossModel.Item): Boolean {
val success = unstarrById(item.id)
@ -282,7 +294,6 @@ class Repository(
}
}
// TODO: Add tests
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false
@ -451,13 +462,23 @@ class Repository(
api.refreshLoginInformation()
}
suspend fun updateApiVersion() {
suspend fun updateApiInformation() {
val apiMajorVersion = appSettingsService.getApiVersion()
if (isNetworkAvailable()) {
val fetchedVersion = api.version()
if (fetchedVersion.success && fetchedVersion.data != null && fetchedVersion.data.getApiMajorVersion() != apiMajorVersion) {
appSettingsService.updateApiVersion(fetchedVersion.data.getApiMajorVersion())
val fetchedInformation = api.apiInformation()
if (fetchedInformation.success && fetchedInformation.data != null) {
if (fetchedInformation.data.getApiMajorVersion() != apiMajorVersion) {
appSettingsService.updateApiVersion(fetchedInformation.data.getApiMajorVersion())
}
// Check if we're accessing the instance in public mode
// This happens when auth and public mode are enabled but
// no credentials are provided to login
if (appSettingsService.getUserName().isEmpty()
&& fetchedInformation.data.getApiConfiguration().isAuthEnabled()
&& fetchedInformation.data.getApiConfiguration().isPublicModeEnabled()) {
appSettingsService.updatePublicAccess(true)
}
}
}
}
@ -484,7 +505,7 @@ class Repository(
}
}
private fun resetDBSourcesWithData(sources: List<SelfossModel.Source>) {
private fun resetDBSourcesWithData(sources: List<SelfossModel.SourceDetail>) {
db.sourcesQueries.deleteAllSources()
db.sourcesQueries.transaction {
@ -542,7 +563,6 @@ class Repository(
return emptyList()
}
// TODO: Add tests
suspend fun handleDBActions() {
val actions: List<ACTION> = getDBActions()
@ -595,7 +615,7 @@ class Repository(
ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
}
fun setSelectedSource(source: SelfossModel.Source) {
fun setSelectedSource(source: SelfossModel.SourceDetail) {
_selectedSource = source
}
@ -603,7 +623,7 @@ class Repository(
_selectedSource = null
}
fun getSelectedSource(): SelfossModel.Source? {
fun getSelectedSource(): SelfossModel.SourceDetail? {
return _selectedSource
}
}

View File

@ -7,6 +7,7 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import io.github.aakira.napier.Napier
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.cookies.*
@ -15,6 +16,9 @@ import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.util.*
import io.ktor.utils.io.charsets.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -63,6 +67,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
expectSuccess = false
}
return client
}
@ -74,6 +79,13 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client = createHttpClient()
}
fun constructBasicAuthValue(credentials: BasicAuthCredentials): String {
val authString = "${credentials.username}:${credentials.password}"
val authBuf = authString.toByteArray(Charsets.UTF_8).encodeBase64()
return "Basic $authBuf"
}
// Api version was introduces after the POST login, so when there is a version, it should be available
private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
private fun hasLoginInfo() =
@ -96,11 +108,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
private suspend fun getLogin() = maybeResponse(client.tryToGet(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
private suspend fun postLogin() = maybeResponse(client.tryToPost(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
private fun shouldHaveNewLogout() =
@ -114,9 +138,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
}
private suspend fun maybeLogoutIfAvailable() =
responseOrSuccessIf404(client.tryToGet(url("/logout")))
responseOrSuccessIf404(client.tryToGet(url("/logout")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current")))
private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun getItems(
type: String,
@ -139,6 +177,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("updatedsince", updatedSince)
parameter("items", items ?: appSettingsService.getItemsNumber())
parameter("offset", offset)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun getItemsWithoutCatch(): StatusAndData<List<SelfossModel.Item>> =
@ -149,6 +193,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
}
parameter("type", "all")
parameter("items", 1)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun stats(): StatusAndData<SelfossModel.Stats> =
@ -157,6 +207,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
@ -165,6 +221,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun update(): StatusAndData<String> =
@ -173,6 +235,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
@ -181,18 +249,51 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> =
suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> =
bodyOrFailure(client.tryToGet(url("/sources/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> =
bodyOrFailure(client.tryToGet(url("/sources/list")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun version(): StatusAndData<SelfossModel.ApiVersion> =
bodyOrFailure(client.tryToGet(url("/api/about")))
suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
bodyOrFailure(client.tryToGet(url("/api/about")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun markAsRead(id: String): SuccessResponse =
maybeResponse(client.tryToPost(url("/mark/$id")) {
@ -200,6 +301,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun unmarkAsRead(id: String): SuccessResponse =
@ -208,6 +315,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun starr(id: String): SuccessResponse =
@ -216,6 +329,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun unstarr(id: String): SuccessResponse =
@ -224,6 +343,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
@ -235,6 +360,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
append("password", appSettingsService.getPassword())
}
ids.map { append("ids[]", it) }
},
block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
}
))
@ -270,6 +403,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
append("url", url)
append("spout", spout)
append(tagsParamName, tags)
},
block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
}
)
@ -307,6 +448,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
append("url", url)
append("spout", spout)
append(tagsParamName, tags)
},
block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
}
)
@ -316,5 +465,18 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword()
)
)
)
}
}
})
}

View File

@ -0,0 +1,70 @@
package bou.amine.apps.readerforselfossv2.service
import com.russhwolf.settings.Settings
// This will be used in ACRA process. For now, it does nothing.
// This is to fix ACRA not sending reports anymore.
// See https://www.acra.ch/docs/Troubleshooting-Guide#applicationoncreate
class ACRASettings : Settings {
override val keys: Set<String> = emptySet()
override val size: Int = 0
override fun clear() {
// Nothing
}
override fun getBoolean(key: String, defaultValue: Boolean): Boolean = false
override fun getBooleanOrNull(key: String): Boolean? = null
override fun getDouble(key: String, defaultValue: Double): Double = 0.0
override fun getDoubleOrNull(key: String): Double? = null
override fun getFloat(key: String, defaultValue: Float): Float = 0.0F
override fun getFloatOrNull(key: String): Float? = null
override fun getInt(key: String, defaultValue: Int): Int = 0
override fun getIntOrNull(key: String): Int? = null
override fun getLong(key: String, defaultValue: Long): Long = 0
override fun getLongOrNull(key: String): Long? = null
override fun getString(key: String, defaultValue: String): String = "0"
override fun getStringOrNull(key: String): String? = null
override fun hasKey(key: String): Boolean = false
override fun putBoolean(key: String, value: Boolean) {
// Nothing
}
override fun putDouble(key: String, value: Double) {
// Nothing
}
override fun putFloat(key: String, value: Float) {
// Nothing
}
override fun putInt(key: String, value: Int) {
// Nothing
}
override fun putLong(key: String, value: Long) {
// Nothing
}
override fun putString(key: String, value: String) {
// Nothing
}
override fun remove(key: String) {
// Nothing
}
}

View File

@ -1,17 +1,18 @@
package bou.amine.apps.readerforselfossv2.service
import com.russhwolf.settings.Settings
import io.github.aakira.napier.Napier
import io.ktor.client.plugins.*
class AppSettingsService {
val settings: Settings = Settings()
class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
val settings: Settings = if (acraSenderServiceProcess) { ACRASettings() } else { Settings() }
// Api related
private var _apiVersion: Int = -1
private var _publicAccess: Boolean? = null
private var _baseUrl: String = ""
private var _userName: String = ""
private var _basicUserName: String = ""
private var _password: String = ""
private var _basicPassword: String = ""
// User settings related
private var _itemsCaching: Boolean? = null
@ -50,10 +51,32 @@ class AppSettingsService {
return _apiVersion
}
fun updateApiVersion(apiMajorVersion: Int) {
settings.putInt(API_VERSION_MAJOR, apiMajorVersion)
refreshApiVersion()
}
private fun refreshApiVersion() {
_apiVersion = settings.getInt(API_VERSION_MAJOR, -1)
}
fun getPublicAccess(): Boolean {
if (_publicAccess == null) {
refreshPublicAccess()
}
return _publicAccess!!
}
fun updatePublicAccess(publicAccess: Boolean) {
settings.putBoolean(API_PUBLIC_ACCESS, publicAccess)
refreshPublicAccess()
}
private fun refreshPublicAccess() {
_publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false)
}
fun getBaseUrl(): String {
if (_baseUrl.isEmpty()) {
refreshBaseUrl()
@ -75,6 +98,20 @@ class AppSettingsService {
return _password
}
fun getBasicUserName(): String {
if (_basicUserName.isEmpty()) {
refreshBasicUsername()
}
return _basicUserName
}
fun getBasicPassword(): String {
if (_basicPassword.isEmpty()) {
refreshBasicPassword()
}
return _basicPassword
}
fun getItemsNumber(): Int {
if (_itemsNumber == null) {
refreshItemsNumber()
@ -128,6 +165,14 @@ class AppSettingsService {
_password = settings.getString(PASSWORD, "")
}
private fun refreshBasicUsername() {
_basicUserName = settings.getString(BASIC_LOGIN, "")
}
private fun refreshBasicPassword() {
_basicPassword = settings.getString(BASIC_PASSWORD, "")
}
private fun refreshArticleViewerEnabled() {
_articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
}
@ -333,8 +378,11 @@ class AppSettingsService {
fun refreshApiSettings() {
refreshPassword()
refreshUsername()
refreshBasicUsername()
refreshBasicPassword()
refreshBaseUrl()
refreshApiVersion()
refreshPublicAccess()
}
fun refreshUserSettings() {
@ -365,7 +413,17 @@ class AppSettingsService {
login: String,
password: String
) {
settings.putString(BASE_URL, url)
val regex = """\/\/(\D+):(\D+)@""".toRegex()
val matchResult = regex.find(url)
if (matchResult != null) {
val (basicLogin, basicPassword) = matchResult.destructured
settings.putString(BASIC_LOGIN, basicLogin)
settings.putString(BASIC_PASSWORD, basicPassword)
val urlWithoutBasicAuth = url.replace(regex, "//")
settings.putString(BASE_URL, urlWithoutBasicAuth)
} else {
settings.putString(BASE_URL, url)
}
settings.putString(LOGIN, login)
settings.putString(PASSWORD, password)
refreshApiSettings()
@ -375,14 +433,11 @@ class AppSettingsService {
settings.remove(BASE_URL)
settings.remove(LOGIN)
settings.remove(PASSWORD)
settings.remove(BASIC_LOGIN)
settings.remove(BASIC_PASSWORD)
refreshApiSettings()
}
fun updateApiVersion(apiMajorVersion: Int) {
settings.putInt(API_VERSION_MAJOR, apiMajorVersion)
refreshApiVersion()
}
fun clearAll() {
settings.clear()
refreshApiSettings()
@ -411,6 +466,8 @@ class AppSettingsService {
const val API_VERSION_MAJOR = "apiVersionMajor"
const val API_PUBLIC_ACCESS = "apiPublicAccess"
const val API_ITEMS_NUMBER = "prefer_api_items_number"
const val API_TIMEOUT = "api_timeout"
@ -421,6 +478,10 @@ class AppSettingsService {
const val PASSWORD = "password"
const val BASIC_LOGIN = "basic_login"
const val BASIC_PASSWORD = "basic_password"
const val PREFER_ARTICLE_VIEWER = "prefer_article_viewer"
const val CARD_VIEW_ACTIVE = "card_view_active"
@ -451,7 +512,6 @@ class AppSettingsService {
const val PERIODIC_REFRESH_MINUTES = "periodic_refresh_minutes"
const val INFINITE_LOADING = "infinite_loading"
const val ITEMS_CACHING = "items_caching"

View File

@ -12,24 +12,25 @@ fun TAG.toView(): SelfossModel.Tag =
this.unread.toInt()
)
fun SOURCE.toView(): SelfossModel.Source =
SelfossModel.Source(
fun SOURCE.toView(): SelfossModel.SourceDetail =
SelfossModel.SourceDetail(
this.id.toInt(),
this.title,
this.tags.split(","),
null,
this.tags?.split(","),
this.spout,
this.error,
this.icon,
if (this.url != null) SelfossModel.SourceParams(this.url) else null
)
fun SelfossModel.Source.toEntity(): SOURCE =
fun SelfossModel.SourceDetail.toEntity(): SOURCE =
SOURCE(
this.id.toString(),
this.title.getHtmlDecoded(),
this.tags.joinToString(","),
this.spout,
this.error,
this.tags?.joinToString(",").orEmpty(),
this.spout.orEmpty(),
this.error.orEmpty(),
this.icon.orEmpty(),
this.params?.url
)