Compare commits

..

69 Commits

Author SHA1 Message Date
ab9c46f0eb Update 'CHANGELOG.md' 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. 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. 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. 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. 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. 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)
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)
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. 2023-01-28 11:23:00 +01:00
9b23053b66 fix: Complete the disconnection before redirecting to the login screen
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
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
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. 2023-01-23 22:32:52 +01:00
29eae4b1f6 fix: scrollable filter sheet. 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. 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. 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)) 2022-12-30 20:10:08 +01:00
4d8076c3cf build: Added back maven repos (see 1fb9d60dc5 (note_1223925153)) 2022-12-29 21:34:13 +01:00
db75c5b74a debug: trying to resolve Canvas: trying to use a recycled bitmap. 2022-12-29 20:40:39 +01:00
966a082147 fix: NPE may be caused by the binding or the title that was null. 2022-12-29 20:35:03 +01:00
cd20a5ec29 chore: Skip drone pipeline on changelog push. 2022-12-29 14:11:29 +01:00
cc4c1c9201 Changelog for v122123621 2022-12-29 13:08:23 +00:00
ff021d572c fix: Automatic CHANGELOG generation. 2022-12-29 13:11:23 +01:00
89992967be Merge pull request 'Sources Upsert' (#119) from sources-edit into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/119
2022-12-28 21:31:49 +00:00
3c68bde62b Source update screen. 2022-12-28 22:19:11 +01:00
c38251f5b3 Sources menu. 2022-12-28 21:45:00 +01:00
a01f6d2322 chore: Automatic CHANGELOG generation. 2022-12-28 21:43:56 +01:00
417a33eb25 Merge pull request 'Running migrations.' (#118) from fix-migration into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/118
2022-12-28 14:49:11 +00:00
2e7f7f23b3 No duplicate builds for PRs. 2022-12-28 15:34:22 +01:00
e5e182761e Running migrations. 2022-12-28 15:27:17 +01:00
a094d88799 Merge pull request 'Make the author field nullable' (#117) from davidoskky/ReaderForSelfoss-multiplatform:author into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/117
2022-12-28 14:25:36 +00:00
e51915d1cd Include author field when updating the database 2022-12-28 14:25:56 +01:00
3a654f6ede Migrate the database table 2022-12-28 14:25:34 +01:00
5227751dca Make the author field nullable 2022-12-28 11:02:43 +01:00
27eafe4ff4 Delete sources from DB and reload items on source deletion. 2022-12-26 22:27:28 +01:00
8c83a9408b Drone should work better. 2022-12-26 22:26:28 +01:00
fe2410f719 Handling author field. 2022-12-26 21:49:55 +01:00
a5e86bfb77 Date format issues. 2022-12-26 15:02:19 +01:00
23be633798 Add api version to the reports. 2022-12-25 22:45:12 +01:00
813e0707d8 Date format issue. 2022-12-25 22:41:34 +01:00
9ed9bf07fc Items in repository. 2022-12-23 22:53:16 +01:00
47265c10d0 Trying nexus build. 2022-12-23 14:59:58 +01:00
5cc633246a Debugging images issues. 2022-12-22 20:28:49 +01:00
1f40385786 Context should not be null, but handle the case for now. 2022-12-19 22:08:28 +01:00
eb2876324a This seems to be needed. 2022-12-19 20:47:04 +01:00
75 changed files with 1775 additions and 1061 deletions

View File

@ -17,6 +17,7 @@ steps:
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- ./gradlew koverMergedXmlReport - ./gradlew koverMergedXmlReport
environment: environment:
TZ: Europe/Paris
SONAR_HOST_URL: SONAR_HOST_URL:
from_secret: sonarScannerHostUrl from_secret: sonarScannerHostUrl
SONAR_LOGIN: SONAR_LOGIN:
@ -42,14 +43,23 @@ type: docker
name: Publish name: Publish
steps: steps:
- name: createTag - name: createTagAndChangelog
image: ubuntu:latest image: ubuntu:latest
commands: commands:
- apt-get update && apt-get install -y git - apt-get update && apt-get install -y git
- git fetch --tags -p
- PREV=$(git describe --tags --abbrev=0)
- ./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 --tags - git push pushing --tags
environment: environment:
TZ: Europe/Paris
GITEA_USR: GITEA_USR:
from_secret: giteaUsr from_secret: giteaUsr
GITEA_PASS: GITEA_PASS:
@ -75,10 +85,7 @@ steps:
from_secret: privateKey from_secret: privateKey
command_timeout: 2m command_timeout: 2m
script: script:
- cd /home/ubuntu - cd /home/ubuntu && sudo rm -rf /var/www/amine/version.txt && sudo chown www-data:www-data ./version.txt && sudo mv version.txt /var/www/amine/
- sudo rm -rf /var/www/amine/version.txt
- sudo chown www-data:www-data ./version.txt
- sudo mv version.txt /var/www/amine/
trigger: trigger:
event: event:
@ -117,6 +124,7 @@ steps:
- echo "Verify" - echo "Verify"
- $ANDROID_HOME/build-tools/31.0.0/apksigner verify signed.apk - $ANDROID_HOME/build-tools/31.0.0/apksigner verify signed.apk
environment: environment:
TZ: Europe/Paris
YOUR_KEYSTORE_PASSWORD: YOUR_KEYSTORE_PASSWORD:
from_secret: keyPass from_secret: keyPass
YOUR_KEY_ALIAS: YOUR_KEY_ALIAS:

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" />
@ -52,7 +53,7 @@
android:value=".HomeActivity" /> android:value=".HomeActivity" />
</activity> </activity>
<activity <activity
android:name=".AddSourceActivity" android:name=".UpsertSourceActivity"
android:parentActivityName=".SourcesActivity" android:parentActivityName=".SourcesActivity"
android:exported="true"> android:exported="true">
<meta-data <meta-data
@ -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,18 +599,22 @@ 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 -> {
settingsLauncher.launch(Intent(this, SettingsActivity::class.java)) settingsLauncher.launch(Intent(this, SettingsActivity::class.java))
return true return true
} }
R.id.action_sources -> {
startActivity(Intent(this, SourcesActivity::class.java))
return true
}
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }
} }

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

