Compare commits
	
		
			15 Commits
		
	
	
		
			16b10dc1b7
			...
			v122113131
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a4527940b8 | ||
|  | 9e8a25ed3e | ||
|  | 8ea46e146b | ||
|  | 5ecf3c3f87 | ||
|  | 325f103417 | ||
|  | ab4b1ae644 | ||
|  | 87ea44754e | ||
|  | 04dec50808 | ||
|  | e36189e2e7 | ||
|  | d6bdf510a4 | ||
|  | a464e93370 | ||
| 4b63afe62a | |||
| ac4c4b9441 | |||
| 02d734eee8 | |||
| c5cdfc0d53 | 
| @@ -8,7 +8,7 @@ steps: | ||||
|     commands: | ||||
|       - echo "---------------------------------------------------------" | ||||
|       - echo "Configure gradle..." | ||||
|       - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false" >> ~/.gradle/gradle.properties | ||||
|       - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties | ||||
|       - echo "---------------------------------------------------------" | ||||
|       - echo "Analysing..." | ||||
|       - ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN | ||||
| @@ -90,7 +90,7 @@ steps: | ||||
|     commands: | ||||
|       - echo "---------------------------------------------------------" | ||||
|       - echo "Configure gradle..." | ||||
|       - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false" >> ~/.gradle/gradle.properties | ||||
|       - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\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 | ||||
|   | ||||
| @@ -6,6 +6,7 @@ plugins { | ||||
|     id("com.android.application") | ||||
|     kotlin("android") | ||||
|     kotlin("kapt") | ||||
|     id("com.mikepenz.aboutlibraries.plugin") | ||||
| } | ||||
|  | ||||
| fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String { | ||||
| @@ -62,7 +63,7 @@ android { | ||||
|     kotlinOptions { | ||||
|         jvmTarget = "11" | ||||
|     } | ||||
|     compileSdk = 32 | ||||
|     compileSdk = 33 | ||||
|     buildToolsVersion = "31.0.0" | ||||
|     buildFeatures { | ||||
|         viewBinding = true | ||||
| @@ -70,7 +71,7 @@ android { | ||||
|     defaultConfig { | ||||
|         applicationId = "bou.amine.apps.readerforselfossv2.android" | ||||
|         minSdk = 21 | ||||
|         targetSdk = 32 | ||||
|         targetSdk = 33 | ||||
|         versionCode = versionCodeFromGit() | ||||
|         versionName = versionNameFromGit() | ||||
|  | ||||
| @@ -138,15 +139,8 @@ dependencies { | ||||
|     implementation("androidx.multidex:multidex:2.0.1") | ||||
|  | ||||
|     // About | ||||
|     implementation("com.mikepenz:aboutlibraries-core:8.9.4") | ||||
|     implementation("com.mikepenz:aboutlibraries:8.9.4") | ||||
|     implementation("com.mikepenz:aboutlibraries-definitions:8.9.4") | ||||
|  | ||||
|     // Retrofit + http logging + okhttp | ||||
|     implementation("com.squareup.retrofit2:retrofit:2.9.0") | ||||
|     implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3") | ||||
|     implementation("com.squareup.retrofit2:converter-gson:2.9.0") | ||||
|     implementation("com.burgstaller:okhttp-digest:2.5") | ||||
|     implementation("com.mikepenz:aboutlibraries-core:10.5.1") | ||||
|     implementation("com.mikepenz:aboutlibraries:10.5.1") | ||||
|  | ||||
|     // Material-ish things | ||||
|     implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0") | ||||
| @@ -210,3 +204,13 @@ tasks.withType<Test> { | ||||
|         showStandardStreams = true | ||||
|     } | ||||
| } | ||||
|  | ||||
| aboutLibraries { | ||||
|     offlineMode = true | ||||
|     fetchRemoteLicense = false | ||||
|     fetchRemoteFunding = false | ||||
|     includePlatform = false | ||||
|     strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL | ||||
|     duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE | ||||
|     duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP | ||||
| } | ||||
							
								
								
									
										7
									
								
								androidApp/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								androidApp/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							| @@ -30,15 +30,8 @@ | ||||
|     <fields>; | ||||
| } | ||||
|  | ||||
| -dontwarn okio.** | ||||
| -dontwarn retrofit2.Platform$Java8 | ||||
| -keep class retrofit.** { *; } | ||||
| -keepclasseswithmembers class * { | ||||
|     @retrofit.http.* <methods>; | ||||
| } | ||||
| -keepattributes *Annotation*,Signature | ||||
| -keepattributes Exceptions | ||||
| -dontwarn okio.** | ||||
| -dontwarn javax.annotation.Nullable | ||||
| -dontwarn javax.annotation.ParametersAreNonnullByDefault | ||||
|  | ||||
|   | ||||
| @@ -15,7 +15,8 @@ | ||||
|         android:supportsRtl="true" | ||||
|         android:networkSecurityConfig="@xml/network_security_config" | ||||
|         android:theme="@style/NoBar" | ||||
|         android:dataExtractionRules="@xml/data_extraction_rules"> | ||||
|         android:dataExtractionRules="@xml/data_extraction_rules" | ||||
|         android:configChanges="uiMode"> | ||||
|         <activity | ||||
|             android:name=".MainActivity" | ||||
|             android:theme="@style/SplashTheme" | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.appcompat.widget.SearchView | ||||
| import androidx.core.view.doOnNextLayout | ||||
| import androidx.drawerlayout.widget.DrawerLayout | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import androidx.recyclerview.widget.* | ||||
| import androidx.work.Constraints | ||||
| import androidx.work.ExistingPeriodicWorkPolicy | ||||
| @@ -178,8 +179,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|  | ||||
|                         adapter.handleItemAtIndex(position) | ||||
|  | ||||
|                         reloadBadgeContent() | ||||
|  | ||||
|                         val tagHashes = i.tags.map { it.longHash() } | ||||
|                         tagsBadge = tagsBadge.map { | ||||
|                             if (tagHashes.contains(it.key)) { | ||||
| @@ -207,6 +206,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|         ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView) | ||||
|     } | ||||
|  | ||||
|     private fun updateBottomBarBadgeCount(badge: TextBadgeItem, count: Int) { | ||||
|         if (count > 0) { | ||||
|             badge | ||||
|                 .setText(count.toString()) | ||||
|                 .maybeShow() | ||||
|         } else { | ||||
|             badge.removeBadge() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun handleBottomBar() { | ||||
|  | ||||
|         tabNewBadge = TextBadgeItem() | ||||
| @@ -219,6 +228,28 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|             .setText("") | ||||
|             .setHideOnSelect(false).hide(false) | ||||
|  | ||||
|         if (appSettingsService.isDisplayUnreadCountEnabled()) { | ||||
|             lifecycleScope.launch { | ||||
|                 repository.badgeUnread.collect { | ||||
|                     updateBottomBarBadgeCount(tabNewBadge, it) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (appSettingsService.isDisplayAllCountEnabled()) { | ||||
|             lifecycleScope.launch { | ||||
|                 repository.badgeAll.collect { | ||||
|                     updateBottomBarBadgeCount(tabArchiveBadge, it) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             lifecycleScope.launch { | ||||
|                 repository.badgeStarred.collect { | ||||
|                     updateBottomBarBadgeCount(tabStarredBadge, it) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         val tabNew = | ||||
|             BottomNavigationItem( | ||||
|                 R.drawable.ic_tab_fiber_new_black_24dp, | ||||
| @@ -714,29 +745,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|  | ||||
|     private fun reloadBadges() { | ||||
|         if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) { | ||||
|             CoroutineScope(Dispatchers.Main).launch { | ||||
|             CoroutineScope(Dispatchers.IO).launch { | ||||
|                 repository.reloadBadges() | ||||
|                 reloadBadgeContent() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun reloadBadgeContent() { | ||||
|         if (appSettingsService.isDisplayUnreadCountEnabled()) { | ||||
|             tabNewBadge | ||||
|                 .setText(repository.badgeUnread.toString()) | ||||
|                 .maybeShow() | ||||
|         } | ||||
|         if (appSettingsService.isDisplayAllCountEnabled()) { | ||||
|             tabArchiveBadge | ||||
|                 .setText(repository.badgeAll.toString()) | ||||
|                 .maybeShow() | ||||
|             tabStarredBadge | ||||
|                 .setText(repository.badgeStarred.toString()) | ||||
|                 .maybeShow() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun reloadTagsBadges() { | ||||
|         tagsBadge.forEach { | ||||
|             binding.mainDrawer.updateBadge(it.key, StringHolder(it.value.toString())) | ||||
| @@ -858,10 +872,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar | ||||
|  | ||||
|     private fun maxItemNumber(): Int = | ||||
|         when (elementsShown) { | ||||
|             ItemType.UNREAD -> repository.badgeUnread | ||||
|             ItemType.ALL -> repository.badgeAll | ||||
|             ItemType.STARRED -> repository.badgeStarred | ||||
|             else -> repository.badgeUnread // if !elementsShown then unread are fetched. | ||||
|             ItemType.UNREAD -> repository.badgeUnread.value | ||||
|             ItemType.ALL -> repository.badgeAll.value | ||||
|             ItemType.STARRED -> repository.badgeStarred.value | ||||
|             else -> repository.badgeUnread.value // if !elementsShown then unread are fetched. | ||||
|         } | ||||
|  | ||||
|     private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) { | ||||
|   | ||||
| @@ -3,11 +3,14 @@ package bou.amine.apps.readerforselfossv2.android | ||||
| import android.app.NotificationChannel | ||||
| import android.app.NotificationManager | ||||
| import android.content.Context | ||||
| import android.content.res.Configuration | ||||
| import android.graphics.drawable.Drawable | ||||
| import android.net.Uri | ||||
| import android.os.Build | ||||
| import android.widget.ImageView | ||||
| import android.widget.Toast | ||||
| import androidx.appcompat.app.AppCompatDelegate | ||||
| import androidx.appcompat.app.AppCompatDelegate.* | ||||
| import androidx.lifecycle.DefaultLifecycleObserver | ||||
| import androidx.lifecycle.LifecycleOwner | ||||
| import androidx.lifecycle.ProcessLifecycleOwner | ||||
| @@ -48,6 +51,7 @@ class MyApp : MultiDexApplication(), DIAware { | ||||
|     private val viewModel: AppViewModel by instance() | ||||
|     private val connectivityStatus: ConnectivityStatus by instance() | ||||
|     private val driverFactory: DriverFactory by instance() | ||||
|     private val appSettingsService : AppSettingsService by instance() | ||||
|  | ||||
|     // TODO: handle with the "previous" way | ||||
|     private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true) | ||||
| @@ -132,6 +136,19 @@ class MyApp : MultiDexApplication(), DIAware { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onConfigurationChanged(newConfig: Configuration) { | ||||
|         super.onConfigurationChanged(newConfig) | ||||
|         if (appSettingsService.getCurrentTheme() == MODE_NIGHT_FOLLOW_SYSTEM) { | ||||
|             var mode = when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) { | ||||
|                 Configuration.UI_MODE_NIGHT_YES -> MODE_NIGHT_YES | ||||
|                 else -> MODE_NIGHT_NO | ||||
|             } | ||||
|             setDefaultNightMode(mode) | ||||
|         } | ||||
|  | ||||
|  | ||||
|     } | ||||
|  | ||||
|     class AppLifeCycleObserver(val connectivityStatus: ConnectivityStatus, val repository: Repository) : DefaultLifecycleObserver { | ||||
|  | ||||
|         override fun onResume(owner: LifecycleOwner) { | ||||
|   | ||||
| @@ -36,9 +36,9 @@ class ReaderActivity : AppCompatActivity(), DIAware { | ||||
|  | ||||
|     private fun showMenuItem(willAddToFavorite: Boolean) { | ||||
|         if (willAddToFavorite) { | ||||
|             toolbarMenu.findItem(R.id.star).icon.setTint(Color.WHITE) | ||||
|             toolbarMenu.findItem(R.id.star).icon?.setTint(Color.WHITE) | ||||
|         } else { | ||||
|             toolbarMenu.findItem(R.id.star).icon.setTint(Color.RED) | ||||
|             toolbarMenu.findItem(R.id.star).icon?.setTint(Color.RED) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -111,13 +111,11 @@ class ItemCardAdapter( | ||||
|                     CoroutineScope(Dispatchers.IO).launch { | ||||
|                         repository.unstarr(item) | ||||
|                     } | ||||
|                     item.starred = false | ||||
|                     binding.favButton.isSelected = false | ||||
|                 } else { | ||||
|                     CoroutineScope(Dispatchers.IO).launch { | ||||
|                         repository.starr(item) | ||||
|                     } | ||||
|                     item.starred = true | ||||
|                     binding.favButton.isSelected = true | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -1,35 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.api.mercury | ||||
|  | ||||
| import com.google.gson.GsonBuilder | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.logging.HttpLoggingInterceptor | ||||
| import retrofit2.Call | ||||
| import retrofit2.Retrofit | ||||
| import retrofit2.converter.gson.GsonConverterFactory | ||||
|  | ||||
| class MercuryApi() { | ||||
|     private val service: MercuryService | ||||
|  | ||||
|     init { | ||||
|  | ||||
|         val interceptor = HttpLoggingInterceptor() | ||||
|         interceptor.level = HttpLoggingInterceptor.Level.NONE | ||||
|         val client = OkHttpClient.Builder().addInterceptor(interceptor).build() | ||||
|  | ||||
|         val gson = GsonBuilder() | ||||
|             .setLenient() | ||||
|             .create() | ||||
|         val retrofit = | ||||
|             Retrofit | ||||
|                 .Builder() | ||||
|                 .baseUrl("https://www.amine-louveau.fr") | ||||
|                 .client(client) | ||||
|                 .addConverterFactory(GsonConverterFactory.create(gson)) | ||||
|                 .build() | ||||
|         service = retrofit.create(MercuryService::class.java) | ||||
|     } | ||||
|  | ||||
|     fun parseUrl(url: String): Call<ParsedContent> { | ||||
|         return service.parseUrl(url) | ||||
|     } | ||||
| } | ||||
| @@ -1,59 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.api.mercury | ||||
|  | ||||
| import android.os.Parcel | ||||
| import android.os.Parcelable | ||||
| import com.google.gson.annotations.SerializedName | ||||
|  | ||||
| class ParsedContent( | ||||
|     @SerializedName("title") val title: String, | ||||
|     @SerializedName("content") val content: String?, | ||||
|     @SerializedName("date_published") val date_published: String, | ||||
|     @SerializedName("lead_image_url") val lead_image_url: String?, | ||||
|     @SerializedName("dek") val dek: String, | ||||
|     @SerializedName("url") val url: String, | ||||
|     @SerializedName("domain") val domain: String, | ||||
|     @SerializedName("excerpt") val excerpt: String, | ||||
|     @SerializedName("total_pages") val total_pages: Int, | ||||
|     @SerializedName("rendered_pages") val rendered_pages: Int, | ||||
|     @SerializedName("next_page_url") val next_page_url: String | ||||
| ) : Parcelable { | ||||
|  | ||||
|     companion object { | ||||
|         @JvmField | ||||
|         val CREATOR: Parcelable.Creator<ParsedContent> = | ||||
|             object : Parcelable.Creator<ParsedContent> { | ||||
|                 override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source) | ||||
|                 override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     constructor(source: Parcel) : this( | ||||
|         title = source.readString().orEmpty(), | ||||
|         content = source.readString(), | ||||
|         date_published = source.readString().orEmpty(), | ||||
|         lead_image_url = source.readString(), | ||||
|         dek = source.readString().orEmpty(), | ||||
|         url = source.readString().orEmpty(), | ||||
|         domain = source.readString().orEmpty(), | ||||
|         excerpt = source.readString().orEmpty(), | ||||
|         total_pages = source.readInt(), | ||||
|         rendered_pages = source.readInt(), | ||||
|         next_page_url = source.readString().orEmpty() | ||||
|     ) | ||||
|  | ||||
|     override fun describeContents() = 0 | ||||
|  | ||||
|     override fun writeToParcel(dest: Parcel, flags: Int) { | ||||
|         dest.writeString(title) | ||||
|         dest.writeString(content) | ||||
|         dest.writeString(date_published) | ||||
|         dest.writeString(lead_image_url) | ||||
|         dest.writeString(dek) | ||||
|         dest.writeString(url) | ||||
|         dest.writeString(domain) | ||||
|         dest.writeString(excerpt) | ||||
|         dest.writeInt(total_pages) | ||||
|         dest.writeInt(rendered_pages) | ||||
|         dest.writeString(next_page_url) | ||||
|     } | ||||
| } | ||||
| @@ -1,10 +0,0 @@ | ||||
| package bou.amine.apps.readerforselfossv2.android.api.mercury | ||||
|  | ||||
| import retrofit2.Call | ||||
| import retrofit2.http.GET | ||||
| import retrofit2.http.Query | ||||
|  | ||||
| interface MercuryService { | ||||
|     @GET("parser.php") | ||||
|     fun parseUrl(@Query("link") link: String): Call<ParsedContent> | ||||
| } | ||||
| @@ -21,8 +21,6 @@ import androidx.core.widget.NestedScrollView | ||||
| import androidx.fragment.app.Fragment | ||||
| import bou.amine.apps.readerforselfossv2.android.ImageActivity | ||||
| import bou.amine.apps.readerforselfossv2.android.R | ||||
| import bou.amine.apps.readerforselfossv2.android.api.mercury.MercuryApi | ||||
| import bou.amine.apps.readerforselfossv2.android.api.mercury.ParsedContent | ||||
| import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding | ||||
| import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem | ||||
| import bou.amine.apps.readerforselfossv2.android.model.toModel | ||||
| @@ -32,6 +30,7 @@ import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask | ||||
| import bou.amine.apps.readerforselfossv2.android.utils.shareLink | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.repository.Repository | ||||
| import bou.amine.apps.readerforselfossv2.rest.MercuryApi | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import bou.amine.apps.readerforselfossv2.utils.getImages | ||||
| @@ -49,9 +48,6 @@ import org.kodein.di.DI | ||||
| import org.kodein.di.DIAware | ||||
| import org.kodein.di.android.x.closestDI | ||||
| import org.kodein.di.instance | ||||
| import retrofit2.Call | ||||
| import retrofit2.Callback | ||||
| import retrofit2.Response | ||||
| import java.net.MalformedURLException | ||||
| import java.net.URL | ||||
| import java.util.* | ||||
| @@ -81,6 +77,9 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|     private var font = "" | ||||
|     private var staticBar = false | ||||
|  | ||||
|     private val mercuryApi : MercuryApi by instance() | ||||
|  | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|  | ||||
| @@ -249,26 +248,21 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|     private fun getContentFromMercury() { | ||||
|         if (repository.isNetworkAvailable()) { | ||||
|             binding.progressBar.visibility = View.VISIBLE | ||||
|             val parser = MercuryApi() | ||||
|  | ||||
|             parser.parseUrl(url).enqueue( | ||||
|                 object : Callback<ParsedContent> { | ||||
|                     override fun onResponse( | ||||
|                         call: Call<ParsedContent>, | ||||
|                         response: Response<ParsedContent> | ||||
|                     ) { | ||||
|                         // TODO: clean all the following after finding the mercury content issue | ||||
|             CoroutineScope(Dispatchers.Main).launch { | ||||
|                  val response = mercuryApi.query(url) | ||||
|                 if (response.success) { | ||||
|                     try { | ||||
|                             if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) { | ||||
|                         if (response.data != null && response.data!!.content != null && !response.data!!.content.isNullOrEmpty()) { | ||||
|                             try { | ||||
|                                     binding.titleView.text = response.body()!!.title | ||||
|                                 binding.titleView.text = response.data!!.title | ||||
|                                 if (typeface != null) { | ||||
|                                     binding.titleView.typeface = typeface | ||||
|                                 } | ||||
|                                 try { | ||||
|                                     // Note: Mercury may return relative urls... If it does the url val will not be changed. | ||||
|                                         URL(response.body()!!.url) | ||||
|                                         url = response.body()!!.url | ||||
|                                     URL(response.data!!.url) | ||||
|                                     url = response.data!!.url | ||||
|                                 } catch (e: MalformedURLException) { | ||||
|                                     // Mercury returned a relative url. We do nothing. | ||||
|                                 } | ||||
| @@ -276,20 +270,20 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                             } | ||||
|  | ||||
|                             try { | ||||
|                                     contentText = response.body()!!.content.orEmpty() | ||||
|                                 contentText = response.data!!.content.orEmpty() | ||||
|                                 htmlToWebview() | ||||
|                             } catch (e: Exception) { | ||||
|                             } | ||||
|  | ||||
|                             try { | ||||
|                                     if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) { | ||||
|                                 if (response.data!!.lead_image_url != null && !response.data!!.lead_image_url.isNullOrEmpty() && context != null) { | ||||
|                                     binding.imageView.visibility = View.VISIBLE | ||||
|                                     try { | ||||
|                                         Glide | ||||
|                                             .with(requireContext()) | ||||
|                                             .asBitmap() | ||||
|                                             .load( | ||||
|                                                     response.body()!!.lead_image_url.orEmpty() | ||||
|                                                 response.data!!.lead_image_url.orEmpty() | ||||
|                                             ) | ||||
|                                             .apply(RequestOptions.fitCenterTransform()) | ||||
|                                             .into(binding.imageView) | ||||
| @@ -323,14 +317,10 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|                         if (context != null) { | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     openInBrowserAfterFailing() | ||||
|                 } | ||||
|  | ||||
|                     override fun onFailure( | ||||
|                         call: Call<ParsedContent>, | ||||
|                         t: Throwable | ||||
|                     ) = openInBrowserAfterFailing() | ||||
|             } | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -389,7 +379,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|         } | ||||
|  | ||||
|         val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() { | ||||
|             override fun onSingleTapUp(e: MotionEvent?): Boolean { | ||||
|             override fun onSingleTapUp(e: MotionEvent): Boolean { | ||||
|                 return performClick() | ||||
|             } | ||||
|         }) | ||||
| @@ -410,6 +400,7 @@ class ArticleFragment : Fragment(), DIAware { | ||||
|         val fontName =  when (font) { | ||||
|             getString(R.string.open_sans_font_id) -> "Open Sans" | ||||
|             getString(R.string.roboto_font_id) -> "Roboto" | ||||
|             getString(R.string.source_code_pro_font_id) -> "Source Code Pro" | ||||
|             else -> "" | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.model | ||||
| import android.os.Parcel | ||||
| import android.os.Parcelable | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import com.google.gson.annotations.SerializedName | ||||
|  | ||||
| fun SelfossModel.Item.toParcelable() : ParecelableItem = | ||||
|     ParecelableItem( | ||||
| @@ -34,17 +33,17 @@ fun ParecelableItem.toModel() : SelfossModel.Item = | ||||
|         this.tags.split(",") | ||||
|     ) | ||||
| data class ParecelableItem( | ||||
|     @SerializedName("id") val id: Int, | ||||
|     @SerializedName("datetime") val datetime: String, | ||||
|     @SerializedName("title") val title: String, | ||||
|     @SerializedName("content") val content: String, | ||||
|     @SerializedName("unread") var unread: Boolean, | ||||
|     @SerializedName("starred") var starred: Boolean, | ||||
|     @SerializedName("thumbnail") val thumbnail: String?, | ||||
|     @SerializedName("icon") val icon: String?, | ||||
|     @SerializedName("link") val link: String, | ||||
|     @SerializedName("sourcetitle") val sourcetitle: String, | ||||
|     @SerializedName("tags") val tags: String | ||||
|     val id: Int, | ||||
|     val datetime: String, | ||||
|     val title: String, | ||||
|     val content: String, | ||||
|     var unread: Boolean, | ||||
|     var starred: Boolean, | ||||
|     val thumbnail: String?, | ||||
|     val icon: String?, | ||||
|     val link: String, | ||||
|     val sourcetitle: String, | ||||
|     val tags: String | ||||
| ) : Parcelable { | ||||
|  | ||||
|     companion object { | ||||
|   | ||||
							
								
								
									
										7
									
								
								androidApp/src/main/res/font/source_code_pro_medium.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								androidApp/src/main/res/font/source_code_pro_medium.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <font-family xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|         app:fontProviderAuthority="com.google.android.gms.fonts" | ||||
|         app:fontProviderPackage="com.google.android.gms" | ||||
|         app:fontProviderQuery="name=Source Code Pro&weight=500" | ||||
|         app:fontProviderCerts="@array/com_google_android_gms_fonts_certs"> | ||||
| </font-family> | ||||
| @@ -9,7 +9,7 @@ | ||||
|     <string-array name="ModeValues"> | ||||
|         <item>1</item> <!--MODE_NIGHT_NO--> | ||||
|         <item>2</item> <!--MODE_NIGHT_YES--> | ||||
|         <item>0</item> <!--MODE_NIGHT_AUTO_TIME--> | ||||
|         <item>-1</item> <!--MODE_NIGHT_FOLLOW_SYSTEM--> | ||||
|     </string-array> | ||||
|  | ||||
|     <string-array name="Voice"> | ||||
|   | ||||
| @@ -3,5 +3,6 @@ | ||||
|     <array name="preloaded_fonts" translatable="false"> | ||||
|         <item>@font/open_sans</item> | ||||
|         <item>@font/roboto</item> | ||||
|         <item>@font/source_code_pro_medium</item> | ||||
|     </array> | ||||
| </resources> | ||||
|   | ||||
| @@ -4,5 +4,6 @@ | ||||
|         <item></item> | ||||
|         <item>@string/open_sans_font_id</item> | ||||
|         <item>@string/roboto_font_id</item> | ||||
|         <item>@string/source_code_pro_font_id</item> | ||||
|     </array> | ||||
| </resources> | ||||
| @@ -4,5 +4,6 @@ | ||||
|         <item>Systems</item> | ||||
|         <item>Open Sans</item> | ||||
|         <item>Roboto</item> | ||||
|         <item>Source Code Pro</item> | ||||
|     </array> | ||||
| </resources> | ||||
| @@ -125,6 +125,7 @@ | ||||
|     <string name="reader_text_align_left">Align left</string> | ||||
|     <string name="reader_text_align_justify">Justify</string> | ||||
|     <string name="settings_reader_font">Reader font</string> | ||||
|     <string name="source_code_pro_font_id" translatable="false">source_code_pro_medium</string> | ||||
|     <string name="open_sans_font_id" translatable="false">open_sans</string> | ||||
|     <string name="roboto_font_id" translatable="false">roboto</string> | ||||
|     <string name="reader_static_bar_title">Static bottom bar in the article viewer</string> | ||||
|   | ||||
| @@ -4,6 +4,8 @@ import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB | ||||
| import bou.amine.apps.readerforselfossv2.dao.SOURCE | ||||
| import bou.amine.apps.readerforselfossv2.dao.TAG | ||||
| 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.rest.SelfossApi | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import bou.amine.apps.readerforselfossv2.utils.ItemType | ||||
| @@ -47,11 +49,11 @@ class RepositoryTest { | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|  | ||||
|         coEvery { api.version() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.version() } returns StatusAndData( | ||||
|             success = true, | ||||
|             data = SelfossModel.ApiVersion("2.19-ba1e8e3", "4.0.0") | ||||
|         ) | ||||
|         coEvery { api.stats() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.stats() } returns StatusAndData( | ||||
|             success = true, | ||||
|             data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED) | ||||
|         ) | ||||
| @@ -83,7 +85,7 @@ class RepositoryTest { | ||||
|     fun get_api_4_date_with_api_1_version_stored() { | ||||
|         every { appSettingsService.getApiVersion() } returns 1 | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|         every { appSettingsService.updateApiVersion(any()) } returns Unit | ||||
|  | ||||
|         initializeRepository() | ||||
| @@ -98,11 +100,11 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_api_1_date_with_api_4_version_stored() { | ||||
|         every { appSettingsService.getApiVersion() } returns 4 | ||||
|         coEvery { api.version() } returns SelfossModel.StatusAndData(success = false, null) | ||||
|         coEvery { api.version() } returns StatusAndData(success = false, null) | ||||
|         val itemParameters = FakeItemParameters() | ||||
|         itemParameters.datetime = "2021-04-23 11:45:32" | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData( | ||||
|                 StatusAndData( | ||||
|                     success = true, | ||||
|                     data = generateTestApiItem(itemParameters) | ||||
|                 ) | ||||
| @@ -118,7 +120,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_newer_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         runBlocking { | ||||
| @@ -133,7 +135,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_all_newer_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.displayedItems = ItemType.ALL | ||||
| @@ -149,7 +151,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_newer_starred_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.displayedItems = ItemType.STARRED | ||||
| @@ -242,7 +244,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_older_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.items = ArrayList(generateTestApiItem()) | ||||
| @@ -258,7 +260,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_all_older_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.items = ArrayList(generateTestApiItem()) | ||||
| @@ -275,7 +277,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun get_older_starred_items() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = true, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = true, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.displayedItems = ItemType.STARRED | ||||
| @@ -299,16 +301,16 @@ class RepositoryTest { | ||||
|         } | ||||
|  | ||||
|         assertSame(true, success) | ||||
|         assertSame(NUMBER_ARTICLES, repository.badgeAll) | ||||
|         assertSame(NUMBER_UNREAD, repository.badgeUnread) | ||||
|         assertSame(NUMBER_STARRED, repository.badgeStarred) | ||||
|         assertEquals(NUMBER_ARTICLES, repository.badgeAll.value) | ||||
|         assertEquals(NUMBER_UNREAD, repository.badgeUnread.value) | ||||
|         assertEquals(NUMBER_STARRED, repository.badgeStarred.value) | ||||
|         coVerify(atLeast = 1) { api.stats() } | ||||
|         verify(exactly = 0) { db.itemsQueries.items().executeAsList() } | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     fun reload_badges_without_response() { | ||||
|         coEvery { api.stats() } returns SelfossModel.StatusAndData(success = false, data = null) | ||||
|         coEvery { api.stats() } returns StatusAndData(success = false, data = null) | ||||
|  | ||||
|         var success: Boolean | ||||
|  | ||||
| @@ -318,9 +320,9 @@ class RepositoryTest { | ||||
|         } | ||||
|  | ||||
|         assertSame(false, success) | ||||
|         assertSame(0, repository.badgeAll) | ||||
|         assertSame(0, repository.badgeUnread) | ||||
|         assertSame(0, repository.badgeStarred) | ||||
|         assertSame(0, repository.badgeAll.value) | ||||
|         assertSame(0, repository.badgeUnread.value) | ||||
|         assertSame(0, repository.badgeStarred.value) | ||||
|         coVerify(atLeast = 1) { api.stats() } | ||||
|         verify(exactly = 0) { db.itemsQueries.items().executeAsList() } | ||||
|     } | ||||
| @@ -338,9 +340,9 @@ class RepositoryTest { | ||||
|         } | ||||
|  | ||||
|         assertTrue(success) | ||||
|         assertSame(1, repository.badgeAll) | ||||
|         assertSame(1, repository.badgeUnread) | ||||
|         assertSame(1, repository.badgeStarred) | ||||
|         assertEquals(1, repository.badgeAll.value) | ||||
|         assertEquals(1, repository.badgeUnread.value) | ||||
|         assertEquals(1, repository.badgeStarred.value) | ||||
|         coVerify(exactly = 0) { api.stats() } | ||||
|         verify(atLeast = 1) { db.itemsQueries.items().executeAsList() } | ||||
|     } | ||||
| @@ -358,9 +360,9 @@ class RepositoryTest { | ||||
|         } | ||||
|  | ||||
|         assertFalse(success) | ||||
|         assertSame(0, repository.badgeAll) | ||||
|         assertSame(0, repository.badgeUnread) | ||||
|         assertSame(0, repository.badgeStarred) | ||||
|         assertSame(0, repository.badgeAll.value) | ||||
|         assertSame(0, repository.badgeUnread.value) | ||||
|         assertSame(0, repository.badgeStarred.value) | ||||
|         coVerify(exactly = 0) { api.stats() } | ||||
|         verify(exactly = 0) { db.itemsQueries.items().executeAsList() } | ||||
|     } | ||||
| @@ -376,7 +378,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
| @@ -403,7 +405,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
| @@ -433,7 +435,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
| @@ -460,7 +462,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
| @@ -489,7 +491,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
| @@ -517,7 +519,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
| @@ -544,7 +546,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
| @@ -572,7 +574,7 @@ class RepositoryTest { | ||||
|             TAG("second_DB", "yellow", 0) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.tags() } returns SelfossModel.StatusAndData(success = true, data = tags) | ||||
|         coEvery { api.tags() } returns StatusAndData(success = true, data = tags) | ||||
|         coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
| @@ -627,7 +629,7 @@ class RepositoryTest { | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -681,7 +683,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -738,7 +740,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -792,7 +794,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository() | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -844,7 +846,7 @@ class RepositoryTest { | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -898,7 +900,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns true | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -952,7 +954,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns true | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -1006,7 +1008,7 @@ class RepositoryTest { | ||||
|  | ||||
|         every { appSettingsService.isItemCachingEnabled() } returns false | ||||
|         every { appSettingsService.isUpdateSourcesEnabled() } returns false | ||||
|         coEvery { api.sources() } returns SelfossModel.StatusAndData(success = true, data = sources) | ||||
|         coEvery { api.sources() } returns StatusAndData(success = true, data = sources) | ||||
|         every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var testSources: List<SelfossModel.Source>? | ||||
| @@ -1022,7 +1024,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun create_source() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.SuccessResponse(true) | ||||
|                 SuccessResponse(true) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1052,7 +1054,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun create_source_but_response_fails() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.SuccessResponse(false) | ||||
|                 SuccessResponse(false) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1082,7 +1084,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun create_source_without_connection() { | ||||
|         coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.SuccessResponse(true) | ||||
|                 SuccessResponse(true) | ||||
|  | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var response: Boolean | ||||
| @@ -1111,7 +1113,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun delete_source() { | ||||
|         coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(true) | ||||
|         coEvery { api.deleteSource(any()) } returns SuccessResponse(true) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1125,7 +1127,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun delete_source_but_response_fails() { | ||||
|         coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false) | ||||
|         coEvery { api.deleteSource(any()) } returns SuccessResponse(false) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1139,7 +1141,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun delete_source_without_connection() { | ||||
|         coEvery { api.deleteSource(any()) } returns SelfossModel.SuccessResponse(false) | ||||
|         coEvery { api.deleteSource(any()) } returns SuccessResponse(false) | ||||
|  | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var response: Boolean | ||||
| @@ -1153,7 +1155,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun update_remote() { | ||||
|         coEvery { api.update() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.update() } returns StatusAndData( | ||||
|             success = true, | ||||
|             data = "finished" | ||||
|         ) | ||||
| @@ -1170,7 +1172,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun update_remote_but_response_fails() { | ||||
|         coEvery { api.update() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.update() } returns StatusAndData( | ||||
|             success = false, | ||||
|             data = "unallowed access" | ||||
|         ) | ||||
| @@ -1187,7 +1189,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun update_remote_with_unallowed_access() { | ||||
|         coEvery { api.update() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.update() } returns StatusAndData( | ||||
|             success = true, | ||||
|             data = "unallowed access" | ||||
|         ) | ||||
| @@ -1204,7 +1206,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun update_remote_without_connection() { | ||||
|         coEvery { api.update() } returns SelfossModel.StatusAndData( | ||||
|         coEvery { api.update() } returns StatusAndData( | ||||
|             success = true, | ||||
|             data = "undocumented..." | ||||
|         ) | ||||
| @@ -1221,7 +1223,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun login() { | ||||
|         coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true) | ||||
|         coEvery { api.login() } returns SuccessResponse(success = true) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1235,7 +1237,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun login_but_response_fails() { | ||||
|         coEvery { api.login() } returns SelfossModel.SuccessResponse(success = false) | ||||
|         coEvery { api.login() } returns SuccessResponse(success = false) | ||||
|  | ||||
|         initializeRepository() | ||||
|         var response: Boolean | ||||
| @@ -1249,7 +1251,7 @@ class RepositoryTest { | ||||
|  | ||||
|     @Test | ||||
|     fun login_but_without_connection() { | ||||
|         coEvery { api.login() } returns SelfossModel.SuccessResponse(success = true) | ||||
|         coEvery { api.login() } returns SuccessResponse(success = true) | ||||
|  | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         var response: Boolean | ||||
| @@ -1297,9 +1299,9 @@ class RepositoryTest { | ||||
|                 any() | ||||
|             ) | ||||
|         } returnsMany listOf( | ||||
|             SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), | ||||
|             SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter2)), | ||||
|             SelfossModel.StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), | ||||
|             StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), | ||||
|             StatusAndData(success = true, data = generateTestApiItem(itemParameter2)), | ||||
|             StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), | ||||
|         ) | ||||
|  | ||||
|         initializeRepository() | ||||
| @@ -1323,7 +1325,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun cache_items_but_response_fails() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = false, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = false, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository() | ||||
|         repository.tagFilter = SelfossModel.Tag("Tag", "read", 0) | ||||
| @@ -1346,7 +1348,7 @@ class RepositoryTest { | ||||
|     @Test | ||||
|     fun cache_items_without_connection() { | ||||
|         coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns | ||||
|                 SelfossModel.StatusAndData(success = false, data = generateTestApiItem()) | ||||
|                 StatusAndData(success = false, data = generateTestApiItem()) | ||||
|  | ||||
|         initializeRepository(MutableStateFlow(false)) | ||||
|         repository.tagFilter = SelfossModel.Tag("Tag", "read", 0) | ||||
|   | ||||
| @@ -12,6 +12,7 @@ plugins { | ||||
|     kotlin("android").version("1.7.20").apply(false) | ||||
|     kotlin("multiplatform").version("1.7.20").apply(false) | ||||
|     id("org.sonarqube").version("3.4.0.2513").apply(false) | ||||
|     id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false) | ||||
| } | ||||
|  | ||||
| apply(plugin = "org.sonarqube") | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package bou.amine.apps.readerforselfossv2.DI | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.rest.MercuryApi | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossApi | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import org.kodein.di.DI | ||||
| @@ -10,4 +11,5 @@ import org.kodein.di.singleton | ||||
| val networkModule by DI.Module { | ||||
|     bind<AppSettingsService>() with singleton { AppSettingsService() } | ||||
|     bind<SelfossApi>() with singleton { SelfossApi(instance()) } | ||||
|     bind<MercuryApi>() with singleton { MercuryApi() } | ||||
| } | ||||
| @@ -0,0 +1,157 @@ | ||||
| package bou.amine.apps.readerforselfossv2.model | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.utils.DateUtils | ||||
| import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded | ||||
| import kotlinx.serialization.KSerializer | ||||
| import kotlinx.serialization.Serializable | ||||
| import kotlinx.serialization.descriptors.PrimitiveKind | ||||
| import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor | ||||
| import kotlinx.serialization.descriptors.SerialDescriptor | ||||
| import kotlinx.serialization.encoding.Decoder | ||||
| import kotlinx.serialization.encoding.Encoder | ||||
| import kotlinx.serialization.json.* | ||||
|  | ||||
| class MercuryModel { | ||||
|  | ||||
|     @Serializable | ||||
|     class ParsedContent( | ||||
|         val title: String, | ||||
|         val content: String?, | ||||
|         val lead_image_url: String?, | ||||
|         val url: String | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     data class Tag( | ||||
|         val tag: String, | ||||
|         val color: String, | ||||
|         val unread: Int | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     class Stats( | ||||
|         val total: Int, | ||||
|         val unread: Int, | ||||
|         val starred: Int | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     data class Spout( | ||||
|         val name: String, | ||||
|         val description: String | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     data class ApiVersion( | ||||
|         val version: String?, | ||||
|         val apiversion: String? | ||||
|     ) { | ||||
|         fun getApiMajorVersion() : Int { | ||||
|             var versionNumber = 0 | ||||
|             if (apiversion != null) { | ||||
|                 versionNumber = apiversion.substringBefore(".").toInt() | ||||
|             } | ||||
|             return versionNumber | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Serializable | ||||
|     data class Source( | ||||
|         val id: Int, | ||||
|         val title: String, | ||||
|         @Serializable(with = TagsListSerializer::class) | ||||
|         val tags: List<String>, | ||||
|         val spout: String, | ||||
|         val error: String, | ||||
|         val icon: String? | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     data class Item( | ||||
|         val id: Int, | ||||
|         val datetime: String, | ||||
|         val title: String, | ||||
|         val content: String, | ||||
|         @Serializable(with = BooleanSerializer::class) | ||||
|         var unread: Boolean, | ||||
|         @Serializable(with = BooleanSerializer::class) | ||||
|         var starred: Boolean, | ||||
|         val thumbnail: String?, | ||||
|         val icon: String?, | ||||
|         val link: String, | ||||
|         val sourcetitle: String, | ||||
|         @Serializable(with = TagsListSerializer::class) | ||||
|         val tags: List<String> | ||||
|     ) { | ||||
|         // TODO: maybe find a better way to handle these kind of urls | ||||
|         fun getLinkDecoded(): String { | ||||
|             var stringUrl: String | ||||
|             stringUrl = | ||||
|                 if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) { | ||||
|                     if (link.contains("&url=")) { | ||||
|                         link.substringAfter("&url=") | ||||
|                     } else { | ||||
|                         this.link.replace("&", "&") | ||||
|                     } | ||||
|                 } else { | ||||
|                     this.link.replace("&", "&") | ||||
|                 } | ||||
|  | ||||
|             // handle :443 => https | ||||
|             if (stringUrl.contains(":443")) { | ||||
|                 stringUrl = stringUrl.replace(":443", "").replace("http://", "https://") | ||||
|             } | ||||
|  | ||||
|             // handle url not starting with http | ||||
|             if (stringUrl.startsWith("//")) { | ||||
|                 stringUrl = "http:$stringUrl" | ||||
|             } | ||||
|  | ||||
|             return stringUrl | ||||
|         } | ||||
|  | ||||
|         fun sourceAndDateText(): String = | ||||
|             this.sourcetitle.getHtmlDecoded() + DateUtils.parseRelativeDate(this.datetime) | ||||
|  | ||||
|         fun toggleStar(): Item { | ||||
|             this.starred = !this.starred | ||||
|             return this | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // 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() } | ||||
|                 else -> json.toString().split(",") | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         override val descriptor: SerialDescriptor | ||||
|             get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING) | ||||
|  | ||||
|         override fun serialize(encoder: Encoder, value: List<String>) { | ||||
|             TODO("Not yet implemented") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     object BooleanSerializer : KSerializer<Boolean> { | ||||
|         override fun deserialize(decoder: Decoder): Boolean { | ||||
|             val json = ((decoder as JsonDecoder).decodeJsonElement()).jsonPrimitive | ||||
|             return if (json.booleanOrNull != null) { | ||||
|                 json.boolean | ||||
|             } else { | ||||
|                 json.int == 1 | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         override val descriptor: SerialDescriptor | ||||
|             get() = PrimitiveSerialDescriptor("b", PrimitiveKind.BOOLEAN) | ||||
|  | ||||
|         override fun serialize(encoder: Encoder, value: Boolean) { | ||||
|             TODO("Not yet implemented") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,40 @@ | ||||
| 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 | ||||
| class SuccessResponse(val success: Boolean) { | ||||
|     val isSuccess: Boolean | ||||
|         get() = success | ||||
| } | ||||
|  | ||||
| class StatusAndData<T>(val success: Boolean, val data: T? = null) { | ||||
|     companion object { | ||||
|         fun <T> succes(d: T): StatusAndData<T> { | ||||
|             return StatusAndData(true, d) | ||||
|         } | ||||
|  | ||||
|         fun <T> error(): StatusAndData<T> { | ||||
|             return StatusAndData(false) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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() | ||||
|     } | ||||
| } | ||||
| @@ -20,12 +20,6 @@ class SelfossModel { | ||||
|         val unread: Int | ||||
|     ) | ||||
|  | ||||
|     @Serializable | ||||
|     class SuccessResponse(val success: Boolean) { | ||||
|         val isSuccess: Boolean | ||||
|             get() = success | ||||
|     } | ||||
|  | ||||
|     @Serializable | ||||
|     class Stats( | ||||
|         val total: Int, | ||||
| @@ -152,16 +146,4 @@ class SelfossModel { | ||||
|             TODO("Not yet implemented") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class StatusAndData<T>(val success: Boolean, val data: T? = null) { | ||||
|         companion object { | ||||
|             fun <T> succes(d: T): StatusAndData<T> { | ||||
|                 return StatusAndData(true, d) | ||||
|             } | ||||
|  | ||||
|             fun <T> error(): StatusAndData<T> { | ||||
|                 return StatusAndData(false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.repository | ||||
| import bou.amine.apps.readerforselfossv2.dao.* | ||||
| import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.StatusAndData | ||||
| import bou.amine.apps.readerforselfossv2.rest.SelfossApi | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import bou.amine.apps.readerforselfossv2.utils.* | ||||
| @@ -10,6 +11,7 @@ import io.github.aakira.napier.Napier | ||||
| 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) { | ||||
| @@ -27,19 +29,19 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|  | ||||
|     var offlineOverride = false | ||||
|  | ||||
|     var badgeUnread = 0 | ||||
|     set(value) {field = if (value < 0) { 0 } else { value } } | ||||
|     var badgeAll = 0 | ||||
|     set(value) {field = if (value < 0) { 0 } else { value } } | ||||
|     var badgeStarred = 0 | ||||
|     set(value) {field = if (value < 0) { 0 } else { value } } | ||||
|     private val _badgeUnread = MutableStateFlow(0) | ||||
|     val badgeUnread = _badgeUnread.asStateFlow() | ||||
|     private val _badgeAll = MutableStateFlow(0) | ||||
|     val badgeAll = _badgeAll.asStateFlow() | ||||
|     private val _badgeStarred = MutableStateFlow(0) | ||||
|     val badgeStarred = _badgeStarred.asStateFlow() | ||||
|  | ||||
|     private var fetchedSources = false | ||||
|     private var fetchedTags = false | ||||
|  | ||||
|     suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { | ||||
|         // TODO: Use the updatedSince parameter | ||||
|         var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error() | ||||
|         var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() | ||||
|         var fromDB = false | ||||
|         if (isNetworkAvailable()) { | ||||
|             fetchedItems = api.getItems( | ||||
| @@ -64,7 +66,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|                 if (sourceFilter != null) { | ||||
|                     dbItems = dbItems.filter { it.sourcetitle == sourceFilter!!.title } | ||||
|                 } | ||||
|                 fetchedItems = SelfossModel.StatusAndData.succes( | ||||
|                 fetchedItems = StatusAndData.succes( | ||||
|                     dbItems.map { it.toView() } | ||||
|                 ) | ||||
|             } | ||||
| @@ -80,7 +82,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|     } | ||||
|  | ||||
|     suspend fun getOlderItems(): ArrayList<SelfossModel.Item> { | ||||
|         var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error() | ||||
|         var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() | ||||
|         if (isNetworkAvailable()) { | ||||
|             val offset = items.size | ||||
|             fetchedItems = api.getItems( | ||||
| @@ -125,17 +127,17 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|         if (isNetworkAvailable()) { | ||||
|             val response = api.stats() | ||||
|             if (response.success && response.data != null) { | ||||
|                 badgeUnread = response.data.unread | ||||
|                 badgeAll = response.data.total | ||||
|                 badgeStarred = response.data.starred | ||||
|                 _badgeUnread.value = response.data.unread | ||||
|                 _badgeAll.value = response.data.total | ||||
|                 _badgeStarred.value = response.data.starred | ||||
|                 success = true | ||||
|             } | ||||
|         } else if (appSettingsService.isItemCachingEnabled()) { | ||||
|             // TODO: do this differently, because it's not efficient | ||||
|             val dbItems = getDBItems() | ||||
|             badgeUnread = dbItems.filter { item -> item.unread }.size | ||||
|             badgeStarred = dbItems.filter { item -> item.starred }.size | ||||
|             badgeAll = dbItems.size | ||||
|             _badgeUnread.value = dbItems.filter { item -> item.unread }.size | ||||
|             _badgeStarred.value = dbItems.filter { item -> item.starred }.size | ||||
|             _badgeAll.value = dbItems.size | ||||
|             success = true | ||||
|         } | ||||
|         return success | ||||
| @@ -283,7 +285,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|     private fun markAsReadLocally(item: SelfossModel.Item) { | ||||
|         if (item.unread) { | ||||
|             item.unread = false | ||||
|             badgeUnread -= 1 | ||||
|             _badgeUnread.value -= 1 | ||||
|         } | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
| @@ -294,7 +296,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|     private fun unmarkAsReadLocally(item: SelfossModel.Item) { | ||||
|         if (!item.unread) { | ||||
|             item.unread = true | ||||
|             badgeUnread += 1 | ||||
|             _badgeUnread.value += 1 | ||||
|         } | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
| @@ -305,7 +307,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|     private fun starrLocally(item: SelfossModel.Item) { | ||||
|         if (!item.starred) { | ||||
|             item.starred = true | ||||
|             badgeStarred += 1 | ||||
|             _badgeStarred.value += 1 | ||||
|         } | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
| @@ -316,7 +318,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap | ||||
|     private fun unstarrLocally(item: SelfossModel.Item) { | ||||
|         if (item.starred) { | ||||
|             item.starred = false | ||||
|             badgeStarred -= 1 | ||||
|             _badgeStarred.value -= 1 | ||||
|         } | ||||
|  | ||||
|         CoroutineScope(Dispatchers.Main).launch { | ||||
|   | ||||
| @@ -0,0 +1,43 @@ | ||||
| package bou.amine.apps.readerforselfossv2.rest | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.model.* | ||||
| import io.github.aakira.napier.Napier | ||||
| import io.ktor.client.* | ||||
| import io.ktor.client.plugins.cache.* | ||||
| import io.ktor.client.plugins.contentnegotiation.* | ||||
| import io.ktor.client.plugins.logging.* | ||||
| import io.ktor.client.request.* | ||||
| import io.ktor.serialization.kotlinx.json.* | ||||
| import kotlinx.serialization.json.Json | ||||
|  | ||||
| class MercuryApi() { | ||||
|  | ||||
|     var client = createHttpClient() | ||||
|  | ||||
|     private fun createHttpClient(): HttpClient { | ||||
|         return HttpClient { | ||||
|             install(ContentNegotiation) { | ||||
|                 install(HttpCache) | ||||
|                 json(Json { | ||||
|                     prettyPrint = true | ||||
|                     isLenient = true | ||||
|                     ignoreUnknownKeys = true | ||||
|                 }) | ||||
|             } | ||||
|             install(Logging) { | ||||
|                 logger = object : Logger { | ||||
|                     override fun log(message: String) { | ||||
|                         Napier.d(message, tag = "LogMercuryCalls") | ||||
|                     } | ||||
|                 } | ||||
|                 level = LogLevel.INFO | ||||
|             } | ||||
|             expectSuccess = false | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> = | ||||
|         bodyOrFailure(client.get("https://amine-louveau.fr/parser.php") { | ||||
|             parameter("link", url) | ||||
|         }) | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| package bou.amine.apps.readerforselfossv2.rest | ||||
|  | ||||
| import bou.amine.apps.readerforselfossv2.model.SelfossModel | ||||
| import bou.amine.apps.readerforselfossv2.model.* | ||||
| import bou.amine.apps.readerforselfossv2.service.AppSettingsService | ||||
| import io.ktor.client.* | ||||
| import io.ktor.client.call.* | ||||
| @@ -66,7 +66,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         client = createHttpClient() | ||||
|     } | ||||
|  | ||||
|     suspend fun login(): SelfossModel.SuccessResponse = | ||||
|     suspend fun login(): SuccessResponse = | ||||
|         maybeResponse(client.get(url("/login")) { | ||||
|             parameter("username", appSettingsService.getUserName()) | ||||
|             parameter("password", appSettingsService.getPassword()) | ||||
| @@ -80,7 +80,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         search: String?, | ||||
|         updatedSince: String?, | ||||
|         items: Int? = null | ||||
|     ): SelfossModel.StatusAndData<List<SelfossModel.Item>> = | ||||
|     ): StatusAndData<List<SelfossModel.Item>> = | ||||
|         bodyOrFailure(client.get(url("/items")) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
| @@ -93,64 +93,64 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|                 parameter("offset", offset) | ||||
|             }) | ||||
|  | ||||
|     suspend fun stats(): SelfossModel.StatusAndData<SelfossModel.Stats> = | ||||
|     suspend fun stats(): StatusAndData<SelfossModel.Stats> = | ||||
|         bodyOrFailure(client.get(url("/stats")) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|  | ||||
|     suspend fun tags(): SelfossModel.StatusAndData<List<SelfossModel.Tag>> = | ||||
|     suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> = | ||||
|         bodyOrFailure(client.get(url("/tags")) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|  | ||||
|     suspend fun update(): SelfossModel.StatusAndData<String> = | ||||
|     suspend fun update(): StatusAndData<String> = | ||||
|         bodyOrFailure(client.get(url("/update")) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|  | ||||
|     suspend fun spouts(): SelfossModel.StatusAndData<Map<String, SelfossModel.Spout>> = | ||||
|     suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> = | ||||
|         bodyOrFailure(client.get(url("/sources/spouts")) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|  | ||||
|     suspend fun sources(): SelfossModel.StatusAndData<ArrayList<SelfossModel.Source>> = | ||||
|     suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> = | ||||
|         bodyOrFailure(client.get(url("/sources/list")) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|  | ||||
|     suspend fun version(): SelfossModel.StatusAndData<SelfossModel.ApiVersion> = | ||||
|     suspend fun version(): StatusAndData<SelfossModel.ApiVersion> = | ||||
|         bodyOrFailure(client.get(url("/api/about"))) | ||||
|  | ||||
|     suspend fun markAsRead(id: String): SelfossModel.SuccessResponse = | ||||
|     suspend fun markAsRead(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/mark/$id")) { | ||||
|             parameter("username", appSettingsService.getUserName()) | ||||
|             parameter("password", appSettingsService.getPassword()) | ||||
|         }) | ||||
|  | ||||
|     suspend fun unmarkAsRead(id: String): SelfossModel.SuccessResponse = | ||||
|     suspend fun unmarkAsRead(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/unmark/$id")) { | ||||
|             parameter("username", appSettingsService.getUserName()) | ||||
|             parameter("password", appSettingsService.getPassword()) | ||||
|         }) | ||||
|  | ||||
|     suspend fun starr(id: String): SelfossModel.SuccessResponse = | ||||
|     suspend fun starr(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/starr/$id")) { | ||||
|             parameter("username", appSettingsService.getUserName()) | ||||
|             parameter("password", appSettingsService.getPassword()) | ||||
|         }) | ||||
|  | ||||
|     suspend fun unstarr(id: String): SelfossModel.SuccessResponse = | ||||
|     suspend fun unstarr(id: String): SuccessResponse = | ||||
|         maybeResponse(client.post(url("/unstarr/$id")) { | ||||
|             parameter("username", appSettingsService.getUserName()) | ||||
|             parameter("password", appSettingsService.getPassword()) | ||||
|         }) | ||||
|  | ||||
|     suspend fun markAllAsRead(ids: List<String>): SelfossModel.SuccessResponse = | ||||
|     suspend fun markAllAsRead(ids: List<String>): SuccessResponse = | ||||
|         maybeResponse(client.submitForm( | ||||
|             url = url("/mark"), | ||||
|             formParameters = Parameters.build { | ||||
| @@ -167,7 +167,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|         tags: String, | ||||
|         filter: String, | ||||
|         version: Int | ||||
|     ): SelfossModel.SuccessResponse = | ||||
|     ): SuccessResponse = | ||||
|         maybeResponse( | ||||
|             if (version > 1) { | ||||
|                 createSource2(title, url, spout, tags, filter) | ||||
| @@ -212,25 +212,9 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     suspend fun deleteSource(id: Int): SelfossModel.SuccessResponse = | ||||
|     suspend fun deleteSource(id: Int): SuccessResponse = | ||||
|         maybeResponse(client.delete(url("/source/$id")) { | ||||
|                 parameter("username", appSettingsService.getUserName()) | ||||
|                 parameter("password", appSettingsService.getPassword()) | ||||
|             }) | ||||
|  | ||||
|     suspend fun maybeResponse(r: HttpResponse): SelfossModel.SuccessResponse { | ||||
|         return if (r.status.isSuccess()) { | ||||
|             r.body() | ||||
|         } else { | ||||
|             SelfossModel.SuccessResponse(false) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend inline fun <reified T> bodyOrFailure(r: HttpResponse): SelfossModel.StatusAndData<T> { | ||||
|         return if (r.status.isSuccess()) { | ||||
|             SelfossModel.StatusAndData.succes(r.body()) | ||||
|         } else { | ||||
|             SelfossModel.StatusAndData.error() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -35,6 +35,7 @@ class AppSettingsService { | ||||
|     private var _fontSize: Int? = null | ||||
|     private var _staticBar: Boolean? = null | ||||
|     private var _font: String = "" | ||||
|     private var _theme: Int? = null | ||||
|  | ||||
|  | ||||
|     init { | ||||
| @@ -318,6 +319,17 @@ class AppSettingsService { | ||||
|         return _font | ||||
|     } | ||||
|  | ||||
|     private fun refreshCurrentTheme() { | ||||
|         _theme = settings.getString(CURRENT_THEME, "-1").toInt() | ||||
|     } | ||||
|  | ||||
|     fun getCurrentTheme(): Int { | ||||
|         if (_theme == null) { | ||||
|             refreshCurrentTheme() | ||||
|         } | ||||
|         return _theme ?: -1 | ||||
|     } | ||||
|  | ||||
|     fun refreshApiSettings() { | ||||
|         refreshPassword() | ||||
|         refreshUsername() | ||||
| @@ -346,6 +358,7 @@ class AppSettingsService { | ||||
|         refreshFontSize() | ||||
|         refreshFont() | ||||
|         refreshStaticBarEnabled() | ||||
|         refreshCurrentTheme() | ||||
|     } | ||||
|  | ||||
|     fun refreshLoginInformation( | ||||
| @@ -444,5 +457,7 @@ class AppSettingsService { | ||||
|         const val INFINITE_LOADING = "infinite_loading" | ||||
|  | ||||
|         const val ITEMS_CACHING = "items_caching" | ||||
|  | ||||
|         const val CURRENT_THEME = "currentMode" | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user