Compare commits
23 Commits
v122113101
...
e68da7764f
Author | SHA1 | Date | |
---|---|---|---|
e68da7764f | |||
c3ff894027 | |||
7b68264dd7 | |||
cfcf030bf8 | |||
0e7d7a5835 | |||
0856ebb889 | |||
25bf68cf0c | |||
afc6f392c6 | |||
a0b5e2052b | |||
87d1ef2bce | |||
537a6d3a0b | |||
dbe97f564e | |||
3a3bf03114 | |||
c09a32e9ad | |||
b02a588dff | |||
a4527940b8 | |||
9e8a25ed3e | |||
8ea46e146b | |||
5ecf3c3f87 | |||
325f103417 | |||
ab4b1ae644 | |||
87ea44754e | |||
04dec50808 |
@ -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\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\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
|
||||
|
25
.github/CONTRIBUTING.md
vendored
25
.github/CONTRIBUTING.md
vendored
@ -46,28 +46,3 @@ Always check if the web version of your instance is working.
|
||||
I won't provide any selfoss instance url. If you want to help, but to not have one, you'll have to install one, and use it.
|
||||
|
||||
All the details to need are [here](https://selfoss.aditu.de/).
|
||||
|
||||
# Build the project
|
||||
|
||||
You can directly import this project into IntellIJ/Android Studio.
|
||||
|
||||
You'll have to:
|
||||
|
||||
- Define some parameters either in `~/.gradle/gradle.properties` or as gradle parameters (see the examples)
|
||||
|
||||
- appLoginUrl, appLoginUsername and appLoginPassword: url, username and password of a selfoss instance. **These are only used for tests. They can be empty if you don't test API calls.**
|
||||
|
||||
### Examples:
|
||||
#### Inside ~/.gradle/gradle.properties
|
||||
|
||||
```
|
||||
appLoginUrl="URL" # It can be empty.
|
||||
appLoginUsername="LOGIN" # It can be empty.
|
||||
appLoginPassword="PASS" # It can be empty.
|
||||
```
|
||||
|
||||
#### As gradle parameters
|
||||
|
||||
```
|
||||
./gradlew .... -P appLoginUrl="URL" -P appLoginUsername="LOGIN" -P appLoginPassword="PASS"
|
||||
```
|
||||
|
@ -1,6 +1,7 @@
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
val ignoreGitVersion: String by project
|
||||
val acraVersion = "5.9.7"
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
@ -96,9 +97,6 @@ android {
|
||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
||||
}
|
||||
getByName("debug") {
|
||||
buildConfigField("String", "LOGIN_URL", properties["appLoginUrl"] as String)
|
||||
buildConfigField("String", "LOGIN_PASSWORD", properties["appLoginPassword"] as String)
|
||||
buildConfigField("String", "LOGIN_USERNAME", properties["appLoginUsername"] as String)
|
||||
}
|
||||
}
|
||||
flavorDimensions.add("build")
|
||||
@ -142,12 +140,6 @@ dependencies {
|
||||
implementation("com.mikepenz:aboutlibraries-core:10.5.1")
|
||||
implementation("com.mikepenz:aboutlibraries:10.5.1")
|
||||
|
||||
// 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")
|
||||
|
||||
// Material-ish things
|
||||
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
|
||||
implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1")
|
||||
@ -195,6 +187,12 @@ dependencies {
|
||||
testImplementation("io.mockk:mockk:1.12.0")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
||||
|
||||
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> {
|
||||
|
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"
|
||||
|
@ -35,7 +35,10 @@ import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
|
||||
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.*
|
||||
import bou.amine.apps.readerforselfossv2.utils.ItemType
|
||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
|
||||
import bou.amine.apps.readerforselfossv2.utils.longHash
|
||||
import com.ashokvarma.bottomnavigation.BottomNavigationBar
|
||||
import com.ashokvarma.bottomnavigation.BottomNavigationItem
|
||||
import com.ashokvarma.bottomnavigation.TextBadgeItem
|
||||
@ -56,9 +59,15 @@ import com.mikepenz.materialdrawer.util.updateBadge
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.acra.ACRA
|
||||
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
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
@ -95,12 +104,16 @@ 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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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)
|
||||
|
||||
@ -297,6 +310,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
||||
|
||||
handleBottomBarActions()
|
||||
|
||||
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
|
||||
|
||||
handleRecurringTask()
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
@ -306,6 +321,26 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
||||
getElementsAccordingToTab()
|
||||
}
|
||||
|
||||
|
||||
private fun handleGDPRDialog(GDPRShown: Boolean) {
|
||||
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
|
||||
messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
|
||||
ACRA.errorReporter.putCustomData("unique_id", String(messageDigest.digest()))
|
||||
if (!GDPRShown) {
|
||||
val alertDialog = AlertDialog.Builder(this).create()
|
||||
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
|
||||
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
|
||||
alertDialog.setButton(
|
||||
AlertDialog.BUTTON_NEUTRAL,
|
||||
"OK"
|
||||
) { dialog, _ ->
|
||||
appSettingsService.settings.putBoolean("GDPR_shown", true)
|
||||
dialog.dismiss()
|
||||
}
|
||||
alertDialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDrawer() {
|
||||
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
||||
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
|
||||
@ -478,6 +513,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
||||
val gdColor = try {
|
||||
Color.parseColor(it.color)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.sendSilentlyWithAcra()
|
||||
resources.getColor(R.color.colorPrimary)
|
||||
}
|
||||
gd.setColor(gdColor)
|
||||
|
@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
@ -24,6 +25,12 @@ 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.DimensionQueue
|
||||
import org.matomo.sdk.extra.DownloadTracker
|
||||
import org.matomo.sdk.extra.TrackHelper
|
||||
import java.security.MessageDigest
|
||||
|
||||
|
||||
class LoginActivity : AppCompatActivity(), DIAware {
|
||||
|
||||
@ -35,10 +42,17 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
|
||||
TrackHelper.track().download().identifier(DownloadTracker.Extra.ApkChecksum(applicationContext))
|
||||
.with(tracker)
|
||||
TrackHelper.track().screen("/login").with(tracker)
|
||||
|
||||
handleTheme()
|
||||
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
@ -56,6 +70,11 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
||||
handleActions()
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant") // Constant is fetched from the settings
|
||||
private fun handleTheme() {
|
||||
AppCompatDelegate.setDefaultNightMode(appSettingsService.getCurrentTheme())
|
||||
}
|
||||
|
||||
private fun handleActions() {
|
||||
|
||||
binding.passwordView.setOnEditorActionListener(
|
||||
@ -95,6 +114,15 @@ 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()
|
||||
}
|
||||
val intent = Intent(this, HomeActivity::class.java)
|
||||
startActivity(intent)
|
||||
|
@ -12,9 +12,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import androidx.preference.PreferenceManager
|
||||
import bou.amine.apps.readerforselfossv2.DI.networkModule
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible
|
||||
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
|
||||
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
|
||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
|
||||
@ -31,7 +29,17 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.acra.ACRA
|
||||
import org.acra.ReportField
|
||||
import org.acra.config.httpSender
|
||||
import org.acra.config.toast
|
||||
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 {
|
||||
|
||||
@ -42,6 +50,8 @@ 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()
|
||||
@ -56,28 +66,57 @@ class MyApp : MultiDexApplication(), DIAware {
|
||||
super.onCreate()
|
||||
Napier.base(DebugAntilog())
|
||||
|
||||
initDrawerImageLoader()
|
||||
if (!ACRA.isACRASenderServiceProcess()) {
|
||||
initDrawerImageLoader()
|
||||
|
||||
tryToHandleBug()
|
||||
tryToHandleBug()
|
||||
|
||||
handleNotificationChannels()
|
||||
handleNotificationChannels()
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository))
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository))
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
viewModel.networkAvailableProvider.collect { networkAvailable ->
|
||||
val toastMessage = if (networkAvailable) {
|
||||
repository.handleDBActions()
|
||||
R.string.network_connectivity_retrieved
|
||||
} else {
|
||||
R.string.network_connectivity_lost
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
viewModel.networkAvailableProvider.collect { networkAvailable ->
|
||||
val toastMessage = if (networkAvailable) {
|
||||
repository.handleDBActions()
|
||||
R.string.network_connectivity_retrieved
|
||||
} else {
|
||||
R.string.network_connectivity_lost
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
toastMessage,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
toastMessage,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
|
||||
initAcra {
|
||||
reportFormat = StringFormat.JSON
|
||||
reportContent = listOf(
|
||||
ReportField.REPORT_ID, ReportField.INSTALLATION_ID,
|
||||
ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME,
|
||||
ReportField.BUILD, ReportField.ANDROID_VERSION, ReportField.BRAND, ReportField.PHONE_MODEL,
|
||||
ReportField.AVAILABLE_MEM_SIZE, ReportField.TOTAL_MEM_SIZE,
|
||||
ReportField.STACK_TRACE, ReportField.APPLICATION_LOG, ReportField.LOGCAT,
|
||||
ReportField.INITIAL_CONFIGURATION, ReportField.CRASH_CONFIGURATION, ReportField.IS_SILENT,
|
||||
ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA)
|
||||
toast {
|
||||
//required
|
||||
text = getString(R.string.crash_toast_text)
|
||||
length = Toast.LENGTH_SHORT
|
||||
}
|
||||
httpSender {
|
||||
uri = "https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/
|
||||
basicAuthLogin = "LMTlLZuazADohTCm"
|
||||
basicAuthPassword = "he6ghHp83F0PYPfh"
|
||||
httpMethod = HttpSender.Method.POST
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,10 +161,9 @@ class MyApp : MultiDexApplication(), DIAware {
|
||||
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, e ->
|
||||
if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any {
|
||||
if (e is NoClassDefFoundError && e.stackTrace.asList().any {
|
||||
it.toString().contains("android.view.ViewDebug")
|
||||
}) {
|
||||
Unit
|
||||
} else {
|
||||
oldHandler.uncaughtException(thread, e)
|
||||
}
|
||||
|
@ -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
|
||||
@ -45,13 +44,12 @@ 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
|
||||
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 +79,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)
|
||||
|
||||
@ -113,6 +114,7 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
typeface = try {
|
||||
ResourcesCompat.getFont(requireContext(), resId)!!
|
||||
} catch (e: java.lang.Exception) {
|
||||
e.sendSilentlyWithAcra()
|
||||
// Just to be sure
|
||||
null
|
||||
}
|
||||
@ -218,6 +220,7 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
)
|
||||
|
||||
} catch (e: InflateException) {
|
||||
e.sendSilentlyWithAcra()
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
|
||||
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
|
||||
@ -249,88 +252,79 @@ 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
|
||||
try {
|
||||
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
|
||||
try {
|
||||
binding.titleView.text = response.body()!!.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
|
||||
} catch (e: MalformedURLException) {
|
||||
// Mercury returned a relative url. We do nothing.
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val response = mercuryApi.query(url)
|
||||
if (response.success) {
|
||||
try {
|
||||
if (response.data != null && response.data!!.content != null && !response.data!!.content.isNullOrEmpty()) {
|
||||
try {
|
||||
binding.titleView.text = response.data!!.title
|
||||
if (typeface != null) {
|
||||
binding.titleView.typeface = typeface
|
||||
}
|
||||
|
||||
try {
|
||||
contentText = response.body()!!.content.orEmpty()
|
||||
htmlToWebview()
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
|
||||
try {
|
||||
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
try {
|
||||
Glide
|
||||
.with(requireContext())
|
||||
.asBitmap()
|
||||
.load(
|
||||
response.body()!!.lead_image_url.orEmpty()
|
||||
)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(binding.imageView)
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
} else {
|
||||
binding.imageView.visibility = View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
binding.nestedScrollView.scrollTo(0, 0)
|
||||
|
||||
binding.progressBar.visibility = View.GONE
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
openInBrowserAfterFailing()
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
}
|
||||
// Note: Mercury may return relative urls... If it does the url val will not be changed.
|
||||
URL(response.data!!.url)
|
||||
url = response.data!!.url
|
||||
} catch (e: MalformedURLException) {
|
||||
// Mercury returned a relative url
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
|
||||
try {
|
||||
contentText = response.data!!.content.orEmpty()
|
||||
htmlToWebview()
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
|
||||
try {
|
||||
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.data!!.lead_image_url.orEmpty()
|
||||
)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(binding.imageView)
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
} else {
|
||||
binding.imageView.visibility = View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
|
||||
try {
|
||||
binding.nestedScrollView.scrollTo(0, 0)
|
||||
|
||||
binding.progressBar.visibility = View.GONE
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
openInBrowserAfterFailing()
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
call: Call<ParsedContent>,
|
||||
t: Throwable
|
||||
) = openInBrowserAfterFailing()
|
||||
} else {
|
||||
openInBrowserAfterFailing()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -369,19 +363,25 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
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) {}
|
||||
} catch ( e : ExecutionException) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
}
|
||||
else if (url.lowercase(Locale.US).contains(".png")) {
|
||||
try {
|
||||
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
|
||||
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG))
|
||||
}catch ( e : ExecutionException) {}
|
||||
} catch ( e : ExecutionException) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
}
|
||||
else if (url.lowercase(Locale.US).contains(".webp")) {
|
||||
try {
|
||||
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
|
||||
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP))
|
||||
}catch ( e : ExecutionException) {}
|
||||
} catch ( e : ExecutionException) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
}
|
||||
|
||||
return super.shouldInterceptRequest(view, url)
|
||||
@ -405,11 +405,13 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
val itemUrl = URL(url)
|
||||
baseUrl = itemUrl.protocol + "://" + itemUrl.host
|
||||
} catch (e: MalformedURLException) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
|
||||
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 -> ""
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import bou.amine.apps.readerforselfossv2.utils.getImages
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import org.acra.ktx.sendSilentlyWithAcra
|
||||
|
||||
fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
|
||||
val imageUrls = this.getImages()
|
||||
@ -23,6 +24,7 @@ fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
|
||||
}
|
||||
}
|
||||
} catch (e : Error) {
|
||||
e.sendSilentlyWithAcra()
|
||||
return false
|
||||
}
|
||||
|
||||
@ -35,7 +37,7 @@ fun String.toTextDrawableString(): String {
|
||||
try {
|
||||
textDrawable.append(s[0])
|
||||
} catch (e: StringIndexOutOfBoundsException) {
|
||||
// We do nothing
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
}
|
||||
return textDrawable.toString()
|
||||
|
@ -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 {
|
||||
|
@ -17,16 +17,28 @@ import androidx.preference.PreferenceFragmentCompat
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
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"
|
||||
|
||||
class SettingsActivity : AppCompatActivity(),
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
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
|
||||
@ -91,6 +103,11 @@ class SettingsActivity : AppCompatActivity(),
|
||||
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.pref_main, rootKey)
|
||||
|
||||
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,6 +124,7 @@ class SettingsActivity : AppCompatActivity(),
|
||||
val input: Int = (dest.toString() + source.toString()).toInt()
|
||||
if (input in 1..200) return@InputFilter null
|
||||
} catch (nfe: NumberFormatException) {
|
||||
nfe.sendSilentlyWithAcra()
|
||||
Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
""
|
||||
@ -130,6 +148,7 @@ class SettingsActivity : AppCompatActivity(),
|
||||
try {
|
||||
editText.textSize = editable.toString().toInt().toFloat()
|
||||
} catch (e: NumberFormatException) {
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
}
|
||||
} }
|
||||
@ -139,6 +158,7 @@ class SettingsActivity : AppCompatActivity(),
|
||||
val input = (dest.toString() + source.toString()).toInt()
|
||||
if (input > 0) return@InputFilter null
|
||||
} catch (nfe: NumberFormatException) {
|
||||
nfe.sendSilentlyWithAcra()
|
||||
}
|
||||
""
|
||||
}
|
||||
|
@ -1,13 +1,9 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.text.Spannable
|
||||
import android.text.style.ClickableSpan
|
||||
import android.util.Patterns
|
||||
|
5
androidApp/src/main/res/drawable/checkerboard.xml
Normal file
5
androidApp/src/main/res/drawable/checkerboard.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<bitmap
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:dither="true"
|
||||
android:src="@drawable/checktile"
|
||||
android:tileMode="repeat"/>
|
BIN
androidApp/src/main/res/drawable/checktile.png
Normal file
BIN
androidApp/src/main/res/drawable/checktile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 235 B |
@ -0,0 +1,5 @@
|
||||
<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="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
|
||||
</vector>
|
@ -0,0 +1,7 @@
|
||||
<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>
|
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>
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@ -9,8 +9,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:background="@android:color/black"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/checkerboard"
|
||||
app:srcCompat="@android:drawable/screen_background_dark" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</RelativeLayout>
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Utiliser les paramètres système</string>
|
||||
<string name="mode_light">Thème clair</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
8
androidApp/src/main/res/values-night/strings.xml
Normal file
8
androidApp/src/main/res/values-night/strings.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?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>
|
||||
</resources>
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">遵循系统设置</string>
|
||||
<string name="mode_light">浅色模式</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -133,4 +133,9 @@
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="drawer_error_loading_sources">Error loading sources…</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>
|
||||
</resources>
|
||||
|
@ -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>
|
||||
@ -135,4 +136,9 @@
|
||||
<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>
|
||||
</resources>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<EditTextPreference
|
||||
android:inputType="number"
|
||||
android:key="api_timeout"
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/title_activity_settings">
|
||||
|
||||
<Preference
|
||||
@ -17,9 +18,13 @@
|
||||
android:title="@string/pref_header_offline"
|
||||
android:icon="@drawable/ic_signal_wifi_off_black_24dp" />
|
||||
|
||||
<Preference
|
||||
android:fragment="bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity$ThemePreferenceFragment"
|
||||
<ListPreference
|
||||
android:defaultValue="0"
|
||||
android:entries="@array/ModeTitles"
|
||||
android:entryValues="@array/ModeValues"
|
||||
android:key="currentMode"
|
||||
android:title="@string/pref_header_theme"
|
||||
app:useSimpleSummaryProvider="false"
|
||||
android:icon="@drawable/ic_color_lens_black_24dp" />
|
||||
|
||||
<Preference
|
||||
@ -32,4 +37,18 @@
|
||||
android:title="@string/pref_header_experimental"
|
||||
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"
|
||||
android:title="@string/pref_switch_disable_acra"
|
||||
android:icon="@drawable/ic_baseline_bug_report_24"/>
|
||||
|
||||
</PreferenceScreen>
|
@ -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
|
||||
@ -308,7 +310,7 @@ class RepositoryTest {
|
||||
|
||||
@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
|
||||
|
||||
@ -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)
|
||||
|
@ -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.*
|
||||
@ -40,7 +41,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
||||
|
||||
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(
|
||||
@ -65,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() }
|
||||
)
|
||||
}
|
||||
@ -81,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(
|
||||
|
@ -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,8 @@ class AppSettingsService {
|
||||
private var _fontSize: Int? = null
|
||||
private var _staticBar: Boolean? = null
|
||||
private var _font: String = ""
|
||||
private var _theme: Int? = null
|
||||
private var _enableAnalytics: Boolean? = null
|
||||
|
||||
|
||||
init {
|
||||
@ -307,6 +309,17 @@ 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, "")
|
||||
}
|
||||
@ -318,6 +331,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 +370,8 @@ class AppSettingsService {
|
||||
refreshFontSize()
|
||||
refreshFont()
|
||||
refreshStaticBarEnabled()
|
||||
refreshCurrentTheme()
|
||||
refreshAnalyticsEnabled()
|
||||
}
|
||||
|
||||
fun refreshLoginInformation(
|
||||
@ -444,5 +470,9 @@ class AppSettingsService {
|
||||
const val INFINITE_LOADING = "infinite_loading"
|
||||
|
||||
const val ITEMS_CACHING = "items_caching"
|
||||
|
||||
const val CURRENT_THEME = "currentMode"
|
||||
|
||||
const val ENABLE_ANALYTICS = "enable_analytics"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user