@ -23,10 +23,10 @@ import com.mikepenz.aboutlibraries.LibsBuilder
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 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 {
@ -128,7 +128,8 @@ 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())
} }
val intent = Intent(this, HomeActivity::class.java) val intent = Intent(this, HomeActivity::class.java)
startActivity(intent) startActivity(intent)
@ -155,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()
@ -175,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

@ -3,10 +3,7 @@ package bou.amine.apps.readerforselfossv2.android
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build import android.os.Build
import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@ -18,8 +15,6 @@ import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.github.ln_12.library.ConnectivityStatus import com.github.ln_12.library.ConnectivityStatus
import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
@ -39,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()) }
} }
@ -64,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 ->
@ -83,6 +91,8 @@ class MyApp : MultiDexApplication(), DIAware {
} }
} }
} }
repository.migrate(driverFactory)
} }
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
@ -91,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
} }
} }
@ -122,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)
@ -136,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

@ -30,6 +30,8 @@ class ReaderActivity : AppCompatActivity(), DIAware {
private lateinit var binding: ActivityReaderBinding private lateinit var binding: ActivityReaderBinding
private var allItems: ArrayList<SelfossModel.Item> = ArrayList()
override val di by closestDI() override val di by closestDI()
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance() private val appSettingsService: AppSettingsService by instance()
@ -61,12 +63,14 @@ class ReaderActivity : AppCompatActivity(), DIAware {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
if (allItems.isEmpty()) { currentItem = intent.getIntExtra("currentItem", 0)
allItems = repository.getReaderItems()
if (allItems.isEmpty() || currentItem > allItems.size) {
finish() finish()
} }
currentItem = intent.getIntExtra("currentItem", 0)
readItem(allItems[currentItem]) readItem(allItems[currentItem])
binding.pager.adapter = ScreenSlidePagerAdapter(this) binding.pager.adapter = ScreenSlidePagerAdapter(this)
@ -80,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)
} }
@ -133,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
} }
@ -173,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()
} }
@ -214,8 +222,4 @@ class ReaderActivity : AppCompatActivity(), DIAware {
startActivity(intent) startActivity(intent)
overridePendingTransition(0, 0) overridePendingTransition(0, 0)
} }
companion object {
var allItems: ArrayList<SelfossModel.Item> = ArrayList()
}
} }

View File

@ -73,7 +73,7 @@ class SourcesActivity : AppCompatActivity(), DIAware {
} }
binding.fab.setOnClickListener { binding.fab.setOnClickListener {
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java)) startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java))
} }
} }
} }

View File

