Compare commits

..

51 Commits

Author SHA1 Message Date
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
43 changed files with 1409 additions and 926 deletions

View File

@ -35,6 +35,7 @@ steps:
trigger: trigger:
event: event:
- push - push
- pull_request
--- ---
kind: pipeline kind: pipeline
@ -46,13 +47,15 @@ steps:
image: ubuntu:latest image: ubuntu:latest
commands: commands:
- apt-get update && apt-get install -y git - apt-get update && apt-get install -y git
- VER=$(git describe --tags --abbrev=0) - git fetch --tags -p
- CHANGELOG=$(git log $VER..HEAD --pretty="- %s") - PREV=$(git describe --tags --abbrev=0)
- echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md
- git add CHANGELOG.md
- git commit -m "Changelog for $VER"
- ./build.sh --publish --from-ci - ./build.sh --publish --from-ci
- git remote add pushing https://$GITEA_USR:$GITEA_PASS@gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform.git - 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 master
- git push pushing --tags - git push pushing --tags
environment: environment:

View File

@ -1,3 +1,89 @@
**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 # V2/Multiplatform rewrite
**v1** **v1**

View File

@ -66,7 +66,7 @@ android {
jvmTarget = "11" jvmTarget = "11"
} }
compileSdk = 33 compileSdk = 33
buildToolsVersion = "31.0.0" buildToolsVersion = "33.0.0"
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
} }
@ -115,7 +115,6 @@ dependencies {
implementation(project(":shared")) implementation(project(":shared"))
implementation("com.google.android.material:material:1.5.0") implementation("com.google.android.material:material:1.5.0")
implementation("androidx.appcompat:appcompat:1.4.1") 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("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
implementation("androidx.preference:preference-ktx:1.1.1") implementation("androidx.preference:preference-ktx:1.1.1")
@ -131,7 +130,7 @@ dependencies {
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.annotation:annotation:1.3.0") implementation("androidx.annotation:annotation:1.3.0")
implementation("androidx.work:work-runtime-ktx:2.7.1") 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") implementation("org.jsoup:jsoup:1.14.3")
//multidex //multidex

View File

@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> 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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -69,7 +70,8 @@
android:name=".ReaderActivity"> android:name=".ReaderActivity">
</activity> </activity>
<activity <activity
android:name=".ImageActivity"> android:name=".ImageActivity"
android:theme="@style/Theme.AppCompat.ImageActivity">
</activity> </activity>
<meta-data android:name="android.webkit.WebView.MetricsOptOut" <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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
@ -114,10 +115,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
} }
val swipeDirs = if (appSettingsService.getPublicAccess()) {
0
} else {
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
}
val simpleItemTouchCallback = val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback( object : ItemTouchHelper.SimpleCallback(
0, 0,
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT swipeDirs
) { ) {
override fun getSwipeDirs( override fun getSwipeDirs(
recyclerView: RecyclerView, recyclerView: RecyclerView,
@ -366,7 +373,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
fun fetchOnEmptyList() { fun fetchOnEmptyList() {
binding.recyclerView.doOnNextLayout { binding.recyclerView.doOnNextLayout {
// TODO: do if last element (or is empty ?)
getElementsAccordingToTab(true) getElementsAccordingToTab(true)
} }
} }
@ -511,6 +517,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater val inflater = menuInflater
inflater.inflate(R.menu.home_menu, menu) 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 searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.getActionView() as SearchView val searchView = searchItem.getActionView() as SearchView
@ -539,11 +549,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
R.id.refresh -> { R.id.refresh -> {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
// TODO: Use Dispatchers.IO
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote() val updatedRemote = repository.updateRemote()
if (updatedRemote) { if (updatedRemote) {
// TODO: Send toast messages from the repository
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG R.string.refresh_success_response, Toast.LENGTH_LONG
@ -591,12 +599,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
return true return true
} }
R.id.action_disconnect -> { R.id.action_disconnect -> {
CoroutineScope(Dispatchers.Main).launch { runBlocking {
repository.logout() repository.logout()
} }
this@HomeActivity.finish()
val intent = Intent(this, LoginActivity::class.java) val intent = Intent(this, LoginActivity::class.java)
this.startActivity(intent) this.startActivity(intent)
finish()
return true return true
} }
R.id.action_settings -> { R.id.action_settings -> {

View File

@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.android
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
@ -23,7 +24,6 @@ class ImageActivity : AppCompatActivity() {
setContentView(view) setContentView(view)
setSupportActionBar(binding.toolBar) setSupportActionBar(binding.toolBar)
supportActionBar?.setDisplayShowTitleEnabled(false)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
allImages = intent.getStringArrayListExtra("allImages") as ArrayList<String> allImages = intent.getStringArrayListExtra("allImages") as ArrayList<String>
@ -31,12 +31,35 @@ class ImageActivity : AppCompatActivity() {
binding.pager.adapter = ScreenSlidePagerAdapter(this) binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(position, false) 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
onBackPressed() onBackPressedDispatcher.onBackPressed()
return true return true
} }
} }

View File

@ -27,7 +27,6 @@ import org.acra.ACRA
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
import java.security.MessageDigest
class LoginActivity : AppCompatActivity(), DIAware { class LoginActivity : AppCompatActivity(), DIAware {
@ -129,7 +128,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
private fun goToMain() { private fun goToMain() {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
repository.updateApiVersion() repository.updateApiInformation()
ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString()) ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString())
} }
val intent = Intent(this, HomeActivity::class.java) val intent = Intent(this, HomeActivity::class.java)
@ -157,13 +156,64 @@ class LoginActivity : AppCompatActivity(), DIAware {
val login = binding.loginView.text.toString().trim() val login = binding.loginView.text.toString().trim()
val password = binding.passwordView.text.toString().trim() val password = binding.passwordView.text.toString().trim()
var cancel = false failInvalidUrl(url)
var focusView: View? = null 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()) { if (url.isBaseUrlInvalid()) {
binding.urlView.error = getString(R.string.login_url_problem)
focusView = binding.urlView
cancel = true cancel = true
binding.urlView.error = getString(R.string.login_url_problem)
inValidCount++ inValidCount++
if (inValidCount == 3) { if (inValidCount == 3) {
val alertDialog = AlertDialog.Builder(this).create() val alertDialog = AlertDialog.Builder(this).create()
@ -177,49 +227,12 @@ class LoginActivity : AppCompatActivity(), DIAware {
inValidCount = 0 inValidCount = 0
} }
} }
maybeCancelAndFocusView(cancel, focusView)
}
if (isWithLogin) { private fun maybeCancelAndFocusView(cancel: Boolean, focusView: View?) {
if (TextUtils.isEmpty(password)) {
binding.passwordView.error = getString(R.string.error_invalid_password)
focusView = binding.passwordView
cancel = true
}
if (TextUtils.isEmpty(login)) {
binding.loginView.error = getString(R.string.error_field_required)
focusView = binding.loginView
cancel = true
}
}
if (cancel) { if (cancel) {
focusView?.requestFocus() 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 { class MyApp : MultiDexApplication(), DIAware {
override val di by DI.lazy { override val di by DI.lazy {
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) }
import(networkModule) import(networkModule)
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) } bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) } bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) } bind<Repository>() with singleton {
Repository(
instance(),
instance(),
isConnectionAvailable,
instance()
)
}
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) } bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
} }
@ -59,7 +67,12 @@ class MyApp : MultiDexApplication(), DIAware {
handleNotificationChannels() handleNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository)) ProcessLifecycleOwner.get().lifecycle.addObserver(
AppLifeCycleObserver(
connectivityStatus,
repository
)
)
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
viewModel.networkAvailableProvider.collect { networkAvailable -> viewModel.networkAvailableProvider.collect { networkAvailable ->
@ -88,22 +101,38 @@ class MyApp : MultiDexApplication(), DIAware {
initAcra { initAcra {
reportFormat = StringFormat.JSON reportFormat = StringFormat.JSON
reportContent = listOf( reportContent = listOf(
ReportField.REPORT_ID, ReportField.INSTALLATION_ID, ReportField.REPORT_ID,
ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME, ReportField.INSTALLATION_ID,
ReportField.BUILD, ReportField.ANDROID_VERSION, ReportField.BRAND, ReportField.PHONE_MODEL, ReportField.APP_VERSION_CODE,
ReportField.AVAILABLE_MEM_SIZE, ReportField.TOTAL_MEM_SIZE, ReportField.APP_VERSION_NAME,
ReportField.STACK_TRACE, ReportField.APPLICATION_LOG, ReportField.LOGCAT, ReportField.BUILD,
ReportField.INITIAL_CONFIGURATION, ReportField.CRASH_CONFIGURATION, ReportField.IS_SILENT, ReportField.ANDROID_VERSION,
ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA) 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 { toast {
//required //required
text = getString(R.string.crash_toast_text) text = getString(R.string.crash_toast_text)
length = Toast.LENGTH_SHORT length = Toast.LENGTH_SHORT
} }
httpSender { httpSender {
uri = "https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/ uri =
basicAuthLogin = "LMTlLZuazADohTCm" "https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/
basicAuthPassword = "he6ghHp83F0PYPfh" basicAuthLogin = "qMEscjj89Gwt6cPR"
basicAuthPassword = "Yo58QFlGzFaWlBzP"
httpMethod = HttpSender.Method.POST httpMethod = HttpSender.Method.POST
} }
} }
@ -119,7 +148,11 @@ class MyApp : MultiDexApplication(), DIAware {
val newItemsChannelname = getString(R.string.new_items_channel_sync) val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = NotificationChannel(AppSettingsService.newItemsChannelId, newItemsChannelname, newItemsChannelimportance) val newItemsChannelmChannel = NotificationChannel(
AppSettingsService.newItemsChannelId,
newItemsChannelname,
newItemsChannelimportance
)
notificationManager.createNotificationChannel(mChannel) notificationManager.createNotificationChannel(mChannel)
notificationManager.createNotificationChannel(newItemsChannelmChannel) notificationManager.createNotificationChannel(newItemsChannelmChannel)
@ -133,13 +166,17 @@ class MyApp : MultiDexApplication(), DIAware {
if (e is NoClassDefFoundError && e.stackTrace.asList().any { if (e is NoClassDefFoundError && e.stackTrace.asList().any {
it.toString().contains("android.view.ViewDebug") it.toString().contains("android.view.ViewDebug")
}) { }) {
// Nothing
} else { } else {
oldHandler.uncaughtException(thread, e) 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) { override fun onResume(owner: LifecycleOwner) {
super.onResume(owner) super.onResume(owner)

View File

@ -84,7 +84,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
} }
private fun readItem(item: SelfossModel.Item) { private fun readItem(item: SelfossModel.Item) {
if (appSettingsService.isMarkOnScrollEnabled()) { if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess()) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(item) repository.markAsRead(item)
} }
@ -137,28 +137,34 @@ class ReaderActivity : AppCompatActivity(), DIAware {
inflater.inflate(R.menu.reader_menu, menu) inflater.inflate(R.menu.reader_menu, menu)
toolbarMenu = menu toolbarMenu = menu
if (allItems.isNotEmpty() && allItems[currentItem].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
alignmentMenu() alignmentMenu()
binding.pager.registerOnPageChangeCallback( if (appSettingsService.getPublicAccess()) {
object : ViewPager2.OnPageChangeCallback() { menu.removeItem(R.id.star)
} else {
override fun onPageSelected(position: Int) { if (allItems.isNotEmpty() && allItems[currentItem].starred) {
super.onPageSelected(position) canRemoveFromFavorite()
} else {
if (allItems[position].starred) { canFavorite()
canRemoveFromFavorite()
} else {
canFavorite()
}
readItem(allItems[position])
}
} }
)
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if (allItems[position].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
readItem(allItems[position])
}
}
)
}
return true return true
} }
@ -177,20 +183,18 @@ class ReaderActivity : AppCompatActivity(), DIAware {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
onBackPressed() onBackPressedDispatcher.onBackPressed()
return true return true
} }
R.id.star -> { R.id.star -> {
if (allItems[binding.pager.currentItem].starred) { if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(allItems[binding.pager.currentItem]) repository.unstarr(allItems[binding.pager.currentItem])
// TODO: Handle failure
} }
afterUnsave() afterUnsave()
} else { } else {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.starr(allItems[binding.pager.currentItem]) repository.starr(allItems[binding.pager.currentItem])
// TODO: Handle failure
} }
afterSave() afterSave()
} }

View File

@ -56,6 +56,10 @@ class ItemCardAdapter(
val itm = items[position] val itm = items[position]
binding.favButton.isSelected = itm.starred binding.favButton.isSelected = itm.starred
if (appSettingsService.getPublicAccess()) {
binding.favButton.visibility = View.GONE
}
binding.title.text = itm.title.getHtmlDecoded() binding.title.text = itm.title.getHtmlDecoded()
binding.title.setOnTouchListener(LinkOnTouchListener()) binding.title.setOnTouchListener(LinkOnTouchListener())

View File

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.Toast import android.widget.Toast
@ -60,6 +61,13 @@ class SourcesListAdapter(
c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
} }
if (itm.error.isNotBlank()) {
binding.errorText.visibility = View.VISIBLE
binding.errorText.text = itm.error
} else {
binding.errorText.visibility = View.GONE
}
binding.sourceTitle.text = itm.title.getHtmlDecoded() binding.sourceTitle.text = itm.title.getHtmlDecoded()
} }

