Compare commits
	
		
			11 Commits
		
	
	
		
			2c558fe6fd
			...
			v123030751
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 08f79cb148 | ||
| e21906e70d | |||
|  | 9d2cc32bc9 | ||
|  | d9d057c8dc | ||
|  | 1f3fa0c4a6 | ||
|  | dea3def385 | ||
|  | f72ef2f5d4 | ||
|  | f28cb759df | ||
|  | b9d69c3e64 | ||
|  | c2a1c9eaac | ||
|  | bf37209a15 | 
							
								
								
									
										24
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,27 @@ | ||||
| **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. | ||||
|   | ||||
| @@ -28,7 +28,7 @@ fun gitVersion(): String { | ||||
|     val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true) | ||||
|     process = if (maybeTagOfCurrentCommit.isEmpty()) { | ||||
|         println("No tag on current commit. Will take the latest one.") | ||||
|         execWithOutput("git -C ../ for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1") | ||||
|         execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1") | ||||
|     } else { | ||||
|         println("Tag found on current commit") | ||||
|         execWithOutput("git -C ../ describe --contains HEAD") | ||||
| @@ -145,8 +145,8 @@ dependencies { | ||||
|     implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1") | ||||
|  | ||||
|     // glide | ||||
|     kapt("com.github.bumptech.glide:compiler:4.14.2") | ||||
|     implementation("com.github.bumptech.glide:okhttp3-integration:4.14.2") | ||||
|     kapt("com.github.bumptech.glide:compiler:4.15.0") | ||||
|     implementation("com.github.bumptech.glide:okhttp3-integration:4.15.0") | ||||
|  | ||||
|     // Themes | ||||
|     implementation("com.github.rubensousa:floatingtoolbar:1.5.1") | ||||
|   | ||||
| @@ -49,13 +49,13 @@ class SourcesActivity : AppCompatActivity(), DIAware { | ||||
|         super.onResume() | ||||
|         val mLayoutManager = LinearLayoutManager(this) | ||||
|  | ||||
|         var items: ArrayList<SelfossModel.Source> | ||||
|         var items: ArrayList<SelfossModel.SourceDetail> | ||||
|  | ||||
|         binding.recyclerView.setHasFixedSize(true) | ||||
|         binding.recyclerView.layoutManager = mLayoutManager | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             val response = repository.getSources() | ||||
|             val response = repository.getSourcesDetails() | ||||
|             if (response.isNotEmpty()) { | ||||
|                 items = response | ||||
|                 val mAdapter = SourcesListAdapter( | ||||
|   | ||||
| @@ -24,7 +24,7 @@ import org.kodein.di.instance | ||||
|  | ||||
| class UpsertSourceActivity : AppCompatActivity(), DIAware { | ||||
|  | ||||
|     private var existingSource: SelfossModel.Source? = null | ||||
|     private var existingSource: SelfossModel.SourceDetail? = null | ||||
|     private var mSpoutsValue: String? = null | ||||
|  | ||||
|     private lateinit var binding: ActivityUpsertSourceBinding | ||||
| @@ -68,7 +68,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware { | ||||
|  | ||||
|     private fun initFields(items: Map<String, SelfossModel.Spout>) { | ||||
|         binding.nameInput.setText(existingSource!!.title) | ||||
|         binding.tags.setText(existingSource!!.tags.joinToString(", ")) | ||||
|         binding.tags.setText(existingSource!!.tags?.joinToString(", ")) | ||||
|         binding.sourceUri.setText(existingSource!!.params?.url) | ||||
|         binding.spoutsSpinner.setSelection(items.keys.indexOf(existingSource!!.spout)) | ||||
|         binding.progress.visibility = View.GONE | ||||
|   | ||||
| @@ -31,7 +31,7 @@ import org.kodein.di.instance | ||||
|  | ||||
| class SourcesListAdapter( | ||||
|     private val app: Activity, | ||||
|     private val items: ArrayList<SelfossModel.Source> | ||||
|     private val items: ArrayList<SelfossModel.SourceDetail> | ||||
| ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware { | ||||
|     private val c: Context = app.baseContext | ||||
|     private val generator: ColorGenerator = ColorGenerator.MATERIAL | ||||
| @@ -61,7 +61,7 @@ class SourcesListAdapter( | ||||
|             c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) | ||||
|         } | ||||
|  | ||||
|         if (itm.error.isNotBlank()) { | ||||
|         if (!itm.error.isNullOrBlank()) { | ||||
|             binding.errorText.visibility = View.VISIBLE | ||||
|             binding.errorText.text = itm.error | ||||
|         } else { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.fragments | ||||
|  | ||||
| import android.content.ActivityNotFoundException | ||||
| import android.content.Intent | ||||
| import android.content.res.ColorStateList | ||||
| import android.content.res.TypedArray | ||||
| @@ -150,17 +151,19 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|  | ||||
|         } catch (e: InflateException) { | ||||
|             e.sendSilentlyWithAcraWithName("webview not available") | ||||
|             AlertDialog.Builder(requireContext()) | ||||
|                 .setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) | ||||
|                 .setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) | ||||
|                 .setPositiveButton( | ||||
|                     android.R.string.ok | ||||
|                 ) { _, _ -> | ||||
|                     appSettingsService.disableArticleViewer() | ||||
|                     requireActivity().finish() | ||||
|                 } | ||||
|                 .create() | ||||
|                 .show() | ||||
|             if (context != null) { | ||||
|                 AlertDialog.Builder(requireContext()) | ||||
|                     .setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) | ||||
|                     .setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) | ||||
|                     .setPositiveButton( | ||||
|                         android.R.string.ok | ||||
|                     ) { _, _ -> | ||||
|                         appSettingsService.disableArticleViewer() | ||||
|                         requireActivity().finish() | ||||
|                     } | ||||
|                     .create() | ||||
|                     .show() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return binding.root | ||||
| @@ -304,10 +307,16 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|         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 if (context != null && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { | ||||
|                     try { | ||||
|                         requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) | ||||
|                     } catch (e: ActivityNotFoundException) { | ||||
|                         e.sendSilentlyWithAcraWithName("activityNotFound > $url") | ||||
|                     } | ||||
|                     true | ||||
|                 } else { | ||||
|                     false | ||||
|                 } | ||||
|                 return true | ||||
|             } | ||||
|  | ||||
|             @Deprecated("Deprecated in Java") | ||||
| @@ -359,74 +368,74 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|     } | ||||
|  | ||||
|     private fun htmlToWebview() { | ||||
|  | ||||
|         val attrs: IntArray = intArrayOf(android.R.attr.fontFamily) | ||||
|         val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs) | ||||
|         if (context != null) { | ||||
|             val attrs: IntArray = intArrayOf(android.R.attr.fontFamily) | ||||
|             val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs) | ||||
|  | ||||
|  | ||||
|         binding.webcontent.settings.standardFontFamily = a.getString(0) | ||||
|         binding.webcontent.visibility = View.VISIBLE | ||||
|             binding.webcontent.settings.standardFontFamily = a.getString(0) | ||||
|             binding.webcontent.visibility = View.VISIBLE | ||||
|  | ||||
|         val colorOnSurface = TypedValue() | ||||
|         requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true) | ||||
|             val colorOnSurface = TypedValue() | ||||
|             requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true) | ||||
|  | ||||
|         val colorSurface = TypedValue() | ||||
|         requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true) | ||||
|             val colorSurface = TypedValue() | ||||
|             requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true) | ||||
|  | ||||
|         binding.webcontent.settings.useWideViewPort = true | ||||
|         binding.webcontent.settings.loadWithOverviewMode = true | ||||
|         binding.webcontent.settings.javaScriptEnabled = false | ||||
|             binding.webcontent.settings.useWideViewPort = true | ||||
|             binding.webcontent.settings.loadWithOverviewMode = true | ||||
|             binding.webcontent.settings.javaScriptEnabled = false | ||||
|  | ||||
|         handleImageLoading() | ||||
|             handleImageLoading() | ||||
|  | ||||
|         val gestureDetector = | ||||
|             GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() { | ||||
|                 override fun onSingleTapUp(e: MotionEvent): Boolean { | ||||
|                     return performClick() | ||||
|                 } | ||||
|             }) | ||||
|             val gestureDetector = | ||||
|                 GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() { | ||||
|                     override fun onSingleTapUp(e: MotionEvent): Boolean { | ||||
|                         return performClick() | ||||
|                     } | ||||
|                 }) | ||||
|  | ||||
|         binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) } | ||||
|             binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) } | ||||
|  | ||||
|         binding.webcontent.settings.layoutAlgorithm = | ||||
|             WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING | ||||
|             binding.webcontent.settings.layoutAlgorithm = | ||||
|                 WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING | ||||
|  | ||||
|         var baseUrl: String? = null | ||||
|             var baseUrl: String? = null | ||||
|  | ||||
|         try { | ||||
|             val itemUrl = URL(url) | ||||
|             baseUrl = itemUrl.protocol + "://" + itemUrl.host | ||||
|         } catch (e: MalformedURLException) { | ||||
|             e.sendSilentlyWithAcraWithName("htmlToWebview > item url") | ||||
|         } | ||||
|             try { | ||||
|                 val itemUrl = URL(url) | ||||
|                 baseUrl = itemUrl.protocol + "://" + itemUrl.host | ||||
|             } catch (e: MalformedURLException) { | ||||
|                 e.sendSilentlyWithAcraWithName("htmlToWebview > $url") | ||||
|             } | ||||
|  | ||||
|         val fontName = when (font) { | ||||
|             getString(R.string.open_sans_font_id) -> "Open Sans" | ||||
|             getString(R.string.roboto_font_id) -> "Roboto" | ||||
|             getString(R.string.source_code_pro_font_id) -> "Source Code Pro" | ||||
|             else -> "" | ||||
|         } | ||||
|             val fontName = when (font) { | ||||
|                 getString(R.string.open_sans_font_id) -> "Open Sans" | ||||
|                 getString(R.string.roboto_font_id) -> "Roboto" | ||||
|                 getString(R.string.source_code_pro_font_id) -> "Source Code Pro" | ||||
|                 else -> "" | ||||
|             } | ||||
|  | ||||
|         val fontLinkAndStyle = if (font.isNotEmpty()) { | ||||
|             """<link href="https://fonts.googleapis.com/css?family=${ | ||||
|                 fontName.replace( | ||||
|                     " ", | ||||
|                     "+" | ||||
|                 ) | ||||
|             }" rel="stylesheet"> | ||||
|             val fontLinkAndStyle = if (font.isNotEmpty()) { | ||||
|                 """<link href="https://fonts.googleapis.com/css?family=${ | ||||
|                     fontName.replace( | ||||
|                         " ", | ||||
|                         "+" | ||||
|                     ) | ||||
|                 }" rel="stylesheet"> | ||||
|                 |<style> | ||||
|                 |   * { | ||||
|                 |       font-family: '$fontName'; | ||||
|                 |   } | ||||
|                 |</style> | ||||
|             """.trimMargin() | ||||
|         } else { | ||||
|             "" | ||||
|         } | ||||
|             } else { | ||||
|                 "" | ||||
|             } | ||||
|  | ||||
|         binding.webcontent.loadDataWithBaseURL( | ||||
|             baseUrl, | ||||
|             """<html> | ||||
|             binding.webcontent.loadDataWithBaseURL( | ||||
|                 baseUrl, | ||||
|                 """<html> | ||||
|                 |<head> | ||||
|                 |   <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|                 |   <style> | ||||
| @@ -438,11 +447,11 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                 |      } | ||||
|                 |      a { | ||||
|                 |        color: ${ | ||||
|                 String.format( | ||||
|                     "#%06X", | ||||
|                     0xFFFFFF and resources.getColor(R.color.colorAccent) | ||||
|                 ) | ||||
|             } !important; | ||||
|                     String.format( | ||||
|                         "#%06X", | ||||
|                         0xFFFFFF and resources.getColor(R.color.colorAccent) | ||||
|                     ) | ||||
|                 } !important; | ||||
|                 |      } | ||||
|                 |      *:not(a) { | ||||
|                 |        color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)}; | ||||
| @@ -454,25 +463,25 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                 |        overflow:hidden; | ||||
|                 |        line-height: 1.5em; | ||||
|                 |        background-color: ${ | ||||
|                 String.format( | ||||
|                     "#%06X", | ||||
|                     0xFFFFFF and colorSurface.data | ||||
|                 ) | ||||
|             }; | ||||
|                     String.format( | ||||
|                         "#%06X", | ||||
|                         0xFFFFFF and colorSurface.data | ||||
|                     ) | ||||
|                 }; | ||||
|                 |      } | ||||
|                 |      body, html { | ||||
|                 |        background-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; | ||||
|                     String.format( | ||||
|                         "#%06X", | ||||
|                         0xFFFFFF and colorSurface.data | ||||
|                     ) | ||||
|                 }  !important; | ||||
|                 |        padding: 0 !important; | ||||
|                 |        margin: 0 !important; | ||||
|                 |      } | ||||
| @@ -483,11 +492,11 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                 |        white-space: pre-wrap; | ||||
|                 |        width:100%; | ||||
|                 |        background-color: ${ | ||||
|                 String.format( | ||||
|                     "#%06X", | ||||
|                     0xFFFFFF and colorSurface.data | ||||
|                 ) | ||||
|             }; | ||||
|                     String.format( | ||||
|                         "#%06X", | ||||
|                         0xFFFFFF and colorSurface.data | ||||
|                     ) | ||||
|                 }; | ||||
|                 |      } | ||||
|                 |   </style> | ||||
|                 |   $fontLinkAndStyle | ||||
| @@ -495,10 +504,11 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                 |<body> | ||||
|                 |   $contentText | ||||
|                 |</body>""".trimMargin(), | ||||
|             "text/html", | ||||
|             "utf-8", | ||||
|             null | ||||
|         ) | ||||
|                 "text/html", | ||||
|                 "utf-8", | ||||
|                 null | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun scrollDown() { | ||||
|   | ||||
| @@ -20,10 +20,8 @@ import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import bou.amine.apps.readerforselfossv2.utils.getIcon | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.load.DataSource | ||||
| import com.bumptech.glide.load.engine.GlideException | ||||
| import com.bumptech.glide.request.RequestListener | ||||
| import com.bumptech.glide.request.target.Target | ||||
| import com.bumptech.glide.request.target.ViewTarget | ||||
| import com.bumptech.glide.request.transition.Transition | ||||
| import com.google.android.material.bottomsheet.BottomSheetDialogFragment | ||||
| import com.google.android.material.chip.Chip | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| @@ -84,37 +82,25 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { | ||||
|     ) { | ||||
|         val sourceGroup = binding.sourcesGroup | ||||
|  | ||||
|         repository.getSources().forEach { source -> | ||||
|         repository.getSourcesDetailsOrStats().forEach { source -> | ||||
|             val c = Chip(context) | ||||
|             c.ellipsize = TextUtils.TruncateAt.END | ||||
|  | ||||
|             Glide.with(context) | ||||
|                 .load(source.getIcon(repository.baseUrl)) | ||||
|                 .listener(object : RequestListener<Drawable?> { | ||||
|                     override fun onLoadFailed( | ||||
|                         e: GlideException?, | ||||
|                         model: Any?, | ||||
|                         target: Target<Drawable?>?, | ||||
|                         isFirstResource: Boolean | ||||
|                     ): Boolean { | ||||
|                         return false | ||||
|                     } | ||||
|  | ||||
|                 .into(object : ViewTarget<Chip?, Drawable?>(c) { | ||||
|                     override fun onResourceReady( | ||||
|                         resource: Drawable?, | ||||
|                         model: Any?, | ||||
|                         target: Target<Drawable?>?, | ||||
|                         dataSource: DataSource?, | ||||
|                         isFirstResource: Boolean | ||||
|                     ): Boolean { | ||||
|                         resource: Drawable, | ||||
|                         transition: Transition<in Drawable?>? | ||||
|                     ) { | ||||
|                         try { | ||||
|                             c.chipIcon = resource | ||||
|                         } catch (e: Exception) { | ||||
|                             e.sendSilentlyWithAcraWithName("sources > onResourceReady") | ||||
|                         } | ||||
|                         return false | ||||
|                     } | ||||
|                 }).preload() | ||||
|  | ||||
|                 }) | ||||
|  | ||||
|             c.text = source.title.getHtmlDecoded() | ||||
|  | ||||
| @@ -141,9 +127,9 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { | ||||
|                 selectedChip = c | ||||
|             } | ||||
|  | ||||
|             c.isEnabled = source.error.isBlank() | ||||
|             c.isEnabled = source.error.isNullOrBlank() | ||||
|  | ||||
|             if (source.error.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             if (!source.error.isNullOrBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|                 c.tooltipText = source.error | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -300,9 +300,10 @@ class RepositoryTest { | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
|  | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         repository.setSourceFilter(SelfossModel.Source( | ||||
|         repository.setSourceFilter(SelfossModel.SourceDetail( | ||||
|             1, | ||||
|             "Test", | ||||
|             null, | ||||
|             listOf("tags"), | ||||
|             SPOUT, | ||||
|             "", | ||||
| @@ -609,30 +610,32 @@ class RepositoryTest { | ||||
|     fun get_sources() { | ||||
|         val (sources, sourcesDB) = prepareSources() | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
|         var testSources: List<SelfossModel.Source> | ||||
|         runBlocking { | ||||
|             testSources = repository.getSources() | ||||
|             testSources = repository.getSourcesDetails() | ||||
|         } | ||||
|  | ||||
|         assertSame(sources, testSources) | ||||
|         assertEquals(sources, testSources) | ||||
|         assertNotEquals(sourcesDB.map { it.toView() }, testSources) | ||||
|         coVerify(exactly = 1) { api.sources() } | ||||
|         coVerify(exactly = 1) { api.sourcesDetailed() } | ||||
|     } | ||||
|  | ||||
|     private fun prepareSources(): Pair<ArrayList<SelfossModel.Source>, List<SOURCE>> { | ||||
|     private fun prepareSources(): Pair<ArrayList<SelfossModel.SourceDetail>, List<SOURCE>> { | ||||
|         val sources = arrayListOf( | ||||
|             SelfossModel.Source( | ||||
|             SelfossModel.SourceDetail( | ||||
|                 1, | ||||
|                 "First source", | ||||
|                 null, | ||||
|                 listOf("Test", "second"), | ||||
|                 SPOUT, | ||||
|                 "", | ||||
|                 IMAGE_URL_2, | ||||
|                 SelfossModel.SourceParams("url") | ||||
|             ), | ||||
|             SelfossModel.Source( | ||||
|             SelfossModel.SourceDetail( | ||||
|                 2, | ||||
|                 "Second source", | ||||
|                 null, | ||||
|                 listOf("second"), | ||||
|                 SPOUT, | ||||
|                 "", | ||||
| @@ -661,7 +664,7 @@ class RepositoryTest { | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         return Pair(sources, sourcesDB) | ||||
|     } | ||||
| @@ -675,13 +678,13 @@ class RepositoryTest { | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
|         runBlocking { | ||||
|             testSources = repository.getSources() | ||||
|             testSources = repository.getSourcesDetails() | ||||
|             // Sources will be fetched from the database on the second call, thus testSources != sources | ||||
|             testSources = repository.getSources() | ||||
|             testSources = repository.getSourcesDetails() | ||||
|         } | ||||
|  | ||||
|         coVerify(exactly = 1) { api.sources() } | ||||
|         assertNotSame(sources, testSources) | ||||
|         coVerify(exactly = 1) { api.sourcesDetailed() } | ||||
|         assertNotEquals(sources, testSources) | ||||
|         assertEquals(sourcesDB.map { it.toView() }, testSources) | ||||
|         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } | ||||
|     } | ||||
| @@ -693,13 +696,13 @@ class RepositoryTest { | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
|         var testSources: List<SelfossModel.Source> | ||||
|         runBlocking { | ||||
|             testSources = repository.getSources() | ||||
|             testSources = repository.getSourcesDetails() | ||||
|         } | ||||
|  | ||||
|         assertSame(sources, testSources) | ||||
|         coVerify(exactly = 1) { api.sources() } | ||||
|         assertEquals(sources, testSources) | ||||
|         coVerify(exactly = 1) { api.sourcesDetailed() } | ||||
|         verify(exactly = 0) { db.sourcesQueries } | ||||
|     } | ||||
|  | ||||
| @@ -710,13 +713,13 @@ class RepositoryTest { | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
|         var testSources: List<SelfossModel.Source> | ||||
|         runBlocking { | ||||
|             testSources = repository.getSources() | ||||
|             testSources = repository.getSourcesDetails() | ||||
|         } | ||||
|  | ||||
|         assertSame(sources, testSources) | ||||
|         coVerify(exactly = 1) { api.sources() } | ||||
|         assertEquals(sources, testSources) | ||||
|         coVerify(exactly = 1) { api.sourcesDetailed() } | ||||
|         verify(atLeast = 1) { db.sourcesQueries } | ||||
|     } | ||||
|  | ||||
| @@ -724,13 +727,13 @@ class RepositoryTest { | ||||
|     fun get_sources_without_connection() { | ||||
|         val (_, sourcesDB) = prepareSources() | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
|         var testSources: List<SelfossModel.Source> | ||||
|         runBlocking { | ||||
|             testSources = repository.getSources() | ||||
|             testSources = repository.getSourcesDetails() | ||||
|         } | ||||
|  | ||||
|         assertEquals(sourcesDB.map { it.toView() }, testSources) | ||||
|         coVerify(exactly = 0) { api.sources() } | ||||
|         coVerify(exactly = 0) { api.sourcesDetailed() } | ||||
|         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } | ||||
|     } | ||||
|  | ||||
| @@ -741,13 +744,13 @@ class RepositoryTest { | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
|         var testSources: List<SelfossModel.Source> | ||||
|         runBlocking { | ||||
|             testSources = repository.getSources() | ||||
|             testSources = repository.getSourcesDetails() | ||||
|         } | ||||
|  | ||||
|         assertEquals(emptyList<SelfossModel.Source>(), testSources) | ||||
|         coVerify(exactly = 0) { api.sources() } | ||||
|         coVerify(exactly = 0) { api.sourcesDetailed() } | ||||
|         verify(exactly = 0) { db.sourcesQueries.sources().executeAsList() } | ||||
|     } | ||||
|  | ||||
| @@ -758,13 +761,13 @@ class RepositoryTest { | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
|         var testSources: List<SelfossModel.Source> | ||||
|         runBlocking { | ||||
|             testSources = repository.getSources() | ||||
|             testSources = repository.getSourcesDetails() | ||||
|         } | ||||
|  | ||||
|         assertEquals(sourcesDB.map { it.toView() }, testSources) | ||||
|         coVerify(exactly = 0) { api.sources() } | ||||
|         coVerify(exactly = 0) { api.sourcesDetailed() } | ||||
|         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } | ||||
|     } | ||||
|  | ||||
| @@ -775,13 +778,13 @@ class RepositoryTest { | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
|         var testSources: List<SelfossModel.Source> | ||||
|         runBlocking { | ||||
|             testSources = repository.getSources() | ||||
|             testSources = repository.getSourcesDetails() | ||||
|         } | ||||
|  | ||||
|         assertEquals(sourcesDB.map { it.toView() }, testSources) | ||||
|         coVerify(exactly = 0) { api.sources() } | ||||
|         coVerify(exactly = 0) { api.sourcesDetailed() } | ||||
|         verify(atLeast = 1) { db.sourcesQueries.sources().executeAsList() } | ||||
|     } | ||||
|  | ||||
| @@ -1102,9 +1105,10 @@ class RepositoryTest { | ||||
|     private fun prepareSearch() { | ||||
|         repository.setTagFilter(SelfossModel.Tag("Tag", "read", 0)) | ||||
|         repository.setSourceFilter( | ||||
|             SelfossModel.Source( | ||||
|             SelfossModel.SourceDetail( | ||||
|                 1, | ||||
|                 "First source", | ||||
|                 5, | ||||
|                 listOf("Test", "second"), | ||||
|                 SPOUT, | ||||
|                 "", | ||||
|   | ||||
| @@ -24,8 +24,8 @@ class SelfossModel { | ||||
|     @Serializable | ||||
|     class Stats( | ||||
|         val total: Int, | ||||
|         val unread: Int, | ||||
|         val starred: Int | ||||
|         val unread: Int?, | ||||
|         val starred: Int? | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
| @@ -63,20 +63,39 @@ class SelfossModel { | ||||
|         fun isPublicModeEnabled() = publicMode ?: false | ||||
|     } | ||||
|  | ||||
|     interface Source { | ||||
|         val id: Int | ||||
|         var title: String | ||||
|         var unread: Int? | ||||
|         var error: String? | ||||
|         var icon: String? | ||||
|     } | ||||
|  | ||||
|     @Serializable | ||||
|     data class Source( | ||||
|         val id: Int, | ||||
|         val title: String, | ||||
|     data class SourceStats( | ||||
|         override val id: Int, | ||||
|         override var title: String, | ||||
|         override var unread: Int?, | ||||
|         override var error: String? = null, | ||||
|         override var icon: String? = null | ||||
|         ) : Source | ||||
|  | ||||
|     @Serializable | ||||
|     data class SourceDetail( | ||||
|         override val id: Int, | ||||
|         override var title: String, | ||||
|         override var unread: Int? = null, | ||||
|         @Serializable(with = TagsListSerializer::class) | ||||
|         val tags: List<String>, | ||||
|         val spout: String, | ||||
|         val error: String, | ||||
|         val icon: String?, | ||||
|         val params: SourceParams? | ||||
|     ) | ||||
|         var tags: List<String>?, | ||||
|         var spout: String?, | ||||
|         override var error: String?, | ||||
|         override var icon: String?, | ||||
|         var params: SourceParams? | ||||
|     ) : Source | ||||
|  | ||||
|     @Serializable | ||||
|     data class SourceParams( | ||||
|         val url: String? | ||||
|         val url: String? = null | ||||
|     ) | ||||
|     @Serializable | ||||
|     data class Item( | ||||
|   | ||||
| @@ -44,11 +44,11 @@ class Repository( | ||||
|     private val _badgeStarred = MutableStateFlow(0) | ||||
|     val badgeStarred = _badgeStarred.asStateFlow() | ||||
|  | ||||
|     private var fetchedSources = false | ||||
|     private var fetchedTags = false | ||||
|     private var fetchedSources = false | ||||
|  | ||||
|     private var _readerItems = ArrayList<SelfossModel.Item>() | ||||
|     private var _selectedSource: SelfossModel.Source? = null | ||||
|     private var _selectedSource: SelfossModel.SourceDetail? = null | ||||
|  | ||||
|     suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { | ||||
|         var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() | ||||
| @@ -132,9 +132,9 @@ class Repository( | ||||
|         if (isNetworkAvailable()) { | ||||
|             val response = api.stats() | ||||
|             if (response.success && response.data != null) { | ||||
|                 _badgeUnread.value = response.data.unread | ||||
|                 _badgeUnread.value = response.data.unread ?: 0 | ||||
|                 _badgeAll.value = response.data.total | ||||
|                 _badgeStarred.value = response.data.starred | ||||
|                 _badgeStarred.value = response.data.starred ?: 0 | ||||
|                 success = true | ||||
|             } | ||||
|         } else if (appSettingsService.isItemCachingEnabled()) { | ||||
| @@ -180,23 +180,46 @@ class Repository( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun getSources(): ArrayList<SelfossModel.Source> { | ||||
|     suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> { | ||||
|         var sources = ArrayList<SelfossModel.Source>() | ||||
|         val isDatabaseEnabled = | ||||
|             appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() | ||||
|         return if (isNetworkAvailable() && !fetchedSources) { | ||||
|             val apiSources = api.sources() | ||||
|             if (apiSources.success && apiSources.data != null && isDatabaseEnabled) { | ||||
|                 resetDBSourcesWithData(apiSources.data) | ||||
|                 if (!appSettingsService.isUpdateSourcesEnabled()) { | ||||
|         val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true | ||||
|         if (shouldFetch && isNetworkAvailable()) { | ||||
|             if (appSettingsService.getPublicAccess()) { | ||||
|                 val apiSources = api.sourcesStats() | ||||
|                 if (apiSources.success && apiSources.data != null) { | ||||
|                     fetchedSources = true | ||||
|                     sources = apiSources.data as ArrayList<SelfossModel.Source> | ||||
|                 } | ||||
|             } else { | ||||
|                 sources = getSourcesDetails() as ArrayList<SelfossModel.Source> | ||||
|             } | ||||
|         } 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) { | ||||
|             ArrayList(getDBSources().map { it.toView() }) | ||||
|         } else { | ||||
|             ArrayList() | ||||
|             sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.SourceDetail> | ||||
|         } | ||||
|         return sources | ||||
|     } | ||||
|  | ||||
|     suspend fun markAsRead(item: SelfossModel.Item): Boolean { | ||||
| @@ -482,7 +505,7 @@ class Repository( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun resetDBSourcesWithData(sources: List<SelfossModel.Source>) { | ||||
|     private fun resetDBSourcesWithData(sources: List<SelfossModel.SourceDetail>) { | ||||
|         db.sourcesQueries.deleteAllSources() | ||||
|  | ||||
|         db.sourcesQueries.transaction { | ||||
| @@ -592,7 +615,7 @@ class Repository( | ||||
|         ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1) | ||||
|     } | ||||
|  | ||||
|     fun setSelectedSource(source: SelfossModel.Source) { | ||||
|     fun setSelectedSource(source: SelfossModel.SourceDetail) { | ||||
|         _selectedSource = source | ||||
|     } | ||||
|  | ||||
| @@ -600,7 +623,7 @@ class Repository( | ||||
|         _selectedSource = null | ||||
|     } | ||||
|  | ||||
|     fun getSelectedSource(): SelfossModel.Source? { | ||||
|     fun getSelectedSource(): SelfossModel.SourceDetail? { | ||||
|         return _selectedSource | ||||
|     } | ||||
| } | ||||
| @@ -183,7 +183,15 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> = | ||||
|     suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> = | ||||
|         bodyOrFailure(client.tryToGet(url("/sources/stats")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|     suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> = | ||||
|         bodyOrFailure(client.tryToGet(url("/sources/list")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|   | ||||
| @@ -12,24 +12,25 @@ fun TAG.toView(): SelfossModel.Tag = | ||||
|         this.unread.toInt() | ||||
|     ) | ||||
|  | ||||
| fun SOURCE.toView(): SelfossModel.Source = | ||||
|     SelfossModel.Source( | ||||
| fun SOURCE.toView(): SelfossModel.SourceDetail = | ||||
|     SelfossModel.SourceDetail( | ||||
|         this.id.toInt(), | ||||
|         this.title, | ||||
|         this.tags.split(","), | ||||
|         null, | ||||
|         this.tags?.split(","), | ||||
|         this.spout, | ||||
|         this.error, | ||||
|         this.icon, | ||||
|         SelfossModel.SourceParams(this.url) | ||||
|         if (this.url != null) SelfossModel.SourceParams(this.url) else null | ||||
|     ) | ||||
|  | ||||
| fun SelfossModel.Source.toEntity(): SOURCE = | ||||
| fun SelfossModel.SourceDetail.toEntity(): SOURCE = | ||||
|     SOURCE( | ||||
|         this.id.toString(), | ||||
|         this.title.getHtmlDecoded(), | ||||
|         this.tags.joinToString(","), | ||||
|         this.spout, | ||||
|         this.error, | ||||
|         this.tags?.joinToString(",").orEmpty(), | ||||
|         this.spout.orEmpty(), | ||||
|         this.error.orEmpty(), | ||||
|         this.icon.orEmpty(), | ||||
|         this.params?.url | ||||
|     ) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user