@ -3,12 +3,15 @@ package bou.amine.apps.readerforselfossv2.android
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.* import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityAddSourceBinding
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -19,38 +22,59 @@ import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class AddSourceActivity : AppCompatActivity(), DIAware { class UpsertSourceActivity : AppCompatActivity(), DIAware {
private var existingSource: SelfossModel.Source? = null
private var mSpoutsValue: String? = null private var mSpoutsValue: String? = null
private lateinit var binding: ActivityAddSourceBinding private lateinit var binding: ActivityUpsertSourceBinding
override val di by closestDI() override val di by closestDI()
private val repository : Repository by instance() private val repository: Repository by instance()
private val appSettingsService : AppSettingsService by instance() private val appSettingsService: AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityAddSourceBinding.inflate(layoutInflater) binding = ActivityUpsertSourceBinding.inflate(layoutInflater)
val view = binding.root val view = binding.root
existingSource = repository.getSelectedSource()
if (existingSource != null) {
binding.formContainer.visibility = View.GONE
binding.progress.visibility = View.VISIBLE
}
val title = if (existingSource == null) R.string.add_source else R.string.update_source
supportFragmentManager.addOnBackStackChangedListener {
if (supportFragmentManager.backStackEntryCount == 0) {
setTitle(title)
}
}
setContentView(view) setContentView(view)
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.title = resources.getString(title)
maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput)
maybeGetDetailsFromIntentSharing(intent)
binding.saveBtn.setOnClickListener { binding.saveBtn.setOnClickListener {
handleSaveSource( handleSaveSource()
binding.tags,
binding.nameInput.text.toString(),
binding.sourceUri.text.toString()
)
} }
} }
private fun initFields(items: Map<String, SelfossModel.Spout>) {
binding.nameInput.setText(existingSource!!.title)
binding.tags.setText(existingSource!!.tags.joinToString(", "))
binding.sourceUri.setText(existingSource!!.params?.url)
binding.spoutsSpinner.setSelection(items.keys.indexOf(existingSource!!.spout))
binding.progress.visibility = View.GONE
binding.formContainer.visibility = View.VISIBLE
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -58,17 +82,13 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
if (baseUrl.isEmpty() || baseUrl.isBaseUrlInvalid()) { if (baseUrl.isEmpty() || baseUrl.isBaseUrlInvalid()) {
mustLoginToAddSource() mustLoginToAddSource()
} else { } else {
handleSpoutsSpinner(binding.spoutsSpinner, binding.progress, binding.formContainer) handleSpoutsSpinner()
} }
} }
private fun handleSpoutsSpinner( private fun handleSpoutsSpinner() {
spoutsSpinner: Spinner,
mProgress: ProgressBar,
formContainer: ConstraintLayout
) {
val spoutsKV = HashMap<String, String>() val spoutsKV = HashMap<String, String>()
spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) { override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) {
if (view != null) { if (view != null) {
val spoutName = (view as TextView).text.toString() val spoutName = (view as TextView).text.toString()
@ -84,11 +104,11 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
fun handleSpoutFailure(networkIssue: Boolean = false) { fun handleSpoutFailure(networkIssue: Boolean = false) {
Toast.makeText( Toast.makeText(
this@AddSourceActivity, this@UpsertSourceActivity,
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts, if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
mProgress.visibility = View.GONE binding.progress.visibility = View.GONE
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -100,17 +120,21 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
spoutsKV[value.name] = key spoutsKV[value.name] = key
} }
mProgress.visibility = View.GONE binding.progress.visibility = View.GONE
formContainer.visibility = View.VISIBLE binding.formContainer.visibility = View.VISIBLE
val spinnerArrayAdapter = val spinnerArrayAdapter =
ArrayAdapter( ArrayAdapter(
this@AddSourceActivity, this@UpsertSourceActivity,
android.R.layout.simple_spinner_item, android.R.layout.simple_spinner_item,
itemsStrings itemsStrings
) )
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spoutsSpinner.adapter = spinnerArrayAdapter binding.spoutsSpinner.adapter = spinnerArrayAdapter
if (existingSource != null) {
initFields(items)
}
} else { } else {
handleSpoutFailure() handleSpoutFailure()
} }
@ -121,13 +145,11 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
} }
private fun maybeGetDetailsFromIntentSharing( private fun maybeGetDetailsFromIntentSharing(
intent: Intent, intent: Intent
sourceUri: EditText,
nameInput: EditText
) { ) {
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) { if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT)) binding.sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE)) binding.nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
} }
} }
@ -138,7 +160,8 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
finish() finish()
} }
private fun handleSaveSource(tags: EditText, title: String, url: String) { private fun handleSaveSource() {
val url = binding.sourceUri.text.toString()
val sourceDetailsUnavailable = val sourceDetailsUnavailable =
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty() title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
@ -149,18 +172,27 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
} }
else -> { else -> {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val successfullyAddedSource = repository.createSource( val successfullyAddedSource = if (existingSource != null) {
title, repository.updateSource(
url, existingSource!!.id,
mSpoutsValue!!, binding.nameInput.text.toString(),
tags.text.toString(), url,
"", mSpoutsValue!!,
) binding.tags.text.toString()
)
} else {
repository.createSource(
binding.nameInput.text.toString(),
url,
mSpoutsValue!!,
binding.tags.text.toString(),
)
}
if (successfullyAddedSource) { if (successfullyAddedSource) {
finish() finish()
} else { } else {
Toast.makeText( Toast.makeText(
this@AddSourceActivity, this@UpsertSourceActivity,
R.string.cant_create_source, R.string.cant_create_source,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
@ -169,4 +201,9 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
} }
} }
} }
override fun onDestroy() {
super.onDestroy()
repository.unsetSelectedSource()
}
} }

View File

@ -56,13 +56,17 @@ 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())
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = itm.sourceAndDateText() binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate()
if (!appSettingsService.isFullHeightCardsEnabled()) { if (!appSettingsService.isFullHeightCardsEnabled()) {
binding.itemImage.maxHeight = imageMaxHeight binding.itemImage.maxHeight = imageMaxHeight
@ -132,8 +136,8 @@ class ItemCardAdapter(
private fun handleLinkOpening() { private fun handleLinkOpening() {
binding.root.setOnClickListener { binding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl( c.openItemUrl(
items,
bindingAdapterPosition, bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(), items[bindingAdapterPosition].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(), appSettingsService.isArticleViewerEnabled(),

View File

@ -51,7 +51,7 @@ class ItemListAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = itm.sourceAndDateText() binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate()
if (itm.getThumbnail(repository.baseUrl).isEmpty()) { if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
@ -84,8 +84,8 @@ class ItemListAdapter(
private fun handleLinkOpening() { private fun handleLinkOpening() {
binding.root.setOnClickListener { binding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl( c.openItemUrl(
items,
bindingAdapterPosition, bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(), items[bindingAdapterPosition].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(), appSettingsService.isArticleViewerEnabled(),

View File

@ -2,12 +2,15 @@ package bou.amine.apps.readerforselfossv2.android.adapters
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
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
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
@ -58,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()
} }
@ -78,9 +88,9 @@ class SourcesListAdapter(
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn) val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener { deleteBtn.setOnClickListener {
val (id) = items[bindingAdapterPosition] val (id, title) = items[bindingAdapterPosition]
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(id) val successfullyDeletedSource = repository.deleteSource(id, title)
if (successfullyDeletedSource) { if (successfullyDeletedSource) {
items.removeAt(bindingAdapterPosition) items.removeAt(bindingAdapterPosition)
notifyItemRemoved(bindingAdapterPosition) notifyItemRemoved(bindingAdapterPosition)
@ -94,6 +104,14 @@ class SourcesListAdapter(
} }
} }
} }
mView.setOnClickListener {
val source = items[bindingAdapterPosition]
repository.setSelectedSource(source)
app.startActivity(Intent(app, UpsertSourceActivity::class.java))
}
} }
} }
} }

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,13 +98,13 @@ 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
contentTitle = item.title.getHtmlDecoded() contentTitle = item.title.getHtmlDecoded()
contentImage = item.getThumbnail(repository.baseUrl) contentImage = item.getThumbnail(repository.baseUrl)
contentSource = item.sourceAndDateText() contentSource = item.sourceAuthorAndDate()
allImages = item.getImages() allImages = item.getImages()
fontSize = appSettingsService.getFontSize() fontSize = appSettingsService.getFontSize()
@ -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) {
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)
} }
} }
} }
@ -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")
}
}
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")
}
}
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")
}
}
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

