From 95e76a55dae61614db4d926ee6273b8c06692143 Mon Sep 17 00:00:00 2001 From: aminecmi Date: Wed, 11 Jan 2023 20:51:53 +0100 Subject: [PATCH] Cleaning. --- .../android/HomeActivity.kt | 3 - .../android/LoginActivity.kt | 103 +++-- .../apps/readerforselfossv2/android/MyApp.kt | 60 ++- .../android/ReaderActivity.kt | 2 - .../android/background/background.kt | 118 ++--- .../android/fragments/ArticleFragment.kt | 425 ++++++++++-------- .../android/fragments/FilterSheetFragment.kt | 247 +++++----- .../android/model/AndroidIModelUtils.kt | 1 - .../android/settings/SettingsActivity.kt | 9 +- androidApp/src/test/kotlin/RepositoryTest.kt | 60 ++- .../readerforselfossv2/model/MercuryModel.kt | 2 +- .../readerforselfossv2/model/SelfossModel.kt | 16 +- .../repository/RepositoryImpl.kt | 47 +- .../service/AppSettingsService.kt | 2 - 14 files changed, 597 insertions(+), 498 deletions(-) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt index a50d535..64f9428 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt @@ -366,7 +366,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar fun fetchOnEmptyList() { binding.recyclerView.doOnNextLayout { - // TODO: do if last element (or is empty ?) getElementsAccordingToTab(true) } } @@ -539,11 +538,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar R.id.refresh -> { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() - // TODO: Use Dispatchers.IO CoroutineScope(Dispatchers.Main).launch { val updatedRemote = repository.updateRemote() if (updatedRemote) { - // TODO: Send toast messages from the repository Toast.makeText( this@HomeActivity, R.string.refresh_success_response, Toast.LENGTH_LONG diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt index 54902e1..a47a1af 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt @@ -27,7 +27,6 @@ import org.acra.ACRA import org.kodein.di.DIAware import org.kodein.di.android.closestDI import org.kodein.di.instance -import java.security.MessageDigest class LoginActivity : AppCompatActivity(), DIAware { @@ -157,13 +156,64 @@ class LoginActivity : AppCompatActivity(), DIAware { val login = binding.loginView.text.toString().trim() val password = binding.passwordView.text.toString().trim() - var cancel = false - var focusView: View? = null + failInvalidUrl(url) + failLoginDetails(password, login) + showProgress(true) + + repository.refreshLoginInformation(url, login, password) + + CoroutineScope(Dispatchers.Main).launch { + val result = repository.login() + if (result) { + val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance() + if (!errorFetching && !displaySelfossOnly) { + goToMain() + } else { + if (displaySelfossOnly) { + Toast.makeText( + applicationContext, + R.string.application_selfoss_only, + Toast.LENGTH_LONG + ).show() + } + preferenceError() + } + } else { + preferenceError() + } + showProgress(false) + } + } + + private fun failLoginDetails( + password: String, + login: String + ) { + var lastFocusedView: View? = null + var cancel = false + if (isWithLogin) { + if (TextUtils.isEmpty(password)) { + binding.passwordView.error = getString(R.string.error_invalid_password) + lastFocusedView = binding.passwordView + cancel = true + } + + if (TextUtils.isEmpty(login)) { + binding.loginView.error = getString(R.string.error_field_required) + lastFocusedView = binding.loginView + cancel = true + } + } + maybeCancelAndFocusView(cancel, lastFocusedView) + } + + private fun failInvalidUrl(url: String) { + val focusView = binding.urlView + var cancel = false if (url.isBaseUrlInvalid()) { - binding.urlView.error = getString(R.string.login_url_problem) - focusView = binding.urlView cancel = true + binding.urlView.error = getString(R.string.login_url_problem) inValidCount++ if (inValidCount == 3) { val alertDialog = AlertDialog.Builder(this).create() @@ -177,49 +227,12 @@ class LoginActivity : AppCompatActivity(), DIAware { inValidCount = 0 } } + maybeCancelAndFocusView(cancel, focusView) + } - 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 - } - } - + private fun maybeCancelAndFocusView(cancel: Boolean, focusView: View?) { if (cancel) { focusView?.requestFocus() - } else { - showProgress(true) - - repository.refreshLoginInformation(url, login, password) - - CoroutineScope(Dispatchers.Main).launch { - val result = repository.login() - if (result) { - val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance() - if (!errorFetching && !displaySelfossOnly) { - goToMain() - } else { - if (displaySelfossOnly) { - Toast.makeText( - applicationContext, - R.string.application_selfoss_only, - Toast.LENGTH_LONG - ).show() - } - preferenceError() - } - } else { - preferenceError() - } - showProgress(false) - } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt index 266f2e5..d46e579 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt @@ -37,7 +37,14 @@ class MyApp : MultiDexApplication(), DIAware { import(networkModule) bind() with singleton { DriverFactory(applicationContext) } bind() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) } - bind() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) } + bind() with singleton { + Repository( + instance(), + instance(), + isConnectionAvailable, + instance() + ) + } bind() with singleton { ConnectivityStatus(applicationContext) } bind() with singleton { AppViewModel(repository = instance()) } } @@ -59,7 +66,12 @@ class MyApp : MultiDexApplication(), DIAware { handleNotificationChannels() - ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository)) + ProcessLifecycleOwner.get().lifecycle.addObserver( + AppLifeCycleObserver( + connectivityStatus, + repository + ) + ) CoroutineScope(Dispatchers.Main).launch { viewModel.networkAvailableProvider.collect { networkAvailable -> @@ -88,20 +100,36 @@ class MyApp : MultiDexApplication(), DIAware { initAcra { reportFormat = StringFormat.JSON reportContent = listOf( - ReportField.REPORT_ID, ReportField.INSTALLATION_ID, - ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME, - ReportField.BUILD, ReportField.ANDROID_VERSION, 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) + ReportField.REPORT_ID, + ReportField.INSTALLATION_ID, + ReportField.APP_VERSION_CODE, + ReportField.APP_VERSION_NAME, + ReportField.BUILD, + ReportField.ANDROID_VERSION, + 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 { //required text = getString(R.string.crash_toast_text) length = Toast.LENGTH_SHORT } 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" basicAuthPassword = "he6ghHp83F0PYPfh" httpMethod = HttpSender.Method.POST @@ -119,7 +147,11 @@ class MyApp : MultiDexApplication(), DIAware { val newItemsChannelname = getString(R.string.new_items_channel_sync) val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT - val newItemsChannelmChannel = NotificationChannel(AppSettingsService.newItemsChannelId, newItemsChannelname, newItemsChannelimportance) + val newItemsChannelmChannel = NotificationChannel( + AppSettingsService.newItemsChannelId, + newItemsChannelname, + newItemsChannelimportance + ) notificationManager.createNotificationChannel(mChannel) notificationManager.createNotificationChannel(newItemsChannelmChannel) @@ -133,13 +165,17 @@ class MyApp : MultiDexApplication(), DIAware { if (e is NoClassDefFoundError && e.stackTrace.asList().any { it.toString().contains("android.view.ViewDebug") }) { + // Nothing } else { 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) { super.onResume(owner) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt index 5a9035b..70ec25d 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt @@ -184,13 +184,11 @@ class ReaderActivity : AppCompatActivity(), DIAware { if (allItems[binding.pager.currentItem].starred) { CoroutineScope(Dispatchers.IO).launch { repository.unstarr(allItems[binding.pager.currentItem]) - // TODO: Handle failure } afterUnsave() } else { CoroutineScope(Dispatchers.IO).launch { repository.starr(allItems[binding.pager.currentItem]) - // TODO: Handle failure } afterSave() } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt index a565507..32b2ac9 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt @@ -26,85 +26,89 @@ import org.kodein.di.instance import java.util.* 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 } - private val repository : Repository by instance() - private val appSettingsService : AppSettingsService by instance() + private val repository: Repository by instance() + private val appSettingsService: AppSettingsService by instance() -override fun doWork(): Result { - if (appSettingsService.isPeriodicRefreshEnabled() && isNetworkAccessible(context)) { + override fun doWork(): Result { + if (appSettingsService.isPeriodicRefreshEnabled() && isNetworkAccessible(context)) { - CoroutineScope(Dispatchers.IO).launch { - val notificationManager = - applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + CoroutineScope(Dispatchers.IO).launch { + val notificationManager = + applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val notification = - NotificationCompat.Builder(applicationContext, AppSettingsService.syncChannelId) - .setContentTitle(context.getString(R.string.loading_notification_title)) - .setContentText(context.getString(R.string.loading_notification_text)) - .setOngoing(true) - .setPriority(PRIORITY_LOW) - .setChannelId(AppSettingsService.syncChannelId) - .setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp) + val notification = + NotificationCompat.Builder(applicationContext, AppSettingsService.syncChannelId) + .setContentTitle(context.getString(R.string.loading_notification_title)) + .setContentText(context.getString(R.string.loading_notification_text)) + .setOngoing(true) + .setPriority(PRIORITY_LOW) + .setChannelId(AppSettingsService.syncChannelId) + .setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp) - notificationManager.notify(1, notification.build()) + notificationManager.notify(1, notification.build()) - repository.handleDBActions() + repository.handleDBActions() - val apiItems = repository.tryToCacheItemsAndGetNewOnes() - if (appSettingsService.isNotifyNewItemsEnabled()) { - launch { - handleNewItemsNotification(apiItems, notificationManager) + val apiItems = repository.tryToCacheItemsAndGetNewOnes() + if (appSettingsService.isNotifyNewItemsEnabled()) { + launch { + handleNewItemsNotification(apiItems, notificationManager) + } } + apiItems.map { it.preloadImages(context) } } - apiItems.map { it.preloadImages(context) } } + return Result.success() } - return Result.success() -} private fun handleNewItemsNotification( newItems: List?, notificationManager: NotificationManager ) { - // TODO: Check if this coroutine is actually required CoroutineScope(Dispatchers.IO).launch { - val apiItems = newItems.orEmpty() + val apiItems = newItems.orEmpty() - val newSize = apiItems.filter { it.unread }.size - if (newSize > 0) { + val newSize = apiItems.filter { it.unread }.size + if (newSize > 0) { - val intent = Intent(context, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_IMMUTABLE - } else { - 0 - } - val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, pflags) - - val newItemsNotification = - NotificationCompat.Builder(applicationContext, AppSettingsService.newItemsChannelId) - .setContentTitle(context.getString(R.string.new_items_notification_title)) - .setContentText( - context.getString( - R.string.new_items_notification_text, - newSize - ) - ) - .setPriority(PRIORITY_DEFAULT) - .setChannelId(AppSettingsService.newItemsChannelId) - .setContentIntent(pendingIntent) - .setAutoCancel(true) - .setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp) - - Timer("", false).schedule(4000) { - notificationManager.notify(2, newItemsNotification.build()) - } + val intent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } + val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_IMMUTABLE + } else { + 0 + } + val pendingIntent: PendingIntent = + PendingIntent.getActivity(context, 0, intent, pflags) + + val newItemsNotification = + NotificationCompat.Builder( + applicationContext, + AppSettingsService.newItemsChannelId + ) + .setContentTitle(context.getString(R.string.new_items_notification_title)) + .setContentText( + context.getString( + R.string.new_items_notification_text, + newSize + ) + ) + .setPriority(PRIORITY_DEFAULT) + .setChannelId(AppSettingsService.newItemsChannelId) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp) + + Timer("", false).schedule(4000) { + notificationManager.notify(2, newItemsNotification.build()) + } + } Timer("", false).schedule(4000) { notificationManager.cancel(1) } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt index ef07d50..3fb842c 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt @@ -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.openInBrowserAsNewTask 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.StatusAndData import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.rest.MercuryApi import bou.amine.apps.readerforselfossv2.service.AppSettingsService @@ -55,6 +57,8 @@ import java.util.* import java.util.concurrent.ExecutionException +private const val IMAGE_JPG = "image/jpg" + class ArticleFragment : Fragment(), DIAware { private var fontSize: Int = 16 private lateinit var item: SelfossModel.Item @@ -63,12 +67,12 @@ class ArticleFragment : Fragment(), DIAware { private lateinit var contentSource: String private lateinit var contentImage: String private lateinit var contentTitle: String - private lateinit var allImages : ArrayList + private lateinit var allImages: ArrayList private lateinit var fab: FloatingActionButton private lateinit var textAlignment: String private lateinit var binding: FragmentArticleBinding - override val di : DI by closestDI() + override val di: DI by closestDI() private val repository: Repository by instance() private val appSettingsService: AppSettingsService by instance() @@ -77,7 +81,7 @@ class ArticleFragment : Fragment(), DIAware { private var font = "" private var staticBar = false - private val mercuryApi : MercuryApi by instance() + private val mercuryApi: MercuryApi by instance() override fun onCreate(savedInstanceState: Bundle?) { @@ -115,48 +119,7 @@ class ArticleFragment : Fragment(), DIAware { fab.rippleColor = resources.getColor(R.color.colorAccentDark) - val floatingToolbar: FloatingToolbar = binding.floatingToolbar - floatingToolbar.attachFab(fab) - - floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent)) - - floatingToolbar.setClickListener( - object : FloatingToolbar.ItemClickListener { - override fun onItemClick(item: MenuItem) { - when (item.itemId) { - R.id.share_action -> requireActivity().shareLink(url, contentTitle) - R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item) - R.id.unread_action -> if (context != null) { - if (this@ArticleFragment.item.unread) { - CoroutineScope(Dispatchers.IO).launch { - repository.markAsRead(this@ArticleFragment.item) - } - this@ArticleFragment.item.unread = false - Toast.makeText( - context, - R.string.marked_as_read, - Toast.LENGTH_LONG - ).show() - } else { - CoroutineScope(Dispatchers.IO).launch { - repository.unmarkAsRead(this@ArticleFragment.item) - } - this@ArticleFragment.item.unread = true - Toast.makeText( - context, - R.string.marked_as_unread, - Toast.LENGTH_LONG - ).show() - } - } - else -> Unit - } - } - - override fun onItemLongClick(item: MenuItem?) { - } - } - ) + val floatingToolbar: FloatingToolbar = handleFloatingToolbar() if (staticBar) { fab.hide() @@ -168,28 +131,7 @@ class ArticleFragment : Fragment(), DIAware { 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 - } - } + handleContent() binding.nestedScrollView.setOnScrollChangeListener( NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> @@ -211,7 +153,8 @@ class ArticleFragment : Fragment(), DIAware { 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 + .setPositiveButton( + android.R.string.ok ) { _, _ -> appSettingsService.disableArticleViewer() requireActivity().finish() @@ -223,6 +166,80 @@ class ArticleFragment : Fragment(), DIAware { 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 + floatingToolbar.attachFab(fab) + + floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent)) + + floatingToolbar.setClickListener( + object : FloatingToolbar.ItemClickListener { + override fun onItemClick(item: MenuItem) { + when (item.itemId) { + R.id.share_action -> requireActivity().shareLink(url, contentTitle) + R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item) + R.id.unread_action -> if (context != null) { + if (this@ArticleFragment.item.unread) { + CoroutineScope(Dispatchers.IO).launch { + repository.markAsRead(this@ArticleFragment.item) + } + this@ArticleFragment.item.unread = false + Toast.makeText( + context, + R.string.marked_as_read, + Toast.LENGTH_LONG + ).show() + } else { + CoroutineScope(Dispatchers.IO).launch { + repository.unmarkAsRead(this@ArticleFragment.item) + } + this@ArticleFragment.item.unread = true + Toast.makeText( + context, + R.string.marked_as_unread, + Toast.LENGTH_LONG + ).show() + } + } + else -> Unit + } + } + + override fun onItemLongClick(item: MenuItem?) { + // We do nothing + } + } + ) + return floatingToolbar + } + private fun refreshAlignment() { textAlignment = when (appSettingsService.getActiveAllignment()) { 1 -> "justify" @@ -232,75 +249,108 @@ class ArticleFragment : Fragment(), DIAware { } private fun getContentFromMercury() { - if (repository.isNetworkAvailable()) { - binding.progressBar.visibility = View.VISIBLE + binding.progressBar.visibility = View.VISIBLE - CoroutineScope(Dispatchers.Main).launch { - try { - val response = mercuryApi.query(url) - if (response.success && response.data != null && !response.data?.content.isNullOrEmpty()) { - binding.titleView.text = response.data!!.title.orEmpty() - try { - if (typeface != null) { - binding.titleView.typeface = typeface - } - } catch (e: Exception) { - e.sendSilentlyWithAcraWithName("getContentFromMercury > typeface") - } - - try { - // Note: Mercury may return relative urls... If it does the url val will not be changed. - URL(response.data!!.url) - url = response.data!!.url - } catch (e: MalformedURLException) { - // Mercury returned a relative url - e.sendSilentlyWithAcraWithName("getContentFromMercury > malformedurlexception") - } - - try { - contentText = response.data!!.content.orEmpty() - htmlToWebview() - } catch (e: Exception) { - e.sendSilentlyWithAcraWithName("getContentFromMercury > contenttext or html") - } - - if (!response.data?.lead_image_url.isNullOrEmpty() && context != null) { - try { - binding.imageView.visibility = View.VISIBLE - try { - Glide - .with(requireContext()) - .asBitmap() - .load( - response.data!!.lead_image_url.orEmpty() - ) - .apply(RequestOptions.fitCenterTransform()) - .into(binding.imageView) - } catch (e: Exception) { - e.sendSilentlyWithAcraWithName("getContentFromMercury > glide lead image") - } - } catch (e: Exception) { - e.sendSilentlyWithAcraWithName("getContentFromMercury > outside glide lead image") - } - } else { - binding.imageView.visibility = View.GONE - } - - try { - binding.nestedScrollView.scrollTo(0, 0) - binding.progressBar.visibility = View.GONE - } catch (e: Exception) { - e.sendSilentlyWithAcraWithName("getContentFromMercury > scrollview") - } - } else { - openInBrowserAfterFailing() + CoroutineScope(Dispatchers.Main).launch { + try { + val response = mercuryApi.query(url) + if (response.success && response.data != null && !response.data?.content.isNullOrEmpty()) { + binding.titleView.text = response.data!!.title.orEmpty() + if (typeface != null) { + binding.titleView.typeface = typeface } - } catch (e: SocketTimeoutException) { - openInBrowserAfterFailing() - } catch (e: Exception) { - e.sendSilentlyWithAcraWithName("getContentFromMercury > whole thing") + URL(response.data!!.url) + url = response.data!!.url + + contentText = response.data!!.content.orEmpty() + htmlToWebview() + + handleLeadImage(response) + + binding.nestedScrollView.scrollTo(0, 0) + binding.progressBar.visibility = View.GONE + } else { openInBrowserAfterFailing() } + } catch (e: SocketTimeoutException) { + openInBrowserAfterFailing() + } catch (e: Exception) { + e.sendSilentlyWithAcraWithName("getContentFromMercury > $url") + openInBrowserAfterFailing() + } + } + } + + private fun handleLeadImage(response: StatusAndData) { + if (!response.data?.lead_image_url.isNullOrEmpty() && context != null) { + binding.imageView.visibility = View.VISIBLE + Glide + .with(requireContext()) + .asBitmap() + .load( + response.data!!.lead_image_url.orEmpty() + ) + .apply(RequestOptions.fitCenterTransform()) + .into(binding.imageView) + } else { + binding.imageView.visibility = View.GONE + } + } + + private fun handleImageLoading() { + binding.webcontent.webViewClient = object : WebViewClient() { + @Deprecated("Deprecated in Java") + override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean { + if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { + requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + } + return true + } + + @Deprecated("Deprecated in Java") + override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? { + val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) + if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US) + .contains(".jpeg") + ) { + try { + val image = + Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() + return WebResourceResponse( + IMAGE_JPG, + "UTF-8", + getBitmapInputStream(image, Bitmap.CompressFormat.JPEG) + ) + } catch (e: ExecutionException) { + e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg > $url") + } + } else if (url.lowercase(Locale.US).contains(".png")) { + try { + val image = + Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() + return WebResourceResponse( + IMAGE_JPG, + "UTF-8", + getBitmapInputStream(image, Bitmap.CompressFormat.PNG) + ) + } catch (e: ExecutionException) { + e.sendSilentlyWithAcraWithName("shouldInterceptRequest > png > $url") + } + } else if (url.lowercase(Locale.US).contains(".webp")) { + try { + val image = + Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() + return WebResourceResponse( + IMAGE_JPG, + "UTF-8", + getBitmapInputStream(image, Bitmap.CompressFormat.WEBP) + ) + } catch (e: ExecutionException) { + e.sendSilentlyWithAcraWithName("shouldInterceptRequest > webp > $url") + } + } + + return super.shouldInterceptRequest(view, url) } } } @@ -324,57 +374,19 @@ class ArticleFragment : Fragment(), DIAware { binding.webcontent.settings.loadWithOverviewMode = true binding.webcontent.settings.javaScriptEnabled = false - 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 - } + handleImageLoading() - @Deprecated("Deprecated in Java") - override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? { - val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) - if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US).contains(".jpeg")) { - try { - val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() - return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)) - } catch ( e : ExecutionException) { - e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg > $url") - } - } - else if (url.lowercase(Locale.US).contains(".png")) { - try { - val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() - return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG)) - } catch ( e : ExecutionException) { - e.sendSilentlyWithAcraWithName("shouldInterceptRequest > png > $url") - } - } - else if (url.lowercase(Locale.US).contains(".webp")) { - try { - val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() - return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)) - } catch ( e : ExecutionException) { - e.sendSilentlyWithAcraWithName("shouldInterceptRequest > webp > $url") - } + val gestureDetector = + GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() { + override fun onSingleTapUp(e: MotionEvent): Boolean { + return performClick() } + }) - return super.shouldInterceptRequest(view, url) - } - } - - 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 + WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING var baseUrl: String? = null @@ -385,7 +397,7 @@ class ArticleFragment : Fragment(), DIAware { e.sendSilentlyWithAcraWithName("htmlToWebview > item url") } - val fontName = when (font) { + val fontName = when (font) { getString(R.string.open_sans_font_id) -> "Open Sans" getString(R.string.roboto_font_id) -> "Roboto" getString(R.string.source_code_pro_font_id) -> "Source Code Pro" @@ -393,7 +405,12 @@ class ArticleFragment : Fragment(), DIAware { } val fontLinkAndStyle = if (font.isNotEmpty()) { - """ + """ | | $fontLinkAndStyle @@ -458,12 +500,12 @@ class ArticleFragment : Fragment(), DIAware { fun scrollDown() { val height = binding.nestedScrollView.measuredHeight - binding.nestedScrollView.smoothScrollBy(0, height/2) + binding.nestedScrollView.smoothScrollBy(0, height / 2) } fun scrollUp() { val height = binding.nestedScrollView.measuredHeight - binding.nestedScrollView.smoothScrollBy(0, -height/2) + binding.nestedScrollView.smoothScrollBy(0, -height / 2) } private fun openInBrowserAfterFailing() { @@ -475,7 +517,7 @@ class ArticleFragment : Fragment(), DIAware { private const val ARG_ITEMS = "items" fun newInstance( - item: SelfossModel.Item + item: SelfossModel.Item ): ArticleFragment { val fragment = ArticleFragment() val args = Bundle() @@ -487,9 +529,10 @@ class ArticleFragment : Fragment(), DIAware { fun performClick(): Boolean { if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || - binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { + binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE + ) { - val position : Int = allImages.indexOf(binding.webcontent.hitTestResult.extra) + val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra) val intent = Intent(activity, ImageActivity::class.java) intent.putExtra("allImages", allImages) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt index d08f241..5ab2a06 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt @@ -13,6 +13,7 @@ import android.view.View.VISIBLE import android.view.ViewGroup import bou.amine.apps.readerforselfossv2.android.HomeActivity 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.repository.Repository import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded @@ -35,6 +36,7 @@ import org.kodein.di.instance class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { + private lateinit var binding: FilterFragmentBinding override val di: DI by closestDI() private val repository: Repository by instance() @@ -45,8 +47,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { container: ViewGroup?, savedInstanceState: Bundle? ): View { - val binding = - bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding.inflate( + binding = + FilterFragmentBinding.inflate( inflater, container, false @@ -54,126 +56,14 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { val context: Context? = context - val tagGroup = binding.tagsGroup - val sourceGroup = binding.sourcesGroup if (context == null) { dismiss() Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView") } else { CoroutineScope(Dispatchers.Main).launch { - val tags = repository.getTags() - - tags.forEach { tag -> - val c = Chip(context) - 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) - } - - repository.getSources().forEach { source -> - val c = Chip(context) - - Glide.with(context) - .load(source.getIcon(repository.baseUrl)) - .listener(object : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean - ): Boolean { - try { - c.chipIcon = resource - } catch (e: Exception) { - e.sendSilentlyWithAcraWithName("sources > onResourceReady") - } - return false - } - }).preload() - - c.text = source.title.getHtmlDecoded() - - c.setOnCloseIconClickListener { - (it as Chip).isCloseIconVisible = false - selectedChip = null - repository.setSourceFilter(null) - } - - c.setOnClickListener { - if (selectedChip != null) { - selectedChip!!.isCloseIconVisible = false - } - (it as Chip).isCloseIconVisible = true - selectedChip = it - repository.setSourceFilter(source) - - repository.setTagFilter(null) - } - - - if (repository.sourceFilter.value?.equals(source) == true) { - c.isCloseIconVisible = true - selectedChip = c - } - - c.isEnabled = source.error.isBlank() - - if (source.error.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - c.tooltipText = source.error - } - - sourceGroup.addView(c) - } + handleTagChips(context) + handleSourceChips(context) binding.progressBar2.visibility = GONE binding.filterView.visibility = VISIBLE @@ -188,6 +78,131 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { return binding.root } + private suspend fun handleSourceChips( + context: Context + ) { + val sourceGroup = binding.sourcesGroup + + repository.getSources().forEach { source -> + val c = Chip(context) + + Glide.with(context) + .load(source.getIcon(repository.baseUrl)) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + try { + c.chipIcon = resource + } catch (e: Exception) { + e.sendSilentlyWithAcraWithName("sources > onResourceReady") + } + return false + } + }).preload() + + c.text = source.title.getHtmlDecoded() + + c.setOnCloseIconClickListener { + (it as Chip).isCloseIconVisible = false + selectedChip = null + repository.setSourceFilter(null) + } + + c.setOnClickListener { + if (selectedChip != null) { + selectedChip!!.isCloseIconVisible = false + } + (it as Chip).isCloseIconVisible = true + selectedChip = it + repository.setSourceFilter(source) + + repository.setTagFilter(null) + } + + + if (repository.sourceFilter.value?.equals(source) == true) { + c.isCloseIconVisible = true + selectedChip = c + } + + c.isEnabled = source.error.isBlank() + + if (source.error.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + c.tooltipText = source.error + } + + sourceGroup.addView(c) + } + } + + private suspend fun handleTagChips( + context: Context, + ) { + val tagGroup = binding.tagsGroup + + val tags = repository.getTags() + + tags.forEach { tag -> + val c = Chip(context) + c.text = tag.tag + + try { + val gd = GradientDrawable() + val gdColor = try { + Color.parseColor(tag.color) + } catch (e: IllegalArgumentException) { + e.sendSilentlyWithAcraWithName("color issue " + tag.color) + resources.getColor(R.color.colorPrimary) + } + gd.setColor(gdColor) + gd.shape = GradientDrawable.RECTANGLE + gd.setSize(30, 30) + gd.cornerRadius = 30F + c.chipIcon = gd + } catch (e: Exception) { + e.sendSilentlyWithAcraWithName("tags > GradientDrawable") + } + + c.setOnCloseIconClickListener { + (it as Chip).isCloseIconVisible = false + selectedChip = null + repository.setTagFilter(null) + } + + c.setOnClickListener { + if (selectedChip != null) { + selectedChip!!.isCloseIconVisible = false + } + (it as Chip).isCloseIconVisible = true + selectedChip = it + repository.setTagFilter(tag) + + repository.setSourceFilter(null) + } + + if (repository.tagFilter.value?.equals(tag) == true) { + c.isCloseIconVisible = true + selectedChip = c + } + + tagGroup.addView(c) + } + } + companion object { const val TAG = "FilterModalBottomSheet" } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/AndroidIModelUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/AndroidIModelUtils.kt index ab0a8d2..47c7718 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/AndroidIModelUtils.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/AndroidIModelUtils.kt @@ -8,7 +8,6 @@ import bou.amine.apps.readerforselfossv2.utils.getImages import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions -import org.acra.ktx.sendSilentlyWithAcra fun SelfossModel.Item.preloadImages(context: Context) : Boolean { val imageUrls = this.getImages() diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt index 0d54147..0de8c99 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt @@ -21,7 +21,6 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService import com.mikepenz.aboutlibraries.LibsBuilder import org.kodein.di.DIAware import org.kodein.di.android.closestDI -import org.kodein.di.instance private const val TITLE_TAG = "settingsActivityTitle" @@ -146,8 +145,12 @@ class SettingsActivity : AppCompatActivity(), fontSize?.setOnBindEditTextListener { editText -> editText.inputType = InputType.TYPE_CLASS_NUMBER editText.addTextChangedListener { object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + override fun beforeTextChanged(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) { try { editText.textSize = editable.toString().toInt().toFloat() diff --git a/androidApp/src/test/kotlin/RepositoryTest.kt b/androidApp/src/test/kotlin/RepositoryTest.kt index 21f7028..38245b2 100644 --- a/androidApp/src/test/kotlin/RepositoryTest.kt +++ b/androidApp/src/test/kotlin/RepositoryTest.kt @@ -18,6 +18,18 @@ import org.junit.Assert.assertNotEquals import org.junit.Before 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 { private val db = mockk(relaxed = true) private val appSettingsService = mockk() @@ -45,7 +57,7 @@ class RepositoryTest { fun setup() { clearAllMocks() 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.isUpdateSourcesEnabled() } returns false @@ -229,9 +241,9 @@ class RepositoryTest { 1, "Test", listOf("tags"), - "spouts\\rss\\fulltextrss", + SPOUT, "", - "b3aa8a664d08eb15d6ff1db2fa83e0d9.png", + IMAGE_URL, SelfossModel.SourceParams("url") )) runBlocking { @@ -550,18 +562,18 @@ class RepositoryTest { 1, "First source", listOf("Test", "second"), - "spouts\\rss\\fulltextrss", + SPOUT, "", - "d8c92cdb1ef119ea85c4b9205c879ca7.png", + IMAGE_URL_2, SelfossModel.SourceParams("url") ), SelfossModel.Source( 2, "Second source", listOf("second"), - "spouts\\rss\\fulltextrss", + SPOUT, "", - "b3aa8a664d08eb15d6ff1db2fa83e0d9.png", + IMAGE_URL, SelfossModel.SourceParams("url") ) ) @@ -570,18 +582,18 @@ class RepositoryTest { "1", "First DB source", "Test,second", - "spouts\\rss\\fulltextrss", + SPOUT, "", - "d8c92cdb1ef119ea85c4b9205c879ca7.png", + IMAGE_URL_2, "url" ), SOURCE( "2", "Second source", "second", - "spouts\\rss\\fulltextrss", + SPOUT, "", - "b3aa8a664d08eb15d6ff1db2fa83e0d9.png", + IMAGE_URL, "url" ) ) @@ -720,9 +732,9 @@ class RepositoryTest { runBlocking { response = repository.createSource( "test", - "https://test.com/feed", - "spouts\\rss\\fulltextrss", - "Test, New", + FEED_URL, + SPOUT, + TAGS, ) } @@ -747,9 +759,9 @@ class RepositoryTest { runBlocking { response = repository.createSource( "test", - "https://test.com/feed", - "spouts\\rss\\fulltextrss", - "Test, New" + FEED_URL, + SPOUT, + TAGS ) } @@ -774,9 +786,9 @@ class RepositoryTest { runBlocking { response = repository.createSource( "test", - "https://test.com/feed", - "spouts\\rss\\fulltextrss", - "Test, New" + FEED_URL, + SPOUT, + TAGS ) } @@ -952,12 +964,12 @@ class RepositoryTest { coEvery { appSettingsService.refreshLoginInformation(any(), any(), any()) } returns Unit initializeRepository() - repository.refreshLoginInformation("https://test.com/selfoss/", "login", "password") + repository.refreshLoginInformation(BASE_URL, "login", "password") coVerify(exactly = 1) { api.refreshLoginInformation() } coVerify(exactly = 1) { appSettingsService.refreshLoginInformation( - "https://test.com/selfoss/", + BASE_URL, "login", "password" ) @@ -1031,9 +1043,9 @@ class RepositoryTest { 1, "First source", listOf("Test", "second"), - "spouts\\rss\\fulltextrss", + SPOUT, "", - "d8c92cdb1ef119ea85c4b9205c879ca7.png", + IMAGE_URL_2, SelfossModel.SourceParams("url") ) ) diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/MercuryModel.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/MercuryModel.kt index a55e5df..7915058 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/MercuryModel.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/MercuryModel.kt @@ -8,7 +8,7 @@ class MercuryModel { class ParsedContent( val title: String?, val content: String?, - val lead_image_url: String?, + val lead_image_url: String?, // NOSONAR val url: String ) } diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt index aec8acd..e5eec2a 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/SelfossModel.kt @@ -81,19 +81,13 @@ class SelfossModel { val tags: List, val author: String? ) { - // TODO: maybe find a better way to handle these kind of urls fun getLinkDecoded(): String { var stringUrl: String - stringUrl = - if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) { - if (link.contains("&url=")) { - link.substringAfter("&url=") - } else { - this.link.replace("&", "&") - } - } else { - this.link.replace("&", "&") - } + stringUrl = if (link.contains("//news.google.com/news/") && link.contains("&url=")) { + link.substringAfter("&url=") + } else { + this.link.replace("&", "&") + } // handle :443 => https if (stringUrl.contains(":443")) { diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt index 04457ac..fb03172 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt @@ -51,9 +51,7 @@ class Repository( private var _selectedSource: SelfossModel.Source? = null suspend fun getNewerItems(): ArrayList { - // TODO: Use the updatedSince parameter var fetchedItems: StatusAndData> = StatusAndData.error() - var fromDB = false if (isNetworkAvailable()) { fetchedItems = api.getItems( displayedItems.type, @@ -63,31 +61,27 @@ class Repository( searchFilter, null ) - } else { - if (appSettingsService.isItemCachingEnabled()) { - fromDB = true - var dbItems = getDBItems().filter { - displayedItems == ItemType.ALL || - (it.unread && displayedItems == ItemType.UNREAD) || - (it.starred && displayedItems == ItemType.STARRED) - } - if (tagFilter.value != null) { - dbItems = dbItems.filter { it.tags.split(',').contains(tagFilter.value!!.tag) } - } - if (sourceFilter.value != null) { - dbItems = dbItems.filter { it.sourcetitle == sourceFilter.value!!.title } - } - fetchedItems = StatusAndData.succes( - dbItems.map { it.toView() } - ) + } else if (appSettingsService.isItemCachingEnabled()) { + var dbItems = getDBItems().filter { + displayedItems == ItemType.ALL || + (it.unread && displayedItems == ItemType.UNREAD) || + (it.starred && displayedItems == ItemType.STARRED) } + if (tagFilter.value != null) { + dbItems = dbItems.filter { it.tags.split(',').contains(tagFilter.value!!.tag) } + } + if (sourceFilter.value != null) { + dbItems = dbItems.filter { it.sourcetitle == sourceFilter.value!!.title } + } + val itemsList = ArrayList(dbItems.map { it.toView() }) + itemsList.sortByDescending { DateUtils.parseDate(it.datetime) } + fetchedItems = StatusAndData.succes( + itemsList + ) } if (fetchedItems.success && fetchedItems.data != null) { items = ArrayList(fetchedItems.data!!) - if (fromDB) { - items.sortByDescending { DateUtils.parseDate(it.datetime) } - } } return items } @@ -173,14 +167,13 @@ class Repository( } } - // TODO: Add tests suspend fun getSpouts(): Map { return if (isNetworkAvailable()) { val spouts = api.spouts() if (spouts.success && spouts.data != null) { spouts.data } else { - emptyMap() // TODO: do something here + emptyMap() } } else { throw NetworkUnavailableException() @@ -206,7 +199,6 @@ class Repository( } } - // TODO: Add tests suspend fun markAsRead(item: SelfossModel.Item): Boolean { val success = markAsReadById(item.id) @@ -225,7 +217,6 @@ class Repository( } } - // TODO: Add tests suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean { val success = unmarkAsReadById(item.id) @@ -244,7 +235,6 @@ class Repository( } } - // TODO: Add tests suspend fun starr(item: SelfossModel.Item): Boolean { val success = starrById(item.id) @@ -263,7 +253,6 @@ class Repository( } } - // TODO: Add tests suspend fun unstarr(item: SelfossModel.Item): Boolean { val success = unstarrById(item.id) @@ -282,7 +271,6 @@ class Repository( } } - // TODO: Add tests suspend fun markAllAsRead(items: ArrayList): Boolean { var success = false @@ -542,7 +530,6 @@ class Repository( return emptyList() } - // TODO: Add tests suspend fun handleDBActions() { val actions: List = getDBActions() diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt index 12b4d94..d5b622d 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt @@ -1,8 +1,6 @@ package bou.amine.apps.readerforselfossv2.service import com.russhwolf.settings.Settings -import io.github.aakira.napier.Napier -import io.ktor.client.plugins.* class AppSettingsService { val settings: Settings = Settings()