Compare commits

...

6 Commits

Author SHA1 Message Date
Amine Louveau
90532cf501 Changelog for v123010041 [CI SKIP] 2023-01-24 12:41:52 +00:00
Amine Louveau
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
aminecmi
a1b7d22d26 fix: added POST_NOTIFICATIONS to fix notifications issues. 2023-01-23 22:32:52 +01:00
aminecmi
29eae4b1f6 fix: scrollable filter sheet. 2023-01-23 21:13:10 +01:00
aminecmi
f5bbc63481 enhancement: Ellipsize chips text. 2023-01-23 21:12:56 +01:00
aminecmi
95e76a55da Cleaning. 2023-01-12 22:01:36 +01:00
20 changed files with 681 additions and 561 deletions

View File

@ -1,3 +1,13 @@
**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** **v122123641**
- feat: Disable the failing source in the filter sheet. - feat: Disable the failing source in the filter sheet.

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
} }

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" />

View File

@ -366,7 +366,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)
} }
} }
@ -539,11 +538,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

View File

@ -27,7 +27,6 @@ import org.acra.ACRA
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
import java.security.MessageDigest
class LoginActivity : AppCompatActivity(), DIAware { class LoginActivity : AppCompatActivity(), DIAware {
@ -157,44 +156,9 @@ 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)
if (url.isBaseUrlInvalid()) {
binding.urlView.error = getString(R.string.login_url_problem)
focusView = binding.urlView
cancel = true
inValidCount++
if (inValidCount == 3) {
val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.warning_wrong_url))
alertDialog.setMessage(getString(R.string.text_wrong_url))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK"
) { dialog, _ -> dialog.dismiss() }
alertDialog.show()
inValidCount = 0
}
}
if (isWithLogin) {
if (TextUtils.isEmpty(password)) {
binding.passwordView.error = getString(R.string.error_invalid_password)
focusView = binding.passwordView
cancel = true
}
if (TextUtils.isEmpty(login)) {
binding.loginView.error = getString(R.string.error_field_required)
focusView = binding.loginView
cancel = true
}
}
if (cancel) {
focusView?.requestFocus()
} else {
showProgress(true) showProgress(true)
repository.refreshLoginInformation(url, login, password) repository.refreshLoginInformation(url, login, password)
@ -221,6 +185,55 @@ class LoginActivity : AppCompatActivity(), DIAware {
showProgress(false) 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()) {
cancel = true
binding.urlView.error = getString(R.string.login_url_problem)
inValidCount++
if (inValidCount == 3) {
val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.warning_wrong_url))
alertDialog.setMessage(getString(R.string.text_wrong_url))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK"
) { dialog, _ -> dialog.dismiss() }
alertDialog.show()
inValidCount = 0
}
}
maybeCancelAndFocusView(cancel, focusView)
}
private fun maybeCancelAndFocusView(cancel: Boolean, focusView: View?) {
if (cancel) {
focusView?.requestFocus()
}
} }
private fun showProgress(show: Boolean) { private fun showProgress(show: Boolean) {

View File

@ -37,7 +37,14 @@ class MyApp : MultiDexApplication(), DIAware {
import(networkModule) import(networkModule)
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) } bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) } bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) } bind<Repository>() with singleton {
Repository(
instance(),
instance(),
isConnectionAvailable,
instance()
)
}
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) } bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
} }
@ -59,7 +66,12 @@ class MyApp : MultiDexApplication(), DIAware {
handleNotificationChannels() handleNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository)) ProcessLifecycleOwner.get().lifecycle.addObserver(
AppLifeCycleObserver(
connectivityStatus,
repository
)
)
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
viewModel.networkAvailableProvider.collect { networkAvailable -> viewModel.networkAvailableProvider.collect { networkAvailable ->
@ -88,20 +100,36 @@ 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 =
"https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/
basicAuthLogin = "LMTlLZuazADohTCm" basicAuthLogin = "LMTlLZuazADohTCm"
basicAuthPassword = "he6ghHp83F0PYPfh" basicAuthPassword = "he6ghHp83F0PYPfh"
httpMethod = HttpSender.Method.POST httpMethod = HttpSender.Method.POST
@ -119,7 +147,11 @@ class MyApp : MultiDexApplication(), DIAware {
val newItemsChannelname = getString(R.string.new_items_channel_sync) val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = NotificationChannel(AppSettingsService.newItemsChannelId, newItemsChannelname, newItemsChannelimportance) val newItemsChannelmChannel = NotificationChannel(
AppSettingsService.newItemsChannelId,
newItemsChannelname,
newItemsChannelimportance
)
notificationManager.createNotificationChannel(mChannel) notificationManager.createNotificationChannel(mChannel)
notificationManager.createNotificationChannel(newItemsChannelmChannel) notificationManager.createNotificationChannel(newItemsChannelmChannel)
@ -133,13 +165,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

@ -184,13 +184,11 @@ class ReaderActivity : AppCompatActivity(), DIAware {
if (allItems[binding.pager.currentItem].starred) { if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(allItems[binding.pager.currentItem]) repository.unstarr(allItems[binding.pager.currentItem])
// TODO: Handle failure
} }
afterUnsave() afterUnsave()
} else { } else {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.starr(allItems[binding.pager.currentItem]) repository.starr(allItems[binding.pager.currentItem])
// TODO: Handle failure
} }
afterSave() afterSave()
} }