View File

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

View File

@ -28,7 +28,9 @@ import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.shareLink import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.MercuryModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.MercuryApi import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@ -55,6 +57,8 @@ import java.util.*
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
private const val IMAGE_JPG = "image/jpg"
class ArticleFragment : Fragment(), DIAware { class ArticleFragment : Fragment(), DIAware {
private var fontSize: Int = 16 private var fontSize: Int = 16
private lateinit var item: SelfossModel.Item private lateinit var item: SelfossModel.Item
@ -63,13 +67,12 @@ class ArticleFragment : Fragment(), DIAware {
private lateinit var contentSource: String private lateinit var contentSource: String
private lateinit var contentImage: String private lateinit var contentImage: String
private lateinit var contentTitle: String private lateinit var contentTitle: String
private lateinit var allImages : ArrayList<String> private lateinit var allImages: ArrayList<String>
private lateinit var fab: FloatingActionButton private lateinit var fab: FloatingActionButton
private lateinit var textAlignment: String private lateinit var textAlignment: String
private var _binding: FragmentArticleBinding? = null private lateinit var binding: FragmentArticleBinding
private val binding get() = _binding
override val di : DI by closestDI() override val di: DI by closestDI()
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance() private val appSettingsService: AppSettingsService by instance()
@ -78,7 +81,7 @@ class ArticleFragment : Fragment(), DIAware {
private var font = "" private var font = ""
private var staticBar = false private var staticBar = false
private val mercuryApi : MercuryApi by instance() private val mercuryApi: MercuryApi by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -95,7 +98,7 @@ class ArticleFragment : Fragment(), DIAware {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
try { try {
_binding = FragmentArticleBinding.inflate(inflater, container, false) binding = FragmentArticleBinding.inflate(inflater, container, false)
url = item.getLinkDecoded() url = item.getLinkDecoded()
contentText = item.content contentText = item.content
@ -110,89 +113,27 @@ class ArticleFragment : Fragment(), DIAware {
refreshAlignment() refreshAlignment()
fab = binding!!.fab fab = binding.fab
fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent)) fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
fab.rippleColor = resources.getColor(R.color.colorAccentDark) fab.rippleColor = resources.getColor(R.color.colorAccentDark)
val floatingToolbar: FloatingToolbar = binding!!.floatingToolbar val floatingToolbar: FloatingToolbar = handleFloatingToolbar()
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?) {
}
}
)
if (staticBar) { if (staticBar) {
fab.hide() fab.hide()
floatingToolbar.show() floatingToolbar.show()
} }
binding!!.source.text = contentSource binding.source.text = contentSource
if (typeface != null) { if (typeface != null) {
binding!!.source.typeface = typeface binding.source.typeface = typeface
} }
if (contentText.isEmptyOrNullOrNullString()) { handleContent()
getContentFromMercury()
} else {
binding!!.titleView.text = contentTitle
if (typeface != null) {
binding!!.titleView.typeface = typeface
}
htmlToWebview() binding.nestedScrollView.setOnScrollChangeListener(
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(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) { if (scrollY > oldScrollY) {
floatingToolbar.hide() floatingToolbar.hide()
@ -212,7 +153,8 @@ class ArticleFragment : Fragment(), DIAware {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton(android.R.string.ok .setPositiveButton(
android.R.string.ok
) { _, _ -> ) { _, _ ->
appSettingsService.disableArticleViewer() appSettingsService.disableArticleViewer()
requireActivity().finish() requireActivity().finish()
@ -221,12 +163,84 @@ class ArticleFragment : Fragment(), DIAware {
.show() .show()
} }
return binding!!.root return binding.root
} }
override fun onDestroyView() { private fun handleContent() {
super.onDestroyView() if (contentText.isEmptyOrNullOrNullString()) {
_binding = null 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() { private fun refreshAlignment() {
@ -238,75 +252,108 @@ class ArticleFragment : Fragment(), DIAware {
} }
private fun getContentFromMercury() { private fun getContentFromMercury() {
if (repository.isNetworkAvailable()) { binding.progressBar.visibility = View.VISIBLE
binding!!.progressBar.visibility = View.VISIBLE
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
try { try {
val response = mercuryApi.query(url) val response = mercuryApi.query(url)
if (response.success && response.data != null && !response.data?.content.isNullOrEmpty()) { if (response.success && response.data != null && !response.data?.content.isNullOrEmpty()) {
binding!!.titleView.text = response.data!!.title.orEmpty() binding.titleView.text = response.data!!.title.orEmpty()
try { if (typeface != null) {
if (typeface != null) { binding.titleView.typeface = typeface
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) { URL(response.data!!.url)
openInBrowserAfterFailing() url = response.data!!.url
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > whole thing") contentText = response.data!!.content.orEmpty()
htmlToWebview()
handleLeadImage(response)
binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE
} else {
openInBrowserAfterFailing() openInBrowserAfterFailing()
} }
} catch (e: SocketTimeoutException) {
openInBrowserAfterFailing()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > $url")
openInBrowserAfterFailing()
}
}
}
private fun handleLeadImage(response: StatusAndData<MercuryModel.ParsedContent>) {
if (!response.data?.lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE
Glide
.with(requireContext())
.asBitmap()
.load(
response.data!!.lead_image_url.orEmpty()
)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else {
binding.imageView.visibility = View.GONE
}
}
private fun handleImageLoading() {
binding.webcontent.webViewClient = object : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean {
if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
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")
) {
try {
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)
)
} catch (e: ExecutionException) {
// Do nothing
}
} else if (url.lowercase(Locale.US).contains(".png")) {
try {
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.PNG)
)
} catch (e: ExecutionException) {
// Do nothing
}
} else if (url.lowercase(Locale.US).contains(".webp")) {
try {
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)
)
} catch (e: ExecutionException) {
// Do nothing
}
}
return super.shouldInterceptRequest(view, url)
} }
} }
} }
@ -317,8 +364,8 @@ class ArticleFragment : Fragment(), DIAware {
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs) val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
binding!!.webcontent.settings.standardFontFamily = a.getString(0) binding.webcontent.settings.standardFontFamily = a.getString(0)
binding!!.webcontent.visibility = View.VISIBLE binding.webcontent.visibility = View.VISIBLE
val colorOnSurface = TypedValue() val colorOnSurface = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true) requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
@ -326,61 +373,23 @@ class ArticleFragment : Fragment(), DIAware {
val colorSurface = TypedValue() val colorSurface = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true) requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
binding!!.webcontent.settings.useWideViewPort = true binding.webcontent.settings.useWideViewPort = true
binding!!.webcontent.settings.loadWithOverviewMode = true binding.webcontent.settings.loadWithOverviewMode = true
binding!!.webcontent.settings.javaScriptEnabled = false binding.webcontent.settings.javaScriptEnabled = false
binding!!.webcontent.webViewClient = object : WebViewClient() { handleImageLoading()
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean { val gestureDetector =
if (binding!!.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick()
} }
return true })
}
@Deprecated("Deprecated in Java") binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US).contains(".jpeg")) {
try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG))
} catch ( e : ExecutionException) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg > $url")
}
}
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")
}
}
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")
}
}
return super.shouldInterceptRequest(view, url) binding.webcontent.settings.layoutAlgorithm =
} WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
}
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
@ -391,7 +400,7 @@ class ArticleFragment : Fragment(), DIAware {
e.sendSilentlyWithAcraWithName("htmlToWebview > item url") e.sendSilentlyWithAcraWithName("htmlToWebview > item url")
} }
val fontName = when (font) { val fontName = when (font) {
getString(R.string.open_sans_font_id) -> "Open Sans" getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto" getString(R.string.roboto_font_id) -> "Roboto"
getString(R.string.source_code_pro_font_id) -> "Source Code Pro" getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
@ -399,7 +408,12 @@ class ArticleFragment : Fragment(), DIAware {
} }
val fontLinkAndStyle = if (font.isNotEmpty()) { val fontLinkAndStyle = if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${fontName.replace(" ", "+")}" rel="stylesheet"> """<link href="https://fonts.googleapis.com/css?family=${
fontName.replace(
" ",
"+"
)
}" rel="stylesheet">
|<style> |<style>
| * { | * {
| font-family: '$fontName'; | font-family: '$fontName';
@ -410,7 +424,7 @@ class ArticleFragment : Fragment(), DIAware {
"" ""
} }
binding!!.webcontent.loadDataWithBaseURL( binding.webcontent.loadDataWithBaseURL(
baseUrl, baseUrl,
"""<html> """<html>
|<head> |<head>
@ -423,7 +437,12 @@ class ArticleFragment : Fragment(), DIAware {
| max-width: 100%; | max-width: 100%;
| } | }
| a { | 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) { | *:not(a) {
| color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)}; | color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)};
@ -434,11 +453,26 @@ class ArticleFragment : Fragment(), DIAware {
| word-break: break-word; | word-break: break-word;
| overflow:hidden; | overflow:hidden;
| line-height: 1.5em; | line-height: 1.5em;
| background-color: ${String.format("#%06X", 0xFFFFFF and colorSurface.data)}; | background-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data
)
};
| } | }
| body, html { | body, html {
| background-color: ${String.format("#%06X", 0xFFFFFF and colorSurface.data)} !important; | background-color: ${
| border-color: ${String.format("#%06X", 0xFFFFFF and colorSurface.data)} !important; String.format(
"#%06X",
0xFFFFFF and colorSurface.data
)
} !important;
| border-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data
)
} !important;
| padding: 0 !important; | padding: 0 !important;
| margin: 0 !important; | margin: 0 !important;
| } | }
@ -448,7 +482,12 @@ class ArticleFragment : Fragment(), DIAware {
| pre, code { | pre, code {
| white-space: pre-wrap; | white-space: pre-wrap;
| width:100%; | width:100%;
| background-color: ${String.format("#%06X", 0xFFFFFF and colorSurface.data)}; | background-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data
)
};
| } | }
| </style> | </style>
| $fontLinkAndStyle | $fontLinkAndStyle
@ -463,17 +502,17 @@ class ArticleFragment : Fragment(), DIAware {
} }
fun scrollDown() { fun scrollDown() {
val height = binding!!.nestedScrollView.measuredHeight val height = binding.nestedScrollView.measuredHeight
binding!!.nestedScrollView.smoothScrollBy(0, height/2) binding.nestedScrollView.smoothScrollBy(0, height / 2)
} }
fun scrollUp() { fun scrollUp() {
val height = binding!!.nestedScrollView.measuredHeight val height = binding.nestedScrollView.measuredHeight
binding!!.nestedScrollView.smoothScrollBy(0, -height/2) binding.nestedScrollView.smoothScrollBy(0, -height / 2)
} }
private fun openInBrowserAfterFailing() { private fun openInBrowserAfterFailing() {
binding!!.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item) requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
} }
@ -481,7 +520,7 @@ class ArticleFragment : Fragment(), DIAware {
private const val ARG_ITEMS = "items" private const val ARG_ITEMS = "items"
fun newInstance( fun newInstance(
item: SelfossModel.Item item: SelfossModel.Item
): ArticleFragment { ): ArticleFragment {
val fragment = ArticleFragment() val fragment = ArticleFragment()
val args = Bundle() val args = Bundle()
@ -492,10 +531,11 @@ class ArticleFragment : Fragment(), DIAware {
} }
fun performClick(): Boolean { fun performClick(): Boolean {
if (binding!!.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding!!.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_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) val intent = Intent(activity, ImageActivity::class.java)
intent.putExtra("allImages", allImages) intent.putExtra("allImages", allImages)

View File

@ -4,7 +4,9 @@ import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
@ -12,6 +14,7 @@ import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import bou.amine.apps.readerforselfossv2.android.HomeActivity import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
@ -34,6 +37,7 @@ import org.kodein.di.instance
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
private lateinit var binding: FilterFragmentBinding
override val di: DI by closestDI() override val di: DI by closestDI()
private val repository: Repository by instance() private val repository: Repository by instance()
@ -44,8 +48,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val binding = binding =
bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding.inflate( FilterFragmentBinding.inflate(
inflater, inflater,
container, container,
false false
@ -53,112 +57,14 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
val context: Context? = context val context: Context? = context
val tagGroup = binding.tagsGroup
val sourceGroup = binding.sourcesGroup
if (context == null) { if (context == null) {
dismiss() dismiss()
Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView") Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} else { } else {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val tags = repository.getTags() handleTagChips(context)
handleSourceChips(context)
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)
}
binding.progressBar2.visibility = GONE binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE binding.filterView.visibility = VISIBLE
@ -173,6 +79,133 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
return binding.root return binding.root
} }
private suspend fun handleSourceChips(
context: Context
) {
val sourceGroup = binding.sourcesGroup
repository.getSources().forEach { source ->
val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END
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 {
try {
c.chipIcon = resource
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("sources > onResourceReady")
}
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
}
c.isEnabled = source.error.isBlank()
if (source.error.isNotBlank() && 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 { companion object {
const val TAG = "FilterModalBottomSheet" 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.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import org.acra.ktx.sendSilentlyWithAcra
fun SelfossModel.Item.preloadImages(context: Context) : Boolean { fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages() val imageUrls = this.getImages()

View File

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

View File

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

View File

@ -1,33 +1,40 @@
<?xml version="1.0" encoding="utf-8"?> <?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" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent" 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 <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout" android:id="@+id/appBarLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar android:theme="@style/ToolBarStyle"
android:id="@+id/toolBar" android:id="@+id/toolBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
/> />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2 <androidx.core.widget.NestedScrollView
android:id="@+id/pager" android:id="@+id/scrollView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent" android:fillViewport="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/appBarLayout">
app:layout_constraintStart_toStartOf="parent"
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" <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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical" android:orientation="vertical"
tools:context="bou.amine.apps.readerforselfossv2.android.LoginActivity"> tools:context="bou.amine.apps.readerforselfossv2.android.LoginActivity">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> 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:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:theme="@style/ToolBarStyle"
/> app:popupTheme="?attr/toolbarPopupTheme" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin" android:padding="@dimen/activity_horizontal_margin">
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<!-- Login progress --> <!-- Login progress -->
<ProgressBar <ProgressBar
android:id="@+id/loginProgress" android:id="@+id/loginProgress"
@ -33,67 +32,65 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:visibility="gone"/> android:visibility="gone" />
<ScrollView <LinearLayout
android:id="@+id/loginForm" android:id="@+id/loginForm"
android:layout_width="match_parent" 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_width="match_parent"
android:layout_height="wrap_content" 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 <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/urlView" android:id="@+id/withLogin"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/prompt_url" android:text="@string/withLoginSwitch"
android:imeOptions="actionUnspecified" android:textAlignment="viewStart" />
android:importantForAutofill="no"
android:inputType="textUri"
android:maxLines="1" />
<com.google.android.material.switchmaterial.SwitchMaterial <EditText
android:text="@string/withLoginSwitch" android:id="@+id/loginView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
android:id="@+id/withLogin" android:autofillHints="username"
android:layout_weight="1"/> android:hint="@string/prompt_login"
android:inputType="text"
android:maxLines="1"
android:minHeight="48dp"
android:visibility="gone" />
<EditText <EditText
android:id="@+id/loginView" android:id="@+id/passwordView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:autofillHints="username" android:autofillHints="password"
android:hint="@string/prompt_login" android:hint="@string/prompt_password"
android:inputType="text" android:inputType="textPassword"
android:maxLines="1" android:maxLines="1"
android:visibility="gone" /> android:minHeight="48dp"
android:visibility="gone" />
<EditText <Button
android:id="@+id/passwordView" android:id="@+id/signInButton"
android:layout_width="match_parent" style="?android:textAppearanceSmall"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:autofillHints="password" android:layout_height="wrap_content"
android:hint="@string/prompt_password" android:layout_marginTop="16dp"
android:inputType="textPassword" android:layout_marginBottom="16dp"
android:maxLines="1" android:text="@string/action_sign_in"
android:visibility="gone" /> android:textStyle="bold" />
<Button </LinearLayout>
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> </LinearLayout>

View File

@ -24,7 +24,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scrollbars="vertical" 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> </androidx.recyclerview.widget.RecyclerView>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?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" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.github.chrisbanes.photoview.PhotoView <com.github.chrisbanes.photoview.PhotoView
android:id="@+id/photoView" android:id="@+id/photoView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:background="@drawable/checkerboard" android:background="@drawable/checkerboard"
app:srcCompat="@android:drawable/screen_background_dark" /> 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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="88dp"> android:layout_height="wrap_content">
<ImageView <ImageView
android:id="@+id/itemImage" android:id="@+id/itemImage"
android:layout_width="46dp" android:layout_width="46dp"
android:layout_height="46dp" android:layout_height="46dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="21dp" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent" />
android:layout_marginLeft="8dp" />
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
@ -24,39 +23,30 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:ellipsize="end" android:ellipsize="end"
android:fontFamily="sans-serif" android:fontFamily="sans-serif"
android:gravity="start"
android:maxLines="3" android:maxLines="3"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textAllCaps="false" android:textColor="?android:textColorPrimary"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" android:textStyle="bold"
android:textColor="?android:textColorPrimary"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/itemImage" app:layout_constraintStart_toEndOf="@+id/itemImage"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Titre" tools:text="Titre" />
android:layout_marginLeft="8dp"
android:layout_marginRight="16dp" />
<TextView <TextView
android:id="@+id/sourceTitleAndDate" android:id="@+id/sourceTitleAndDate"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="66dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:gravity="start" android:gravity="start"
android:maxLines="1" android:maxLines="1"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textSize="14sp"
android:textColor="?android:textColorPrimary" android:textColor="?android:textColorPrimary"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/itemImage" app:layout_constraintStart_toEndOf="@+id/itemImage"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@+id/itemImage"
tools:text="Google Actualité Il y a 5h" tools:text="Google Actualité Il y a 5h" />
android:layout_marginLeft="8dp"
android:layout_marginRight="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,48 +3,74 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<Button
android:id="@+id/deleteBtn"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="48dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/ic_remove_circle_outline_black_24dp"
android:backgroundTint="?android:textColorSecondary"
android:contentDescription="@string/remove_source"
android:elevation="4dp"
app:iconSize="34dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<ImageView <ImageView
android:id="@+id/itemImage" android:id="@+id/itemImage"
android:layout_width="36dp" android:layout_width="36dp"
android:layout_height="36dp" android:layout_height="36dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<TextView <TextView
android:id="@+id/sourceTitle" android:id="@+id/sourceTitle"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="17dp" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:ellipsize="end" android:ellipsize="end"
android:gravity="start"
android:maxLines="1" android:maxLines="1"
android:textAlignment="textStart" android:textAlignment="viewStart"
android:textSize="13sp"
android:textColor="?android:textColorPrimary" android:textColor="?android:textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent" android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/errorText"
app:layout_constraintEnd_toStartOf="@+id/deleteBtn" app:layout_constraintEnd_toStartOf="@+id/deleteBtn"
app:layout_constraintStart_toEndOf="@+id/itemImage" app:layout_constraintStart_toEndOf="@+id/itemImage"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="source title" /> tools:text="Source title" />
<Button <TextView
android:id="@+id/deleteBtn" android:id="@+id/errorText"
style="@style/Widget.AppCompat.Button.Borderless" android:layout_width="0dp"
android:layout_width="34dp" android:layout_height="wrap_content"
android:layout_height="34dp" android:layout_marginStart="10sp"
android:layout_marginEnd="8dp" android:layout_marginTop="8dp"
android:background="@drawable/ic_remove_circle_outline_black_24dp" android:layout_marginBottom="8dp"
android:backgroundTint="?android:textColorSecondary" android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:elevation="4dp" android:textColor="@color/red"
android:contentDescription="@string/remove_source" android:textStyle="italic"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@+id/deleteBtn"
app:layout_constraintTop_toTopOf="parent" /> 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> </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_2">@color/colorAccent</color>
<color name="refresh_progress_3">@color/pink</color> <color name="refresh_progress_3">@color/pink</color>
<color name="dark">#FF282828</color> <color name="dark">#FF282828</color>
<color name="transparent_dark_background">#33000000</color>
</resources> </resources>

View File

@ -26,4 +26,10 @@
<item name="android:textColorSecondary">@color/white</item> <item name="android:textColorSecondary">@color/white</item>
<item name="actionMenuTextColor">@color/white</item> <item name="actionMenuTextColor">@color/white</item>
</style> </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>
</resources> </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.Before
import org.junit.Test 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 { class RepositoryTest {
private val db = mockk<ReaderForSelfossDB>(relaxed = true) private val db = mockk<ReaderForSelfossDB>(relaxed = true)
private val appSettingsService = mockk<AppSettingsService>() private val appSettingsService = mockk<AppSettingsService>()
@ -37,7 +51,7 @@ class RepositoryTest {
repository = Repository(api, appSettingsService, isConnectionAvailable, db) repository = Repository(api, appSettingsService, isConnectionAvailable, db)
runBlocking { runBlocking {
repository.updateApiVersion() repository.updateApiInformation()
} }
} }
@ -45,13 +59,14 @@ class RepositoryTest {
fun setup() { fun setup() {
clearAllMocks() clearAllMocks()
every { appSettingsService.getApiVersion() } returns 4 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.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.version() } returns StatusAndData( coEvery { api.apiInformation() } returns StatusAndData(
success = true, 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( coEvery { api.stats() } returns StatusAndData(
success = true, success = true,
@ -69,7 +84,7 @@ class RepositoryTest {
fun instantiate_repository() { fun instantiate_repository() {
initializeRepository() initializeRepository()
coVerify(exactly = 1) { api.version() } coVerify(exactly = 1) { api.apiInformation() }
} }
@Test @Test
@ -78,7 +93,7 @@ class RepositoryTest {
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
coVerify(exactly = 0) { api.version() } coVerify(exactly = 0) { api.apiInformation() }
coVerify(exactly = 0) { api.stats() } coVerify(exactly = 0) { api.stats() }
} }
@ -98,10 +113,70 @@ class RepositoryTest {
verify(exactly = 1) { appSettingsService.updateApiVersion(4) } 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 @Test
fun get_api_1_date_with_api_4_version_stored() { fun get_api_1_date_with_api_4_version_stored() {
every { appSettingsService.getApiVersion() } returns 4 every { appSettingsService.getApiVersion() } returns 4
coEvery { api.version() } returns StatusAndData(success = false, null) coEvery { api.apiInformation() } returns StatusAndData(success = false, null)
val itemParameters = FakeItemParameters() val itemParameters = FakeItemParameters()
itemParameters.datetime = "2021-04-23 11:45:32" itemParameters.datetime = "2021-04-23 11:45:32"
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
@ -229,9 +304,9 @@ class RepositoryTest {
1, 1,
"Test", "Test",
listOf("tags"), listOf("tags"),
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"b3aa8a664d08eb15d6ff1db2fa83e0d9.png", IMAGE_URL,
SelfossModel.SourceParams("url") SelfossModel.SourceParams("url")
)) ))
runBlocking { runBlocking {
@ -550,18 +625,18 @@ class RepositoryTest {
1, 1,
"First source", "First source",
listOf("Test", "second"), listOf("Test", "second"),
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"d8c92cdb1ef119ea85c4b9205c879ca7.png", IMAGE_URL_2,
SelfossModel.SourceParams("url") SelfossModel.SourceParams("url")
), ),
SelfossModel.Source( SelfossModel.Source(
2, 2,
"Second source", "Second source",
listOf("second"), listOf("second"),
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"b3aa8a664d08eb15d6ff1db2fa83e0d9.png", IMAGE_URL,
SelfossModel.SourceParams("url") SelfossModel.SourceParams("url")
) )
) )
@ -570,17 +645,19 @@ class RepositoryTest {
"1", "1",
"First DB source", "First DB source",
"Test,second", "Test,second",
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"d8c92cdb1ef119ea85c4b9205c879ca7.png" IMAGE_URL_2,
"url"
), ),
SOURCE( SOURCE(
"2", "2",
"Second source", "Second source",
"second", "second",
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"b3aa8a664d08eb15d6ff1db2fa83e0d9.png" IMAGE_URL,
"url"
) )
) )
@ -718,9 +795,9 @@ class RepositoryTest {
runBlocking { runBlocking {
response = repository.createSource( response = repository.createSource(
"test", "test",
"https://test.com/feed", FEED_URL,
"spouts\\rss\\fulltextrss", SPOUT,
"Test, New", TAGS,
) )
} }
@ -745,9 +822,9 @@ class RepositoryTest {
runBlocking { runBlocking {
response = repository.createSource( response = repository.createSource(
"test", "test",
"https://test.com/feed", FEED_URL,
"spouts\\rss\\fulltextrss", SPOUT,
"Test, New" TAGS
) )
} }
@ -772,9 +849,9 @@ class RepositoryTest {
runBlocking { runBlocking {
response = repository.createSource( response = repository.createSource(
"test", "test",
"https://test.com/feed", FEED_URL,
"spouts\\rss\\fulltextrss", SPOUT,
"Test, New" TAGS
) )
} }
@ -950,12 +1027,12 @@ class RepositoryTest {
coEvery { appSettingsService.refreshLoginInformation(any(), any(), any()) } returns Unit coEvery { appSettingsService.refreshLoginInformation(any(), any(), any()) } returns Unit
initializeRepository() initializeRepository()
repository.refreshLoginInformation("https://test.com/selfoss/", "login", "password") repository.refreshLoginInformation(BASE_URL, "login", "password")
coVerify(exactly = 1) { api.refreshLoginInformation() } coVerify(exactly = 1) { api.refreshLoginInformation() }
coVerify(exactly = 1) { coVerify(exactly = 1) {
appSettingsService.refreshLoginInformation( appSettingsService.refreshLoginInformation(
"https://test.com/selfoss/", BASE_URL,
"login", "login",
"password" "password"
) )
@ -1029,9 +1106,9 @@ class RepositoryTest {
1, 1,
"First source", "First source",
listOf("Test", "second"), listOf("Test", "second"),
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"d8c92cdb1ef119ea85c4b9205c879ca7.png", IMAGE_URL_2,
SelfossModel.SourceParams("url") SelfossModel.SourceParams("url")
) )
) )

View File

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

View File

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

View File

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

View File

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

View File

@ -35,17 +35,32 @@ class SelfossModel {
) )
@Serializable @Serializable
data class ApiVersion( data class ApiInformation(
val version: String?, val version: String?,
val apiversion: String? val apiversion: String?,
val configuration: ApiConfiguration?
) { ) {
fun getApiMajorVersion() : Int { fun getApiMajorVersion(): Int {
var versionNumber = 0 var versionNumber = 0
if (apiversion != null) { if (apiversion != null) {
versionNumber = apiversion.substringBefore(".").toInt() versionNumber = apiversion.substringBefore(".").toInt()
} }
return versionNumber return versionNumber
} }
fun getApiConfiguration() = configuration ?: ApiConfiguration(null, null)
}
@Serializable
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
} }
@Serializable @Serializable
@ -61,7 +76,7 @@ class SelfossModel {
) )
@Serializable @Serializable
data class SourceParams( data class SourceParams(
val url: String val url: String?
) )
@Serializable @Serializable
data class Item( data class Item(
@ -81,19 +96,13 @@ class SelfossModel {
val tags: List<String>, val tags: List<String>,
val author: String? val author: String?
) { ) {
// TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String { fun getLinkDecoded(): String {
var stringUrl: String var stringUrl: String
stringUrl = stringUrl = if (link.contains("//news.google.com/news/") && link.contains("&amp;url=")) {
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) { link.substringAfter("&amp;url=")
if (link.contains("&amp;url=")) { } else {
link.substringAfter("&amp;url=") this.link.replace("&amp;", "&")
} else { }
this.link.replace("&amp;", "&")
}
} else {
this.link.replace("&amp;", "&")
}
// handle :443 => https // handle :443 => https
if (stringUrl.contains(":443")) { if (stringUrl.contains(":443")) {

View File

@ -51,9 +51,7 @@ class Repository(
private var _selectedSource: SelfossModel.Source? = null private var _selectedSource: SelfossModel.Source? = null
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// TODO: Use the updatedSince parameter
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
var fromDB = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
fetchedItems = api.getItems( fetchedItems = api.getItems(
displayedItems.type, displayedItems.type,
@ -63,31 +61,27 @@ class Repository(
searchFilter, searchFilter,
null null
) )
} else { } else if (appSettingsService.isItemCachingEnabled()) {
if (appSettingsService.isItemCachingEnabled()) { var dbItems = getDBItems().filter {
fromDB = true displayedItems == ItemType.ALL ||
var dbItems = getDBItems().filter { (it.unread && displayedItems == ItemType.UNREAD) ||
displayedItems == ItemType.ALL || (it.starred && displayedItems == ItemType.STARRED)
(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() }
)
} }
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) { if (fetchedItems.success && fetchedItems.data != null) {
items = ArrayList(fetchedItems.data!!) items = ArrayList(fetchedItems.data!!)
if (fromDB) {
items.sortByDescending { DateUtils.parseDate(it.datetime) }
}
} }
return items return items
} }
@ -173,14 +167,13 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun getSpouts(): Map<String, SelfossModel.Spout> { suspend fun getSpouts(): Map<String, SelfossModel.Spout> {
return if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
val spouts = api.spouts() val spouts = api.spouts()
if (spouts.success && spouts.data != null) { if (spouts.success && spouts.data != null) {
spouts.data spouts.data
} else { } else {
emptyMap() // TODO: do something here emptyMap()
} }
} else { } else {
throw NetworkUnavailableException() throw NetworkUnavailableException()
@ -206,7 +199,6 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun markAsRead(item: SelfossModel.Item): Boolean { suspend fun markAsRead(item: SelfossModel.Item): Boolean {
val success = markAsReadById(item.id) val success = markAsReadById(item.id)
@ -225,7 +217,6 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean { suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
val success = unmarkAsReadById(item.id) val success = unmarkAsReadById(item.id)
@ -244,7 +235,6 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun starr(item: SelfossModel.Item): Boolean { suspend fun starr(item: SelfossModel.Item): Boolean {
val success = starrById(item.id) val success = starrById(item.id)
@ -263,7 +253,6 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun unstarr(item: SelfossModel.Item): Boolean { suspend fun unstarr(item: SelfossModel.Item): Boolean {
val success = unstarrById(item.id) val success = unstarrById(item.id)
@ -282,7 +271,6 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean { suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false var success = false
@ -451,13 +439,23 @@ class Repository(
api.refreshLoginInformation() api.refreshLoginInformation()
} }
suspend fun updateApiVersion() { suspend fun updateApiInformation() {
val apiMajorVersion = appSettingsService.getApiVersion() val apiMajorVersion = appSettingsService.getApiVersion()
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val fetchedVersion = api.version() val fetchedInformation = api.apiInformation()
if (fetchedVersion.success && fetchedVersion.data != null && fetchedVersion.data.getApiMajorVersion() != apiMajorVersion) { if (fetchedInformation.success && fetchedInformation.data != null) {
appSettingsService.updateApiVersion(fetchedVersion.data.getApiMajorVersion()) 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)
}
} }
} }
} }
@ -542,7 +540,6 @@ class Repository(
return emptyList() return emptyList()
} }
// TODO: Add tests
suspend fun handleDBActions() { suspend fun handleDBActions() {
val actions: List<ACTION> = getDBActions() val actions: List<ACTION> = getDBActions()

View File

@ -191,7 +191,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
}) })
suspend fun version(): StatusAndData<SelfossModel.ApiVersion> = suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
bodyOrFailure(client.tryToGet(url("/api/about"))) bodyOrFailure(client.tryToGet(url("/api/about")))
suspend fun markAsRead(id: String): SuccessResponse = suspend fun markAsRead(id: String): SuccessResponse =

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,14 +1,13 @@
package bou.amine.apps.readerforselfossv2.service package bou.amine.apps.readerforselfossv2.service
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
import io.github.aakira.napier.Napier
import io.ktor.client.plugins.*
class AppSettingsService { class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
val settings: Settings = Settings() val settings: Settings = if (acraSenderServiceProcess) { ACRASettings() } else { Settings() }
// Api related // Api related
private var _apiVersion: Int = -1 private var _apiVersion: Int = -1
private var _publicAccess: Boolean? = null
private var _baseUrl: String = "" private var _baseUrl: String = ""
private var _userName: String = "" private var _userName: String = ""
private var _password: String = "" private var _password: String = ""
@ -50,10 +49,32 @@ class AppSettingsService {
return _apiVersion return _apiVersion
} }
fun updateApiVersion(apiMajorVersion: Int) {
settings.putInt(API_VERSION_MAJOR, apiMajorVersion)
refreshApiVersion()
}
private fun refreshApiVersion() { private fun refreshApiVersion() {
_apiVersion = settings.getInt(API_VERSION_MAJOR, -1) _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 { fun getBaseUrl(): String {
if (_baseUrl.isEmpty()) { if (_baseUrl.isEmpty()) {
refreshBaseUrl() refreshBaseUrl()
@ -335,6 +356,7 @@ class AppSettingsService {
refreshUsername() refreshUsername()
refreshBaseUrl() refreshBaseUrl()
refreshApiVersion() refreshApiVersion()
refreshPublicAccess()
} }
fun refreshUserSettings() { fun refreshUserSettings() {
@ -378,11 +400,6 @@ class AppSettingsService {
refreshApiSettings() refreshApiSettings()
} }
fun updateApiVersion(apiMajorVersion: Int) {
settings.putInt(API_VERSION_MAJOR, apiMajorVersion)
refreshApiVersion()
}
fun clearAll() { fun clearAll() {
settings.clear() settings.clear()
refreshApiSettings() refreshApiSettings()
@ -411,6 +428,8 @@ class AppSettingsService {
const val API_VERSION_MAJOR = "apiVersionMajor" const val API_VERSION_MAJOR = "apiVersionMajor"
const val API_PUBLIC_ACCESS = "apiPublicAccess"
const val API_ITEMS_NUMBER = "prefer_api_items_number" const val API_ITEMS_NUMBER = "prefer_api_items_number"
const val API_TIMEOUT = "api_timeout" const val API_TIMEOUT = "api_timeout"

View File

@ -20,7 +20,7 @@ fun SOURCE.toView(): SelfossModel.Source =
this.spout, this.spout,
this.error, this.error,
this.icon, this.icon,
if (this.url != null) SelfossModel.SourceParams(this.url) else null SelfossModel.SourceParams(this.url)
) )
fun SelfossModel.Source.toEntity(): SOURCE = fun SelfossModel.Source.toEntity(): SOURCE =