@ -1,10 +1,12 @@
package bou.amine.apps.readerforselfossv2.android.fragments package bou.amine.apps.readerforselfossv2.android.fragments
import android.annotation.SuppressLint 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,10 +14,9 @@ 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.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@ -36,93 +37,38 @@ 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()
private val appSettingsService: AppSettingsService by instance()
private var selectedChip: Chip? = null private var selectedChip: Chip? = null
@SuppressLint("ResourceAsColor")
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
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
) )
val tagGroup = binding.tagsGroup val context: Context? = context
val sourceGroup = binding.sourcesGroup
CoroutineScope(Dispatchers.Main).launch {
val tags = repository.getTags()
tags.forEach { tag -> if (context == null) {
val c = chipForTag(tag) dismiss()
tagGroup.addView(c) Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} else {
CoroutineScope(Dispatchers.Main).launch {
handleTagChips(context)
handleSourceChips(context)
binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE
} }
repository.getSources().forEach { source ->
val c = Chip(requireContext())
Glide.with(requireContext())
.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.filterView.visibility = VISIBLE
} }
binding.floatingActionButton2.setOnClickListener { binding.floatingActionButton2.setOnClickListener {
@ -133,49 +79,135 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
return binding.root return binding.root
} }
private fun chipForTag(tag: SelfossModel.Tag): Chip { private suspend fun handleSourceChips(
val c = Chip(requireContext()) context: Context
c.text = tag.tag ) {
val sourceGroup = binding.sourcesGroup
val gd = GradientDrawable() repository.getSources().forEach { source ->
val gdColor = try { val c = Chip(context)
Color.parseColor(tag.color) c.ellipsize = TextUtils.TruncateAt.END
} 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 { Glide.with(context)
(it as Chip).isCloseIconVisible = false .load(source.getIcon(repository.baseUrl))
selectedChip = null .listener(object : RequestListener<Drawable?> {
repository.setTagFilter(null) override fun onLoadFailed(
} e: GlideException?,
model: Any?,
target: Target<Drawable?>?,
isFirstResource: Boolean
): Boolean {
return false
}
c.setOnClickListener { override fun onResourceReady(
if (selectedChip != null) { resource: Drawable?,
selectedChip!!.isCloseIconVisible = false 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)
} }
(it as Chip).isCloseIconVisible = true
selectedChip = it
repository.setTagFilter(tag)
repository.setSourceFilter(null) c.setOnClickListener {
} if (selectedChip != null) {
selectedChip!!.isCloseIconVisible = false
}
(it as Chip).isCloseIconVisible = true
selectedChip = it
repository.setSourceFilter(source)
if (repository.tagFilter.value?.equals(tag) == true) { repository.setTagFilter(null)
c.isCloseIconVisible = true }
selectedChip = c
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)
} }
return c
} }
companion object { companion object {
const val TAG = "ModalBottomSheet" 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

@ -16,7 +16,8 @@ fun SelfossModel.Item.toParcelable() : ParecelableItem =
this.icon, this.icon,
this.link, this.link,
this.sourcetitle, this.sourcetitle,
this.tags.joinToString(",") this.tags.joinToString(","),
this.author
) )
fun ParecelableItem.toModel() : SelfossModel.Item = fun ParecelableItem.toModel() : SelfossModel.Item =
SelfossModel.Item( SelfossModel.Item(
@ -30,7 +31,8 @@ fun ParecelableItem.toModel() : SelfossModel.Item =
this.icon, this.icon,
this.link, this.link,
this.sourcetitle, this.sourcetitle,
this.tags.split(",") this.tags.split(","),
this.author
) )
data class ParecelableItem( data class ParecelableItem(
val id: Int, val id: Int,
@ -43,7 +45,8 @@ data class ParecelableItem(
val icon: String?, val icon: String?,
val link: String, val link: String,
val sourcetitle: String, val sourcetitle: String,
val tags: String val tags: String,
val author: String?
) : Parcelable { ) : Parcelable {
companion object { companion object {
@ -65,7 +68,8 @@ data class ParecelableItem(
icon = source.readString(), icon = source.readString(),
link = source.readString().orEmpty(), link = source.readString().orEmpty(),
sourcetitle = source.readString().orEmpty(), sourcetitle = source.readString().orEmpty(),
tags = source.readString().orEmpty() tags = source.readString().orEmpty(),
author = source.readString().orEmpty()
) )
override fun describeContents() = 0 override fun describeContents() = 0
@ -82,5 +86,6 @@ data class ParecelableItem(
dest.writeString(link) dest.writeString(link)
dest.writeString(sourcetitle) dest.writeString(sourcetitle)
dest.writeString(tags) dest.writeString(tags)
dest.writeString(author)
} }
} }

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

@ -18,7 +18,6 @@ import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.openItemUrl( fun Context.openItemUrl(
allItems: ArrayList<SelfossModel.Item>,
currentItem: Int, currentItem: Int,
linkDecoded: String, linkDecoded: String,
articleViewer: Boolean, articleViewer: Boolean,
@ -33,7 +32,6 @@ fun Context.openItemUrl(
).show() ).show()
} else { } else {
if (articleViewer) { if (articleViewer) {
ReaderActivity.allItems = allItems
val intent = Intent(this, ReaderActivity::class.java) val intent = Intent(this, ReaderActivity::class.java)
intent.putExtra("currentItem", currentItem) intent.putExtra("currentItem", currentItem)
app.startActivity(intent) app.startActivity(intent)

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

@ -4,7 +4,7 @@
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"
tools:context="bou.amine.apps.readerforselfossv2.android.AddSourceActivity"> tools:context="bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -17,116 +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">
<TextView
android:text="@string/add_source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textView2"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textAlignment="center"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent"
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:gravity="center_horizontal" />
<EditText <EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:id="@+id/nameInput" android:id="@+id/nameInput"
android:layout_marginTop="32dp" android:layout_width="match_parent"
app:layout_constraintTop_toBottomOf="@+id/textView2" android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent" android:minHeight="48dp"
app:layout_constraintRight_toRightOf="parent" android:layout_marginTop="16dp"
android:inputType="text" android:autofillHints="false"
android:hint="@string/add_source_hint_name" android:hint="@string/add_source_hint_name"
android:textColorHint="?android:textColorPrimary"
android:autofillHints="false" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:ems="10"
android:id="@+id/sourceUri"
android:hint="@string/add_source_hint_url"
android:textColorHint="?android:textColorPrimary"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/nameInput"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:autofillHints="false" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:id="@+id/tags"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/sourceUri"
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_toTopOf="parent" />
<EditText
android:id="@+id/sourceUri"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginTop="16dp"
android:autofillHints="false"
android:hint="@string/add_source_hint_url"
android:inputType="textUri"
android:textColorHint="?android:textColorPrimary"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/nameInput" />
<EditText
android:id="@+id/tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginTop="16dp"
android:autofillHints="false"
android:hint="@string/add_source_hint_tags"
android:inputType="text"
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>
@ -135,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

@ -20,6 +20,11 @@
android:orderInCategory="2" android:orderInCategory="2"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item android:id="@+id/action_sources"
android:title="@string/menu_home_sources"
android:orderInCategory="97"
app:showAsAction="never"/>
<item android:id="@+id/action_settings" <item android:id="@+id/action_settings"
android:title="@string/title_activity_settings" android:title="@string/title_activity_settings"
android:orderInCategory="98" android:orderInCategory="98"

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

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -6,4 +6,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"禁用自动错误报告 "</string> <string name="pref_switch_disable_acra">"禁用自动错误报告 "</string>
<string name="menu_home_filter">筛选器</string> <string name="menu_home_filter">筛选器</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

View File

@ -126,4 +126,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources> </resources>

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

@ -129,4 +129,6 @@
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</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

@ -11,11 +11,14 @@ class DatesTest {
private val v3Date = "2013-04-07T13:43:00+01:00" private val v3Date = "2013-04-07T13:43:00+01:00"
private val v4Date = "2013-04-07 13:43:00" private val v4Date = "2013-04-07 13:43:00"
private val bug1Date = "2022-12-24T17:00:08+00"
@Test @Test
fun v3_date_should_be_parsed() { fun v3_date_should_be_parsed() {
val date = DateUtils.parseDate(v3Date) val date = DateUtils.parseDate(v3Date)
val expected = LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.of("UTC+1")) .toEpochMilliseconds() val expected =
LocalDateTime(2013, 4, 7, 14, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(date, expected) assertEquals(date, expected)
} }
@ -30,4 +33,14 @@ class DatesTest {
assertEquals(date, expected) assertEquals(date, expected)
} }
@Test
fun bug1_date_should_be_parsed() {
val date = DateUtils.parseDate(bug1Date)
val expected =
LocalDateTime(2022, 12, 24, 18, 0, 8, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(date, expected)
}
} }

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,19 +59,21 @@ 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,
data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED) data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED)
) )
every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit
every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems() every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems()
every { db.tagsQueries.deleteAllTags() } returns Unit every { db.tagsQueries.deleteAllTags() } returns Unit
every { db.tagsQueries.transaction(any(), any()) } returns Unit every { db.tagsQueries.transaction(any(), any()) } returns Unit
@ -68,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
@ -77,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() }
} }
@ -97,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
@ -228,9 +304,10 @@ class RepositoryTest {
1, 1,
"Test", "Test",
listOf("tags"), listOf("tags"),
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"b3aa8a664d08eb15d6ff1db2fa83e0d9.png" IMAGE_URL,
SelfossModel.SourceParams("url")
)) ))
runBlocking { runBlocking {
repository.getNewerItems() repository.getNewerItems()
@ -548,17 +625,19 @@ 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.Source( SelfossModel.Source(
2, 2,
"Second source", "Second source",
listOf("second"), listOf("second"),
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"b3aa8a664d08eb15d6ff1db2fa83e0d9.png" IMAGE_URL,
SelfossModel.SourceParams("url")
) )
) )
val sourcesDB = listOf( val sourcesDB = listOf(
@ -566,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"
) )
) )
@ -706,7 +787,7 @@ class RepositoryTest {
@Test @Test
fun create_source() { fun create_source() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository() initializeRepository()
@ -714,10 +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,
""
) )
} }
@ -727,7 +807,6 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any(),
) )
} }
assertSame(true, response) assertSame(true, response)
@ -735,7 +814,7 @@ class RepositoryTest {
@Test @Test
fun create_source_but_response_fails() { fun create_source_but_response_fails() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(false) SuccessResponse(false)
initializeRepository() initializeRepository()
@ -743,10 +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
""
) )
} }
@ -755,8 +833,7 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any(), any()
any(),
) )
} }
assertSame(false, response) assertSame(false, response)
@ -764,7 +841,7 @@ class RepositoryTest {
@Test @Test
fun create_source_without_connection() { fun create_source_without_connection() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
@ -772,10 +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
""
) )
} }
@ -785,7 +861,6 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any()
) )
} }
assertSame(false, response) assertSame(false, response)
@ -798,10 +873,11 @@ class RepositoryTest {
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.deleteSource(5) response = repository.deleteSource(5, "src")
} }
coVerify(exactly = 1) { api.deleteSource(5) } coVerify(exactly = 1) { api.deleteSource(5) }
coVerify(exactly = 1) { db.itemsQueries.deleteItemsWhereSource("src") }
assertSame(true, response) assertSame(true, response)
} }
@ -812,10 +888,11 @@ class RepositoryTest {
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.deleteSource(5) response = repository.deleteSource(5, "src")
} }
coVerify(exactly = 1) { api.deleteSource(5) } coVerify(exactly = 1) { api.deleteSource(5) }
coVerify(exactly = 0) { db.itemsQueries.deleteItemsWhereSource("src") }
assertSame(false, response) assertSame(false, response)
} }
@ -826,10 +903,11 @@ class RepositoryTest {
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.deleteSource(5) response = repository.deleteSource(5, "src")
} }
coVerify(exactly = 0) { api.deleteSource(5) } coVerify(exactly = 0) { api.deleteSource(5) }
coVerify(exactly = 1) { db.itemsQueries.deleteItemsWhereSource("src") }
assertSame(false, response) assertSame(false, response)
} }
@ -949,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"
) )
@ -1028,9 +1106,10 @@ 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")
) )
) )
repository.searchFilter = "search" repository.searchFilter = "search"