View File

@ -26,7 +26,8 @@ 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()
@ -68,7 +69,6 @@ override fun doWork(): Result {
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()
@ -84,10 +84,14 @@ override fun doWork(): Result {
} else { } else {
0 0
} }
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, pflags) val pendingIntent: PendingIntent =
PendingIntent.getActivity(context, 0, intent, pflags)
val newItemsNotification = val newItemsNotification =
NotificationCompat.Builder(applicationContext, AppSettingsService.newItemsChannelId) NotificationCompat.Builder(
applicationContext,
AppSettingsService.newItemsChannelId
)
.setContentTitle(context.getString(R.string.new_items_notification_title)) .setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText( .setContentText(
context.getString( context.getString(

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
@ -115,6 +119,81 @@ class ArticleFragment : Fragment(), DIAware {
fab.rippleColor = resources.getColor(R.color.colorAccentDark) fab.rippleColor = resources.getColor(R.color.colorAccentDark)
val floatingToolbar: FloatingToolbar = handleFloatingToolbar()
if (staticBar) {
fab.hide()
floatingToolbar.show()
}
binding.source.text = contentSource
if (typeface != null) {
binding.source.typeface = typeface
}
handleContent()
binding.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
floatingToolbar.hide()
fab.hide()
} else {
if (staticBar) {
floatingToolbar.show()
} else {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
}
}
}
)
} 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()
}
return binding.root
}
private fun handleContent() {
if (contentText.isEmptyOrNullOrNullString()) {
if (repository.isNetworkAvailable()) {
getContentFromMercury()
}
} else {
binding.titleView.text = contentTitle
if (typeface != null) {
binding.titleView.typeface = typeface
}
htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding.imageView.visibility = View.VISIBLE
Glide
.with(requireContext())
.asBitmap()
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else {
binding.imageView.visibility = View.GONE
}
}
}
private fun handleFloatingToolbar(): FloatingToolbar {
val floatingToolbar: FloatingToolbar = binding.floatingToolbar val floatingToolbar: FloatingToolbar = binding.floatingToolbar
floatingToolbar.attachFab(fab) floatingToolbar.attachFab(fab)
@ -154,73 +233,11 @@ class ArticleFragment : Fragment(), DIAware {
} }
override fun onItemLongClick(item: MenuItem?) { override fun onItemLongClick(item: MenuItem?) {
// We do nothing
} }
} }
) )
return floatingToolbar
if (staticBar) {
fab.hide()
floatingToolbar.show()
}
binding.source.text = contentSource
if (typeface != null) {
binding.source.typeface = typeface
}
if (contentText.isEmptyOrNullOrNullString()) {
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
}
}
binding.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
floatingToolbar.hide()
fab.hide()
} else {
if (staticBar) {
floatingToolbar.show()
} else {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
}
}
}
)
} 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()
}
return binding.root
} }
private fun refreshAlignment() { private fun refreshAlignment() {
@ -232,7 +249,6 @@ 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 {
@ -240,34 +256,34 @@ class ArticleFragment : Fragment(), DIAware {
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)
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() contentText = response.data!!.content.orEmpty()
htmlToWebview() htmlToWebview()
handleLeadImage(response)
binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE
} else {
openInBrowserAfterFailing()
}
} catch (e: SocketTimeoutException) {
openInBrowserAfterFailing()
} catch (e: Exception) { } catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > contenttext or html") e.sendSilentlyWithAcraWithName("getContentFromMercury > $url")
openInBrowserAfterFailing()
}
}
} }
private fun handleLeadImage(response: StatusAndData<MercuryModel.ParsedContent>) {
if (!response.data?.lead_image_url.isNullOrEmpty() && context != null) { if (!response.data?.lead_image_url.isNullOrEmpty() && context != null) {
try {
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
try {
Glide Glide
.with(requireContext()) .with(requireContext())
.asBitmap() .asBitmap()
@ -276,32 +292,66 @@ class ArticleFragment : Fragment(), DIAware {
) )
.apply(RequestOptions.fitCenterTransform()) .apply(RequestOptions.fitCenterTransform())
.into(binding.imageView) .into(binding.imageView)
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > glide lead image")
}
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("getContentFromMercury > outside glide lead image")
}
} else { } else {
binding.imageView.visibility = View.GONE 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 { try {
binding.nestedScrollView.scrollTo(0, 0) val image =
binding.progressBar.visibility = View.GONE Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
} catch (e: Exception) { return WebResourceResponse(
e.sendSilentlyWithAcraWithName("getContentFromMercury > scrollview") IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)
)
} catch (e: ExecutionException) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg > $url")
} }
} else { } else if (url.lowercase(Locale.US).contains(".png")) {
openInBrowserAfterFailing() 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")
} }
} catch (e: SocketTimeoutException) { } else if (url.lowercase(Locale.US).contains(".webp")) {
openInBrowserAfterFailing() try {
} catch (e: Exception) { val image =
e.sendSilentlyWithAcraWithName("getContentFromMercury > whole thing") Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
openInBrowserAfterFailing() return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)
)
} catch (e: ExecutionException) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > webp > $url")
} }
} }
return super.shouldInterceptRequest(view, url)
}
} }
} }
@ -324,48 +374,10 @@ class ArticleFragment : Fragment(), DIAware {
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 {
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") val gestureDetector =
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? { GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
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)
}
}
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean { override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick() return performClick()
} }
@ -393,7 +405,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';
@ -417,7 +434,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)};
@ -428,11 +450,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;
| } | }
@ -442,7 +479,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
@ -487,7 +529,8 @@ 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)

