Compare commits

..

No commits in common. "629ca01d9958ff8bb5ffc1a967c27d20a1f9a59d" and "e1c64cef46a37506b024ec6ed0125b9f0464eba9" have entirely different histories.

17 changed files with 207 additions and 409 deletions

View File

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

View File

@ -1,62 +1,3 @@
**v123030751**
- debug: added a lot to pinpoint the url issue.
- feat: Use /sources/stats in the home (#133)
- Changelog for v123030681 [CI SKIP]
--------------------------------------------------------------------
**v123030681**
- fix: Unread and starred can be null.
- Fixed version number issue.
- Changelog for v123030621 [CI SKIP]
--------------------------------------------------------------------
**v123030621**
- fix: url required issue.
- fix: Canvas reused issue.
- Changelog for v123020572 [CI SKIP]
--------------------------------------------------------------------
**v123020572**
- fix: requirecontext issues ?
- debug: activity not found exception.
- Changelog for v123020571 [CI SKIP]
--------------------------------------------------------------------
**v123020571**
- chore: remove errors logging.
- fix: quickfix for url param not provided for some sources.
- Update 'CHANGELOG.md'
- Changelog for v123020523 [CI SKIP]
--------------------------------------------------------------------
**v123020523**
- fix: Git changelog.
--------------------------------------------------------------------
**v123020491**
- fix: Fixed acra bug reporting.
--------------------------------------------------------------------
**v123010301**
- Chore: acra config.
--------------------------------------------------------------------
**v123010281** **v123010281**
- improvement: Improve right to left support (#130) Co-authored-by: davidoskky <davidoskky@hidden.hidden> Co-committed-by: davidoskky <davidoskky@hidden.hidden> - improvement: Improve right to left support (#130) Co-authored-by: davidoskky <davidoskky@hidden.hidden> Co-committed-by: davidoskky <davidoskky@hidden.hidden>

View File

@ -28,7 +28,7 @@ fun gitVersion(): String {
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true) val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
process = if (maybeTagOfCurrentCommit.isEmpty()) { process = if (maybeTagOfCurrentCommit.isEmpty()) {
println("No tag on current commit. Will take the latest one.") println("No tag on current commit. Will take the latest one.")
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1") execWithOutput("git -C ../ for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1")
} else { } else {
println("Tag found on current commit") println("Tag found on current commit")
execWithOutput("git -C ../ describe --contains HEAD") execWithOutput("git -C ../ describe --contains HEAD")
@ -145,8 +145,8 @@ dependencies {
implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1") implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1")
// glide // glide
kapt("com.github.bumptech.glide:compiler:4.15.0") kapt("com.github.bumptech.glide:compiler:4.14.2")
implementation("com.github.bumptech.glide:okhttp3-integration:4.15.0") implementation("com.github.bumptech.glide:okhttp3-integration:4.14.2")
// Themes // Themes
implementation("com.github.rubensousa:floatingtoolbar:1.5.1") implementation("com.github.rubensousa:floatingtoolbar:1.5.1")

View File

@ -34,7 +34,6 @@ 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()) }
@ -131,8 +130,8 @@ class MyApp : MultiDexApplication(), DIAware {
httpSender { httpSender {
uri = uri =
"https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/ "https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/
basicAuthLogin = "qMEscjj89Gwt6cPR" basicAuthLogin = "LMTlLZuazADohTCm"
basicAuthPassword = "Yo58QFlGzFaWlBzP" basicAuthPassword = "he6ghHp83F0PYPfh"
httpMethod = HttpSender.Method.POST httpMethod = HttpSender.Method.POST
} }
} }

View File

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

View File

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

View File

@ -31,7 +31,7 @@ import org.kodein.di.instance
class SourcesListAdapter( class SourcesListAdapter(
private val app: Activity, private val app: Activity,
private val items: ArrayList<SelfossModel.SourceDetail> private val items: ArrayList<SelfossModel.Source>
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware { ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL private val generator: ColorGenerator = ColorGenerator.MATERIAL
@ -61,7 +61,7 @@ class SourcesListAdapter(
c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
} }
if (!itm.error.isNullOrBlank()) { if (itm.error.isNotBlank()) {
binding.errorText.visibility = View.VISIBLE binding.errorText.visibility = View.VISIBLE
binding.errorText.text = itm.error binding.errorText.text = itm.error
} else { } else {

View File

@ -1,6 +1,5 @@
package bou.amine.apps.readerforselfossv2.android.fragments package bou.amine.apps.readerforselfossv2.android.fragments
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.TypedArray import android.content.res.TypedArray
@ -151,19 +150,17 @@ class ArticleFragment : Fragment(), DIAware {
} catch (e: InflateException) { } catch (e: InflateException) {
e.sendSilentlyWithAcraWithName("webview not available") e.sendSilentlyWithAcraWithName("webview not available")
if (context != null) { 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(
.setPositiveButton( android.R.string.ok
android.R.string.ok ) { _, _ ->
) { _, _ -> appSettingsService.disableArticleViewer()
appSettingsService.disableArticleViewer() requireActivity().finish()
requireActivity().finish() }
} .create()
.create() .show()
.show()
}
} }
return binding.root return binding.root
@ -307,16 +304,10 @@ class ArticleFragment : Fragment(), DIAware {
binding.webcontent.webViewClient = object : WebViewClient() { binding.webcontent.webViewClient = object : WebViewClient() {
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean { override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean {
return if (context != null && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
try { requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (e: ActivityNotFoundException) {
e.sendSilentlyWithAcraWithName("activityNotFound > $url")
}
true
} else {
false
} }
return true
} }
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
@ -334,7 +325,7 @@ class ArticleFragment : Fragment(), DIAware {
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG) getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)
) )
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg > $url")
} }
} else if (url.lowercase(Locale.US).contains(".png")) { } else if (url.lowercase(Locale.US).contains(".png")) {
try { try {
@ -346,7 +337,7 @@ class ArticleFragment : Fragment(), DIAware {
getBitmapInputStream(image, Bitmap.CompressFormat.PNG) getBitmapInputStream(image, Bitmap.CompressFormat.PNG)
) )
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing e.sendSilentlyWithAcraWithName("shouldInterceptRequest > png > $url")
} }
} else if (url.lowercase(Locale.US).contains(".webp")) { } else if (url.lowercase(Locale.US).contains(".webp")) {
try { try {
@ -358,7 +349,7 @@ class ArticleFragment : Fragment(), DIAware {
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP) getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)
) )
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing e.sendSilentlyWithAcraWithName("shouldInterceptRequest > webp > $url")
} }
} }
@ -368,74 +359,74 @@ class ArticleFragment : Fragment(), DIAware {
} }
private fun htmlToWebview() { private fun htmlToWebview() {
if (context != null) {
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily) val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs) val a: TypedArray = 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)
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
handleImageLoading() handleImageLoading()
val gestureDetector = val gestureDetector =
GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() { GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean { override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick() return performClick()
} }
}) })
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) } binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
binding.webcontent.settings.layoutAlgorithm = binding.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
var baseUrl: String? = null var baseUrl: String? = null
try { try {
val itemUrl = URL(url) val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) { } catch (e: MalformedURLException) {
e.sendSilentlyWithAcraWithName("htmlToWebview > $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"
else -> "" else -> ""
} }
val fontLinkAndStyle = if (font.isNotEmpty()) { val fontLinkAndStyle = if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${ """<link href="https://fonts.googleapis.com/css?family=${
fontName.replace( fontName.replace(
" ", " ",
"+" "+"
) )
}" rel="stylesheet"> }" rel="stylesheet">
|<style> |<style>
| * { | * {
| font-family: '$fontName'; | font-family: '$fontName';
| } | }
|</style> |</style>
""".trimMargin() """.trimMargin()
} else { } else {
"" ""
} }
binding.webcontent.loadDataWithBaseURL( binding.webcontent.loadDataWithBaseURL(
baseUrl, baseUrl,
"""<html> """<html>
|<head> |<head>
| <meta name="viewport" content="width=device-width, initial-scale=1"> | <meta name="viewport" content="width=device-width, initial-scale=1">
| <style> | <style>
@ -447,11 +438,11 @@ class ArticleFragment : Fragment(), DIAware {
| } | }
| a { | a {
| color: ${ | color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and resources.getColor(R.color.colorAccent) 0xFFFFFF and resources.getColor(R.color.colorAccent)
) )
} !important; } !important;
| } | }
| *:not(a) { | *:not(a) {
| color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)}; | color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)};
@ -463,25 +454,25 @@ class ArticleFragment : Fragment(), DIAware {
| overflow:hidden; | overflow:hidden;
| line-height: 1.5em; | line-height: 1.5em;
| background-color: ${ | background-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data 0xFFFFFF and colorSurface.data
) )
}; };
| } | }
| body, html { | body, html {
| background-color: ${ | background-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data 0xFFFFFF and colorSurface.data
) )
} !important; } !important;
| border-color: ${ | border-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data 0xFFFFFF and colorSurface.data
) )
} !important; } !important;
| padding: 0 !important; | padding: 0 !important;
| margin: 0 !important; | margin: 0 !important;
| } | }
@ -492,11 +483,11 @@ class ArticleFragment : Fragment(), DIAware {
| white-space: pre-wrap; | white-space: pre-wrap;
| width:100%; | width:100%;
| background-color: ${ | background-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data 0xFFFFFF and colorSurface.data
) )
}; };
| } | }
| </style> | </style>
| $fontLinkAndStyle | $fontLinkAndStyle
@ -504,11 +495,10 @@ class ArticleFragment : Fragment(), DIAware {
|<body> |<body>
| $contentText | $contentText
|</body>""".trimMargin(), |</body>""".trimMargin(),
"text/html", "text/html",
"utf-8", "utf-8",
null null
) )
}
} }
fun scrollDown() { fun scrollDown() {

View File

@ -20,8 +20,10 @@ import bou.amine.apps.readerforselfossv2.repository.Repository
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
import com.bumptech.glide.request.target.ViewTarget import com.bumptech.glide.load.DataSource
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -82,25 +84,37 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
) { ) {
val sourceGroup = binding.sourcesGroup val sourceGroup = binding.sourcesGroup
repository.getSourcesDetailsOrStats().forEach { source -> repository.getSources().forEach { source ->
val c = Chip(context) val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END c.ellipsize = TextUtils.TruncateAt.END
Glide.with(context) Glide.with(context)
.load(source.getIcon(repository.baseUrl)) .load(source.getIcon(repository.baseUrl))
.into(object : ViewTarget<Chip?, Drawable?>(c) { .listener(object : RequestListener<Drawable?> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable?>?,
isFirstResource: Boolean
): Boolean {
return false
}
override fun onResourceReady( override fun onResourceReady(
resource: Drawable, resource: Drawable?,
transition: Transition<in Drawable?>? model: Any?,
) { target: Target<Drawable?>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
try { try {
c.chipIcon = resource c.chipIcon = resource
} catch (e: Exception) { } catch (e: Exception) {
e.sendSilentlyWithAcraWithName("sources > onResourceReady") e.sendSilentlyWithAcraWithName("sources > onResourceReady")
} }
return false
} }
}).preload()
})
c.text = source.title.getHtmlDecoded() c.text = source.title.getHtmlDecoded()
@ -127,9 +141,9 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
selectedChip = c selectedChip = c
} }
c.isEnabled = source.error.isNullOrBlank() c.isEnabled = source.error.isBlank()
if (!source.error.isNullOrBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (source.error.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
c.tooltipText = source.error c.tooltipText = source.error
} }

View File

@ -300,10 +300,9 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
repository.setSourceFilter(SelfossModel.SourceDetail( repository.setSourceFilter(SelfossModel.Source(
1, 1,
"Test", "Test",
null,
listOf("tags"), listOf("tags"),
SPOUT, SPOUT,
"", "",
@ -610,32 +609,30 @@ class RepositoryTest {
fun get_sources() { fun get_sources() {
val (sources, sourcesDB) = prepareSources() val (sources, sourcesDB) = prepareSources()
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>?
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sources, testSources) assertSame(sources, testSources)
assertNotEquals(sourcesDB.map { it.toView() }, testSources) assertNotEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 1) { api.sourcesDetailed() } coVerify(exactly = 1) { api.sources() }
} }
private fun prepareSources(): Pair<ArrayList<SelfossModel.SourceDetail>, List<SOURCE>> { private fun prepareSources(): Pair<ArrayList<SelfossModel.Source>, List<SOURCE>> {
val sources = arrayListOf( val sources = arrayListOf(
SelfossModel.SourceDetail( SelfossModel.Source(
1, 1,
"First source", "First source",
null,
listOf("Test", "second"), listOf("Test", "second"),
SPOUT, SPOUT,
"", "",
IMAGE_URL_2, IMAGE_URL_2,
SelfossModel.SourceParams("url") SelfossModel.SourceParams("url")
), ),
SelfossModel.SourceDetail( SelfossModel.Source(
2, 2,
"Second source", "Second source",
null,
listOf("second"), listOf("second"),
SPOUT, SPOUT,
"", "",
@ -664,7 +661,7 @@ class RepositoryTest {
) )
) )
coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources) coEvery { api.sources() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
return Pair(sources, sourcesDB) return Pair(sources, sourcesDB)
} }
@ -678,13 +675,13 @@ class RepositoryTest {
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source>? var testSources: List<SelfossModel.Source>?
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSources()
// Sources will be fetched from the database on the second call, thus testSources != sources // Sources will be fetched from the database on the second call, thus testSources != sources
testSources = repository.getSourcesDetails() testSources = repository.getSources()
} }
coVerify(exactly = 1) { api.sourcesDetailed() } coVerify(exactly = 1) { api.sources() }
assertNotEquals(sources, testSources) assertNotSame(sources, testSources)
assertEquals(sourcesDB.map { it.toView() }, testSources) assertEquals(sourcesDB.map { it.toView() }, testSources)
verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
} }
@ -696,13 +693,13 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>?
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sources, testSources) assertSame(sources, testSources)
coVerify(exactly = 1) { api.sourcesDetailed() } coVerify(exactly = 1) { api.sources() }
verify(exactly = 0) { db.sourcesQueries } verify(exactly = 0) { db.sourcesQueries }
} }
@ -713,13 +710,13 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
initializeRepository() initializeRepository()
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>?
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sources, testSources) assertSame(sources, testSources)
coVerify(exactly = 1) { api.sourcesDetailed() } coVerify(exactly = 1) { api.sources() }
verify(atLeast = 1) { db.sourcesQueries } verify(atLeast = 1) { db.sourcesQueries }
} }
@ -727,13 +724,13 @@ class RepositoryTest {
fun get_sources_without_connection() { fun get_sources_without_connection() {
val (_, sourcesDB) = prepareSources() val (_, sourcesDB) = prepareSources()
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>?
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sourcesDB.map { it.toView() }, testSources) assertEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 0) { api.sourcesDetailed() } coVerify(exactly = 0) { api.sources() }
verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
} }
@ -744,13 +741,13 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>?
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(emptyList<SelfossModel.Source>(), testSources) assertEquals(emptyList<SelfossModel.Source>(), testSources)
coVerify(exactly = 0) { api.sourcesDetailed() } coVerify(exactly = 0) { api.sources() }
verify(exactly = 0) { db.sourcesQueries.sources().executeAsList() } verify(exactly = 0) { db.sourcesQueries.sources().executeAsList() }
} }
@ -761,13 +758,13 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>?
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sourcesDB.map { it.toView() }, testSources) assertEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 0) { api.sourcesDetailed() } coVerify(exactly = 0) { api.sources() }
verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
} }
@ -778,13 +775,13 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>?
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSources()
} }
assertEquals(sourcesDB.map { it.toView() }, testSources) assertEquals(sourcesDB.map { it.toView() }, testSources)
coVerify(exactly = 0) { api.sourcesDetailed() } coVerify(exactly = 0) { api.sources() }
verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() }
} }
@ -1105,10 +1102,9 @@ class RepositoryTest {
private fun prepareSearch() { private fun prepareSearch() {
repository.setTagFilter(SelfossModel.Tag("Tag", "read", 0)) repository.setTagFilter(SelfossModel.Tag("Tag", "read", 0))
repository.setSourceFilter( repository.setSourceFilter(
SelfossModel.SourceDetail( SelfossModel.Source(
1, 1,
"First source", "First source",
5,
listOf("Test", "second"), listOf("Test", "second"),
SPOUT, SPOUT,
"", "",

View File

@ -9,6 +9,7 @@ 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

@ -24,8 +24,8 @@ class SelfossModel {
@Serializable @Serializable
class Stats( class Stats(
val total: Int, val total: Int,
val unread: Int?, val unread: Int,
val starred: Int? val starred: Int
) )
@Serializable @Serializable
@ -63,39 +63,20 @@ class SelfossModel {
fun isPublicModeEnabled() = publicMode ?: false fun isPublicModeEnabled() = publicMode ?: false
} }
interface Source {
val id: Int
var title: String
var unread: Int?
var error: String?
var icon: String?
}
@Serializable @Serializable
data class SourceStats( data class Source(
override val id: Int, val id: Int,
override var title: String, val title: String,
override var unread: Int?,
override var error: String? = null,
override var icon: String? = null
) : Source
@Serializable
data class SourceDetail(
override val id: Int,
override var title: String,
override var unread: Int? = null,
@Serializable(with = TagsListSerializer::class) @Serializable(with = TagsListSerializer::class)
var tags: List<String>?, val tags: List<String>,
var spout: String?, val spout: String,
override var error: String?, val error: String,
override var icon: String?, val icon: String?,
var params: SourceParams? val params: SourceParams?
) : Source )
@Serializable @Serializable
data class SourceParams( data class SourceParams(
val url: String? = null val url: String
) )
@Serializable @Serializable
data class Item( data class Item(

View File

@ -44,11 +44,11 @@ class Repository(
private val _badgeStarred = MutableStateFlow(0) private val _badgeStarred = MutableStateFlow(0)
val badgeStarred = _badgeStarred.asStateFlow() val badgeStarred = _badgeStarred.asStateFlow()
private var fetchedTags = false
private var fetchedSources = false private var fetchedSources = false
private var fetchedTags = false
private var _readerItems = ArrayList<SelfossModel.Item>() private var _readerItems = ArrayList<SelfossModel.Item>()
private var _selectedSource: SelfossModel.SourceDetail? = null private var _selectedSource: SelfossModel.Source? = null
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
@ -132,9 +132,9 @@ class Repository(
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val response = api.stats() val response = api.stats()
if (response.success && response.data != null) { if (response.success && response.data != null) {
_badgeUnread.value = response.data.unread ?: 0 _badgeUnread.value = response.data.unread
_badgeAll.value = response.data.total _badgeAll.value = response.data.total
_badgeStarred.value = response.data.starred ?: 0 _badgeStarred.value = response.data.starred
success = true success = true
} }
} else if (appSettingsService.isItemCachingEnabled()) { } else if (appSettingsService.isItemCachingEnabled()) {
@ -180,46 +180,23 @@ class Repository(
} }
} }
suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> { suspend fun getSources(): ArrayList<SelfossModel.Source> {
var sources = ArrayList<SelfossModel.Source>()
val isDatabaseEnabled = val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true return if (isNetworkAvailable() && !fetchedSources) {
if (shouldFetch && isNetworkAvailable()) { val apiSources = api.sources()
if (appSettingsService.getPublicAccess()) { if (apiSources.success && apiSources.data != null && isDatabaseEnabled) {
val apiSources = api.sourcesStats() resetDBSourcesWithData(apiSources.data)
if (apiSources.success && apiSources.data != null) { if (!appSettingsService.isUpdateSourcesEnabled()) {
fetchedSources = true fetchedSources = true
sources = apiSources.data as ArrayList<SelfossModel.Source>
}
} else {
sources = getSourcesDetails() as ArrayList<SelfossModel.Source>
}
} else if (isDatabaseEnabled) {
sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.Source>
}
return sources
}
suspend fun getSourcesDetails(): ArrayList<SelfossModel.SourceDetail> {
var sources = ArrayList<SelfossModel.SourceDetail>()
val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
if (shouldFetch && isNetworkAvailable()) {
val apiSources = api.sourcesDetailed()
if (apiSources.success && apiSources.data != null) {
fetchedSources = true
sources = apiSources.data
if (isDatabaseEnabled) {
resetDBSourcesWithData(sources)
} }
} }
apiSources.data ?: ArrayList()
} else if (isDatabaseEnabled) { } else if (isDatabaseEnabled) {
sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.SourceDetail> ArrayList(getDBSources().map { it.toView() })
} else {
ArrayList()
} }
return sources
} }
suspend fun markAsRead(item: SelfossModel.Item): Boolean { suspend fun markAsRead(item: SelfossModel.Item): Boolean {
@ -505,7 +482,7 @@ class Repository(
} }
} }
private fun resetDBSourcesWithData(sources: List<SelfossModel.SourceDetail>) { private fun resetDBSourcesWithData(sources: List<SelfossModel.Source>) {
db.sourcesQueries.deleteAllSources() db.sourcesQueries.deleteAllSources()
db.sourcesQueries.transaction { db.sourcesQueries.transaction {
@ -615,7 +592,7 @@ class Repository(
ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1) ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
} }
fun setSelectedSource(source: SelfossModel.SourceDetail) { fun setSelectedSource(source: SelfossModel.Source) {
_selectedSource = source _selectedSource = source
} }
@ -623,7 +600,7 @@ class Repository(
_selectedSource = null _selectedSource = null
} }
fun getSelectedSource(): SelfossModel.SourceDetail? { fun getSelectedSource(): SelfossModel.Source? {
return _selectedSource return _selectedSource
} }
} }

View File

@ -183,15 +183,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
}) })
suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> = suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> =
bodyOrFailure(client.tryToGet(url("/sources/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
})
suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> =
bodyOrFailure(client.tryToGet(url("/sources/list")) { bodyOrFailure(client.tryToGet(url("/sources/list")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())

View File

@ -1,70 +0,0 @@
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

@ -2,8 +2,8 @@ package bou.amine.apps.readerforselfossv2.service
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
class AppSettingsService(acraSenderServiceProcess: Boolean = false) { class AppSettingsService {
val settings: Settings = if (acraSenderServiceProcess) { ACRASettings() } else { Settings() } val settings: Settings = Settings()
// Api related // Api related
private var _apiVersion: Int = -1 private var _apiVersion: Int = -1

View File

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