Compare commits
	
		
			23 Commits
		
	
	
		
			8e9b87f00c
			...
			v124113251
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 236e1cca90 | |||
| 3a33cb4510 | |||
| 0bf9ca9a49 | |||
|  | 61e0087894 | ||
|  | 1ec05d9913 | ||
|  | 859bd91bbb | ||
|  | 204b736c53 | ||
|  | f24609c143 | ||
|  | b94d7dc537 | ||
|  | 41910cc4cd | ||
|  | db166ca9d4 | ||
|  | db0d5a4a85 | ||
|  | 3bc0d7cf95 | ||
|  | 8f464d95fd | ||
|  | 5ccd6a3368 | ||
|  | cdbded246e | ||
|  | 750c7758bd | ||
|  | 22f8b14ecd | ||
|  | 6e27d6d4e6 | ||
|  | 14ff4dbd05 | ||
|  | 390c2d0cf3 | ||
|  | e58914ef58 | ||
|  | a03f08fca1 | 
							
								
								
									
										60
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,63 @@ | ||||
| **v124041081** | ||||
|  | ||||
| - chore: comment. | ||||
| - fix: Last time fixing the parsing date hack before moving it to os version. | ||||
| - Changelog for v124030731 [CI SKIP] | ||||
|  | ||||
| -------------------------------------------------------------------- | ||||
|  | ||||
| **v124030731** | ||||
|  | ||||
| - fix: Basic auth and password can have non whitspace characters. Fixes 142. | ||||
| - Changelog for v124020451 [CI SKIP] | ||||
|  | ||||
| -------------------------------------------------------------------- | ||||
|  | ||||
| **v124020451** | ||||
|  | ||||
| - fix: Fixed handling of position in card adapter. | ||||
| - Changelog for v124010301 [CI SKIP] | ||||
|  | ||||
| -------------------------------------------------------------------- | ||||
|  | ||||
| **v124010301** | ||||
|  | ||||
| - fix: This may fix the oom errors. | ||||
| - Changelog for v124010191 [CI SKIP] | ||||
|  | ||||
| -------------------------------------------------------------------- | ||||
|  | ||||
| **v124010191** | ||||
|  | ||||
| - fix: moving listeners. | ||||
| - chore: removed a useless log. | ||||
| - Changelog for v124010032 [CI SKIP] | ||||
|  | ||||
| -------------------------------------------------------------------- | ||||
|  | ||||
| **v124010032** | ||||
|  | ||||
| - fix: Another date format thing. | ||||
| - Changelog for v124010031 [CI SKIP] | ||||
|  | ||||
| -------------------------------------------------------------------- | ||||
|  | ||||
| **v124010031** | ||||
|  | ||||
| - fix: Checking if selfoss instance. | ||||
| - fix: handle three characters lenght hexcode colors. | ||||
| - Changelog for v123113311 [CI SKIP] | ||||
|  | ||||
| -------------------------------------------------------------------- | ||||
|  | ||||
| **v123113311** | ||||
|  | ||||
| - chore: Source tracker url in the menu. | ||||
| - fix: Handle kodein proguard rules. | ||||
| - Changelog for v123102961 [CI SKIP] | ||||
|  | ||||
| -------------------------------------------------------------------- | ||||
|  | ||||
| **v123102961** | ||||
|  | ||||
| - chore: domain changes. | ||||
|   | ||||
							
								
								
									
										9
									
								
								androidApp/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								androidApp/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							| @@ -86,3 +86,12 @@ | ||||