View File

@ -17,7 +17,8 @@ fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<I
icon = item.icon, icon = item.icon,
link = item.link, link = item.link,
sourcetitle = item.sourcetitle, sourcetitle = item.sourcetitle,
tags = item.tags tags = item.tags,
author = item.author
) )
) )
} }
@ -35,7 +36,8 @@ fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<S
icon = item.icon, icon = item.icon,
link = item.link, link = item.link,
sourcetitle = item.sourcetitle, sourcetitle = item.sourcetitle,
tags = item.tags.split(',') tags = item.tags.split(','),
author = item.author
) )
) )
} }
@ -54,4 +56,5 @@ class FakeItemParameters {
"https://ilblogdellasci.wordpress.com/2022/09/09/etica-della-ricerca-sotto-i-riflettori/" "https://ilblogdellasci.wordpress.com/2022/09/09/etica-della-ricerca-sotto-i-riflettori/"
var sourcetitle = "La Chimica e la Società" var sourcetitle = "La Chimica e la Società"
var tags = "Chimica, Testing" var tags = "Chimica, Testing"
var author = "Someone important"
} }

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,6 +17,7 @@ plugins {
allprojects { allprojects {
repositories { repositories {
// maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
google() google()
mavenCentral() mavenCentral()
jcenter() jcenter()

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,6 +2,7 @@ val pushCache: String by settings
pluginManagement { pluginManagement {
repositories { repositories {
// maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
google() google()
gradlePluginPortal() gradlePluginPortal()
mavenCentral() mavenCentral()
@ -10,6 +11,7 @@ pluginManagement {
dependencyResolutionManagement { dependencyResolutionManagement {
repositories { repositories {
// maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
google() google()
mavenCentral() mavenCentral()
} }

View File

@ -10,7 +10,11 @@ actual class DateUtils {
return try { return try {
Instant.parse(dateString).toEpochMilliseconds() Instant.parse(dateString).toEpochMilliseconds()
} catch (e: Exception) { } catch (e: Exception) {
LocalDateTime.parse(dateString.replace(" ", "T")).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() var str = dateString.replace(" ", "T")
if (str.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{2}".toRegex())) {
str = str.split("+")[0]
}
LocalDateTime.parse(str).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
} }
} }

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

@ -9,6 +9,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.encodeCollection
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
class SelfossModel { class SelfossModel {
@ -34,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
@ -55,9 +71,13 @@ class SelfossModel {
val tags: List<String>, val tags: List<String>,
val spout: String, val spout: String,
val error: String, val error: String,
val icon: String? val icon: String?,
val params: SourceParams?
)
@Serializable
data class SourceParams(
val url: String
) )
@Serializable @Serializable
data class Item( data class Item(
val id: Int, val id: Int,
@ -73,21 +93,16 @@ class SelfossModel {
val link: String, val link: String,
val sourcetitle: String, val sourcetitle: String,
@Serializable(with = TagsListSerializer::class) @Serializable(with = TagsListSerializer::class)
val tags: List<String> val tags: List<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")) {
@ -102,8 +117,14 @@ class SelfossModel {
return stringUrl return stringUrl
} }
fun sourceAndDateText(): String = fun sourceAuthorAndDate(): String {
this.sourcetitle.getHtmlDecoded() + DateUtils.parseRelativeDate(this.datetime) var txt = this.sourcetitle.getHtmlDecoded()
if (!this.author.isNullOrBlank()) {
txt += " (by ${this.author}) "
}
txt += DateUtils.parseRelativeDate(this.datetime)
return txt
}
fun toggleStar(): Item { fun toggleStar(): Item {
this.starred = !this.starred this.starred = !this.starred
@ -111,11 +132,12 @@ class SelfossModel {
} }
} }
// TODO: this seems to be super slow. // TODO: this seems to be super slow.
object TagsListSerializer : KSerializer<List<String>> { object TagsListSerializer : KSerializer<List<String>> {
override fun deserialize(decoder: Decoder): List<String> { override fun deserialize(decoder: Decoder): List<String> {
return when(val json = ((decoder as JsonDecoder).decodeJsonElement())) { return when(val json = ((decoder as JsonDecoder).decodeJsonElement())) {
is JsonArray -> json.toList().map { it.toString() } is JsonArray -> json.toList().map { it.toString().replace("^\"|\"$".toRegex(), "") }
else -> json.toString().split(",") else -> json.toString().split(",")
} }
@ -125,7 +147,7 @@ class SelfossModel {
get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING) get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: List<String>) { override fun serialize(encoder: Encoder, value: List<String>) {
TODO("Not yet implemented") encoder.encodeCollection(PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING), value.size) { this.toString() }
} }
} }

View File

@ -47,10 +47,11 @@ class Repository(
private var fetchedSources = false private var fetchedSources = false
private var fetchedTags = false private var fetchedTags = false
private var _readerItems = ArrayList<SelfossModel.Item>()
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,
@ -60,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
} }
@ -170,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()
@ -203,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)
@ -222,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)
@ -241,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)
@ -260,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)
@ -279,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
@ -341,7 +332,6 @@ class Repository(
url: String, url: String,
spout: String, spout: String,
tags: String, tags: String,
filter: String
): Boolean { ): Boolean {
var response = false var response = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
@ -350,20 +340,41 @@ class Repository(
url, url,
spout, spout,
tags, tags,
filter
).isSuccess == true ).isSuccess == true
} }
return response return response
} }
suspend fun deleteSource(id: Int): Boolean { suspend fun updateSource(
id: Int,
title: String,
url: String,
spout: String,
tags: String
): Boolean {
var response = false
if (isNetworkAvailable()) {
response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true
}
return response
}
suspend fun deleteSource(id: Int, title: String): Boolean {
var success = false var success = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val response = api.deleteSource(id) val response = api.deleteSource(id)
success = response.isSuccess success = response.isSuccess
} }
// We filter on success or if the network isn't available
if (success || !isNetworkAvailable()) {
items = ArrayList(items.filter { it.sourcetitle != title })
setReaderItems(items)
db.itemsQueries.deleteItemsWhereSource(title)
}
return success return success
} }
@ -428,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)
}
} }
} }
} }
@ -502,6 +523,7 @@ class Repository(
item.link, item.link,
item.sourcetitle, item.sourcetitle,
item.tags.joinToString(","), item.tags.joinToString(","),
item.author,
item.id.toString() item.id.toString()
) )
@ -518,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()
@ -558,4 +579,28 @@ class Repository(
fun setSourceFilter(source: SelfossModel.Source?) { fun setSourceFilter(source: SelfossModel.Source?) {
_sourceFilter.value = source _sourceFilter.value = source
} }
fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) {
_readerItems = readerItems
}
fun getReaderItems(): ArrayList<SelfossModel.Item> {
return _readerItems
}
fun migrate(driverFactory: DriverFactory) {
ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
}
fun setSelectedSource(source: SelfossModel.Source) {
_selectedSource = source
}
fun unsetSelectedSource() {
_selectedSource = null
}
fun getSelectedSource(): SelfossModel.Source? {
return _selectedSource
}
} }

