forked from Louvorg/ReaderForSelfoss-multiplatform
		
	Compare commits
	
		
			31 Commits
		
	
	
		
			v122123532
			...
			v123010041
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | e0c118a73e | ||
|  | 4e61b2aed6 | ||
|  | ba2758c0a3 | ||
|  | c718b966a1 | ||
|  | 99438e142f | ||
|  | 4d8076c3cf | ||
|  | db75c5b74a | ||
|  | 966a082147 | ||
|  | cd20a5ec29 | ||
|  | cc4c1c9201 | ||
|  | ff021d572c | ||
|  | 89992967be | ||
|  | 3c68bde62b | ||
|  | c38251f5b3 | ||
|  | a01f6d2322 | ||
|  | 417a33eb25 | ||
|  | 2e7f7f23b3 | ||
|  | e5e182761e | ||
|  | a094d88799 | ||
| e51915d1cd | |||
| 3a654f6ede | |||
| 5227751dca | |||
|  | 27eafe4ff4 | ||
|  | 8c83a9408b | ||
|  | fe2410f719 | ||
|  | a5e86bfb77 | ||
|  | 23be633798 | ||
|  | 813e0707d8 | ||
|  | 9ed9bf07fc | ||
|  | 47265c10d0 | ||
|  | 5cc633246a | 
							
								
								
									
										18
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								.drone.yml
									
									
									
									
									
								
							| @@ -17,6 +17,7 @@ steps: | ||||