|  | ||||
| -dontwarn io.mockk.** | ||||
| -keep class io.mockk.** { *; } | ||||
|  | ||||
|  | ||||
|  | ||||
| # Kodein | ||||
| -keep, allowobfuscation, allowoptimization class org.kodein.type.TypeReference | ||||
| -keep, allowobfuscation, allowoptimization class org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest | ||||
|  | ||||
| -keep, allowobfuscation, allowoptimization class * extends org.kodein.type.TypeReference | ||||
| -keep, allowobfuscation, allowoptimization class * extends org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest | ||||
| @@ -1,6 +1,7 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android | ||||
|  | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.view.Menu | ||||
| import android.view.MenuItem | ||||
| @@ -12,7 +13,11 @@ import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.appcompat.widget.SearchView | ||||
| import androidx.core.view.doOnNextLayout | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import androidx.recyclerview.widget.* | ||||
| import androidx.recyclerview.widget.DividerItemDecoration | ||||
| import androidx.recyclerview.widget.GridLayoutManager | ||||
| import androidx.recyclerview.widget.ItemTouchHelper | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import androidx.recyclerview.widget.StaggeredGridLayoutManager | ||||
| import androidx.work.Constraints | ||||
| import androidx.work.ExistingPeriodicWorkPolicy | ||||
| import androidx.work.PeriodicWorkRequestBuilder | ||||
| @@ -57,7 +62,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|     private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener | ||||
|     private lateinit var binding: ActivityHomeBinding | ||||
|  | ||||
|     private var recyclerAdapter: RecyclerView.Adapter<*>? = null | ||||
|     private var recyclerAdapter: ItemsAdapter<out RecyclerView.ViewHolder>? = null | ||||
|  | ||||
|     private var fromTabShortcut: Boolean = false | ||||
|  | ||||
| @@ -497,7 +502,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|             } | ||||
|             binding.recyclerView.adapter = recyclerAdapter | ||||
|         } else { | ||||
|             (recyclerAdapter as ItemsAdapter<*>).updateAllItems(items) | ||||
|             recyclerAdapter!!.updateAllItems(items) | ||||
|         } | ||||
|  | ||||
|         reloadBadges() | ||||
| @@ -565,6 +570,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|  | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         when (item.itemId) { | ||||
|             R.id.issue_tracker -> { | ||||
|                 val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) | ||||
|                 startActivity(browserIntent) | ||||
|                 return true | ||||
|             } | ||||
|             R.id.action_filter -> { | ||||
|                 val filterSheetFragment = FilterSheetFragment() | ||||
|                 filterSheetFragment.show(supportFragmentManager, FilterSheetFragment.TAG) | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import android.animation.Animator | ||||
| import android.animation.AnimatorListenerAdapter | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.text.TextUtils | ||||
| import android.view.Menu | ||||
| @@ -139,20 +140,25 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|         repository.refreshLoginInformation(url, login, password) | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             repository.updateApiInformation() | ||||
|             try { | ||||
|                 repository.updateApiInformation() | ||||
|             } catch (e: Exception) { | ||||
|                 if (e.message?.startsWith("No transformation found") == true) { | ||||
|                     Toast.makeText( | ||||
|                         applicationContext, | ||||
|                         R.string.application_selfoss_only, | ||||
|                         Toast.LENGTH_LONG, | ||||
|                     ).show() | ||||
|                     preferenceError() | ||||
|                     showProgress(false) | ||||
|                 } | ||||
|             } | ||||
|             val result = repository.login() | ||||
|             if (result) { | ||||
|                 val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance() | ||||
|                 if (!errorFetching && !displaySelfossOnly) { | ||||
|                 val errorFetching = repository.checkIfFetchFails() | ||||
|                 if (!errorFetching) { | ||||
|                     goToMain() | ||||
|                 } else { | ||||
|                     if (displaySelfossOnly) { | ||||
|                         Toast.makeText( | ||||
|                             applicationContext, | ||||
|                             R.string.application_selfoss_only, | ||||
|                             Toast.LENGTH_LONG, | ||||
|                         ).show() | ||||
|                     } | ||||
|                     preferenceError() | ||||
|                 } | ||||
|             } else { | ||||
| @@ -254,10 +260,17 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|  | ||||
|     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||
|         return when (item.itemId) { | ||||
|             R.id.issue_tracker -> { | ||||
|                 val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) | ||||
|                 startActivity(browserIntent) | ||||
|                 return true | ||||
|             } | ||||
|             R.id.about -> { | ||||
|                 LibsBuilder() | ||||
|                     .withAboutIconShown(true) | ||||
|                     .withAboutVersionShown(true) | ||||
|                     .withAboutSpecial2("Bug reports").withAboutSpecial2Description(AppSettingsService.trackerUrl) | ||||
|                     .withAboutSpecial1("Project Page").withAboutSpecial1Description(AppSettingsService.sourceUrl) | ||||
|                     .start(this) | ||||
|                 true | ||||
|             } | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.adapters | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| @@ -9,11 +8,11 @@ import android.widget.ImageView.ScaleType | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.shareLink | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| @@ -31,10 +30,10 @@ import org.kodein.di.instance | ||||
|  | ||||
| class ItemCardAdapter( | ||||
|     override val app: Activity, | ||||
|     override var items: ArrayList<SelfossModel.Item>, | ||||
|     override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit, | ||||
|     override val items: ArrayList<SelfossModel.Item>, | ||||
|     override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit, | ||||
| ) : ItemsAdapter<ItemCardAdapter.ViewHolder>() { | ||||
|     private val c: Context = app.baseContext | ||||
|     override lateinit var binding: CardItemBinding | ||||
|     private val imageMaxHeight: Int = | ||||
|         c.resources.getDimension(R.dimen.card_image_max_height).toInt() | ||||
|  | ||||
| @@ -46,10 +45,36 @@ class ItemCardAdapter( | ||||
|         parent: ViewGroup, | ||||
|         viewType: Int, | ||||
|     ): ViewHolder { | ||||
|         val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||||
|         binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||||
|         return ViewHolder(binding) | ||||
|     } | ||||
|  | ||||
|     private fun handleClickListeners(holderBinding: CardItemBinding, position: Int) { | ||||
|         holderBinding.favButton.setOnClickListener { | ||||
|             val item = items[position] | ||||
|             if (item.starred) { | ||||
|                 CoroutineScope(Dispatchers.IO).launch { | ||||
|                     repository.unstarr(item) | ||||
|                 } | ||||
|                 binding.favButton.isSelected = false | ||||
|             } else { | ||||
|                 CoroutineScope(Dispatchers.IO).launch { | ||||
|                     repository.starr(item) | ||||
|                 } | ||||
|                 binding.favButton.isSelected = true | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         binding.shareBtn.setOnClickListener { | ||||
|             val item = items[position] | ||||
|             c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded()) | ||||
|         } | ||||
|  | ||||
|         binding.browserBtn.setOnClickListener { | ||||
|             c.openInBrowserAsNewTask(items[position]) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onBindViewHolder( | ||||
|         holder: ViewHolder, | ||||
|         position: Int, | ||||
| @@ -57,6 +82,9 @@ class ItemCardAdapter( | ||||
|         with(holder) { | ||||
|             val itm = items[position] | ||||
|  | ||||
|             handleClickListeners(binding, position) | ||||
|             handleLinkOpening(binding, position) | ||||
|  | ||||
|             binding.favButton.isSelected = itm.starred | ||||
|             if (appSettingsService.getPublicAccess()) { | ||||
|                 binding.favButton.visibility = View.GONE | ||||
| @@ -68,7 +96,12 @@ class ItemCardAdapter( | ||||
|  | ||||
|             binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) | ||||
|  | ||||
|             binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate() | ||||
|             binding.sourceTitleAndDate.text = try { | ||||
|                 itm.sourceAuthorAndDate() | ||||
|             } catch (e: Exception) { | ||||
|                 e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date") | ||||
|                 itm.sourceAuthorOnly() | ||||
|             } | ||||
|  | ||||
|             if (!appSettingsService.isFullHeightCardsEnabled()) { | ||||
|                 binding.itemImage.maxHeight = imageMaxHeight | ||||
| @@ -92,52 +125,5 @@ class ItemCardAdapter( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun getItemCount(): Int { | ||||
|         return items.size | ||||
|     } | ||||
|  | ||||
|     inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) { | ||||
|         init { | ||||
|             handleClickListeners() | ||||
|             handleLinkOpening() | ||||
|         } | ||||
|  | ||||
|         private fun handleClickListeners() { | ||||
|             binding.favButton.setOnClickListener { | ||||
|                 val item = items[bindingAdapterPosition] | ||||
|                 if (item.starred) { | ||||
|                     CoroutineScope(Dispatchers.IO).launch { | ||||
|                         repository.unstarr(item) | ||||
|                     } | ||||
|                     binding.favButton.isSelected = false | ||||
|                 } else { | ||||
|                     CoroutineScope(Dispatchers.IO).launch { | ||||
|                         repository.starr(item) | ||||
|                     } | ||||
|                     binding.favButton.isSelected = true | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             binding.shareBtn.setOnClickListener { | ||||
|                 val item = items[bindingAdapterPosition] | ||||
|                 c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded()) | ||||
|             } | ||||
|  | ||||
|             binding.browserBtn.setOnClickListener { | ||||
|                 c.openInBrowserAsNewTask(items[bindingAdapterPosition]) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private fun handleLinkOpening() { | ||||
|             binding.root.setOnClickListener { | ||||
|                 repository.setReaderItems(items) | ||||
|                 c.openItemUrl( | ||||
|                     bindingAdapterPosition, | ||||
|                     items[bindingAdapterPosition].getLinkDecoded(), | ||||
|                     appSettingsService.isArticleViewerEnabled(), | ||||
|                     app, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,14 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.adapters | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| @@ -22,10 +21,10 @@ import org.kodein.di.instance | ||||
|  | ||||
| class ItemListAdapter( | ||||
|     override val app: Activity, | ||||
|     override var items: ArrayList<SelfossModel.Item>, | ||||
|     override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit, | ||||
|     override val items: ArrayList<SelfossModel.Item>, | ||||
|     override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit, | ||||
| ) : ItemsAdapter<ItemListAdapter.ViewHolder>() { | ||||
|     private val c: Context = app.baseContext | ||||
|     override lateinit var binding: ListItemBinding | ||||
|  | ||||
|     override val di: DI by closestDI(app) | ||||
|     override val repository: Repository by instance() | ||||
| @@ -35,7 +34,7 @@ class ItemListAdapter( | ||||
|         parent: ViewGroup, | ||||
|         viewType: Int, | ||||
|     ): ViewHolder { | ||||
|         val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||||
|         binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||||
|         return ViewHolder(binding) | ||||
|     } | ||||
|  | ||||
| @@ -46,13 +45,20 @@ class ItemListAdapter( | ||||
|         with(holder) { | ||||
|             val itm = items[position] | ||||
|  | ||||
|             handleLinkOpening(binding, position) | ||||
|  | ||||
|             binding.title.text = itm.title.getHtmlDecoded() | ||||
|  | ||||
|             binding.title.setOnTouchListener(LinkOnTouchListener()) | ||||
|  | ||||
|             binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) | ||||
|  | ||||
|             binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate() | ||||
|             binding.sourceTitleAndDate.text = try { | ||||
|                 itm.sourceAuthorAndDate() | ||||
|             } catch (e: Exception) { | ||||
|                 e.sendSilentlyWithAcraWithName("ItemListAdapter parse date") | ||||
|                 itm.sourceAuthorOnly() | ||||
|             } | ||||
|  | ||||
|             if (itm.getThumbnail(repository.baseUrl).isEmpty()) { | ||||
|                 if (itm.getIcon(repository.baseUrl).isEmpty()) { | ||||
| @@ -66,23 +72,5 @@ class ItemListAdapter( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun getItemCount(): Int = items.size | ||||
|  | ||||
|     inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) { | ||||
|         init { | ||||
|             handleLinkOpening() | ||||
|         } | ||||
|  | ||||
|         private fun handleLinkOpening() { | ||||
|             binding.root.setOnClickListener { | ||||
|                 repository.setReaderItems(items) | ||||
|                 c.openItemUrl( | ||||
|                     bindingAdapterPosition, | ||||
|                     items[bindingAdapterPosition].getLinkDecoded(), | ||||
|                     appSettingsService.isArticleViewerEnabled(), | ||||
|                     app, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,13 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.adapters | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.graphics.Color | ||||
| import android.widget.TextView | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import androidx.viewbinding.ViewBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| @@ -16,16 +19,20 @@ import kotlinx.coroutines.launch | ||||
| import org.kodein.di.DIAware | ||||
|  | ||||
| abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware { | ||||
|     abstract var items: ArrayList<SelfossModel.Item> | ||||
|     abstract val items: ArrayList<SelfossModel.Item> | ||||
|     abstract val repository: Repository | ||||
|     abstract val binding: ViewBinding | ||||
|     abstract val appSettingsService: AppSettingsService | ||||
|     abstract val app: Activity | ||||
|     abstract val updateItems: (ArrayList<SelfossModel.Item>) -> Unit | ||||
|     abstract val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit | ||||
|  | ||||
|     protected val c: Context get() = app.baseContext | ||||
|  | ||||
|     fun updateAllItems(items: ArrayList<SelfossModel.Item>) { | ||||
|         this.items = items | ||||
|         this.items.clear() | ||||
|         this.items.addAll(items) | ||||
|         updateHomeItems(items) | ||||
|         notifyDataSetChanged() | ||||
|         updateItems(this.items) | ||||
|     } | ||||
|  | ||||
|     private fun unmarkSnackbar( | ||||
| @@ -70,6 +77,18 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte | ||||
|         s.show() | ||||
|     } | ||||
|  | ||||
|     protected fun handleLinkOpening(holderBinding: ViewBinding, position: Int) { | ||||
|         holderBinding.root.setOnClickListener { | ||||
|             repository.setReaderItems(items) | ||||
|             c.openItemUrl( | ||||
|                 position, | ||||
|                 items[position].getLinkDecoded(), | ||||
|                 appSettingsService.isArticleViewerEnabled(), | ||||
|                 app, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun handleItemAtIndex(position: Int) { | ||||
|         if (items[position].unread) { | ||||
|             readItemAtIndex(items[position], position) | ||||
| @@ -89,7 +108,8 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte | ||||
|         if (repository.displayedItems == ItemType.UNREAD) { | ||||
|             items.remove(item) | ||||
|             notifyItemRemoved(position) | ||||
|             updateItems(items) | ||||
|             notifyItemRangeChanged(position, itemCount) | ||||
|             updateHomeItems(items) | ||||
|         } else { | ||||
|             notifyItemChanged(position) | ||||
|         } | ||||
| @@ -118,13 +138,15 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte | ||||
|     ) { | ||||
|         items.add(position, item) | ||||
|         notifyItemInserted(position) | ||||
|         updateItems(items) | ||||
|         updateHomeItems(items) | ||||
|     } | ||||
|  | ||||
|     fun addItemsAtEnd(newItems: List<SelfossModel.Item>) { | ||||
|         val oldSize = items.size | ||||
|         items.addAll(newItems) | ||||
|         notifyItemRangeInserted(oldSize, newItems.size) | ||||
|         updateItems(items) | ||||
|         updateHomeItems(items) | ||||
|     } | ||||
|  | ||||
|     override fun getItemCount(): Int = items.size | ||||
| } | ||||
|   | ||||
| @@ -50,6 +50,33 @@ class SourcesListAdapter( | ||||
|     ) { | ||||
|         val itm = items[position] | ||||
|  | ||||
|         val deleteBtn: Button = holder.mView.findViewById(R.id.deleteBtn) | ||||
|  | ||||
|         deleteBtn.setOnClickListener { | ||||
|             val (id, title) = items[position] | ||||
|             CoroutineScope(Dispatchers.IO).launch { | ||||
|                 val successfullyDeletedSource = repository.deleteSource(id, title) | ||||
|                 if (successfullyDeletedSource) { | ||||
|                     items.removeAt(position) | ||||
|                     notifyItemRemoved(position) | ||||
|                     notifyItemRangeChanged(position, itemCount) | ||||
|                 } else { | ||||
|                     Toast.makeText( | ||||
|                         app, | ||||
|                         R.string.can_delete_source, | ||||
|                         Toast.LENGTH_SHORT, | ||||
|                     ).show() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         holder.mView.setOnClickListener { | ||||
|             val source = items[position] | ||||
|  | ||||
|             repository.setSelectedSource(source) | ||||
|             app.startActivity(Intent(app, UpsertSourceActivity::class.java)) | ||||
|         } | ||||
|  | ||||
|         if (itm.getIcon(repository.baseUrl).isEmpty()) { | ||||
|             binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded()) | ||||
|         } else { | ||||
| @@ -72,38 +99,5 @@ class SourcesListAdapter( | ||||
|  | ||||
|     override fun getItemCount(): Int = items.size | ||||
|  | ||||
|     inner class ViewHolder(private val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) { | ||||
|         init { | ||||
|             handleClickListeners() | ||||
|         } | ||||
|  | ||||
|         private fun handleClickListeners() { | ||||
|             val deleteBtn: Button = mView.findViewById(R.id.deleteBtn) | ||||
|  | ||||
|             deleteBtn.setOnClickListener { | ||||
|                 val (id, title) = items[bindingAdapterPosition] | ||||
|                 CoroutineScope(Dispatchers.IO).launch { | ||||
|                     val successfullyDeletedSource = repository.deleteSource(id, title) | ||||
|                     if (successfullyDeletedSource) { | ||||
|                         items.removeAt(bindingAdapterPosition) | ||||
|                         notifyItemRemoved(bindingAdapterPosition) | ||||
|                         notifyItemRangeChanged(bindingAdapterPosition, itemCount) | ||||
|                     } else { | ||||
|                         Toast.makeText( | ||||
|                             app, | ||||
|                             R.string.can_delete_source, | ||||
|                             Toast.LENGTH_SHORT, | ||||
|                         ).show() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             mView.setOnClickListener { | ||||
|                 val source = items[bindingAdapterPosition] | ||||
|  | ||||
|                 repository.setSelectedSource(source) | ||||
|                 app.startActivity(Intent(app, UpsertSourceActivity::class.java)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) | ||||
| } | ||||
|   | ||||
| @@ -52,7 +52,6 @@ import org.kodein.di.DIAware | ||||
| import org.kodein.di.android.x.closestDI | ||||
| import org.kodein.di.instance | ||||
| import java.net.MalformedURLException | ||||
| import java.net.SocketTimeoutException | ||||
| import java.net.URL | ||||
| import java.util.* | ||||
| import java.util.concurrent.ExecutionException | ||||
| @@ -103,7 +102,12 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|             contentText = item.content | ||||
|             contentTitle = item.title.getHtmlDecoded() | ||||
|             contentImage = item.getThumbnail(repository.baseUrl) | ||||
|             contentSource = item.sourceAuthorAndDate() | ||||
|             contentSource = try { | ||||
|                 item.sourceAuthorAndDate() | ||||
|             } catch (e: Exception) { | ||||
|                 e.sendSilentlyWithAcraWithName("Article Fragment parse date") | ||||
|                 item.sourceAuthorOnly() | ||||
|             } | ||||
|             allImages = item.getImages() | ||||
|  | ||||
|             fontSize = appSettingsService.getFontSize() | ||||
| @@ -264,10 +268,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                 } else { | ||||
|                     openInBrowserAfterFailing() | ||||
|                 } | ||||
|             } catch (e: SocketTimeoutException) { | ||||
|                 openInBrowserAfterFailing() | ||||
|             } catch (e: Exception) { | ||||
|                 e.sendSilentlyWithAcraWithName("getContentFromMercury > $url") | ||||
|                 openInBrowserAfterFailing() | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -17,6 +17,7 @@ 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.getColorHexCode | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import bou.amine.apps.readerforselfossv2.utils.getIcon | ||||
| import com.bumptech.glide.Glide | ||||
| @@ -147,9 +148,9 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { | ||||
|                     val gd = GradientDrawable() | ||||
|                     val gdColor = | ||||
|                         try { | ||||
|                             Color.parseColor(tag.color) | ||||
|                             Color.parseColor(tag.getColorHexCode()) | ||||
|                         } catch (e: IllegalArgumentException) { | ||||
|                             e.sendSilentlyWithAcraWithName("color issue " + tag.color) | ||||
|                             e.sendSilentlyWithAcraWithName("color issue " + tag.color + " / " + tag.getColorHexCode()) | ||||
|                             resources.getColor(R.color.colorPrimary) | ||||
|                         } | ||||
|                     gd.setColor(gdColor) | ||||
|   | ||||
| @@ -36,6 +36,12 @@ | ||||
|         android:orderInCategory="101" | ||||
|         android:title="@string/menu_home_refresh" /> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/issue_tracker" | ||||
|         app:showAsAction="never" | ||||
|         android:orderInCategory="103" | ||||
|         android:title="@string/issue_tracker_link" /> | ||||
|  | ||||
|     <item android:id="@+id/action_disconnect" | ||||
|           android:title="@string/action_disconnect" | ||||
|           android:orderInCategory="104" | ||||
|   | ||||
| @@ -3,6 +3,13 @@ | ||||
|       xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
|  | ||||
|  | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/issue_tracker" | ||||
|         app:showAsAction="never" | ||||
|         android:orderInCategory="101" | ||||
|         android:title="@string/issue_tracker_link" /> | ||||
|  | ||||
|     <item android:id="@+id/about" | ||||
|           android:title="@string/action_about" | ||||
|           android:orderInCategory="102" | ||||
|   | ||||
| @@ -8,10 +8,12 @@ import kotlinx.datetime.toInstant | ||||
| import org.junit.Test | ||||
|  | ||||
| class DatesTest { | ||||
|     private val newVersionDateVariant = "2022-12-24T17:00:08+00" | ||||
|     private val newVersionDate = "2013-04-07T13:43:00+01:00" | ||||
|     private val oldVersionDate = "2013-05-07 13:46:00" | ||||
|     private val oldVersionDateVariant = "2021-03-21 10:32:00.000000" | ||||
|     private val newVersionDateVariant =     "2022-12-24T17:00:08+00" | ||||
|     private val newVersionDate =            "2013-04-07T13:43:00+01:00" | ||||
|     private val newVersionDate2 =            "2013-04-07T13:43:00-01:00" | ||||
|     private val oldVersionDate =            "2013-05-07 13:46:00" | ||||
|     private val oldVersionDateVariant =     "2021-03-21 10:32:00.000000" | ||||
|  | ||||
|  | ||||
|     @Test | ||||
|     fun new_version_date_should_be_parsed() { | ||||
| @@ -22,6 +24,15 @@ class DatesTest { | ||||
|  | ||||
|         assertEquals(expected, date) | ||||
|     } | ||||
|     @Test | ||||
|     fun new_version_date2_should_be_parsed() { | ||||
|         val date = DateUtils.parseDate(newVersionDate2) | ||||
|         val expected = | ||||
|             LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault()) | ||||
|                 .toEpochMilliseconds() | ||||
|  | ||||
|         assertEquals(expected, date) | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     fun old_version_date_should_be_parsed() { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package bou.amine.apps.readerforselfossv2.utils | ||||
|  | ||||
| import android.text.format.DateUtils | ||||
| import io.github.aakira.napier.Napier | ||||
| import kotlinx.datetime.* | ||||
|  | ||||
| actual class DateUtils { | ||||
| @@ -10,18 +11,21 @@ actual class DateUtils { | ||||
|         private val oldVersionFormat = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(.()\\d*)?".toRegex() | ||||
|  | ||||
|         // yyyy-MM-dd'T'HH:mm:ss[.SSS]XXX (RFC3339) | ||||
|         private val newVersionFormat = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{2}(:\\d{2})?".toRegex() | ||||
|         private val newVersionFormat = "(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2})[+-](\\d{2}(:\\d{2})?)?".toRegex() | ||||
|  | ||||
|         // We may need to consider moving the formatting to platform specific code, even if the tests are doubled | ||||
|         // For now, we handle this in a hacky way, because kotlin only accepts iso formats | ||||
|         // TODO: do not fix any more issues here. Move everything to plateform specific code. | ||||
|         actual fun parseDate(dateString: String): Long { | ||||
|             var isoDateString: String = | ||||
|                 if (dateString.matches(oldVersionFormat)) { | ||||
|                     dateString.replace(" ", "T") | ||||
|                 } else if (dateString.matches(newVersionFormat)) { | ||||
|                     dateString.split("+")[0] | ||||
|                 } else { | ||||
|                     throw Exception("Unrecognized format for $dateString") | ||||
|                 try { | ||||
|                     if (dateString.matches(oldVersionFormat)) { | ||||
|                         dateString.replace(" ", "T") | ||||
|                     } else if (dateString.matches(newVersionFormat)) { | ||||
|                         newVersionFormat.find(dateString)?.groups?.get(1)?.value ?: throw Exception("Couldn't parse $dateString") | ||||
|                     } else { | ||||
|                         throw Exception("Unrecognized format for $dateString") | ||||
|                     } | ||||
|                 } catch (e: Exception) { | ||||
|                     throw Exception("parseDate failed for $dateString", e) | ||||
|                 } | ||||
|  | ||||
|             return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() | ||||
|   | ||||
| @@ -146,6 +146,14 @@ class SelfossModel { | ||||
|             return txt | ||||
|         } | ||||
|  | ||||
|         fun sourceAuthorOnly(): String { | ||||
|             var txt = this.sourcetitle.getHtmlDecoded() | ||||
|             if (!this.author.isNullOrBlank()) { | ||||
|                 txt += " (by ${this.author}) " | ||||
|             } | ||||
|             return txt | ||||
|         } | ||||
|  | ||||
|         fun toggleStar(): Item { | ||||
|             this.starred = !this.starred | ||||
|             return this | ||||
|   | ||||
| @@ -8,7 +8,6 @@ import bou.amine.apps.readerforselfossv2.rest.SelfossApi | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import bou.amine.apps.readerforselfossv2.utils.* | ||||
| import io.github.aakira.napier.Napier | ||||
| import io.ktor.client.call.* | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| @@ -423,28 +422,25 @@ class Repository( | ||||
|                 val response = api.login() | ||||
|                 result = response.isSuccess == true | ||||
|             } catch (cause: Throwable) { | ||||
|                 Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.login") | ||||
|                 Napier.e("login failed", cause, tag = "RepositoryImpl.login") | ||||
|             } | ||||
|         } | ||||
|         return result | ||||
|     } | ||||
|  | ||||
|     suspend fun shouldBeSelfossInstance(): Pair<Boolean, Boolean> { | ||||
|     suspend fun checkIfFetchFails(): Boolean { | ||||
|         var fetchFailed = true | ||||
|         var showSelfossOnlyModal = false | ||||
|         if (isNetworkAvailable()) { | ||||
|             try { | ||||
|                 // Trying to fetch one item, and check someone is trying to use the app with | ||||
|                 // a random rss feed, that would throw a NoTransformationFoundException | ||||
|                 fetchFailed = !api.getItemsWithoutCatch().success | ||||
|             } catch (e: NoTransformationFoundException) { | ||||
|                 showSelfossOnlyModal = true | ||||
|             } catch (e: Throwable) { | ||||
|                 Napier.e(e.stackTraceToString(), tag = "RepositoryImpl.shouldBeSelfossInstance") | ||||
|                 Napier.e("checkIfFetchFails failed", e, tag = "RepositoryImpl.shouldBeSelfossInstance") | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return Pair(fetchFailed, showSelfossOnlyModal) | ||||
|         return fetchFailed | ||||
|     } | ||||
|  | ||||
|     suspend fun logout() { | ||||
| @@ -455,7 +451,7 @@ class Repository( | ||||
|                     Napier.e("Couldn't logout.", tag = "RepositoryImpl.logout") | ||||
|                 } | ||||
|             } catch (cause: Throwable) { | ||||
|                 Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.logout") | ||||
|                 Napier.e("logout failed", cause, tag = "RepositoryImpl.logout") | ||||
|             } | ||||
|             appSettingsService.clearAll() | ||||
|         } else { | ||||
|   | ||||
| @@ -441,7 +441,7 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) { | ||||
|         login: String, | ||||
|         password: String, | ||||
|     ) { | ||||
|         val regex = """\/\/(\D+):(\D+)@""".toRegex() | ||||
|         val regex = """\/\/(\S+):(\S+)@""".toRegex() | ||||
|         val matchResult = regex.find(url) | ||||
|         if (matchResult != null) { | ||||
|             val (basicLogin, basicPassword) = matchResult.destructured | ||||
|   | ||||
| @@ -73,3 +73,13 @@ fun SelfossModel.Item.toEntity(): ITEM = | ||||
|         this.tags.joinToString(","), | ||||
|         this.author, | ||||
|     ) | ||||
|  | ||||
| fun SelfossModel.Tag.getColorHexCode(): String = | ||||
|     if (this.color.length == 4) { // #000 | ||||
|         val char1 = this.color.get(1) | ||||
|         val char2 = this.color.get(2) | ||||
|         val char3 = this.color.get(3) | ||||
|         "#$char1$char1$char2$char2$char3$char3" | ||||
|     } else { | ||||
|         this.color | ||||
|     } | ||||
		Reference in New Issue
	
	Block a user