View File

@ -6,6 +6,7 @@ import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.os.Build 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
@ -13,6 +14,7 @@ import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import bou.amine.apps.readerforselfossv2.android.HomeActivity import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
@ -35,6 +37,7 @@ import org.kodein.di.instance
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
private lateinit var binding: FilterFragmentBinding
override val di: DI by closestDI() override val di: DI by closestDI()
private val repository: Repository by instance() private val repository: Repository by instance()
@ -45,8 +48,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val binding = binding =
bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding.inflate( FilterFragmentBinding.inflate(
inflater, inflater,
container, container,
false false
@ -54,64 +57,36 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
val context: Context? = context val context: Context? = context
val tagGroup = binding.tagsGroup
val sourceGroup = binding.sourcesGroup
if (context == null) { if (context == null) {
dismiss() dismiss()
Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView") Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} else { } else {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val tags = repository.getTags() handleTagChips(context)
handleSourceChips(context)
tags.forEach { tag -> binding.progressBar2.visibility = GONE
val c = Chip(context) binding.filterView.visibility = VISIBLE
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 { binding.floatingActionButton2.setOnClickListener {
(it as Chip).isCloseIconVisible = false (activity as HomeActivity).getElementsAccordingToTab()
selectedChip = null (activity as HomeActivity).fetchOnEmptyList()
repository.setTagFilter(null) dismiss()
}
return binding.root
} }
c.setOnClickListener { private suspend fun handleSourceChips(
if (selectedChip != null) { context: Context
selectedChip!!.isCloseIconVisible = false ) {
} val sourceGroup = binding.sourcesGroup
(it as Chip).isCloseIconVisible = true
selectedChip = it
repository.setTagFilter(tag)
repository.setSourceFilter(null)
}
if (repository.tagFilter.value?.equals(tag) == true) {
c.isCloseIconVisible = true
selectedChip = c
}
tagGroup.addView(c)
}
repository.getSources().forEach { source -> repository.getSources().forEach { source ->
val c = Chip(context) val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END
Glide.with(context) Glide.with(context)
.load(source.getIcon(repository.baseUrl)) .load(source.getIcon(repository.baseUrl))
@ -174,18 +149,61 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
sourceGroup.addView(c) sourceGroup.addView(c)
} }
binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE
}
} }
binding.floatingActionButton2.setOnClickListener { private suspend fun handleTagChips(
(activity as HomeActivity).getElementsAccordingToTab() context: Context,
(activity as HomeActivity).fetchOnEmptyList() ) {
dismiss() 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 binding.root
} }
companion object { companion object {

View File

@ -8,7 +8,6 @@ import bou.amine.apps.readerforselfossv2.utils.getImages
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import org.acra.ktx.sendSilentlyWithAcra
fun SelfossModel.Item.preloadImages(context: Context) : Boolean { fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages() val imageUrls = this.getImages()

View File

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

View File

@ -17,13 +17,65 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" /> tools:visibility="gone" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/filterView" android:id="@+id/filterView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
<TextView
android:id="@+id/filterTagsTitle"
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:text="@string/filter_item_tags"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/filterSourcesTitle"
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:text="@string/filter_item_sources"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tagsGroup" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/tagsGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filterTagsTitle"
app:singleSelection="true">
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.chip.ChipGroup
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 <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton2" android:id="@+id/floatingActionButton2"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -38,52 +90,7 @@
app:rippleColor="@color/colorAccentDark" app:rippleColor="@color/colorAccentDark"
app:srcCompat="@drawable/ic_menu_search_white_24dp" /> app:srcCompat="@drawable/ic_menu_search_white_24dp" />
<TextView </androidx.constraintlayout.widget.ConstraintLayout>
android:id="@+id/filterTagsTitle" </androidx.core.widget.NestedScrollView>
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_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filterTagsTitle"
app:singleSelection="true">
</com.google.android.material.chip.ChipGroup>
<TextView
android:id="@+id/filterSourcesTitle"
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:text="@string/filter_item_sources"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tagsGroup" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/sourcesGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filterSourcesTitle">
</com.google.android.material.chip.ChipGroup>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -18,6 +18,18 @@ 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 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>()
@ -45,7 +57,7 @@ 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.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
@ -229,9 +241,9 @@ class RepositoryTest {
1, 1,
"Test", "Test",
listOf("tags"), listOf("tags"),
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"b3aa8a664d08eb15d6ff1db2fa83e0d9.png", IMAGE_URL,
SelfossModel.SourceParams("url") SelfossModel.SourceParams("url")
)) ))
runBlocking { runBlocking {
@ -550,18 +562,18 @@ class RepositoryTest {
1, 1,
"First source", "First source",
listOf("Test", "second"), listOf("Test", "second"),
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"d8c92cdb1ef119ea85c4b9205c879ca7.png", IMAGE_URL_2,
SelfossModel.SourceParams("url") SelfossModel.SourceParams("url")
), ),
SelfossModel.Source( SelfossModel.Source(
2, 2,
"Second source", "Second source",
listOf("second"), listOf("second"),
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"b3aa8a664d08eb15d6ff1db2fa83e0d9.png", IMAGE_URL,
SelfossModel.SourceParams("url") SelfossModel.SourceParams("url")
) )
) )
@ -570,18 +582,18 @@ class RepositoryTest {
"1", "1",
"First DB source", "First DB source",
"Test,second", "Test,second",
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"d8c92cdb1ef119ea85c4b9205c879ca7.png", IMAGE_URL_2,
"url" "url"
), ),
SOURCE( SOURCE(
"2", "2",
"Second source", "Second source",
"second", "second",
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"b3aa8a664d08eb15d6ff1db2fa83e0d9.png", IMAGE_URL,
"url" "url"
) )
) )
@ -720,9 +732,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,
) )
} }
@ -747,9 +759,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
) )
} }
@ -774,9 +786,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
) )
} }
@ -952,12 +964,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"
) )
@ -1031,9 +1043,9 @@ class RepositoryTest {
1, 1,
"First source", "First source",
listOf("Test", "second"), listOf("Test", "second"),
"spouts\\rss\\fulltextrss", SPOUT,
"", "",
"d8c92cdb1ef119ea85c4b9205c879ca7.png", IMAGE_URL_2,
SelfossModel.SourceParams("url") SelfossModel.SourceParams("url")
) )
) )