|       - echo "---------------------------------------------------------" | ||||
|       - ./gradlew koverMergedXmlReport | ||||
|     environment: | ||||
|       TZ: Europe/Paris | ||||
|       SONAR_HOST_URL: | ||||
|         from_secret: sonarScannerHostUrl | ||||
|       SONAR_LOGIN: | ||||
| @@ -34,7 +35,6 @@ steps: | ||||
| trigger: | ||||
|   event: | ||||
|     - push | ||||
|     - pull_request | ||||
|  | ||||
| --- | ||||
| kind: pipeline | ||||
| @@ -42,14 +42,22 @@ type: docker | ||||
| name: Publish | ||||
|  | ||||
| steps: | ||||
|   - name: createTag | ||||
|   - name: createTagAndChangelog | ||||
|     image: ubuntu:latest | ||||
|     commands: | ||||
|       - apt-get update && apt-get install -y git | ||||
|       - git fetch --tags -p | ||||
|       - VER=$(git describe --tags --abbrev=0) | ||||
|       - CHANGELOG=$(git log $VER..HEAD --pretty="- %s") | ||||
|       - echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md | ||||
|       - git add CHANGELOG.md | ||||
|       - git commit -m "Changelog for $VER [CI SKIP]" | ||||
|       - ./build.sh --publish --from-ci | ||||
|       - git remote add pushing https://$GITEA_USR:$GITEA_PASS@gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform.git | ||||
|       - git push pushing master | ||||
|       - git push pushing --tags | ||||
|     environment: | ||||
|       TZ: Europe/Paris | ||||
|       GITEA_USR: | ||||
|         from_secret: giteaUsr | ||||
|       GITEA_PASS: | ||||
| @@ -75,10 +83,7 @@ steps: | ||||
|         from_secret: privateKey | ||||
|       command_timeout: 2m | ||||
|       script: | ||||
|         - cd /home/ubuntu | ||||
|         - sudo rm -rf /var/www/amine/version.txt | ||||
|         - sudo chown www-data:www-data ./version.txt | ||||
|         - sudo mv version.txt /var/www/amine/ | ||||
|         - cd /home/ubuntu && sudo rm -rf /var/www/amine/version.txt && sudo chown www-data:www-data ./version.txt && sudo mv version.txt /var/www/amine/ | ||||
|  | ||||
| trigger: | ||||
|   event: | ||||
| @@ -117,6 +122,7 @@ steps: | ||||
|       - echo "Verify" | ||||
|       - $ANDROID_HOME/build-tools/31.0.0/apksigner verify signed.apk | ||||
|     environment: | ||||
|       TZ: Europe/Paris | ||||
|       YOUR_KEYSTORE_PASSWORD: | ||||
|         from_secret: keyPass | ||||
|       YOUR_KEY_ALIAS: | ||||
|   | ||||
							
								
								
									
										27
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,30 @@ | ||||
| **v122123641** | ||||
|  | ||||
| - feat: Disable the failing source in the filter sheet. | ||||
| - feat: Display the source error in the sources list. | ||||
|  | ||||
| -------------------------------------------------------------------- | ||||
|  | ||||
| **v122123631** | ||||
|  | ||||
| - build: Added back maven repos (see https://gitlab.com/fdroid/fdroiddata/-/commit/1fb9d60dc58511abc2bb4eb321977922a0682c8b#note_1223925153) | ||||
| - build: Added back maven repos (see https://gitlab.com/fdroid/fdroiddata/-/commit/1fb9d60dc58511abc2bb4eb321977922a0682c8b#note_1223925153) | ||||
| - debug: trying to resolve `Canvas: trying to use a recycled bitmap`. | ||||
| - fix: NPE may be caused by the binding or the title that was null. | ||||
| - chore: Skip drone pipeline on changelog push. | ||||
|  | ||||
| -------------------------------------------------------------------- | ||||
|  | ||||
| **v122123621** | ||||
|  | ||||
| - fix: Automatic CHANGELOG generation. | ||||
| - Merge pull request 'Sources Upsert' (#119) from sources-edit into master | ||||
| - Source update screen. | ||||
| - Sources menu. | ||||
| - chore: Automatic CHANGELOG generation. | ||||
|  | ||||
| -------------------------------------------------------------------- | ||||
|  | ||||
| # V2/Multiplatform rewrite | ||||
|  | ||||
| **v1** | ||||
|   | ||||
| @@ -52,7 +52,7 @@ | ||||
|                 android:value=".HomeActivity" /> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".AddSourceActivity" | ||||
|             android:name=".UpsertSourceActivity" | ||||
|             android:parentActivityName=".SourcesActivity" | ||||
|             android:exported="true"> | ||||
|             <meta-data | ||||
|   | ||||
| @@ -603,6 +603,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|                 settingsLauncher.launch(Intent(this, SettingsActivity::class.java)) | ||||
|                 return true | ||||
|             } | ||||
|             R.id.action_sources -> { | ||||
|                 startActivity(Intent(this, SourcesActivity::class.java)) | ||||
|                 return true | ||||
|             } | ||||
|             else -> return super.onOptionsItemSelected(item) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import com.mikepenz.aboutlibraries.LibsBuilder | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import org.acra.ACRA | ||||
| import org.kodein.di.DIAware | ||||
| import org.kodein.di.android.closestDI | ||||
| import org.kodein.di.instance | ||||
| @@ -129,6 +130,7 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|     private fun goToMain() { | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             repository.updateApiVersion() | ||||
|             ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString()) | ||||
|         } | ||||
|         val intent = Intent(this, HomeActivity::class.java) | ||||
|         startActivity(intent) | ||||
|   | ||||
| @@ -3,10 +3,7 @@ package bou.amine.apps.readerforselfossv2.android | ||||
| import android.app.NotificationChannel | ||||
| import android.app.NotificationManager | ||||
| import android.content.Context | ||||
| import android.graphics.drawable.Drawable | ||||
| import android.net.Uri | ||||
| import android.os.Build | ||||
| import android.widget.ImageView | ||||
| import android.widget.Toast | ||||
| import androidx.lifecycle.DefaultLifecycleObserver | ||||
| import androidx.lifecycle.LifecycleOwner | ||||
| @@ -18,8 +15,6 @@ import bou.amine.apps.readerforselfossv2.dao.DriverFactory | ||||
| import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.request.RequestOptions | ||||
| import com.github.ln_12.library.ConnectivityStatus | ||||
| import io.github.aakira.napier.DebugAntilog | ||||
| import io.github.aakira.napier.Napier | ||||
| @@ -83,6 +78,8 @@ class MyApp : MultiDexApplication(), DIAware { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         repository.migrate(driverFactory) | ||||
|     } | ||||
|  | ||||
|     override fun attachBaseContext(base: Context?) { | ||||
|   | ||||
| @@ -30,6 +30,8 @@ class ReaderActivity : AppCompatActivity(), DIAware { | ||||
|  | ||||
|     private lateinit var binding: ActivityReaderBinding | ||||
|  | ||||
|     private var allItems: ArrayList<SelfossModel.Item> = ArrayList() | ||||
|  | ||||
|     override val di by closestDI() | ||||
|     private val repository: Repository by instance() | ||||
|     private val appSettingsService: AppSettingsService by instance() | ||||
| @@ -61,12 +63,14 @@ class ReaderActivity : AppCompatActivity(), DIAware { | ||||
|         supportActionBar?.setDisplayHomeAsUpEnabled(true) | ||||
|         supportActionBar?.setDisplayShowHomeEnabled(true) | ||||
|  | ||||
|         if (allItems.isEmpty()) { | ||||
|         currentItem = intent.getIntExtra("currentItem", 0) | ||||
|  | ||||
|         allItems = repository.getReaderItems() | ||||
|  | ||||
|         if (allItems.isEmpty() || currentItem > allItems.size) { | ||||
|             finish() | ||||
|         } | ||||
|  | ||||
|         currentItem = intent.getIntExtra("currentItem", 0) | ||||
|  | ||||
|         readItem(allItems[currentItem]) | ||||
|  | ||||
|         binding.pager.adapter = ScreenSlidePagerAdapter(this) | ||||
| @@ -214,8 +218,4 @@ class ReaderActivity : AppCompatActivity(), DIAware { | ||||
|         startActivity(intent) | ||||
|         overridePendingTransition(0, 0) | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         var allItems: ArrayList<SelfossModel.Item> = ArrayList() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -73,7 +73,7 @@ class SourcesActivity : AppCompatActivity(), DIAware { | ||||
|         } | ||||
|  | ||||
|         binding.fab.setOnClickListener { | ||||
|             startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java)) | ||||
|             startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -3,12 +3,15 @@ package bou.amine.apps.readerforselfossv2.android | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| import android.view.View | ||||
| import android.widget.* | ||||
| import android.widget.AdapterView | ||||
| import android.widget.ArrayAdapter | ||||
| import android.widget.TextView | ||||
| import android.widget.Toast | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.constraintlayout.widget.ConstraintLayout | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.ActivityAddSourceBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid | ||||
| import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| @@ -19,38 +22,59 @@ import org.kodein.di.android.closestDI | ||||
| import org.kodein.di.instance | ||||
| 
 | ||||
| 
 | ||||
| class AddSourceActivity : AppCompatActivity(), DIAware { | ||||
| class UpsertSourceActivity : AppCompatActivity(), DIAware { | ||||
| 
 | ||||
|     private var existingSource: SelfossModel.Source? = null | ||||
|     private var mSpoutsValue: String? = null | ||||
| 
 | ||||
|     private lateinit var binding: ActivityAddSourceBinding | ||||
|     private lateinit var binding: ActivityUpsertSourceBinding | ||||
| 
 | ||||
|     override val di by closestDI() | ||||
|     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 onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         binding = ActivityAddSourceBinding.inflate(layoutInflater) | ||||
|         binding = ActivityUpsertSourceBinding.inflate(layoutInflater) | ||||
|         val view = binding.root | ||||
| 
 | ||||
|         existingSource = repository.getSelectedSource() | ||||
|         if (existingSource != null) { | ||||
|             binding.formContainer.visibility = View.GONE | ||||
|             binding.progress.visibility = View.VISIBLE | ||||
|         } | ||||
|         val title = if (existingSource == null) R.string.add_source else R.string.update_source | ||||
| 
 | ||||
|         supportFragmentManager.addOnBackStackChangedListener { | ||||
|             if (supportFragmentManager.backStackEntryCount == 0) { | ||||
|                 setTitle(title) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         setContentView(view) | ||||
| 
 | ||||
|         setSupportActionBar(binding.toolbar) | ||||
|         supportActionBar?.setDisplayHomeAsUpEnabled(true) | ||||
|         supportActionBar?.setDisplayShowHomeEnabled(true) | ||||
|         supportActionBar?.title = resources.getString(title) | ||||
| 
 | ||||
|         maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput) | ||||
| 
 | ||||
|         maybeGetDetailsFromIntentSharing(intent) | ||||
| 
 | ||||
|         binding.saveBtn.setOnClickListener { | ||||
|             handleSaveSource( | ||||
|                 binding.tags, | ||||
|                 binding.nameInput.text.toString(), | ||||
|                 binding.sourceUri.text.toString() | ||||
|             ) | ||||
|             handleSaveSource() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun initFields(items: Map<String, SelfossModel.Spout>) { | ||||
|         binding.nameInput.setText(existingSource!!.title) | ||||
|         binding.tags.setText(existingSource!!.tags.joinToString(", ")) | ||||
|         binding.sourceUri.setText(existingSource!!.params?.url) | ||||
|         binding.spoutsSpinner.setSelection(items.keys.indexOf(existingSource!!.spout)) | ||||
|         binding.progress.visibility = View.GONE | ||||
|         binding.formContainer.visibility = View.VISIBLE | ||||
|     } | ||||
| 
 | ||||
|     override fun onResume() { | ||||
|         super.onResume() | ||||
| 
 | ||||
| @@ -58,17 +82,13 @@ class AddSourceActivity : AppCompatActivity(), DIAware { | ||||
|         if (baseUrl.isEmpty() || baseUrl.isBaseUrlInvalid()) { | ||||
|             mustLoginToAddSource() | ||||
|         } else { | ||||
|             handleSpoutsSpinner(binding.spoutsSpinner, binding.progress, binding.formContainer) | ||||
|             handleSpoutsSpinner() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun handleSpoutsSpinner( | ||||
|         spoutsSpinner: Spinner, | ||||
|         mProgress: ProgressBar, | ||||
|         formContainer: ConstraintLayout | ||||
|     ) { | ||||
|     private fun handleSpoutsSpinner() { | ||||
|         val spoutsKV = HashMap<String, String>() | ||||
|         spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { | ||||
|         binding.spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { | ||||
|             override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) { | ||||
|                 if (view != null) { | ||||
|                     val spoutName = (view as TextView).text.toString() | ||||
| @@ -84,11 +104,11 @@ class AddSourceActivity : AppCompatActivity(), DIAware { | ||||
| 
 | ||||
|         fun handleSpoutFailure(networkIssue: Boolean = false) { | ||||
|             Toast.makeText( | ||||
|                 this@AddSourceActivity, | ||||
|                 this@UpsertSourceActivity, | ||||
|                 if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts, | ||||
|                 Toast.LENGTH_SHORT | ||||
|             ).show() | ||||
|             mProgress.visibility = View.GONE | ||||
|             binding.progress.visibility = View.GONE | ||||
|         } | ||||
| 
 | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
| @@ -100,17 +120,21 @@ class AddSourceActivity : AppCompatActivity(), DIAware { | ||||
|                         spoutsKV[value.name] = key | ||||
|                     } | ||||
| 
 | ||||
|                     mProgress.visibility = View.GONE | ||||
|                     formContainer.visibility = View.VISIBLE | ||||
|                     binding.progress.visibility = View.GONE | ||||
|                     binding.formContainer.visibility = View.VISIBLE | ||||
| 
 | ||||
|                     val spinnerArrayAdapter = | ||||
|                         ArrayAdapter( | ||||
|                             this@AddSourceActivity, | ||||
|                             this@UpsertSourceActivity, | ||||
|                             android.R.layout.simple_spinner_item, | ||||
|                             itemsStrings | ||||
|                         ) | ||||
|                     spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) | ||||
|                     spoutsSpinner.adapter = spinnerArrayAdapter | ||||
|                     binding.spoutsSpinner.adapter = spinnerArrayAdapter | ||||
| 
 | ||||
|                     if (existingSource != null) { | ||||
|                         initFields(items) | ||||
|                     } | ||||
|                 } else { | ||||
|                     handleSpoutFailure() | ||||
|                 } | ||||
| @@ -121,13 +145,11 @@ class AddSourceActivity : AppCompatActivity(), DIAware { | ||||
|     } | ||||
| 
 | ||||
|     private fun maybeGetDetailsFromIntentSharing( | ||||
|         intent: Intent, | ||||
|         sourceUri: EditText, | ||||
|         nameInput: EditText | ||||
|         intent: Intent | ||||
|     ) { | ||||
|         if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) { | ||||
|             sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT)) | ||||
|             nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE)) | ||||
|             binding.sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT)) | ||||
|             binding.nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @@ -138,7 +160,8 @@ class AddSourceActivity : AppCompatActivity(), DIAware { | ||||
|         finish() | ||||
|     } | ||||
| 
 | ||||
|     private fun handleSaveSource(tags: EditText, title: String, url: String) { | ||||
|     private fun handleSaveSource() { | ||||
|         val url = binding.sourceUri.text.toString() | ||||
| 
 | ||||
|         val sourceDetailsUnavailable = | ||||
|             title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty() | ||||
| @@ -149,18 +172,27 @@ class AddSourceActivity : AppCompatActivity(), DIAware { | ||||
|             } | ||||
|             else -> { | ||||
|                 CoroutineScope(Dispatchers.Main).launch { | ||||
|                     val successfullyAddedSource = repository.createSource( | ||||
|                         title, | ||||
|                         url, | ||||
|                         mSpoutsValue!!, | ||||
|                         tags.text.toString(), | ||||
|                         "", | ||||
|                     ) | ||||
|                     val successfullyAddedSource = if (existingSource != null) { | ||||
|                         repository.updateSource( | ||||
|                             existingSource!!.id, | ||||
|                             binding.nameInput.text.toString(), | ||||
|                             url, | ||||
|                             mSpoutsValue!!, | ||||
|                             binding.tags.text.toString() | ||||
|                         ) | ||||
|                     } else { | ||||
|                         repository.createSource( | ||||
|                             binding.nameInput.text.toString(), | ||||
|                             url, | ||||
|                             mSpoutsValue!!, | ||||
|                             binding.tags.text.toString(), | ||||
|                         ) | ||||
|                     } | ||||
|                     if (successfullyAddedSource) { | ||||
|                         finish() | ||||
|                     } else { | ||||
|                         Toast.makeText( | ||||
|                             this@AddSourceActivity, | ||||
|                             this@UpsertSourceActivity, | ||||
|                             R.string.cant_create_source, | ||||
|                             Toast.LENGTH_SHORT | ||||
|                         ).show() | ||||
| @@ -169,4 +201,9 @@ class AddSourceActivity : AppCompatActivity(), DIAware { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         repository.unsetSelectedSource() | ||||
|     } | ||||
| } | ||||
| @@ -62,7 +62,7 @@ class ItemCardAdapter( | ||||
|  | ||||
|             binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) | ||||
|  | ||||
|             binding.sourceTitleAndDate.text = itm.sourceAndDateText() | ||||
|             binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate() | ||||
|  | ||||
|             if (!appSettingsService.isFullHeightCardsEnabled()) { | ||||
|                 binding.itemImage.maxHeight = imageMaxHeight | ||||
| @@ -132,8 +132,8 @@ class ItemCardAdapter( | ||||
|  | ||||
|         private fun handleLinkOpening() { | ||||
|             binding.root.setOnClickListener { | ||||
|                 repository.setReaderItems(items) | ||||
|                 c.openItemUrl( | ||||
|                     items, | ||||
|                     bindingAdapterPosition, | ||||
|                     items[bindingAdapterPosition].getLinkDecoded(), | ||||
|                     appSettingsService.isArticleViewerEnabled(), | ||||
|   | ||||
| @@ -51,7 +51,7 @@ class ItemListAdapter( | ||||
|  | ||||
|             binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) | ||||
|  | ||||
|             binding.sourceTitleAndDate.text = itm.sourceAndDateText() | ||||
|             binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate() | ||||
|  | ||||
|             if (itm.getThumbnail(repository.baseUrl).isEmpty()) { | ||||
|  | ||||
| @@ -84,8 +84,8 @@ class ItemListAdapter( | ||||
|  | ||||
|         private fun handleLinkOpening() { | ||||
|             binding.root.setOnClickListener { | ||||
|                 repository.setReaderItems(items) | ||||
|                 c.openItemUrl( | ||||
|                     items, | ||||
|                     bindingAdapterPosition, | ||||
|                     items[bindingAdapterPosition].getLinkDecoded(), | ||||
|                     appSettingsService.isArticleViewerEnabled(), | ||||
|   | ||||
| @@ -2,12 +2,15 @@ package bou.amine.apps.readerforselfossv2.android.adapters | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.Button | ||||
| import android.widget.Toast | ||||
| import androidx.constraintlayout.widget.ConstraintLayout | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString | ||||
| @@ -58,6 +61,13 @@ class SourcesListAdapter( | ||||
|             c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) | ||||
|         } | ||||
|  | ||||
|         if (itm.error.isNotBlank()) { | ||||
|             binding.errorText.visibility = View.VISIBLE | ||||
|             binding.errorText.text = itm.error | ||||
|         } else { | ||||
|             binding.errorText.visibility = View.GONE | ||||
|         } | ||||
|  | ||||
|         binding.sourceTitle.text = itm.title.getHtmlDecoded() | ||||
|     } | ||||
|  | ||||
| @@ -78,9 +88,9 @@ class SourcesListAdapter( | ||||
|             val deleteBtn: Button = mView.findViewById(R.id.deleteBtn) | ||||
|  | ||||
|             deleteBtn.setOnClickListener { | ||||
|                 val (id) = items[bindingAdapterPosition] | ||||
|                 val (id, title) = items[bindingAdapterPosition] | ||||
|                 CoroutineScope(Dispatchers.IO).launch { | ||||
|                     val successfullyDeletedSource = repository.deleteSource(id) | ||||
|                     val successfullyDeletedSource = repository.deleteSource(id, title) | ||||
|                     if (successfullyDeletedSource) { | ||||
|                         items.removeAt(bindingAdapterPosition) | ||||
|                         notifyItemRemoved(bindingAdapterPosition) | ||||
| @@ -94,6 +104,14 @@ class SourcesListAdapter( | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             mView.setOnClickListener { | ||||
|                 val source = items[bindingAdapterPosition] | ||||
|  | ||||
|                 repository.setSelectedSource(source) | ||||
|                 app.startActivity(Intent(app, UpsertSourceActivity::class.java)) | ||||
|  | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -66,8 +66,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|     private lateinit var allImages : ArrayList<String> | ||||
|     private lateinit var fab: FloatingActionButton | ||||
|     private lateinit var textAlignment: String | ||||
|     private var _binding: FragmentArticleBinding? = null | ||||
|     private val binding get() = _binding | ||||
|     private lateinit var binding: FragmentArticleBinding | ||||
|  | ||||
|     override val di : DI by closestDI() | ||||
|     private val repository: Repository by instance() | ||||
| @@ -95,13 +94,13 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View { | ||||
|         try { | ||||
|             _binding = FragmentArticleBinding.inflate(inflater, container, false) | ||||
|             binding = FragmentArticleBinding.inflate(inflater, container, false) | ||||
|  | ||||
|             url = item.getLinkDecoded() | ||||
|             contentText = item.content | ||||
|             contentTitle = item.title.getHtmlDecoded() | ||||
|             contentImage = item.getThumbnail(repository.baseUrl) | ||||
|             contentSource = item.sourceAndDateText() | ||||
|             contentSource = item.sourceAuthorAndDate() | ||||
|             allImages = item.getImages() | ||||
|  | ||||
|             fontSize = appSettingsService.getFontSize() | ||||
| @@ -110,13 +109,13 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|  | ||||
|             refreshAlignment() | ||||
|  | ||||
|             fab = binding!!.fab | ||||
|             fab = binding.fab | ||||
|  | ||||
|             fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent)) | ||||
|  | ||||
|             fab.rippleColor = resources.getColor(R.color.colorAccentDark) | ||||
|  | ||||
|             val floatingToolbar: FloatingToolbar = binding!!.floatingToolbar | ||||
|             val floatingToolbar: FloatingToolbar = binding.floatingToolbar | ||||
|             floatingToolbar.attachFab(fab) | ||||
|  | ||||
|             floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent)) | ||||
| @@ -164,35 +163,35 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                 floatingToolbar.show() | ||||
|             } | ||||
|  | ||||
|             binding!!.source.text = contentSource | ||||
|             binding.source.text = contentSource | ||||
|             if (typeface != null) { | ||||
|                 binding!!.source.typeface = typeface | ||||
|                 binding.source.typeface = typeface | ||||
|             } | ||||
|  | ||||
|             if (contentText.isEmptyOrNullOrNullString()) { | ||||
|                 getContentFromMercury() | ||||
|             } else { | ||||
|                 binding!!.titleView.text = contentTitle | ||||
|                 binding.titleView.text = contentTitle | ||||
|                 if (typeface != null) { | ||||
|                     binding!!.titleView.typeface = typeface | ||||
|                     binding.titleView.typeface = typeface | ||||
|                 } | ||||
|  | ||||
|                 htmlToWebview() | ||||
|  | ||||
|                 if (!contentImage.isEmptyOrNullOrNullString() && context != null) { | ||||
|                     binding!!.imageView.visibility = View.VISIBLE | ||||
|                     binding.imageView.visibility = View.VISIBLE | ||||
|                     Glide | ||||
|                         .with(requireContext()) | ||||
|                         .asBitmap() | ||||
|                         .load(contentImage) | ||||
|                         .apply(RequestOptions.fitCenterTransform()) | ||||
|                         .into(binding!!.imageView) | ||||
|                         .into(binding.imageView) | ||||
|                 } else { | ||||
|                     binding!!.imageView.visibility = View.GONE | ||||
|                     binding.imageView.visibility = View.GONE | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             binding!!.nestedScrollView.setOnScrollChangeListener( | ||||
|             binding.nestedScrollView.setOnScrollChangeListener( | ||||
|                 NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> | ||||
|                     if (scrollY > oldScrollY) { | ||||
|                         floatingToolbar.hide() | ||||
| @@ -221,12 +220,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                 .show() | ||||
|         } | ||||
|  | ||||
|         return binding!!.root | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView() { | ||||
|         super.onDestroyView() | ||||
|         _binding = null | ||||
|         return binding.root | ||||
|     } | ||||
|  | ||||
|     private fun refreshAlignment() { | ||||
| @@ -239,16 +233,16 @@ 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() | ||||
|                         binding.titleView.text = response.data!!.title.orEmpty() | ||||
|                         try { | ||||
|                             if (typeface != null) { | ||||
|                                 binding!!.titleView.typeface = typeface | ||||
|                                 binding.titleView.typeface = typeface | ||||
|                             } | ||||
|                         } catch (e: Exception) { | ||||
|                             e.sendSilentlyWithAcraWithName("getContentFromMercury > typeface") | ||||
| @@ -272,7 +266,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|  | ||||
|                         if (!response.data?.lead_image_url.isNullOrEmpty() && context != null) { | ||||
|                             try { | ||||
|                                 binding!!.imageView.visibility = View.VISIBLE | ||||
|                                 binding.imageView.visibility = View.VISIBLE | ||||
|                                 try { | ||||
|                                     Glide | ||||
|                                         .with(requireContext()) | ||||
| @@ -281,7 +275,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                                             response.data!!.lead_image_url.orEmpty() | ||||
|                                         ) | ||||
|                                         .apply(RequestOptions.fitCenterTransform()) | ||||
|                                         .into(binding!!.imageView) | ||||
|                                         .into(binding.imageView) | ||||
|                                 } catch (e: Exception) { | ||||
|                                     e.sendSilentlyWithAcraWithName("getContentFromMercury > glide lead image") | ||||
|                                 } | ||||
| @@ -289,12 +283,12 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                                 e.sendSilentlyWithAcraWithName("getContentFromMercury > outside glide lead image") | ||||
|                             } | ||||
|                         } else { | ||||
|                             binding!!.imageView.visibility = View.GONE | ||||
|                             binding.imageView.visibility = View.GONE | ||||
|                         } | ||||
|  | ||||
|                         try { | ||||
|                             binding!!.nestedScrollView.scrollTo(0, 0) | ||||
|                             binding!!.progressBar.visibility = View.GONE | ||||
|                             binding.nestedScrollView.scrollTo(0, 0) | ||||
|                             binding.progressBar.visibility = View.GONE | ||||
|                         } catch (e: Exception) { | ||||
|                             e.sendSilentlyWithAcraWithName("getContentFromMercury > scrollview") | ||||
|                         } | ||||
| @@ -317,8 +311,8 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|         val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs) | ||||
|  | ||||
|  | ||||
|         binding!!.webcontent.settings.standardFontFamily = a.getString(0) | ||||
|         binding!!.webcontent.visibility = View.VISIBLE | ||||
|         binding.webcontent.settings.standardFontFamily = a.getString(0) | ||||
|         binding.webcontent.visibility = View.VISIBLE | ||||
|  | ||||
|         val colorOnSurface = TypedValue() | ||||
|         requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true) | ||||
| @@ -326,14 +320,14 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|         val colorSurface = TypedValue() | ||||
|         requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true) | ||||
|  | ||||
|         binding!!.webcontent.settings.useWideViewPort = true | ||||
|         binding!!.webcontent.settings.loadWithOverviewMode = true | ||||
|         binding!!.webcontent.settings.javaScriptEnabled = false | ||||
|         binding.webcontent.settings.useWideViewPort = true | ||||
|         binding.webcontent.settings.loadWithOverviewMode = true | ||||
|         binding.webcontent.settings.javaScriptEnabled = false | ||||
|  | ||||
|         binding!!.webcontent.webViewClient = object : WebViewClient() { | ||||
|         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) { | ||||
|                 if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { | ||||
|                     requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) | ||||
|                 } | ||||
|                 return true | ||||
| @@ -347,7 +341,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                         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") | ||||
|                         e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg > $url") | ||||
|                     } | ||||
|                 } | ||||
|                 else if (url.lowercase(Locale.US).contains(".png")) { | ||||
| @@ -355,7 +349,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                         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") | ||||
|                         e.sendSilentlyWithAcraWithName("shouldInterceptRequest > png > $url") | ||||
|                     } | ||||
|                 } | ||||
|                 else if (url.lowercase(Locale.US).contains(".webp")) { | ||||
| @@ -363,7 +357,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                         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") | ||||
|                         e.sendSilentlyWithAcraWithName("shouldInterceptRequest > webp > $url") | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
| @@ -377,9 +371,9 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         binding!!.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)} | ||||
|         binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)} | ||||
|  | ||||
|         binding!!.webcontent.settings.layoutAlgorithm = | ||||
|         binding.webcontent.settings.layoutAlgorithm = | ||||
|                 WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING | ||||
|  | ||||
|         var baseUrl: String? = null | ||||
| @@ -410,7 +404,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|             "" | ||||
|         } | ||||
|  | ||||
|         binding!!.webcontent.loadDataWithBaseURL( | ||||
|         binding.webcontent.loadDataWithBaseURL( | ||||
|             baseUrl, | ||||
|             """<html> | ||||
|                 |<head> | ||||
| @@ -463,17 +457,17 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|     } | ||||
|  | ||||
|     fun scrollDown() { | ||||
|         val height = binding!!.nestedScrollView.measuredHeight | ||||
|         binding!!.nestedScrollView.smoothScrollBy(0, height/2) | ||||
|         val height = binding.nestedScrollView.measuredHeight | ||||
|         binding.nestedScrollView.smoothScrollBy(0, height/2) | ||||
|     } | ||||
|  | ||||
|     fun scrollUp() { | ||||
|         val height = binding!!.nestedScrollView.measuredHeight | ||||
|         binding!!.nestedScrollView.smoothScrollBy(0, -height/2) | ||||
|         val height = binding.nestedScrollView.measuredHeight | ||||
|         binding.nestedScrollView.smoothScrollBy(0, -height/2) | ||||
|     } | ||||
|  | ||||
|     private fun openInBrowserAfterFailing() { | ||||
|         binding!!.progressBar.visibility = View.GONE | ||||
|         binding.progressBar.visibility = View.GONE | ||||
|         requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item) | ||||
|     } | ||||
|  | ||||
| @@ -492,10 +486,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) { | ||||
|         if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_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) | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import android.content.Context | ||||
| import android.graphics.Color | ||||
| import android.graphics.drawable.Drawable | ||||
| import android.graphics.drawable.GradientDrawable | ||||
| import android.os.Build | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| @@ -67,18 +68,22 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { | ||||
|                     val c = Chip(context) | ||||
|                     c.text = tag.tag | ||||
|  | ||||
|                     val gd = GradientDrawable() | ||||
|                     val gdColor = try { | ||||
|                         Color.parseColor(tag.color) | ||||
|                     } catch (e: IllegalArgumentException) { | ||||
|                         e.sendSilentlyWithAcraWithName("color issue " + tag.color) | ||||
|                         resources.getColor(R.color.colorPrimary) | ||||
|                     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") | ||||
|                     } | ||||
|                     gd.setColor(gdColor) | ||||
|                     gd.shape = GradientDrawable.RECTANGLE | ||||
|                     gd.setSize(30, 30) | ||||
|                     gd.cornerRadius = 30F | ||||
|                     c.chipIcon = gd | ||||
|  | ||||
|                     c.setOnCloseIconClickListener { | ||||
|                         (it as Chip).isCloseIconVisible = false | ||||
| @@ -127,7 +132,11 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { | ||||
|                                 dataSource: DataSource?, | ||||
|                                 isFirstResource: Boolean | ||||
|                             ): Boolean { | ||||
|                                 c.chipIcon = resource | ||||
|                                 try { | ||||
|                                     c.chipIcon = resource | ||||
|                                 } catch (e: Exception) { | ||||
|                                     e.sendSilentlyWithAcraWithName("sources > onResourceReady") | ||||
|                                 } | ||||
|                                 return false | ||||
|                             } | ||||
|                         }).preload() | ||||
| @@ -157,6 +166,12 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { | ||||
|                         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) | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -16,7 +16,8 @@ fun SelfossModel.Item.toParcelable() : ParecelableItem = | ||||
|         this.icon, | ||||
|         this.link, | ||||
|         this.sourcetitle, | ||||
|         this.tags.joinToString(",") | ||||
|         this.tags.joinToString(","), | ||||
|         this.author | ||||
|     ) | ||||
| fun ParecelableItem.toModel() : SelfossModel.Item = | ||||
|     SelfossModel.Item( | ||||
| @@ -30,7 +31,8 @@ fun ParecelableItem.toModel() : SelfossModel.Item = | ||||
|         this.icon, | ||||
|         this.link, | ||||
|         this.sourcetitle, | ||||
|         this.tags.split(",") | ||||
|         this.tags.split(","), | ||||
|         this.author | ||||
|     ) | ||||
| data class ParecelableItem( | ||||
|     val id: Int, | ||||
| @@ -43,7 +45,8 @@ data class ParecelableItem( | ||||
|     val icon: String?, | ||||
|     val link: String, | ||||
|     val sourcetitle: String, | ||||
|     val tags: String | ||||
|     val tags: String, | ||||
|     val author: String? | ||||
| ) : Parcelable { | ||||
|  | ||||
|     companion object { | ||||
| @@ -65,7 +68,8 @@ data class ParecelableItem( | ||||
|         icon = source.readString(), | ||||
|         link = source.readString().orEmpty(), | ||||
|         sourcetitle = source.readString().orEmpty(), | ||||
|         tags = source.readString().orEmpty() | ||||
|         tags = source.readString().orEmpty(), | ||||
|         author = source.readString().orEmpty() | ||||
|     ) | ||||
|  | ||||
|     override fun describeContents() = 0 | ||||
| @@ -82,5 +86,6 @@ data class ParecelableItem( | ||||
|         dest.writeString(link) | ||||
|         dest.writeString(sourcetitle) | ||||
|         dest.writeString(tags) | ||||
|         dest.writeString(author) | ||||
|     } | ||||
| } | ||||
| @@ -18,7 +18,6 @@ import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp | ||||
| import okhttp3.HttpUrl.Companion.toHttpUrlOrNull | ||||
|  | ||||
| fun Context.openItemUrl( | ||||
|     allItems: ArrayList<SelfossModel.Item>, | ||||
|     currentItem: Int, | ||||
|     linkDecoded: String, | ||||
|     articleViewer: Boolean, | ||||
| @@ -33,7 +32,6 @@ fun Context.openItemUrl( | ||||
|         ).show() | ||||
|     } else { | ||||
|         if (articleViewer) { | ||||
|             ReaderActivity.allItems = allItems | ||||
|             val intent = Intent(this, ReaderActivity::class.java) | ||||
|             intent.putExtra("currentItem", currentItem) | ||||
|             app.startActivity(intent) | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     tools:context="bou.amine.apps.readerforselfossv2.android.AddSourceActivity"> | ||||
|     tools:context="bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity"> | ||||
|     <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
| @@ -41,36 +41,20 @@ | ||||
|             app:layout_constraintHorizontal_bias="1.0" | ||||
|             app:layout_constraintVertical_bias="0.0"> | ||||
| 
 | ||||
|             <TextView | ||||
|                 android:text="@string/add_source" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:id="@+id/textView2" | ||||
|                 android:textAppearance="@style/TextAppearance.AppCompat.Large" | ||||
|                 android:textAlignment="center" | ||||
|                 android:layout_marginTop="16dp" | ||||
|                 app:layout_constraintTop_toTopOf="parent" | ||||
|                 android:layout_marginEnd="16dp" | ||||
|                 app:layout_constraintRight_toRightOf="parent" | ||||
|                 android:layout_marginRight="16dp" | ||||
|                 android:layout_marginStart="16dp" | ||||
|                 app:layout_constraintLeft_toLeftOf="parent" | ||||
|                 android:layout_marginLeft="16dp" | ||||
|                 android:gravity="center_horizontal" /> | ||||
| 
 | ||||
|             <EditText | ||||
|                 android:id="@+id/nameInput" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_marginTop="16dp" | ||||
|                 android:autofillHints="false" | ||||
|                 android:ems="10" | ||||
|                 android:id="@+id/nameInput" | ||||
|                 android:layout_marginTop="32dp" | ||||
|                 app:layout_constraintTop_toBottomOf="@+id/textView2" | ||||
|                 android:hint="@string/add_source_hint_name" | ||||
|                 android:inputType="text" | ||||
|                 android:textColorHint="?android:textColorPrimary" | ||||
|                 app:layout_constraintLeft_toLeftOf="parent" | ||||
|                 app:layout_constraintRight_toRightOf="parent" | ||||
|                 android:inputType="text" | ||||
|                 android:hint="@string/add_source_hint_name" | ||||
|                 android:textColorHint="?android:textColorPrimary" | ||||
|                 android:autofillHints="false" /> | ||||
|                 app:layout_constraintTop_toTopOf="parent" /> | ||||
| 
 | ||||
|             <EditText | ||||
|                 android:layout_width="match_parent" | ||||
| @@ -3,18 +3,40 @@ | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="48dp" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:orientation="vertical"> | ||||
|  | ||||
|  | ||||
|     <Button | ||||
|         android:id="@+id/deleteBtn" | ||||
|         style="@style/Widget.AppCompat.Button.Borderless" | ||||
|         android:layout_width="48dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginBottom="8dp" | ||||
|         android:layout_marginEnd="8dp" | ||||
|         android:layout_marginTop="8dp" | ||||
|         android:background="@drawable/ic_remove_circle_outline_black_24dp" | ||||
|         android:backgroundTint="?android:textColorSecondary" | ||||
|         android:contentDescription="@string/remove_source" | ||||
|         android:elevation="4dp" | ||||
|         app:iconSize="34dp" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         app:layout_constraintVertical_bias="0.0" /> | ||||
|  | ||||
|     <ImageView | ||||
|         android:id="@+id/itemImage" | ||||
|         android:layout_width="36dp" | ||||
|         android:layout_height="36dp" | ||||
|         android:layout_marginBottom="8dp" | ||||
|         android:layout_marginLeft="8dp" | ||||
|         android:layout_marginTop="8dp" | ||||
|         android:importantForAccessibility="no" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintLeft_toLeftOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" /> | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         app:layout_constraintVertical_bias="0.0" /> | ||||
|  | ||||
|     <TextView | ||||
|         android:id="@+id/sourceTitle" | ||||
| @@ -25,26 +47,30 @@ | ||||
|         android:gravity="start" | ||||
|         android:maxLines="1" | ||||
|         android:textAlignment="textStart" | ||||
|         android:textSize="13sp" | ||||
|         android:textColor="?android:textColorPrimary" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         android:textSize="13sp" | ||||
|         app:layout_constraintBottom_toTopOf="@+id/errorText" | ||||
|         app:layout_constraintEnd_toStartOf="@+id/deleteBtn" | ||||
|         app:layout_constraintStart_toEndOf="@+id/itemImage" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         tools:text="source title" /> | ||||
|  | ||||
|     <Button | ||||
|         android:id="@+id/deleteBtn" | ||||
|         style="@style/Widget.AppCompat.Button.Borderless" | ||||
|         android:layout_width="34dp" | ||||
|         android:layout_height="34dp" | ||||
|         android:layout_marginEnd="8dp" | ||||
|         android:background="@drawable/ic_remove_circle_outline_black_24dp" | ||||
|         android:backgroundTint="?android:textColorSecondary" | ||||
|         android:elevation="4dp" | ||||
|         android:contentDescription="@string/remove_source" | ||||
|     <TextView | ||||
|         android:id="@+id/errorText" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginBottom="8dp" | ||||
|         android:layout_marginEnd="16dp" | ||||
|         android:layout_marginStart="16dp" | ||||
|         android:layout_marginTop="8dp" | ||||
|         android:textAppearance="@style/TextAppearance.AppCompat.Small" | ||||
|         android:textColor="@color/red" | ||||
|         android:visibility="gone" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" /> | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toBottomOf="@+id/itemImage" | ||||
|         tools:text="Test" | ||||
|         tools:visibility="visible" /> | ||||
|  | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| @@ -20,6 +20,11 @@ | ||||
|           android:orderInCategory="2" | ||||
|           app:showAsAction="ifRoom"/> | ||||
|  | ||||
|     <item android:id="@+id/action_sources" | ||||
|         android:title="@string/menu_home_sources" | ||||
|         android:orderInCategory="97" | ||||
|         app:showAsAction="never"/> | ||||
|  | ||||
|     <item android:id="@+id/action_settings" | ||||
|         android:title="@string/title_activity_settings" | ||||
|         android:orderInCategory="98" | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -6,4 +6,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"禁用自动错误报告 "</string> | ||||
|     <string name="menu_home_filter">筛选器</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -126,4 +126,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -129,4 +129,6 @@ | ||||
|     <string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> | ||||
|     <string name="menu_home_filter">Filters</string> | ||||
|     <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> | ||||
|     <string name="menu_home_sources">Sources</string> | ||||
|     <string name="update_source">Update source</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -11,11 +11,14 @@ class DatesTest { | ||||
|  | ||||
|     private val v3Date = "2013-04-07T13:43:00+01:00" | ||||
|     private val v4Date = "2013-04-07 13:43:00" | ||||
|     private val bug1Date = "2022-12-24T17:00:08+00" | ||||
|  | ||||
|     @Test | ||||
|     fun v3_date_should_be_parsed() { | ||||
|         val date = DateUtils.parseDate(v3Date) | ||||
|         val expected = LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.of("UTC+1")) .toEpochMilliseconds() | ||||
|         val expected = | ||||
|             LocalDateTime(2013, 4, 7, 14, 43, 0, 0).toInstant(TimeZone.currentSystemDefault()) | ||||
|                 .toEpochMilliseconds() | ||||
|  | ||||
|         assertEquals(date, expected) | ||||
|     } | ||||
| @@ -30,4 +33,14 @@ class DatesTest { | ||||
|         assertEquals(date, expected) | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     fun bug1_date_should_be_parsed() { | ||||
|         val date = DateUtils.parseDate(bug1Date) | ||||
|         val expected = | ||||
|             LocalDateTime(2022, 12, 24, 18, 0, 8, 0).toInstant(TimeZone.currentSystemDefault()) | ||||
|                 .toEpochMilliseconds() | ||||
|  | ||||
|         assertEquals(date, expected) | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -58,6 +58,7 @@ class RepositoryTest { | ||||
|             data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED) | ||||
|         ) | ||||
|  | ||||
|         every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit | ||||
|         every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems() | ||||
|         every { db.tagsQueries.deleteAllTags() } returns Unit | ||||
|         every { db.tagsQueries.transaction(any(), any()) } returns Unit | ||||
| @@ -230,7 +231,8 @@ class RepositoryTest { | ||||
|             listOf("tags"), | ||||
|             "spouts\\rss\\fulltextrss", | ||||
|             "", | ||||
|             "b3aa8a664d08eb15d6ff1db2fa83e0d9.png" | ||||
|             "b3aa8a664d08eb15d6ff1db2fa83e0d9.png", | ||||
|             SelfossModel.SourceParams("url") | ||||
|         )) | ||||
|         runBlocking { | ||||
|             repository.getNewerItems() | ||||
| @@ -550,7 +552,8 @@ class RepositoryTest { | ||||
|                 listOf("Test", "second"), | ||||
|                 "spouts\\rss\\fulltextrss", | ||||
|                 "", | ||||
|                 "d8c92cdb1ef119ea85c4b9205c879ca7.png" | ||||
|                 "d8c92cdb1ef119ea85c4b9205c879ca7.png", | ||||
|                 SelfossModel.SourceParams("url") | ||||
|             ), | ||||
|             SelfossModel.Source( | ||||
|                 2, | ||||
| @@ -558,7 +561,8 @@ class RepositoryTest { | ||||
|                 listOf("second"), | ||||
|                 "spouts\\rss\\fulltextrss", | ||||
|                 "", | ||||
|                 "b3aa8a664d08eb15d6ff1db2fa83e0d9.png" | ||||
|                 "b3aa8a664d08eb15d6ff1db2fa83e0d9.png", | ||||
|                 SelfossModel.SourceParams("url") | ||||
|             ) | ||||
|         ) | ||||
|         val sourcesDB = listOf( | ||||
| @@ -568,7 +572,8 @@ class RepositoryTest { | ||||
|                 "Test,second", | ||||
|                 "spouts\\rss\\fulltextrss", | ||||
|                 "", | ||||
|                 "d8c92cdb1ef119ea85c4b9205c879ca7.png" | ||||
|                 "d8c92cdb1ef119ea85c4b9205c879ca7.png", | ||||
|                 "url" | ||||
|             ), | ||||
|             SOURCE( | ||||
|                 "2", | ||||
| @@ -576,7 +581,8 @@ class RepositoryTest { | ||||
|                 "second", | ||||
|                 "spouts\\rss\\fulltextrss", | ||||
|                 "", | ||||
|                 "b3aa8a664d08eb15d6ff1db2fa83e0d9.png" | ||||
|                 "b3aa8a664d08eb15d6ff1db2fa83e0d9.png", | ||||
|                 "url" | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
| @@ -706,7 +712,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun create_source() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns | ||||
|                 SuccessResponse(true) | ||||
|  | ||||
|         initializeRepository() | ||||
| @@ -717,7 +723,6 @@ class RepositoryTest { | ||||
|                 "https://test.com/feed", | ||||
|                 "spouts\\rss\\fulltextrss", | ||||
|                 "Test, New", | ||||
|                 "" | ||||
|             ) | ||||
|         } | ||||
|  | ||||
| @@ -727,7 +732,6 @@ class RepositoryTest { | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|             ) | ||||
|         } | ||||
|         assertSame(true, response) | ||||
| @@ -735,7 +739,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun create_source_but_response_fails() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns | ||||
|                 SuccessResponse(false) | ||||
|  | ||||
|         initializeRepository() | ||||
| @@ -745,8 +749,7 @@ class RepositoryTest { | ||||
|                 "test", | ||||
|                 "https://test.com/feed", | ||||
|                 "spouts\\rss\\fulltextrss", | ||||
|                 "Test, New", | ||||
|                 "" | ||||
|                 "Test, New" | ||||
|             ) | ||||
|         } | ||||
|  | ||||
| @@ -755,8 +758,7 @@ class RepositoryTest { | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any() | ||||
|             ) | ||||
|         } | ||||
|         assertSame(false, response) | ||||
| @@ -764,7 +766,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun create_source_without_connection() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns | ||||
|                 SuccessResponse(true) | ||||
|  | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
| @@ -774,8 +776,7 @@ class RepositoryTest { | ||||
|                 "test", | ||||
|                 "https://test.com/feed", | ||||
|                 "spouts\\rss\\fulltextrss", | ||||
|                 "Test, New", | ||||
|                 "" | ||||
|                 "Test, New" | ||||
|             ) | ||||
|         } | ||||
|  | ||||
| @@ -785,7 +786,6 @@ class RepositoryTest { | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any() | ||||
|             ) | ||||
|         } | ||||
|         assertSame(false, response) | ||||
| @@ -798,10 +798,11 @@ class RepositoryTest { | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
|         runBlocking { | ||||
|             response = repository.deleteSource(5) | ||||
|             response = repository.deleteSource(5, "src") | ||||
|         } | ||||
|  | ||||
|         coVerify(exactly = 1) { api.deleteSource(5) } | ||||
|         coVerify(exactly = 1) { db.itemsQueries.deleteItemsWhereSource("src") } | ||||
|         assertSame(true, response) | ||||
|     } | ||||
|  | ||||
| @@ -812,10 +813,11 @@ class RepositoryTest { | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
|         runBlocking { | ||||
|             response = repository.deleteSource(5) | ||||
|             response = repository.deleteSource(5, "src") | ||||
|         } | ||||
|  | ||||
|         coVerify(exactly = 1) { api.deleteSource(5) } | ||||
|         coVerify(exactly = 0) { db.itemsQueries.deleteItemsWhereSource("src") } | ||||
|         assertSame(false, response) | ||||
|     } | ||||
|  | ||||
| @@ -826,10 +828,11 @@ class RepositoryTest { | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var response: Boolean | ||||
|         runBlocking { | ||||
|             response = repository.deleteSource(5) | ||||
|             response = repository.deleteSource(5, "src") | ||||
|         } | ||||
|  | ||||
|         coVerify(exactly = 0) { api.deleteSource(5) } | ||||
|         coVerify(exactly = 1) { db.itemsQueries.deleteItemsWhereSource("src") } | ||||
|         assertSame(false, response) | ||||
|     } | ||||
|  | ||||
| @@ -1030,7 +1033,8 @@ class RepositoryTest { | ||||
|                 listOf("Test", "second"), | ||||
|                 "spouts\\rss\\fulltextrss", | ||||
|                 "", | ||||
|                 "d8c92cdb1ef119ea85c4b9205c879ca7.png" | ||||
|                 "d8c92cdb1ef119ea85c4b9205c879ca7.png", | ||||
|                 SelfossModel.SourceParams("url") | ||||
|             ) | ||||
|         ) | ||||
|         repository.searchFilter = "search" | ||||
|   | ||||
| @@ -17,7 +17,8 @@ fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<I | ||||
|             icon = item.icon, | ||||
|             link = item.link, | ||||
|             sourcetitle = item.sourcetitle, | ||||
|             tags = item.tags | ||||
|             tags = item.tags, | ||||
|             author = item.author | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
| @@ -35,7 +36,8 @@ fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<S | ||||
|             icon = item.icon, | ||||
|             link = item.link, | ||||
|             sourcetitle = item.sourcetitle, | ||||
|             tags = item.tags.split(',') | ||||
|             tags = item.tags.split(','), | ||||
|             author = item.author | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
| @@ -54,4 +56,5 @@ class FakeItemParameters { | ||||
|         "https://ilblogdellasci.wordpress.com/2022/09/09/etica-della-ricerca-sotto-i-riflettori/" | ||||
|     var sourcetitle = "La Chimica e la Società" | ||||
|     var tags = "Chimica, Testing" | ||||
|     var author = "Someone important" | ||||
| } | ||||
| @@ -17,6 +17,7 @@ plugins { | ||||
|  | ||||
| allprojects { | ||||
|     repositories { | ||||
|         // maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")} | ||||
|         google() | ||||
|         mavenCentral() | ||||
|         jcenter() | ||||
|   | ||||
| @@ -2,6 +2,7 @@ val pushCache: String by settings | ||||
|  | ||||
| pluginManagement { | ||||
|     repositories { | ||||
|         // maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")} | ||||
|         google() | ||||
|         gradlePluginPortal() | ||||
|         mavenCentral() | ||||
| @@ -10,6 +11,7 @@ pluginManagement { | ||||
|  | ||||
| dependencyResolutionManagement { | ||||
|     repositories { | ||||
|         // maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")} | ||||
|         google() | ||||
|         mavenCentral() | ||||
|     } | ||||
|   | ||||
| @@ -10,7 +10,11 @@ actual class DateUtils { | ||||
|             return try { | ||||
|                 Instant.parse(dateString).toEpochMilliseconds() | ||||
|             } catch (e: Exception) { | ||||
|                 LocalDateTime.parse(dateString.replace(" ", "T")).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() | ||||
|                 var str = dateString.replace(" ", "T") | ||||
|                 if (str.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{2}".toRegex())) { | ||||
|                     str = str.split("+")[0] | ||||
|                 } | ||||
|                 LocalDateTime.parse(str).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ class MercuryModel { | ||||
|  | ||||
|     @Serializable | ||||
|     class ParsedContent( | ||||
|         val title: String, | ||||
|         val title: String?, | ||||
|         val content: String?, | ||||
|         val lead_image_url: String?, | ||||
|         val url: String | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor | ||||
| import kotlinx.serialization.descriptors.SerialDescriptor | ||||
| import kotlinx.serialization.encoding.Decoder | ||||
| import kotlinx.serialization.encoding.Encoder | ||||
| import kotlinx.serialization.encoding.encodeCollection | ||||
| import kotlinx.serialization.json.* | ||||
|  | ||||
| class SelfossModel { | ||||
| @@ -55,9 +56,13 @@ class SelfossModel { | ||||
|         val tags: List<String>, | ||||
|         val spout: String, | ||||
|         val error: String, | ||||
|         val icon: String? | ||||
|         val icon: String?, | ||||
|         val params: SourceParams? | ||||
|     ) | ||||
|     @Serializable | ||||
|     data class SourceParams( | ||||
|         val url: String | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     data class Item( | ||||
|         val id: Int, | ||||
| @@ -73,7 +78,8 @@ class SelfossModel { | ||||
|         val link: String, | ||||
|         val sourcetitle: String, | ||||
|         @Serializable(with = TagsListSerializer::class) | ||||
|         val tags: List<String> | ||||
|         val tags: List<String>, | ||||
|         val author: String? | ||||
|     ) { | ||||
|         // TODO: maybe find a better way to handle these kind of urls | ||||
|         fun getLinkDecoded(): String { | ||||
| @@ -102,8 +108,14 @@ class SelfossModel { | ||||
|             return stringUrl | ||||
|         } | ||||
|  | ||||
|         fun sourceAndDateText(): String = | ||||
|             this.sourcetitle.getHtmlDecoded() + DateUtils.parseRelativeDate(this.datetime) | ||||
|         fun sourceAuthorAndDate(): String { | ||||
|             var txt = this.sourcetitle.getHtmlDecoded() | ||||
|             if (!this.author.isNullOrBlank()) { | ||||
|                 txt += " (by ${this.author}) " | ||||
|             } | ||||
|             txt += DateUtils.parseRelativeDate(this.datetime) | ||||
|             return txt | ||||
|         } | ||||
|  | ||||
|         fun toggleStar(): Item { | ||||
|             this.starred = !this.starred | ||||
| @@ -111,11 +123,12 @@ class SelfossModel { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     // TODO: this seems to be super slow. | ||||
|     object TagsListSerializer : KSerializer<List<String>> { | ||||
|         override fun deserialize(decoder: Decoder): List<String> { | ||||
|             return when(val json = ((decoder as JsonDecoder).decodeJsonElement())) { | ||||
|                 is JsonArray -> json.toList().map { it.toString() } | ||||
|                 is JsonArray -> json.toList().map { it.toString().replace("^\"|\"$".toRegex(), "") } | ||||
|                 else -> json.toString().split(",") | ||||
|             } | ||||
|  | ||||
| @@ -125,7 +138,7 @@ class SelfossModel { | ||||
|             get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING) | ||||
|  | ||||
|         override fun serialize(encoder: Encoder, value: List<String>) { | ||||
|             TODO("Not yet implemented") | ||||
|             encoder.encodeCollection(PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING), value.size) { this.toString() } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -47,6 +47,9 @@ class Repository( | ||||
|     private var fetchedSources = false | ||||
|     private var fetchedTags = false | ||||
|  | ||||
|     private var _readerItems = ArrayList<SelfossModel.Item>() | ||||
|     private var _selectedSource: SelfossModel.Source? = null | ||||
|  | ||||
|     suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { | ||||
|         // TODO: Use the updatedSince parameter | ||||
|         var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() | ||||
| @@ -341,7 +344,6 @@ class Repository( | ||||
|         url: String, | ||||
|         spout: String, | ||||
|         tags: String, | ||||
|         filter: String | ||||
|     ): Boolean { | ||||
|         var response = false | ||||
|         if (isNetworkAvailable()) { | ||||
| @@ -350,20 +352,41 @@ class Repository( | ||||
|                 url, | ||||
|                 spout, | ||||
|                 tags, | ||||
|                 filter | ||||
|             ).isSuccess == true | ||||
|         } | ||||
|  | ||||
|         return response | ||||
|     } | ||||
|  | ||||
|     suspend fun deleteSource(id: Int): Boolean { | ||||
|     suspend fun updateSource( | ||||
|         id: Int, | ||||
|         title: String, | ||||
|         url: String, | ||||
|         spout: String, | ||||
|         tags: String | ||||
|     ): Boolean { | ||||
|         var response = false | ||||
|         if (isNetworkAvailable()) { | ||||
|             response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true | ||||
|         } | ||||
|  | ||||
|         return response | ||||
|     } | ||||
|  | ||||
|     suspend fun deleteSource(id: Int, title: String): Boolean { | ||||
|         var success = false | ||||
|         if (isNetworkAvailable()) { | ||||
|             val response = api.deleteSource(id) | ||||
|             success = response.isSuccess | ||||
|         } | ||||
|  | ||||
|         // We filter on success or if the network isn't available | ||||
|         if (success || !isNetworkAvailable()) { | ||||
|             items = ArrayList(items.filter { it.sourcetitle != title }) | ||||
|             setReaderItems(items) | ||||
|             db.itemsQueries.deleteItemsWhereSource(title) | ||||
|         } | ||||
|  | ||||
|         return success | ||||
|     } | ||||
|  | ||||
| @@ -502,6 +525,7 @@ class Repository( | ||||
|             item.link, | ||||
|             item.sourcetitle, | ||||
|             item.tags.joinToString(","), | ||||
|             item.author, | ||||
|             item.id.toString() | ||||
|         ) | ||||
|  | ||||
| @@ -558,4 +582,28 @@ class Repository( | ||||
|     fun setSourceFilter(source: SelfossModel.Source?) { | ||||
|         _sourceFilter.value = source | ||||
|     } | ||||
|  | ||||
|     fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) { | ||||
|         _readerItems = readerItems | ||||
|     } | ||||
|  | ||||
|     fun getReaderItems(): ArrayList<SelfossModel.Item> { | ||||
|         return _readerItems | ||||
|     } | ||||
|  | ||||
|     fun migrate(driverFactory: DriverFactory) { | ||||
|         ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1) | ||||
|     } | ||||
|  | ||||
|     fun setSelectedSource(source: SelfossModel.Source) { | ||||
|         _selectedSource = source | ||||
|     } | ||||
|  | ||||
|     fun unsetSelectedSource() { | ||||
|         _selectedSource = null | ||||
|     } | ||||
|  | ||||
|     fun getSelectedSource(): SelfossModel.Source? { | ||||
|         return _selectedSource | ||||
|     } | ||||
| } | ||||
| @@ -23,6 +23,9 @@ suspend fun maybeResponse(r: HttpResponse?): SuccessResponse { | ||||
|     return if (r != null && r.status.isSuccess()) { | ||||
|         r.body() | ||||
|     } else { | ||||
|         if (r != null) { | ||||
|             Napier.i("Error ${r.status}", tag = "maybeResponse") | ||||
|         } | ||||
|         SuccessResponse(false) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -76,10 +76,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|  | ||||
|     // Api version was introduces after the POST login, so when there is a version, it should be available | ||||
|     private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1 | ||||
|     private fun hasLoginInfo() = appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword().isNotEmpty() | ||||
|     private fun hasLoginInfo() = | ||||
|         appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword() | ||||
|             .isNotEmpty() | ||||
|  | ||||
|     suspend fun login(): SuccessResponse = | ||||
|         if (appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword().isNotEmpty()) { | ||||
|         if (appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword() | ||||
|                 .isNotEmpty() | ||||
|         ) { | ||||
|             if (shouldHavePostLogin()) { | ||||
|                 postLogin() | ||||
|             } else { | ||||
| @@ -99,7 +103,9 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         parameter("password", appSettingsService.getPassword()) | ||||
|     }) | ||||
|  | ||||
|     private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0 | ||||
|     private fun shouldHaveNewLogout() = | ||||
|         appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0 | ||||
|  | ||||
|     suspend fun logout(): SuccessResponse = | ||||
|         if (shouldHaveNewLogout()) { | ||||
|             doLogout() | ||||
| @@ -107,7 +113,8 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|             maybeLogoutIfAvailable() | ||||
|         } | ||||
|  | ||||
|     private suspend fun maybeLogoutIfAvailable() = responseOrSuccessIf404(client.tryToGet(url("/logout"))) | ||||
|     private suspend fun maybeLogoutIfAvailable() = | ||||
|         responseOrSuccessIf404(client.tryToGet(url("/logout"))) | ||||
|  | ||||
|     private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current"))) | ||||
|  | ||||
| @@ -236,13 +243,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         url: String, | ||||
|         spout: String, | ||||
|         tags: String, | ||||
|         filter: String | ||||
|     ): SuccessResponse = | ||||
|         maybeResponse( | ||||
|             if (appSettingsService.getApiVersion() > 1) { | ||||
|                 createSource("tags[]", title, url, spout, tags, filter) | ||||
|                 createSource("tags[]", title, url, spout, tags) | ||||
|             } else { | ||||
|                 createSource("tags", title, url, spout, tags, filter) | ||||
|                 createSource("tags", title, url, spout, tags) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
| @@ -251,8 +257,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         title: String, | ||||
|         url: String, | ||||
|         spout: String, | ||||
|         tags: String, | ||||
|         filter: String | ||||
|         tags: String | ||||
|     ): HttpResponse? = | ||||
|         client.tryToSubmitForm( | ||||
|             url = url("/source"), | ||||
| @@ -265,7 +270,43 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|                 append("url", url) | ||||
|                 append("spout", spout) | ||||
|                 append(tagsParamName, tags) | ||||
|                 append("filter", filter) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     suspend fun updateSourceForVersion( | ||||
|         id: Int, | ||||
|         title: String, | ||||
|         url: String, | ||||
|         spout: String, | ||||
|         tags: String | ||||
|     ): SuccessResponse = | ||||
|         maybeResponse( | ||||
|             if (appSettingsService.getApiVersion() > 1) { | ||||
|                 updateSource(id, "tags[]", title, url, spout, tags) | ||||
|             } else { | ||||
|                 updateSource(id, "tags", title, url, spout, tags) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     private suspend fun updateSource( | ||||
|         id: Int, | ||||
|         tagsParamName: String, | ||||
|         title: String, | ||||
|         url: String, | ||||
|         spout: String, | ||||
|         tags: String, | ||||
|     ): HttpResponse? = | ||||
|         client.tryToSubmitForm( | ||||
|             url = url("/source/$id"), | ||||
|             formParameters = Parameters.build { | ||||
|                 if (!shouldHavePostLogin()) { | ||||
|                     append("username", appSettingsService.getUserName()) | ||||
|                     append("password", appSettingsService.getPassword()) | ||||
|                 } | ||||
|                 append("title", title) | ||||
|                 append("url", url) | ||||
|                 append("spout", spout) | ||||
|                 append(tagsParamName, tags) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -19,7 +19,8 @@ fun SOURCE.toView(): SelfossModel.Source = | ||||
|         this.tags.split(","), | ||||
|         this.spout, | ||||
|         this.error, | ||||
|         this.icon | ||||
|         this.icon, | ||||
|         if (this.url != null) SelfossModel.SourceParams(this.url) else null | ||||
|     ) | ||||
|  | ||||
| fun SelfossModel.Source.toEntity(): SOURCE = | ||||
| @@ -29,7 +30,8 @@ fun SelfossModel.Source.toEntity(): SOURCE = | ||||
|         this.tags.joinToString(","), | ||||
|         this.spout, | ||||
|         this.error, | ||||
|         this.icon.orEmpty() | ||||
|         this.icon.orEmpty(), | ||||
|         this.params?.url | ||||
|     ) | ||||
|  | ||||
| fun SelfossModel.Tag.toEntity(): TAG = | ||||
| @@ -51,7 +53,8 @@ fun ITEM.toView(): SelfossModel.Item = | ||||
|         this.icon, | ||||
|         this.link, | ||||
|         this.sourcetitle, | ||||
|         this.tags.split(",") | ||||
|         this.tags.split(","), | ||||
|         this.author | ||||
|     ) | ||||
|  | ||||
| fun SelfossModel.Item.toEntity(): ITEM = | ||||
| @@ -66,5 +69,6 @@ fun SelfossModel.Item.toEntity(): ITEM = | ||||
|         this.icon, | ||||
|         this.link, | ||||
|         this.sourcetitle.getHtmlDecoded(), | ||||
|         this.tags.joinToString(",") | ||||
|         this.tags.joinToString(","), | ||||
|         this.author | ||||
|     ) | ||||
| @@ -0,0 +1,6 @@ | ||||
| CREATE TABLE ITEM_BACKUP AS SELECT `id`, `datetime`, `title`, `content`, | ||||
|  `unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`, | ||||
|  `tags` FROM ITEM; | ||||
| ALTER TABLE ITEM_BACKUP ADD COLUMN `author` TEXT; | ||||
| DROP TABLE ITEM; | ||||
| ALTER TABLE ITEM_BACKUP RENAME TO ITEM; | ||||
| @@ -0,0 +1 @@ | ||||
| ALTER TABLE SOURCE ADD COLUMN `url` TEXT; | ||||
| @@ -10,6 +10,7 @@ CREATE TABLE ITEM ( | ||||
|     `link` TEXT NOT NULL, | ||||
|     `sourcetitle` TEXT NOT NULL, | ||||
|     `tags` TEXT NOT NULL, | ||||
|     `author` TEXT, | ||||
|     PRIMARY KEY(`id`) | ||||
| ); | ||||
|  | ||||
| @@ -26,5 +27,8 @@ INSERT OR REPLACE INTO ITEM VALUES ?; | ||||
| deleteItem: | ||||
| DELETE FROM ITEM WHERE `id` = ?; | ||||
|  | ||||
| deleteItemsWhereSource: | ||||
| DELETE FROM ITEM WHERE `sourcetitle` = ?; | ||||
|  | ||||
| updateItem: | ||||
| UPDATE ITEM SET `datetime` = ?, `title` = ?, `content` = ?, `unread` = ?, `starred` = ?, `thumbnail` = ?, `icon` = ?, `link` = ?, `sourcetitle` = ?, `tags` = ? WHERE `id` = ?; | ||||
| UPDATE ITEM SET `datetime` = ?, `title` = ?, `content` = ?, `unread` = ?, `starred` = ?, `thumbnail` = ?, `icon` = ?, `link` = ?, `sourcetitle` = ?, `tags` = ?, `author` = ? WHERE `id` = ?; | ||||
| @@ -5,6 +5,7 @@ CREATE TABLE SOURCE ( | ||||
|     `spout` TEXT NOT NULL, | ||||
|     `error` TEXT NOT NULL, | ||||
|     `icon` TEXT NOT NULL, | ||||
|     `url` TEXT, | ||||
|     PRIMARY KEY(`id`) | ||||
| ); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user