forked from Louvorg/ReaderForSelfoss-multiplatform
		
	Compare commits
	
		
			29 Commits
		
	
	
		
			v122123482
			...
			v122123631
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | cc4c1c9201 | ||
|  | ff021d572c | ||
|  | 89992967be | ||
|  | 3c68bde62b | ||
|  | c38251f5b3 | ||
|  | a01f6d2322 | ||
|  | 417a33eb25 | ||
|  | 2e7f7f23b3 | ||
|  | e5e182761e | ||
|  | a094d88799 | ||
| e51915d1cd | |||
| 3a654f6ede | |||
| 5227751dca | |||
|  | 27eafe4ff4 | ||
|  | 8c83a9408b | ||
|  | fe2410f719 | ||
|  | a5e86bfb77 | ||
|  | 23be633798 | ||
|  | 813e0707d8 | ||
|  | 9ed9bf07fc | ||
|  | 47265c10d0 | ||
|  | 5cc633246a | ||
|  | 1f40385786 | ||
|  | eb2876324a | ||
|  | 633b817d76 | ||
|  | 2cfaa9b285 | ||
|  | f42ae97326 | ||
|  | 3b0028164b | ||
|  | 7420adeb5c | 
							
								
								
									
										20
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								.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" | ||||
|       - ./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: | ||||
| @@ -100,7 +105,7 @@ steps: | ||||
|       - git fetch --tags | ||||
|       - echo "---------------------------------------------------------" | ||||
|       - echo "Configure gradle..." | ||||
|       - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties | ||||
|       - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=false\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties | ||||
|       - echo "---------------------------------------------------------" | ||||
|       - echo "Generate APK" | ||||
|       - ./gradlew :androidApp:assembleGithubConfigRelease  -P pushCache=false | ||||
| @@ -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: | ||||
|   | ||||
							
								
								
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,13 @@ | ||||
| **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** | ||||
|   | ||||
| @@ -146,8 +146,8 @@ dependencies { | ||||
|     implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1") | ||||
|  | ||||
|     // glide | ||||
|     kapt("com.github.bumptech.glide:compiler:4.11.0") | ||||
|     implementation("com.github.bumptech.glide:okhttp3-integration:4.1.1") | ||||
|     kapt("com.github.bumptech.glide:compiler:4.14.2") | ||||
|     implementation("com.github.bumptech.glide:okhttp3-integration:4.14.2") | ||||
|  | ||||
|     // Themes | ||||
|     implementation("com.github.rubensousa:floatingtoolbar:1.5.1") | ||||
| @@ -188,9 +188,6 @@ dependencies { | ||||
|  | ||||
|     implementation("ch.acra:acra-http:$acraVersion") | ||||
|     implementation("ch.acra:acra-toast:$acraVersion") | ||||
|  | ||||
|     // Matomo | ||||
|     implementation("com.github.matomo-org:matomo-sdk-android:4.1.4") | ||||
| } | ||||
|  | ||||
| tasks.withType<Test> { | ||||
|   | ||||
| @@ -52,7 +52,7 @@ | ||||
|                 android:value=".HomeActivity" /> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".AddSourceActivity" | ||||
|             android:name=".UpsertSourceActivity" | ||||
|             android:parentActivityName=".SourcesActivity" | ||||
|             android:exported="true"> | ||||
|             <meta-data | ||||
|   | ||||
| @@ -4,6 +4,6 @@ import org.acra.ACRA | ||||
| import org.acra.ktx.sendSilentlyWithAcra | ||||
|  | ||||
| fun Throwable.sendSilentlyWithAcraWithName(name: String) { | ||||
|         ACRA.errorReporter.putCustomData("error_source", name) | ||||
|     ACRA.errorReporter.putCustomData("error_source", name) | ||||
|     this.sendSilentlyWithAcra() | ||||
| } | ||||
| @@ -1,7 +1,6 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android | ||||
|  | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.view.Menu | ||||
| import android.view.MenuItem | ||||
| @@ -40,8 +39,6 @@ import kotlinx.coroutines.launch | ||||
| import org.kodein.di.DIAware | ||||
| import org.kodein.di.android.closestDI | ||||
| import org.kodein.di.instance | ||||
| import org.matomo.sdk.Tracker | ||||
| import org.matomo.sdk.extra.TrackHelper | ||||
| import java.security.MessageDigest | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| @@ -72,7 +69,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|     override val di by closestDI() | ||||
|     private val repository : Repository by instance() | ||||
|     private val appSettingsService : AppSettingsService by instance() | ||||
|     private val tracker : Tracker by instance() | ||||
|  | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
| @@ -80,8 +76,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|         binding = ActivityHomeBinding.inflate(layoutInflater) | ||||
|         val view = binding.root | ||||
|  | ||||
|         TrackHelper.track().screen("/home").with(tracker) | ||||
|  | ||||
|         fromTabShortcut =  intent.getIntExtra("shortcutTab", -1) != -1 | ||||
|         repository.offlineOverride =  intent.getBooleanExtra("startOffline", false) | ||||
|  | ||||
| @@ -600,15 +594,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|                 CoroutineScope(Dispatchers.Main).launch { | ||||
|                     repository.logout() | ||||
|                 } | ||||
|                 this@HomeActivity.finish() | ||||
|                 val intent = Intent(this, LoginActivity::class.java) | ||||
|                 this.startActivity(intent) | ||||
|                 this@HomeActivity.finish() | ||||
|                 return true | ||||
|             } | ||||
|             R.id.action_settings -> { | ||||
|                 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) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import android.view.MenuItem | ||||
| import android.view.View | ||||
| import android.view.inputmethod.EditorInfo | ||||
| import android.widget.TextView | ||||
| import android.widget.Toast | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.appcompat.app.AppCompatDelegate | ||||
| @@ -22,13 +23,10 @@ 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 | ||||
| import org.matomo.sdk.Tracker | ||||
| import org.matomo.sdk.extra.DimensionQueue | ||||
| import org.matomo.sdk.extra.DownloadTracker | ||||
| import org.matomo.sdk.extra.TrackHelper | ||||
| import java.security.MessageDigest | ||||
|  | ||||
|  | ||||
| @@ -40,18 +38,13 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|     private lateinit var binding: ActivityLoginBinding | ||||
|  | ||||
|     override val di by closestDI() | ||||
|     private val repository : Repository by instance() | ||||
|     private val appSettingsService : AppSettingsService by instance() | ||||
|     private val tracker : Tracker by instance() | ||||
|     private val repository: Repository by instance() | ||||
|     private val appSettingsService: AppSettingsService by instance() | ||||
|  | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|  | ||||
|         TrackHelper.track().download().identifier(DownloadTracker.Extra.ApkChecksum(applicationContext)) | ||||
|             .with(tracker) | ||||
|         TrackHelper.track().screen("/login").with(tracker) | ||||
|  | ||||
|         handleTheme() | ||||
|  | ||||
|         binding = ActivityLoginBinding.inflate(layoutInflater) | ||||
| @@ -64,7 +57,30 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|         handleBaseUrlFail() | ||||
|  | ||||
|         if (appSettingsService.getBaseUrl().isNotEmpty()) { | ||||
|             goToMain() | ||||
|             showProgress(true) | ||||
|             // This should be reverted when "old" users connected with a non-selfoss rss | ||||
|             // are handled. Revert to "simple" way. | ||||
|             CoroutineScope(Dispatchers.Main).launch { | ||||
|                 try { | ||||
|                     val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance() | ||||
|                     if (!errorFetching && !displaySelfossOnly) { | ||||
|                         goToMain() | ||||
|                     } else { | ||||
|                         showProgress(false) | ||||
|                         if (displaySelfossOnly) { | ||||
|                             Toast.makeText( | ||||
|                                 applicationContext, | ||||
|                                 R.string.application_selfoss_only, | ||||
|                                 Toast.LENGTH_LONG | ||||
|                             ).show() | ||||
|                         } | ||||
|                         repository.logout() | ||||
|                     } | ||||
|                 } catch (e: Throwable) { | ||||
|                     repository.logout() | ||||
|                     showProgress(false) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         handleActions() | ||||
| @@ -114,15 +130,7 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|     private fun goToMain() { | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             repository.updateApiVersion() | ||||
|  | ||||
|             val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256") | ||||
|             messageDigest.update(appSettingsService.getBaseUrl().toByteArray()) | ||||
|             tracker.userId = String(messageDigest.digest()) | ||||
|  | ||||
|             val mDimensionQueue = DimensionQueue(tracker) | ||||
|             mDimensionQueue.add(1, appSettingsService.getApiVersion().toString()) | ||||
|  | ||||
|             tracker.isOptOut = !appSettingsService.isAnalyticsEnabled() | ||||
|             ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString()) | ||||
|         } | ||||
|         val intent = Intent(this, HomeActivity::class.java) | ||||
|         startActivity(intent) | ||||
| @@ -191,17 +199,27 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|  | ||||
|             repository.refreshLoginInformation(url, login, password) | ||||
|  | ||||
|             CoroutineScope(Dispatchers.IO).launch { | ||||
|             CoroutineScope(Dispatchers.Main).launch { | ||||
|                 val result = repository.login() | ||||
|                 if (result) { | ||||
|                     goToMain() | ||||
|                 } else { | ||||
|                     CoroutineScope(Dispatchers.Main).launch { | ||||
|                     val (errorFetching, displaySelfossOnly) = repository.shouldBeSelfossInstance() | ||||
|                     if (!errorFetching && !displaySelfossOnly) { | ||||
|                         goToMain() | ||||
|                     } else { | ||||
|                         if (displaySelfossOnly) { | ||||
|                             Toast.makeText( | ||||
|                                 applicationContext, | ||||
|                                 R.string.application_selfoss_only, | ||||
|                                 Toast.LENGTH_LONG | ||||
|                             ).show() | ||||
|                         } | ||||
|                         preferenceError() | ||||
|                     } | ||||
|                 } else { | ||||
|                     preferenceError() | ||||
|                 } | ||||
|                 showProgress(false) | ||||
|             } | ||||
|             showProgress(false) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -215,11 +233,11 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|             .alpha( | ||||
|                 if (show) 0F else 1F | ||||
|             ).setListener(object : AnimatorListenerAdapter() { | ||||
|             override fun onAnimationEnd(animation: Animator) { | ||||
|                 binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE | ||||
|                 override fun onAnimationEnd(animation: Animator) { | ||||
|                     binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         ) | ||||
|             ) | ||||
|  | ||||
|         binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE | ||||
|         binding.loginProgress | ||||
| @@ -228,11 +246,11 @@ class LoginActivity : AppCompatActivity(), DIAware { | ||||
|             .alpha( | ||||
|                 if (show) 1F else 0F | ||||
|             ).setListener(object : AnimatorListenerAdapter() { | ||||
|             override fun onAnimationEnd(animation: Animator) { | ||||
|                 binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE | ||||
|                 override fun onAnimationEnd(animation: Animator) { | ||||
|                     binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         ) | ||||
|             ) | ||||
|     } | ||||
|  | ||||
|     override fun onCreateOptionsMenu(menu: Menu): Boolean { | ||||
|   | ||||
| @@ -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 | ||||
| @@ -35,9 +30,6 @@ import org.acra.data.StringFormat | ||||
| import org.acra.ktx.initAcra | ||||
| import org.acra.sender.HttpSender | ||||
| import org.kodein.di.* | ||||
| import org.matomo.sdk.Matomo | ||||
| import org.matomo.sdk.Tracker | ||||
| import org.matomo.sdk.TrackerBuilder | ||||
|  | ||||
| class MyApp : MultiDexApplication(), DIAware { | ||||
|  | ||||
| @@ -48,8 +40,6 @@ class MyApp : MultiDexApplication(), DIAware { | ||||
|         bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) } | ||||
|         bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } | ||||
|         bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) } | ||||
|         bind<Tracker>() with singleton { TrackerBuilder.createDefault("https://matomo.amine-louveau.fr/matomo.php", if (BuildConfig.DEBUG) 4 else 5).build( | ||||
|             Matomo.getInstance(applicationContext)) } | ||||
|     } | ||||
|  | ||||
|     private val repository: Repository by instance() | ||||
| @@ -88,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(), | ||||
|   | ||||
| @@ -36,9 +36,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte | ||||
|                 Snackbar.LENGTH_LONG | ||||
|             ) | ||||
|             .setAction(R.string.undo_string) { | ||||
|                 CoroutineScope(Dispatchers.IO).launch { | ||||
|                     unreadItemAtIndex(item, position, false) | ||||
|                 } | ||||
|                 unreadItemAtIndex(item, position, false) | ||||
|             } | ||||
|  | ||||
|         val view = s.view | ||||
|   | ||||
| @@ -2,12 +2,14 @@ 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.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 | ||||
| @@ -78,9 +80,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 +96,14 @@ class SourcesListAdapter( | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             mView.setOnClickListener { | ||||
|                 val source = items[bindingAdapterPosition] | ||||
|  | ||||
|                 repository.setSelectedSource(source) | ||||
|                 app.startActivity(Intent(app, UpsertSourceActivity::class.java)) | ||||
|  | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -16,7 +16,6 @@ import android.webkit.WebView | ||||
| import android.webkit.WebViewClient | ||||
| import android.widget.Toast | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.core.content.res.ResourcesCompat | ||||
| import androidx.core.widget.NestedScrollView | ||||
| import androidx.fragment.app.Fragment | ||||
| import bou.amine.apps.readerforselfossv2.android.ImageActivity | ||||
| @@ -45,8 +44,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.launch | ||||
| import org.acra.ktx.sendSilentlyWithAcra | ||||
| import org.acra.ktx.sendWithAcra | ||||
| import org.kodein.di.DI | ||||
| import org.kodein.di.DIAware | ||||
| import org.kodein.di.android.x.closestDI | ||||
| @@ -104,7 +101,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|             contentText = item.content | ||||
|             contentTitle = item.title.getHtmlDecoded() | ||||
|             contentImage = item.getThumbnail(repository.baseUrl) | ||||
|             contentSource = item.sourceAndDateText() | ||||
|             contentSource = item.sourceAuthorAndDate() | ||||
|             allImages = item.getImages() | ||||
|  | ||||
|             fontSize = appSettingsService.getFontSize() | ||||
| @@ -343,14 +340,14 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|             } | ||||
|  | ||||
|             @Deprecated("Deprecated in Java") | ||||
|             override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? { | ||||
|             override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? { | ||||
|                 val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) | ||||
|                 if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US).contains(".jpeg")) { | ||||
|                     try { | ||||
|                         val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() | ||||
|                         return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)) | ||||
|                     } catch ( e : ExecutionException) { | ||||
|                         e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg") | ||||
|                         e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg > $url") | ||||
|                     } | ||||
|                 } | ||||
|                 else if (url.lowercase(Locale.US).contains(".png")) { | ||||
| @@ -358,7 +355,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")) { | ||||
| @@ -366,7 +363,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") | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.fragments | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.graphics.Color | ||||
| import android.graphics.drawable.Drawable | ||||
| import android.graphics.drawable.GradientDrawable | ||||
| @@ -13,9 +13,7 @@ import android.view.ViewGroup | ||||
| import bou.amine.apps.readerforselfossv2.android.HomeActivity | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import bou.amine.apps.readerforselfossv2.utils.getIcon | ||||
| import com.bumptech.glide.Glide | ||||
| @@ -38,16 +36,14 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { | ||||
|  | ||||
|     override val di: DI by closestDI() | ||||
|     private val repository: Repository by instance() | ||||
|     private val appSettingsService: AppSettingsService by instance() | ||||
|  | ||||
|     private var selectedChip: Chip? = null | ||||
|  | ||||
|     @SuppressLint("ResourceAsColor") | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View? { | ||||
|     ): View { | ||||
|         val binding = | ||||
|             bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding.inflate( | ||||
|                 inflater, | ||||
| @@ -55,74 +51,118 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { | ||||
|                 false | ||||
|             ) | ||||
|  | ||||
|         val context: Context? = context | ||||
|  | ||||
|         val tagGroup = binding.tagsGroup | ||||
|         val sourceGroup = binding.sourcesGroup | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|             val tags = repository.getTags() | ||||
|         if (context == null) { | ||||
|             dismiss() | ||||
|             Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView") | ||||
|         } else { | ||||
|             CoroutineScope(Dispatchers.Main).launch { | ||||
|                 val tags = repository.getTags() | ||||
|  | ||||
|             tags.forEach { tag -> | ||||
|                 val c = chipForTag(tag) | ||||
|                 tagGroup.addView(c) | ||||
|             } | ||||
|                 tags.forEach { tag -> | ||||
|                     val c = Chip(context) | ||||
|                     c.text = tag.tag | ||||
|  | ||||
|             repository.getSources().forEach { source -> | ||||
|                 val c = Chip(requireContext()) | ||||
|  | ||||
|                 Glide.with(requireContext()) | ||||
|                     .load(source.getIcon(repository.baseUrl)) | ||||
|                     .listener(object : RequestListener<Drawable?> { | ||||
|                         override fun onLoadFailed( | ||||
|                             e: GlideException?, | ||||
|                             model: Any?, | ||||
|                             target: Target<Drawable?>?, | ||||
|                             isFirstResource: Boolean | ||||
|                         ): Boolean { | ||||
|                             return false | ||||
|                         } | ||||
|  | ||||
|                         override fun onResourceReady( | ||||
|                             resource: Drawable?, | ||||
|                             model: Any?, | ||||
|                             target: Target<Drawable?>?, | ||||
|                             dataSource: DataSource?, | ||||
|                             isFirstResource: Boolean | ||||
|                         ): Boolean { | ||||
|                             c.chipIcon = resource | ||||
|                             return false | ||||
|                         } | ||||
|                     }).preload() | ||||
|  | ||||
|                 c.text = source.title.getHtmlDecoded() | ||||
|  | ||||
|                 c.setOnCloseIconClickListener { | ||||
|                     (it as Chip).isCloseIconVisible = false | ||||
|                     selectedChip = null | ||||
|                     repository.setSourceFilter(null) | ||||
|                 } | ||||
|  | ||||
|                 c.setOnClickListener { | ||||
|                     if (selectedChip != null) { | ||||
|                         selectedChip!!.isCloseIconVisible = false | ||||
|                     val gd = GradientDrawable() | ||||
|                     val gdColor = try { | ||||
|                         Color.parseColor(tag.color) | ||||
|                     } catch (e: IllegalArgumentException) { | ||||
|                         e.sendSilentlyWithAcraWithName("color issue " + tag.color) | ||||
|                         resources.getColor(R.color.colorPrimary) | ||||
|                     } | ||||
|                     (it as Chip).isCloseIconVisible = true | ||||
|                     selectedChip = it | ||||
|                     repository.setSourceFilter(source) | ||||
|                     gd.setColor(gdColor) | ||||
|                     gd.shape = GradientDrawable.RECTANGLE | ||||
|                     gd.setSize(30, 30) | ||||
|                     gd.cornerRadius = 30F | ||||
|                     c.chipIcon = gd | ||||
|  | ||||
|                     repository.setTagFilter(null) | ||||
|                     c.setOnCloseIconClickListener { | ||||
|                         (it as Chip).isCloseIconVisible = false | ||||
|                         selectedChip = null | ||||
|                         repository.setTagFilter(null) | ||||
|                     } | ||||
|  | ||||
|                     c.setOnClickListener { | ||||
|                         if (selectedChip != null) { | ||||
|                             selectedChip!!.isCloseIconVisible = false | ||||
|                         } | ||||
|                         (it as Chip).isCloseIconVisible = true | ||||
|                         selectedChip = it | ||||
|                         repository.setTagFilter(tag) | ||||
|  | ||||
|                         repository.setSourceFilter(null) | ||||
|                     } | ||||
|  | ||||
|                     if (repository.tagFilter.value?.equals(tag) == true) { | ||||
|                         c.isCloseIconVisible = true | ||||
|                         selectedChip = c | ||||
|                     } | ||||
|  | ||||
|                     tagGroup.addView(c) | ||||
|                 } | ||||
|  | ||||
|                 repository.getSources().forEach { source -> | ||||
|                     val c = Chip(context) | ||||
|  | ||||
|                 if (repository.sourceFilter.value?.equals(source) == true) { | ||||
|                     c.isCloseIconVisible = true | ||||
|                     selectedChip = c | ||||
|                     Glide.with(context) | ||||
|                         .load(source.getIcon(repository.baseUrl)) | ||||
|                         .listener(object : RequestListener<Drawable?> { | ||||
|                             override fun onLoadFailed( | ||||
|                                 e: GlideException?, | ||||
|                                 model: Any?, | ||||
|                                 target: Target<Drawable?>?, | ||||
|                                 isFirstResource: Boolean | ||||
|                             ): Boolean { | ||||
|                                 return false | ||||
|                             } | ||||
|  | ||||
|                             override fun onResourceReady( | ||||
|                                 resource: Drawable?, | ||||
|                                 model: Any?, | ||||
|                                 target: Target<Drawable?>?, | ||||
|                                 dataSource: DataSource?, | ||||
|                                 isFirstResource: Boolean | ||||
|                             ): Boolean { | ||||
|                                 c.chipIcon = resource | ||||
|                                 return false | ||||
|                             } | ||||
|                         }).preload() | ||||
|  | ||||
|                     c.text = source.title.getHtmlDecoded() | ||||
|  | ||||
|                     c.setOnCloseIconClickListener { | ||||
|                         (it as Chip).isCloseIconVisible = false | ||||
|                         selectedChip = null | ||||
|                         repository.setSourceFilter(null) | ||||
|                     } | ||||
|  | ||||
|                     c.setOnClickListener { | ||||
|                         if (selectedChip != null) { | ||||
|                             selectedChip!!.isCloseIconVisible = false | ||||
|                         } | ||||
|                         (it as Chip).isCloseIconVisible = true | ||||
|                         selectedChip = it | ||||
|                         repository.setSourceFilter(source) | ||||
|  | ||||
|                         repository.setTagFilter(null) | ||||
|                     } | ||||
|  | ||||
|  | ||||
|                     if (repository.sourceFilter.value?.equals(source) == true) { | ||||
|                         c.isCloseIconVisible = true | ||||
|                         selectedChip = c | ||||
|                     } | ||||
|  | ||||
|                     sourceGroup.addView(c) | ||||
|                 } | ||||
|  | ||||
|                 sourceGroup.addView(c) | ||||
|                 binding.progressBar2.visibility = GONE | ||||
|                 binding.filterView.visibility = VISIBLE | ||||
|             } | ||||
|  | ||||
|             binding.progressBar2.visibility = GONE | ||||
|             binding.filterView.visibility = VISIBLE | ||||
|         } | ||||
|  | ||||
|         binding.floatingActionButton2.setOnClickListener { | ||||
| @@ -133,49 +173,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { | ||||
|         return binding.root | ||||
|     } | ||||
|  | ||||
|     private fun chipForTag(tag: SelfossModel.Tag): Chip { | ||||
|         val c = Chip(requireContext()) | ||||
|         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) | ||||
|         } | ||||
|         gd.setColor(gdColor) | ||||
|         gd.shape = GradientDrawable.RECTANGLE | ||||
|         gd.setSize(30, 30) | ||||
|         gd.cornerRadius = 30F | ||||
|         c.chipIcon = gd | ||||
|  | ||||
|         c.setOnCloseIconClickListener { | ||||
|             (it as Chip).isCloseIconVisible = false | ||||
|             selectedChip = null | ||||
|             repository.setTagFilter(null) | ||||
|         } | ||||
|  | ||||
|         c.setOnClickListener { | ||||
|             if (selectedChip != null) { | ||||
|                 selectedChip!!.isCloseIconVisible = false | ||||
|             } | ||||
|             (it as Chip).isCloseIconVisible = true | ||||
|             selectedChip = it | ||||
|             repository.setTagFilter(tag) | ||||
|  | ||||
|             repository.setSourceFilter(null) | ||||
|         } | ||||
|  | ||||
|         if (repository.tagFilter.value?.equals(tag) == true) { | ||||
|             c.isCloseIconVisible = true | ||||
|             selectedChip = c | ||||
|         } | ||||
|         return c | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         const val TAG = "ModalBottomSheet" | ||||
|         const val TAG = "FilterModalBottomSheet" | ||||
|     } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -28,7 +28,7 @@ class ImageFragment : Fragment() { | ||||
|         val view = binding?.root | ||||
|  | ||||
|         binding!!.photoView.visibility = View.VISIBLE | ||||
|         Glide.with(activity) | ||||
|         Glide.with(requireActivity()) | ||||
|                 .asBitmap() | ||||
|                 .apply(glideOptions) | ||||
|                 .load(imageUrl) | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
| @@ -19,13 +19,9 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBin | ||||
| import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import com.mikepenz.aboutlibraries.LibsBuilder | ||||
| import org.acra.ktx.sendSilentlyWithAcra | ||||
| import org.acra.ktx.sendWithAcra | ||||
| import org.kodein.di.DIAware | ||||
| import org.kodein.di.android.closestDI | ||||
| import org.kodein.di.instance | ||||
| import org.matomo.sdk.Tracker | ||||
| import org.matomo.sdk.extra.TrackHelper | ||||
|  | ||||
| private const val TITLE_TAG = "settingsActivityTitle" | ||||
|  | ||||
| @@ -33,14 +29,10 @@ class SettingsActivity : AppCompatActivity(), | ||||
|         PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, DIAware { | ||||
|     override val di by closestDI() | ||||
|  | ||||
|     private val tracker : Tracker by instance() | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         val binding = ActivitySettingsBinding.inflate(layoutInflater) | ||||
|  | ||||
|         TrackHelper.track().screen("/settings").with(tracker) | ||||
|  | ||||
|         setContentView(binding.root) | ||||
|         if (savedInstanceState == null) { | ||||
|             supportFragmentManager | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -1,7 +0,0 @@ | ||||
| <vector android:height="24dp" android:tint="#000000" | ||||
|     android:viewportHeight="24" android:viewportWidth="24" | ||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M21,8c-1.45,0 -2.26,1.44 -1.93,2.51l-3.55,3.56c-0.3,-0.09 -0.74,-0.09 -1.04,0l-2.55,-2.55C12.27,10.45 11.46,9 10,9c-1.45,0 -2.27,1.44 -1.93,2.52l-4.56,4.55C2.44,15.74 1,16.55 1,18c0,1.1 0.9,2 2,2c1.45,0 2.26,-1.44 1.93,-2.51l4.55,-4.56c0.3,0.09 0.74,0.09 1.04,0l2.55,2.55C12.73,16.55 13.54,18 15,18c1.45,0 2.27,-1.44 1.93,-2.52l3.56,-3.55C21.56,12.26 23,11.45 23,10C23,8.9 22.1,8 21,8z"/> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M15,9l0.94,-2.07l2.06,-0.93l-2.06,-0.93l-0.94,-2.07l-0.92,2.07l-2.08,0.93l2.08,0.93z"/> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M3.5,11l0.5,-2l2,-0.5l-2,-0.5l-0.5,-2l-0.5,2l-2,0.5l2,0.5z"/> | ||||
| </vector> | ||||
| @@ -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" | ||||
| @@ -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" | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Thème sombre</string> | ||||
|     <string name="mode_system">Utiliser les paramètres système</string> | ||||
|     <string name="mode_light">Thème clair</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">深色模式</string> | ||||
|     <string name="mode_system">遵循系统设置</string> | ||||
|     <string name="mode_light">浅色模式</string> | ||||
|     <string name="pref_switch_enable_analytics">启用分析</string> | ||||
|     <string name="gdpr_dialog_title">该应用不分享任何关于您的个人数据。</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[崩溃报告发送现已启用。 可以从设置页面禁用它。 请记住,崩溃报告对于应用程序开发是必需的。]]></string> | ||||
|     <string name="crash_toast_text">发生崩溃。请将细节发送给开发人员。</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -120,10 +120,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -123,10 +123,12 @@ | ||||
|     <string name="mode_dark">Dark mode</string> | ||||
|     <string name="mode_system">Follow the system setting</string> | ||||
|     <string name="mode_light">Light mode</string> | ||||
|     <string name="pref_switch_enable_analytics">Enable analytics</string> | ||||
|     <string name="gdpr_dialog_title">The app does not share any personal data about you.</string> | ||||
|     <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> | ||||
|     <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> | ||||
|     <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> | ||||
|   | ||||
| @@ -38,13 +38,6 @@ | ||||
|         android:icon="@drawable/ic_widgets_black_24dp" /> | ||||
|  | ||||
|  | ||||
|     <SwitchPreference | ||||
|         android:defaultValue="false" | ||||
|         android:key="enable_analytics" | ||||
|         android:title="@string/pref_switch_enable_analytics" | ||||
|         android:icon="@drawable/ic_baseline_insights_24"/> | ||||
|  | ||||
|  | ||||
|     <SwitchPreference | ||||
|         android:defaultValue="false" | ||||
|         android:key="acra.disable" | ||||
|   | ||||
| @@ -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,10 +17,12 @@ plugins { | ||||
|  | ||||
| allprojects { | ||||
|     repositories { | ||||
|         google() | ||||
|         mavenCentral() | ||||
|         jcenter() | ||||
|         maven { url = uri("https://www.jitpack.io") } | ||||
|         maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")} | ||||
|         // IMPORTANT : Add back when new library added | ||||
|         // google() | ||||
|         // mavenCentral() | ||||
|         // jcenter() | ||||
|         // maven { url = uri("https://www.jitpack.io") } | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -2,16 +2,20 @@ val pushCache: String by settings | ||||
|  | ||||
| pluginManagement { | ||||
|     repositories { | ||||
|         google() | ||||
|         gradlePluginPortal() | ||||
|         mavenCentral() | ||||
|         maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")} | ||||
|         // IMPORTANT : Add back when new plugin added | ||||
|         // google() | ||||
|         // gradlePluginPortal() | ||||
|         // mavenCentral() | ||||
|     } | ||||
| } | ||||
|  | ||||
| dependencyResolutionManagement { | ||||
|     repositories { | ||||
|         google() | ||||
|         mavenCentral() | ||||
|         maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")} | ||||
|         // IMPORTANT : Add back when new library added | ||||
|         // 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() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,5 @@ | ||||
| package bou.amine.apps.readerforselfossv2.model | ||||
|  | ||||
| import io.ktor.client.call.* | ||||
| import io.ktor.client.statement.* | ||||
| import io.ktor.http.* | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| @@ -21,28 +18,4 @@ class StatusAndData<T>(val success: Boolean, val data: T? = null) { | ||||
|             return StatusAndData(false) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| suspend fun responseOrSuccessIf404(r: HttpResponse): SuccessResponse { | ||||
|     return if (r.status === HttpStatusCode.NotFound) { | ||||
|         SuccessResponse(true) | ||||
|     } else { | ||||
|         maybeResponse(r) | ||||
|     } | ||||
| } | ||||
|  | ||||
| suspend fun maybeResponse(r: HttpResponse): SuccessResponse { | ||||
|     return if (r.status.isSuccess()) { | ||||
|         r.body() | ||||
|     } else { | ||||
|         SuccessResponse(false) | ||||
|     } | ||||
| } | ||||
|  | ||||
| suspend inline fun <reified T> bodyOrFailure(r: HttpResponse): StatusAndData<T> { | ||||
|     return if (r.status.isSuccess()) { | ||||
|         StatusAndData.succes(r.body()) | ||||
|     } else { | ||||
|         StatusAndData.error() | ||||
|     } | ||||
| } | ||||
| @@ -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() } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -8,13 +8,19 @@ import bou.amine.apps.readerforselfossv2.rest.SelfossApi | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import bou.amine.apps.readerforselfossv2.utils.* | ||||
| import io.github.aakira.napier.Napier | ||||
| import io.ktor.client.call.* | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.asStateFlow | ||||
| import kotlinx.coroutines.launch | ||||
|  | ||||
| class Repository(private val api: SelfossApi, private val appSettingsService: AppSettingsService, val isConnectionAvailable: MutableStateFlow<Boolean>, private val db: ReaderForSelfossDB) { | ||||
| class Repository( | ||||
|     private val api: SelfossApi, | ||||
|     private val appSettingsService: AppSettingsService, | ||||
|     val isConnectionAvailable: MutableStateFlow<Boolean>, | ||||
|     private val db: ReaderForSelfossDB | ||||
| ) { | ||||
|  | ||||
|     var items = ArrayList<SelfossModel.Item>() | ||||
|     var connectionMonitored = false | ||||
| @@ -41,6 +47,9 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|     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() | ||||
| @@ -146,7 +155,8 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|     } | ||||
|  | ||||
|     suspend fun getTags(): List<SelfossModel.Tag> { | ||||
|         val isDatabaseEnabled = appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() | ||||
|         val isDatabaseEnabled = | ||||
|             appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() | ||||
|         return if (isNetworkAvailable() && !fetchedTags) { | ||||
|             val apiTags = api.tags() | ||||
|             if (apiTags.success && apiTags.data != null && isDatabaseEnabled) { | ||||
| @@ -178,7 +188,8 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|     } | ||||
|  | ||||
|     suspend fun getSources(): ArrayList<SelfossModel.Source> { | ||||
|         val isDatabaseEnabled = appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() | ||||
|         val isDatabaseEnabled = | ||||
|             appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() | ||||
|         return if (isNetworkAvailable() && !fetchedSources) { | ||||
|             val apiSources = api.sources() | ||||
|             if (apiSources.success && apiSources.data != null && isDatabaseEnabled) { | ||||
| @@ -333,7 +344,6 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|         url: String, | ||||
|         spout: String, | ||||
|         tags: String, | ||||
|         filter: String | ||||
|     ): Boolean { | ||||
|         var response = false | ||||
|         if (isNetworkAvailable()) { | ||||
| @@ -342,20 +352,41 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|                 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 | ||||
|     } | ||||
|  | ||||
| @@ -380,18 +411,35 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|         return result | ||||
|     } | ||||
|  | ||||
|     suspend fun shouldBeSelfossInstance(): Pair<Boolean, Boolean> { | ||||
|         var fetchFailed = true | ||||
|         var showSelfossOnlyModal = false | ||||
|         if (isNetworkAvailable()) { | ||||
|             try { | ||||
|                 // Trying to fetch one item, and check someone is trying to use the app with | ||||
|                 // a random rss feed, that would throw a NoTransformationFoundException | ||||
|                 fetchFailed = !api.getItemsWithoutCatch().success | ||||
|             } catch (e: NoTransformationFoundException) { | ||||
|                 showSelfossOnlyModal = true | ||||
|             } catch (e: Throwable) { | ||||
|                 Napier.e(e.stackTraceToString(), tag = "RepositoryImpl.shouldBeSelfossInstance") | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return Pair(fetchFailed, showSelfossOnlyModal) | ||||
|     } | ||||
|  | ||||
|     suspend fun logout() { | ||||
|         if (isNetworkAvailable()) { | ||||
|             try { | ||||
|                 val response = api.logout() | ||||
|                 if (response.isSuccess) { | ||||
|                 if (!response.isSuccess) { | ||||
|                     Napier.e("Couldn't logout.", tag = "RepositoryImpl.logout") | ||||
|                 } | ||||
|             } catch (cause: Throwable) { | ||||
|                 Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.logout") | ||||
|             } finally { | ||||
|                 appSettingsService.clearAll() | ||||
|             } | ||||
|             appSettingsService.clearAll() | ||||
|         } else { | ||||
|             appSettingsService.clearAll() | ||||
|         } | ||||
| @@ -456,11 +504,30 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|  | ||||
|     private fun getDBItems(): List<ITEM> = db.itemsQueries.items().executeAsList() | ||||
|  | ||||
|     private fun insertDBAction(articleid: String, read: Boolean = false, unread: Boolean = false, starred: Boolean = false, unstarred: Boolean = false) = | ||||
|     private fun insertDBAction( | ||||
|         articleid: String, | ||||
|         read: Boolean = false, | ||||
|         unread: Boolean = false, | ||||
|         starred: Boolean = false, | ||||
|         unstarred: Boolean = false | ||||
|     ) = | ||||
|         db.actionsQueries.insertAction(articleid, read, unread, starred, unstarred) | ||||
|  | ||||
|     private fun updateDBItem(item: SelfossModel.Item) = | ||||
|         db.itemsQueries.updateItem(item.datetime, item.title.getHtmlDecoded(), item.content, item.unread, item.starred, item.thumbnail, item.icon, item.link, item.sourcetitle, item.tags.joinToString(","), item.id.toString()) | ||||
|         db.itemsQueries.updateItem( | ||||
|             item.datetime, | ||||
|             item.title.getHtmlDecoded(), | ||||
|             item.content, | ||||
|             item.unread, | ||||
|             item.starred, | ||||
|             item.thumbnail, | ||||
|             item.icon, | ||||
|             item.link, | ||||
|             item.sourcetitle, | ||||
|             item.tags.joinToString(","), | ||||
|             item.author, | ||||
|             item.id.toString() | ||||
|         ) | ||||
|  | ||||
|     suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> { | ||||
|         try { | ||||
| @@ -515,4 +582,28 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|     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 | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,82 @@ | ||||
| package bou.amine.apps.readerforselfossv2.rest | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.model.StatusAndData | ||||
| import bou.amine.apps.readerforselfossv2.model.SuccessResponse | ||||
| import io.github.aakira.napier.Napier | ||||
| import io.ktor.client.* | ||||
| import io.ktor.client.call.* | ||||
| import io.ktor.client.request.* | ||||
| import io.ktor.client.request.forms.* | ||||
| import io.ktor.client.statement.* | ||||
| import io.ktor.http.* | ||||
|  | ||||
|  | ||||
| suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse { | ||||
|     return if (r != null && r.status === HttpStatusCode.NotFound) { | ||||
|         SuccessResponse(true) | ||||
|     } else { | ||||
|         maybeResponse(r) | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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) | ||||
|     } | ||||
| } | ||||
|  | ||||
| suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> { | ||||
|     return if (r != null && r.status.isSuccess()) { | ||||
|         StatusAndData.succes(r.body()) | ||||
|     } else { | ||||
|         StatusAndData.error() | ||||
|     } | ||||
| } | ||||
|  | ||||
| inline fun tryToRequest( | ||||
|     requestType: String, | ||||
|     fn: () -> HttpResponse | ||||
| ): HttpResponse? { | ||||
|     var response: HttpResponse? = null | ||||
|     try { | ||||
|         response = fn() | ||||
|     } catch (ex: Exception) { | ||||
|         Napier.e("Couldn't execute $requestType request", ex, "tryTo$requestType") | ||||
|     } | ||||
|     return response | ||||
| } | ||||
|  | ||||
| suspend inline fun HttpClient.tryToGet( | ||||
|     urlString: String, | ||||
|     crossinline block: HttpRequestBuilder.() -> Unit = {} | ||||
| ): HttpResponse? = tryToRequest("Get") { return this.get { url(urlString); block() } } | ||||
|  | ||||
|  | ||||
| suspend inline fun HttpClient.tryToPost( | ||||
|     urlString: String, | ||||
|     block: HttpRequestBuilder.() -> Unit = {} | ||||
| ): HttpResponse? = tryToRequest("Post") { return this.post { url(urlString); block() } } | ||||
|  | ||||
| suspend inline fun HttpClient.tryToDelete( | ||||
|     urlString: String, | ||||
|     block: HttpRequestBuilder.() -> Unit = {} | ||||
| ): HttpResponse? = tryToRequest("Delete") { return this.delete { url(urlString); block() } } | ||||
|  | ||||
|  | ||||
| suspend fun HttpClient.tryToSubmitForm( | ||||
|     url: String, | ||||
|     formParameters: Parameters = Parameters.Empty, | ||||
|     encodeInQuery: Boolean = false, | ||||
|     block: HttpRequestBuilder.() -> Unit = {} | ||||
| ): HttpResponse? = | ||||
|     tryToRequest("SubmitForm") { | ||||
|         return this.submitForm(formParameters, encodeInQuery) { | ||||
|             url(url) | ||||
|             block() | ||||
|         } | ||||
|     } | ||||
| @@ -1,6 +1,8 @@ | ||||
| package bou.amine.apps.readerforselfossv2.rest | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.model.* | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.StatusAndData | ||||
| import bou.amine.apps.readerforselfossv2.model.SuccessResponse | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import io.github.aakira.napier.Napier | ||||
| import io.ktor.client.* | ||||
| @@ -10,7 +12,6 @@ import io.ktor.client.plugins.contentnegotiation.* | ||||
| import io.ktor.client.plugins.cookies.* | ||||
| import io.ktor.client.plugins.logging.* | ||||
| import io.ktor.client.request.* | ||||
| import io.ktor.client.request.forms.* | ||||
| import io.ktor.client.statement.* | ||||
| import io.ktor.http.* | ||||
| import io.ktor.serialization.kotlinx.json.* | ||||
| @@ -50,7 +51,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|                 retryIf { _, response -> | ||||
|                     response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo() | ||||
|                 } | ||||
|                 modifyRequest { _ -> | ||||
|                 modifyRequest { | ||||
|                     Napier.i("Will modify", tag = "HttpSend") | ||||
|                     CoroutineScope(Dispatchers.Main).launch { | ||||
|                         Napier.i("Will login", tag = "HttpSend") | ||||
| @@ -75,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() != null && appSettingsService.getPassword() != null | ||||
|     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 { | ||||
| @@ -88,17 +93,19 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|             SuccessResponse(true) | ||||
|         } | ||||
|  | ||||
|     private suspend fun getLogin() = maybeResponse(client.get(url("/login")) { | ||||
|     private suspend fun getLogin() = maybeResponse(client.tryToGet(url("/login")) { | ||||
|         parameter("username", appSettingsService.getUserName()) | ||||
|         parameter("password", appSettingsService.getPassword()) | ||||
|     }) | ||||
|  | ||||
|     private suspend fun postLogin() = maybeResponse(client.post(url("/login")) { | ||||
|     private suspend fun postLogin() = maybeResponse(client.tryToPost(url("/login")) { | ||||
|         parameter("username", appSettingsService.getUserName()) | ||||
|         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() | ||||
| @@ -106,9 +113,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|             maybeLogoutIfAvailable() | ||||
|         } | ||||
|  | ||||
|     private suspend fun maybeLogoutIfAvailable() = responseOrSuccessIf404(client.get(url("/logout"))) | ||||
|     private suspend fun maybeLogoutIfAvailable() = | ||||
|         responseOrSuccessIf404(client.tryToGet(url("/logout"))) | ||||
|  | ||||
|     private suspend fun doLogout() = maybeResponse(client.delete(url("/api/session/current"))) | ||||
|     private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current"))) | ||||
|  | ||||
|     suspend fun getItems( | ||||
|         type: String, | ||||
| @@ -119,7 +127,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         updatedSince: String?, | ||||
|         items: Int? = null | ||||
|     ): StatusAndData<List<SelfossModel.Item>> = | ||||
|         bodyOrFailure(client.get(url("/items")) { | ||||
|         bodyOrFailure(client.tryToGet(url("/items")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
| @@ -133,8 +141,18 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|             parameter("offset", offset) | ||||
|         }) | ||||
|  | ||||
|     suspend fun getItemsWithoutCatch(): StatusAndData<List<SelfossModel.Item>> = | ||||
|         bodyOrFailure(client.get(url("/items")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             } | ||||
|             parameter("type", "all") | ||||
|             parameter("items", 1) | ||||
|         }) | ||||
|  | ||||
|     suspend fun stats(): StatusAndData<SelfossModel.Stats> = | ||||
|         bodyOrFailure(client.get(url("/stats")) { | ||||
|         bodyOrFailure(client.tryToGet(url("/stats")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
| @@ -142,7 +160,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         }) | ||||
|  | ||||
|     suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> = | ||||
|         bodyOrFailure(client.get(url("/tags")) { | ||||
|         bodyOrFailure(client.tryToGet(url("/tags")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
| @@ -150,7 +168,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         }) | ||||
|  | ||||
|     suspend fun update(): StatusAndData<String> = | ||||
|         bodyOrFailure(client.get(url("/update")) { | ||||
|         bodyOrFailure(client.tryToGet(url("/update")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
| @@ -158,7 +176,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         }) | ||||
|  | ||||
|     suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> = | ||||
|         bodyOrFailure(client.get(url("/sources/spouts")) { | ||||
|         bodyOrFailure(client.tryToGet(url("/sources/spouts")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
| @@ -166,7 +184,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         }) | ||||
|  | ||||
|     suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> = | ||||
|         bodyOrFailure(client.get(url("/sources/list")) { | ||||
|         bodyOrFailure(client.tryToGet(url("/sources/list")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
| @@ -174,10 +192,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         }) | ||||
|  | ||||
|     suspend fun version(): StatusAndData<SelfossModel.ApiVersion> = | ||||
|         bodyOrFailure(client.get(url("/api/about"))) | ||||
|         bodyOrFailure(client.tryToGet(url("/api/about"))) | ||||
|  | ||||
|     suspend fun markAsRead(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/mark/$id")) { | ||||
|         maybeResponse(client.tryToPost(url("/mark/$id")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
| @@ -185,7 +203,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         }) | ||||
|  | ||||
|     suspend fun unmarkAsRead(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/unmark/$id")) { | ||||
|         maybeResponse(client.tryToPost(url("/unmark/$id")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
| @@ -193,7 +211,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         }) | ||||
|  | ||||
|     suspend fun starr(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/starr/$id")) { | ||||
|         maybeResponse(client.tryToPost(url("/starr/$id")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
| @@ -201,7 +219,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         }) | ||||
|  | ||||
|     suspend fun unstarr(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/unstarr/$id")) { | ||||
|         maybeResponse(client.tryToPost(url("/unstarr/$id")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
| @@ -209,7 +227,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         }) | ||||
|  | ||||
|     suspend fun markAllAsRead(ids: List<String>): SuccessResponse = | ||||
|         maybeResponse(client.submitForm( | ||||
|         maybeResponse(client.tryToSubmitForm( | ||||
|             url = url("/mark"), | ||||
|             formParameters = Parameters.build { | ||||
|                 if (!shouldHavePostLogin()) { | ||||
| @@ -225,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) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
| @@ -240,10 +257,9 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         title: String, | ||||
|         url: String, | ||||
|         spout: String, | ||||
|         tags: String, | ||||
|         filter: String | ||||
|     ): HttpResponse = | ||||
|         client.submitForm( | ||||
|         tags: String | ||||
|     ): HttpResponse? = | ||||
|         client.tryToSubmitForm( | ||||
|             url = url("/source"), | ||||
|             formParameters = Parameters.build { | ||||
|                 if (!shouldHavePostLogin()) { | ||||
| @@ -254,12 +270,48 @@ 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) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     suspend fun deleteSource(id: Int): SuccessResponse = | ||||
|         maybeResponse(client.delete(url("/source/$id")) { | ||||
|         maybeResponse(client.tryToDelete(url("/source/$id")) { | ||||
|             if (!shouldHavePostLogin()) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|   | ||||
| @@ -35,7 +35,6 @@ class AppSettingsService { | ||||
|     private var _staticBar: Boolean? = null | ||||
|     private var _font: String = "" | ||||
|     private var _theme: Int? = null | ||||
|     private var _enableAnalytics: Boolean? = null | ||||
|  | ||||
|  | ||||
|     init { | ||||
| @@ -309,17 +308,6 @@ class AppSettingsService { | ||||
|         return _staticBar == true | ||||
|     } | ||||
|  | ||||
|     private fun refreshAnalyticsEnabled() { | ||||
|         _enableAnalytics = settings.getBoolean(ENABLE_ANALYTICS, true) | ||||
|     } | ||||
|  | ||||
|     fun isAnalyticsEnabled(): Boolean { | ||||
|         if (_enableAnalytics != null) { | ||||
|             refreshAnalyticsEnabled() | ||||
|         } | ||||
|         return _enableAnalytics == true | ||||
|     } | ||||
|  | ||||
|     private fun refreshFont() { | ||||
|         _font = settings.getString(READER_FONT, "") | ||||
|     } | ||||
| @@ -370,7 +358,6 @@ class AppSettingsService { | ||||
|         refreshFont() | ||||
|         refreshStaticBarEnabled() | ||||
|         refreshCurrentTheme() | ||||
|         refreshAnalyticsEnabled() | ||||
|     } | ||||
|  | ||||
|     fun refreshLoginInformation( | ||||
| @@ -471,6 +458,5 @@ class AppSettingsService { | ||||
|  | ||||
|         const val CURRENT_THEME = "currentMode" | ||||
|  | ||||
|         const val ENABLE_ANALYTICS = "enable_analytics" | ||||
|     } | ||||
| } | ||||
| @@ -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