View File

@ -23,6 +23,9 @@ suspend fun maybeResponse(r: HttpResponse?): SuccessResponse {
return if (r != null && r.status.isSuccess()) { return if (r != null && r.status.isSuccess()) {
r.body() r.body()
} else { } else {
if (r != null) {
Napier.i("Error ${r.status}", tag = "maybeResponse")
}
SuccessResponse(false) SuccessResponse(false)
} }
} }

View File

@ -47,7 +47,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
install(HttpCookies) install(HttpCookies)
install(HttpRequestRetry) { install(HttpRequestRetry) {
maxRetries = 1 maxRetries = 2
retryIf { _, response -> retryIf { _, response ->
response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo() response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo()
} }
@ -76,10 +76,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
// Api version was introduces after the POST login, so when there is a version, it should be available // Api version was introduces after the POST login, so when there is a version, it should be available
private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1 private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
private fun hasLoginInfo() = appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword().isNotEmpty() private fun hasLoginInfo() =
appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword()
.isNotEmpty()
suspend fun login(): SuccessResponse = suspend fun login(): SuccessResponse =
if (appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword().isNotEmpty()) { if (appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword()
.isNotEmpty()
) {
if (shouldHavePostLogin()) { if (shouldHavePostLogin()) {
postLogin() postLogin()
} else { } else {
@ -99,7 +103,9 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) })
private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0 private fun shouldHaveNewLogout() =
appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
suspend fun logout(): SuccessResponse = suspend fun logout(): SuccessResponse =
if (shouldHaveNewLogout()) { if (shouldHaveNewLogout()) {
doLogout() doLogout()
@ -107,7 +113,8 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
maybeLogoutIfAvailable() maybeLogoutIfAvailable()
} }
private suspend fun maybeLogoutIfAvailable() = responseOrSuccessIf404(client.tryToGet(url("/logout"))) private suspend fun maybeLogoutIfAvailable() =
responseOrSuccessIf404(client.tryToGet(url("/logout")))
private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current"))) private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current")))
@ -184,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 =
@ -236,13 +243,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
url: String, url: String,
spout: String, spout: String,
tags: String, tags: String,
filter: String
): SuccessResponse = ): SuccessResponse =
maybeResponse( maybeResponse(
if (appSettingsService.getApiVersion() > 1) { if (appSettingsService.getApiVersion() > 1) {
createSource("tags[]", title, url, spout, tags, filter) createSource("tags[]", title, url, spout, tags)
} else { } else {
createSource("tags", title, url, spout, tags, filter) createSource("tags", title, url, spout, tags)
} }
) )
@ -251,8 +257,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
title: String, title: String,
url: String, url: String,
spout: String, spout: String,
tags: String, tags: String
filter: String
): HttpResponse? = ): HttpResponse? =
client.tryToSubmitForm( client.tryToSubmitForm(
url = url("/source"), url = url("/source"),
@ -265,7 +270,43 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
append("url", url) append("url", url)
append("spout", spout) append("spout", spout)
append(tagsParamName, tags) append(tagsParamName, tags)
append("filter", filter) }
)
suspend fun updateSourceForVersion(
id: Int,
title: String,
url: String,
spout: String,
tags: String
): SuccessResponse =
maybeResponse(
if (appSettingsService.getApiVersion() > 1) {
updateSource(id, "tags[]", title, url, spout, tags)
} else {
updateSource(id, "tags", title, url, spout, tags)
}
)
private suspend fun updateSource(
id: Int,
tagsParamName: String,
title: String,
url: String,
spout: String,
tags: String,
): HttpResponse? =
client.tryToSubmitForm(
url = url("/source/$id"),
formParameters = Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
append("title", title)
append("url", url)
append("spout", spout)
append(tagsParamName, tags)
} }
) )

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