View File

@ -7,8 +7,8 @@ buildscript {
plugins { plugins {
//trick: for the same plugin versions in all sub-modules //trick: for the same plugin versions in all sub-modules
id("com.android.application").version("7.3.1").apply(false) id("com.android.application").version("7.4.0").apply(false)
id("com.android.library").version("7.3.1").apply(false) id("com.android.library").version("7.4.0").apply(false)
kotlin("android").version("1.7.20").apply(false) kotlin("android").version("1.7.20").apply(false)
kotlin("multiplatform").version("1.7.20").apply(false) kotlin("multiplatform").version("1.7.20").apply(false)
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false) id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)

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

@ -8,7 +8,7 @@ class MercuryModel {
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

@ -81,19 +81,13 @@ class SelfossModel {
val tags: List<String>, val tags: List<String>,
val author: String? val author: String?
) { ) {
// TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String { fun getLinkDecoded(): String {
var stringUrl: String var stringUrl: String
stringUrl = stringUrl = if (link.contains("//news.google.com/news/") && link.contains("&amp;url=")) {
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
link.substringAfter("&amp;url=") link.substringAfter("&amp;url=")
} else { } else {
this.link.replace("&amp;", "&") this.link.replace("&amp;", "&")
} }
} else {
this.link.replace("&amp;", "&")
}
// handle :443 => https // handle :443 => https
if (stringUrl.contains(":443")) { if (stringUrl.contains(":443")) {

View File

@ -51,9 +51,7 @@ class Repository(
private var _selectedSource: SelfossModel.Source? = null private var _selectedSource: SelfossModel.Source? = null
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// TODO: Use the updatedSince parameter
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
var fromDB = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
fetchedItems = api.getItems( fetchedItems = api.getItems(
displayedItems.type, displayedItems.type,
@ -63,9 +61,7 @@ class Repository(
searchFilter, searchFilter,
null null
) )
} else { } else if (appSettingsService.isItemCachingEnabled()) {
if (appSettingsService.isItemCachingEnabled()) {
fromDB = true
var dbItems = getDBItems().filter { var dbItems = getDBItems().filter {
displayedItems == ItemType.ALL || displayedItems == ItemType.ALL ||
(it.unread && displayedItems == ItemType.UNREAD) || (it.unread && displayedItems == ItemType.UNREAD) ||
@ -77,17 +73,15 @@ class Repository(
if (sourceFilter.value != null) { if (sourceFilter.value != null) {
dbItems = dbItems.filter { it.sourcetitle == sourceFilter.value!!.title } dbItems = dbItems.filter { it.sourcetitle == sourceFilter.value!!.title }
} }
val itemsList = ArrayList(dbItems.map { it.toView() })
itemsList.sortByDescending { DateUtils.parseDate(it.datetime) }
fetchedItems = StatusAndData.succes( fetchedItems = StatusAndData.succes(
dbItems.map { it.toView() } itemsList
) )
} }
}
if (fetchedItems.success && fetchedItems.data != null) { if (fetchedItems.success && fetchedItems.data != null) {
items = ArrayList(fetchedItems.data!!) items = ArrayList(fetchedItems.data!!)
if (fromDB) {
items.sortByDescending { DateUtils.parseDate(it.datetime) }
}
} }
return items return items
} }
@ -173,14 +167,13 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun getSpouts(): Map<String, SelfossModel.Spout> { suspend fun getSpouts(): Map<String, SelfossModel.Spout> {
return if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
val spouts = api.spouts() val spouts = api.spouts()
if (spouts.success && spouts.data != null) { if (spouts.success && spouts.data != null) {
spouts.data spouts.data
} else { } else {
emptyMap() // TODO: do something here emptyMap()
} }
} else { } else {
throw NetworkUnavailableException() throw NetworkUnavailableException()
@ -206,7 +199,6 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun markAsRead(item: SelfossModel.Item): Boolean { suspend fun markAsRead(item: SelfossModel.Item): Boolean {
val success = markAsReadById(item.id) val success = markAsReadById(item.id)
@ -225,7 +217,6 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean { suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
val success = unmarkAsReadById(item.id) val success = unmarkAsReadById(item.id)
@ -244,7 +235,6 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun starr(item: SelfossModel.Item): Boolean { suspend fun starr(item: SelfossModel.Item): Boolean {
val success = starrById(item.id) val success = starrById(item.id)
@ -263,7 +253,6 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun unstarr(item: SelfossModel.Item): Boolean { suspend fun unstarr(item: SelfossModel.Item): Boolean {
val success = unstarrById(item.id) val success = unstarrById(item.id)
@ -282,7 +271,6 @@ class Repository(
} }
} }
// TODO: Add tests
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean { suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false var success = false
@ -542,7 +530,6 @@ class Repository(
return emptyList() return emptyList()
} }
// TODO: Add tests
suspend fun handleDBActions() { suspend fun handleDBActions() {
val actions: List<ACTION> = getDBActions() val actions: List<ACTION> = getDBActions()

View File

@ -1,8 +1,6 @@
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 {
val settings: Settings = Settings() val settings: Settings = Settings()