@ -19,7 +19,8 @@ fun SOURCE.toView(): SelfossModel.Source =
this.tags.split(","), this.tags.split(","),
this.spout, this.spout,
this.error, this.error,
this.icon this.icon,
if (this.url != null) SelfossModel.SourceParams(this.url) else null
) )
fun SelfossModel.Source.toEntity(): SOURCE = fun SelfossModel.Source.toEntity(): SOURCE =
@ -29,7 +30,8 @@ fun SelfossModel.Source.toEntity(): SOURCE =
this.tags.joinToString(","), this.tags.joinToString(","),
this.spout, this.spout,
this.error, this.error,
this.icon.orEmpty() this.icon.orEmpty(),
this.params?.url
) )
fun SelfossModel.Tag.toEntity(): TAG = fun SelfossModel.Tag.toEntity(): TAG =
@ -51,7 +53,8 @@ fun ITEM.toView(): SelfossModel.Item =
this.icon, this.icon,
this.link, this.link,
this.sourcetitle, this.sourcetitle,
this.tags.split(",") this.tags.split(","),
this.author
) )
fun SelfossModel.Item.toEntity(): ITEM = fun SelfossModel.Item.toEntity(): ITEM =
@ -66,5 +69,6 @@ fun SelfossModel.Item.toEntity(): ITEM =
this.icon, this.icon,
this.link, this.link,
this.sourcetitle.getHtmlDecoded(), this.sourcetitle.getHtmlDecoded(),
this.tags.joinToString(",") this.tags.joinToString(","),
this.author
) )

View File

@ -0,0 +1,6 @@
CREATE TABLE ITEM_BACKUP AS SELECT `id`, `datetime`, `title`, `content`,
`unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`,
`tags` FROM ITEM;
ALTER TABLE ITEM_BACKUP ADD COLUMN `author` TEXT;
DROP TABLE ITEM;
ALTER TABLE ITEM_BACKUP RENAME TO ITEM;

View File

@ -0,0 +1 @@
ALTER TABLE SOURCE ADD COLUMN `url` TEXT;

View File

@ -10,6 +10,7 @@ CREATE TABLE ITEM (
`link` TEXT NOT NULL, `link` TEXT NOT NULL,
`sourcetitle` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL,
`tags` TEXT NOT NULL, `tags` TEXT NOT NULL,
`author` TEXT,
PRIMARY KEY(`id`) PRIMARY KEY(`id`)
); );
@ -26,5 +27,8 @@ INSERT OR REPLACE INTO ITEM VALUES ?;
deleteItem: deleteItem:
DELETE FROM ITEM WHERE `id` = ?; DELETE FROM ITEM WHERE `id` = ?;
deleteItemsWhereSource:
DELETE FROM ITEM WHERE `sourcetitle` = ?;
updateItem: updateItem:
UPDATE ITEM SET `datetime` = ?, `title` = ?, `content` = ?, `unread` = ?, `starred` = ?, `thumbnail` = ?, `icon` = ?, `link` = ?, `sourcetitle` = ?, `tags` = ? WHERE `id` = ?; UPDATE ITEM SET `datetime` = ?, `title` = ?, `content` = ?, `unread` = ?, `starred` = ?, `thumbnail` = ?, `icon` = ?, `link` = ?, `sourcetitle` = ?, `tags` = ?, `author` = ? WHERE `id` = ?;

View File

@ -5,6 +5,7 @@ CREATE TABLE SOURCE (
`spout` TEXT NOT NULL, `spout` TEXT NOT NULL,
`error` TEXT NOT NULL, `error` TEXT NOT NULL,
`icon` TEXT NOT NULL, `icon` TEXT NOT NULL,
`url` TEXT,
PRIMARY KEY(`id`) PRIMARY KEY(`id`)
); );