forked from Louvorg/ReaderForSelfoss-multiplatform
Compare commits
97 Commits
v122092701
...
v122113131
Author | SHA1 | Date | |
---|---|---|---|
a4527940b8 | |||
9e8a25ed3e | |||
8ea46e146b | |||
5ecf3c3f87 | |||
325f103417 | |||
ab4b1ae644 | |||
87ea44754e | |||
04dec50808 | |||
e36189e2e7 | |||
d6bdf510a4 | |||
a464e93370 | |||
4b63afe62a | |||
ac4c4b9441 | |||
16b10dc1b7 | |||
02d734eee8 | |||
c5cdfc0d53 | |||
6d610ed61a | |||
792950be7c | |||
af8969ce4a | |||
27c55e59a1 | |||
94a0747947 | |||
d862bfba4f | |||
b0d1d9c29a | |||
7b40a31979 | |||
823a8c3692 | |||
5494978db8 | |||
6076eb1cee | |||
131101d2ee | |||
62ad1f45ba | |||
402d18b889 | |||
e32699c93f | |||
059a237b99 | |||
d2bdbae6c8 | |||
510fcbe47e | |||
667e9c1a5d | |||
53b1d1f8b2 | |||
c25e8889a4 | |||
8b0bbe71c9 | |||
8bfe14c019 | |||
208babbce3 | |||
02098a7aa9 | |||
d0a982f385 | |||
1d1c121aab | |||
fe12819163 | |||
023a30c008 | |||
a2862a2587 | |||
054e936657 | |||
1d2e5069b8 | |||
a147646743 | |||
32e7fc0038 | |||
c15bf44032 | |||
0bcd55bd4e | |||
ebef0b3511 | |||
713ceb05bf | |||
dc8381b661 | |||
b5b820c64b | |||
f7055626d9 | |||
6ec3e96909 | |||
22da30eaa8 | |||
79fd115f5e | |||
8dc3d319cd | |||
27bb056397 | |||
f9ba13dc32 | |||
6f60ef4346 | |||
28b950f467 | |||
a9c7ec3dc1 | |||
920d4ac1ef | |||
0e96d313ec | |||
7211fdb1a3 | |||
381d6acc82 | |||
d311c2cdeb | |||
219cae5d74 | |||
2968aee309 | |||
6cb4b35c93 | |||
15ec0f2d26 | |||
4781e30da2 | |||
c8759cc035 | |||
cb4f2f02ef | |||
7517626ab7 | |||
41c951b659 | |||
e2afff0b8e | |||
a382fc89ea | |||
3f0a3903ae | |||
f46f98cef0 | |||
bf6f1a917e | |||
71c0a4d340 | |||
63c550ead3 | |||
366b2e10f1 | |||
d2436bb976 | |||
ef994460c1 | |||
758708e18d | |||
c0381144d1 | |||
cda3ba6cb4 | |||
a4636cc0c8 | |||
60c24fc75a | |||
5853a19937 | |||
99f2c04bf6 |
26
.drone.yml
26
.drone.yml
@ -3,28 +3,22 @@ type: docker
|
|||||||
name: test
|
name: test
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Anylyse
|
- name: AnylyseBuildTest
|
||||||
image: mingc/android-build-box:latest
|
image: mingc/android-build-box:latest
|
||||||
failure: ignore
|
|
||||||
detach: true
|
|
||||||
commands:
|
commands:
|
||||||
|
- echo "---------------------------------------------------------"
|
||||||
|
- echo "Configure gradle..."
|
||||||
|
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
|
||||||
- echo "---------------------------------------------------------"
|
- echo "---------------------------------------------------------"
|
||||||
- echo "Analysing..."
|
- echo "Analysing..."
|
||||||
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\""
|
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN
|
||||||
- echo "---------------------------------------------------------"
|
- echo "---------------------------------------------------------"
|
||||||
environment:
|
|
||||||
SONAR_HOST_URL:
|
|
||||||
from_secret: sonarScannerHostUrl
|
|
||||||
SONAR_LOGIN:
|
|
||||||
from_secret: sonarScannerLogin
|
|
||||||
- name: BuildAndTest
|
|
||||||
image: mingc/android-build-box:latest
|
|
||||||
commands:
|
|
||||||
- echo "Building..."
|
- echo "Building..."
|
||||||
- ./gradlew :androidApp:build -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\"" -P pushCache=false
|
- ./gradlew build
|
||||||
- echo "---------------------------------------------------------"
|
- echo "---------------------------------------------------------"
|
||||||
- echo "Testing..."
|
- echo "Testing..."
|
||||||
- echo "---------------------------------------------------------"
|
- echo "---------------------------------------------------------"
|
||||||
|
- ./gradlew test
|
||||||
environment:
|
environment:
|
||||||
SONAR_HOST_URL:
|
SONAR_HOST_URL:
|
||||||
from_secret: sonarScannerHostUrl
|
from_secret: sonarScannerHostUrl
|
||||||
@ -94,8 +88,12 @@ steps:
|
|||||||
- name: build
|
- name: build
|
||||||
image: mingc/android-build-box:latest
|
image: mingc/android-build-box:latest
|
||||||
commands:
|
commands:
|
||||||
|
- echo "---------------------------------------------------------"
|
||||||
|
- echo "Configure gradle..."
|
||||||
|
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nappLoginUrl=\"URL\"\nappLoginUsername=\"LOGIN\"\nappLoginPassword=\"PASS\"\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
|
||||||
|
- echo "---------------------------------------------------------"
|
||||||
- echo "Generate APK"
|
- echo "Generate APK"
|
||||||
- ./gradlew :androidApp:assembleGithubConfigRelease -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\"" -P pushCache=false
|
- ./gradlew :androidApp:assembleGithubConfigRelease -P pushCache=false
|
||||||
- echo "---------------------------------------------------------"
|
- echo "---------------------------------------------------------"
|
||||||
- echo "Get Key"
|
- echo "Get Key"
|
||||||
- wget https://amine-louveau.fr/key
|
- wget https://amine-louveau.fr/key
|
||||||
|
@ -6,6 +6,7 @@ plugins {
|
|||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
kotlin("kapt")
|
kotlin("kapt")
|
||||||
|
id("com.mikepenz.aboutlibraries.plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
|
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
|
||||||
@ -54,11 +55,15 @@ fun versionNameFromGit(): String {
|
|||||||
android {
|
android {
|
||||||
compileOptions {
|
compileOptions {
|
||||||
// Flag to enable support for the new language APIs
|
// Flag to enable support for the new language APIs
|
||||||
isCoreLibraryDesugaringEnabled = true
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
}
|
}
|
||||||
compileSdk = 31
|
|
||||||
|
// For Kotlin projects
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "11"
|
||||||
|
}
|
||||||
|
compileSdk = 33
|
||||||
buildToolsVersion = "31.0.0"
|
buildToolsVersion = "31.0.0"
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
@ -66,7 +71,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "bou.amine.apps.readerforselfossv2.android"
|
applicationId = "bou.amine.apps.readerforselfossv2.android"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 31
|
targetSdk = 33
|
||||||
versionCode = versionCodeFromGit()
|
versionCode = versionCodeFromGit()
|
||||||
versionName = versionNameFromGit()
|
versionName = versionNameFromGit()
|
||||||
|
|
||||||
@ -79,6 +84,11 @@ android {
|
|||||||
// tests
|
// tests
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
getByName("release") {
|
getByName("release") {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
@ -98,9 +108,6 @@ android {
|
|||||||
dimension = "build"
|
dimension = "build"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
}
|
|
||||||
namespace = "bou.amine.apps.readerforselfossv2.android"
|
namespace = "bou.amine.apps.readerforselfossv2.android"
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -114,13 +121,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||||
|
|
||||||
// Testing
|
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0-alpha02")
|
|
||||||
androidTestImplementation("androidx.test:runner:1.3.1-alpha02")
|
|
||||||
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
|
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.4.0-alpha02")
|
|
||||||
// Espresso-intents for validation and stubbing of Intents
|
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-intents:3.4.0-alpha02")
|
|
||||||
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
|
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
|
||||||
|
|
||||||
// Android Support
|
// Android Support
|
||||||
@ -135,24 +135,12 @@ dependencies {
|
|||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||||
implementation("org.jsoup:jsoup:1.14.3")
|
implementation("org.jsoup:jsoup:1.14.3")
|
||||||
|
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
|
||||||
|
|
||||||
//multidex
|
//multidex
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("androidx.multidex:multidex:2.0.1")
|
||||||
|
|
||||||
// About
|
// About
|
||||||
implementation("com.mikepenz:aboutlibraries-core:8.9.4")
|
implementation("com.mikepenz:aboutlibraries-core:10.5.1")
|
||||||
implementation("com.mikepenz:aboutlibraries:8.9.4")
|
implementation("com.mikepenz:aboutlibraries:10.5.1")
|
||||||
implementation("com.mikepenz:aboutlibraries-definitions:8.9.4")
|
|
||||||
|
|
||||||
// Async
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
|
|
||||||
|
|
||||||
// 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
|
// Material-ish things
|
||||||
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
|
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
|
||||||
@ -188,14 +176,41 @@ dependencies {
|
|||||||
|
|
||||||
implementation("androidx.core:core-ktx:1.8.0")
|
implementation("androidx.core:core-ktx:1.8.0")
|
||||||
|
|
||||||
// implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
|
|
||||||
// implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
|
|
||||||
// implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
|
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
|
||||||
|
|
||||||
// Network information
|
// Network information
|
||||||
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
|
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
|
||||||
|
|
||||||
// SQLDELIGHT
|
// SQLDELIGHT
|
||||||
implementation("com.squareup.sqldelight:android-driver:1.5.3")
|
implementation("com.squareup.sqldelight:android-driver:1.5.4")
|
||||||
|
|
||||||
|
//test
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test> {
|
||||||
|
outputs.upToDateWhen { false }
|
||||||
|
useJUnit()
|
||||||
|
testLogging {
|
||||||
|
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||||
|
events = setOf(
|
||||||
|
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
|
||||||
|
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
|
||||||
|
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR
|
||||||
|
)
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aboutLibraries {
|
||||||
|
offlineMode = true
|
||||||
|
fetchRemoteLicense = false
|
||||||
|
fetchRemoteFunding = false
|
||||||
|
includePlatform = false
|
||||||
|
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||||
|
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||||
|
duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
||||||
}
|
}
|
9
androidApp/proguard-rules.pro
vendored
9
androidApp/proguard-rules.pro
vendored
@ -30,15 +30,8 @@
|
|||||||
<fields>;
|
<fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
-dontwarn okio.**
|
|
||||||
-dontwarn retrofit2.Platform$Java8
|
|
||||||
-keep class retrofit.** { *; }
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@retrofit.http.* <methods>;
|
|
||||||
}
|
|
||||||
-keepattributes *Annotation*,Signature
|
-keepattributes *Annotation*,Signature
|
||||||
-keepattributes Exceptions
|
-keepattributes Exceptions
|
||||||
-dontwarn okio.**
|
|
||||||
-dontwarn javax.annotation.Nullable
|
-dontwarn javax.annotation.Nullable
|
||||||
-dontwarn javax.annotation.ParametersAreNonnullByDefault
|
-dontwarn javax.annotation.ParametersAreNonnullByDefault
|
||||||
|
|
||||||
@ -90,3 +83,5 @@
|
|||||||
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
||||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||||
|
|
||||||
|
-dontwarn io.mockk.**
|
||||||
|
-keep class io.mockk.** { *; }
|
||||||
|
@ -15,7 +15,8 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:theme="@style/NoBar"
|
android:theme="@style/NoBar"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules">
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:configChanges="uiMode">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:theme="@style/SplashTheme"
|
android:theme="@style/SplashTheme"
|
||||||
|
@ -94,7 +94,7 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
|
|||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
try {
|
try {
|
||||||
val items = repository.getSpouts()
|
val items = repository.getSpouts()
|
||||||
if (items != null) {
|
if (items.isNotEmpty()) {
|
||||||
val itemsStrings = items.map { it.value.name }
|
val itemsStrings = items.map { it.value.name }
|
||||||
for ((key, value) in items) {
|
for ((key, value) in items) {
|
||||||
spoutsKV[value.name] = key
|
spoutsKV[value.name] = key
|
||||||
|
@ -15,12 +15,10 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
|
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.view.doOnNextLayout
|
import androidx.core.view.doOnNextLayout
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.*
|
import androidx.recyclerview.widget.*
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
@ -98,8 +96,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
private val repository : Repository by instance()
|
private val repository : Repository by instance()
|
||||||
private val appSettingsService : AppSettingsService by instance()
|
private val appSettingsService : AppSettingsService by instance()
|
||||||
|
|
||||||
data class DrawerData(val tags: List<SelfossModel.Tag>?, val sources: List<SelfossModel.Source>?)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||||
@ -183,8 +179,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
|
|
||||||
adapter.handleItemAtIndex(position)
|
adapter.handleItemAtIndex(position)
|
||||||
|
|
||||||
reloadBadgeContent()
|
|
||||||
|
|
||||||
val tagHashes = i.tags.map { it.longHash() }
|
val tagHashes = i.tags.map { it.longHash() }
|
||||||
tagsBadge = tagsBadge.map {
|
tagsBadge = tagsBadge.map {
|
||||||
if (tagHashes.contains(it.key)) {
|
if (tagHashes.contains(it.key)) {
|
||||||
@ -212,6 +206,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
|
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateBottomBarBadgeCount(badge: TextBadgeItem, count: Int) {
|
||||||
|
if (count > 0) {
|
||||||
|
badge
|
||||||
|
.setText(count.toString())
|
||||||
|
.maybeShow()
|
||||||
|
} else {
|
||||||
|
badge.removeBadge()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleBottomBar() {
|
private fun handleBottomBar() {
|
||||||
|
|
||||||
tabNewBadge = TextBadgeItem()
|
tabNewBadge = TextBadgeItem()
|
||||||
@ -224,6 +228,28 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
.setText("")
|
.setText("")
|
||||||
.setHideOnSelect(false).hide(false)
|
.setHideOnSelect(false).hide(false)
|
||||||
|
|
||||||
|
if (appSettingsService.isDisplayUnreadCountEnabled()) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repository.badgeUnread.collect {
|
||||||
|
updateBottomBarBadgeCount(tabNewBadge, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appSettingsService.isDisplayAllCountEnabled()) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repository.badgeAll.collect {
|
||||||
|
updateBottomBarBadgeCount(tabArchiveBadge, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repository.badgeStarred.collect {
|
||||||
|
updateBottomBarBadgeCount(tabStarredBadge, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val tabNew =
|
val tabNew =
|
||||||
BottomNavigationItem(
|
BottomNavigationItem(
|
||||||
R.drawable.ic_tab_fiber_new_black_24dp,
|
R.drawable.ic_tab_fiber_new_black_24dp,
|
||||||
@ -352,27 +378,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
)
|
)
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val drawerData = DrawerData(repository.getDBTags().map { it.toView() },
|
val tags = repository.getTags()
|
||||||
repository.getDBSources().map { it.toView() })
|
val sources = repository.getSources()
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
// Only refresh if there is no data in the DB, or if the `UpdateSources` setting is enabled
|
handleDrawerData(tags, sources)
|
||||||
if (drawerData.sources?.isEmpty() == true || appSettingsService.isUpdateSourcesEnabled()) {
|
|
||||||
drawerApiCalls(drawerData)
|
|
||||||
} else {
|
|
||||||
handleDrawerData(drawerData, loadedFromCache = true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun drawerApiCalls(drawerData: DrawerData) {
|
private fun handleDrawerData(tags: List<SelfossModel.Tag>, sources: List<SelfossModel.Source>) {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
val apiDrawerData = DrawerData(repository.getTags(), repository.getSources())
|
|
||||||
handleDrawerData(if (drawerData != apiDrawerData) apiDrawerData else drawerData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDrawerData(drawerData: DrawerData, loadedFromCache: Boolean = false) {
|
|
||||||
binding.mainDrawer.itemAdapter.clear()
|
binding.mainDrawer.itemAdapter.clear()
|
||||||
|
|
||||||
// Filters title with clear action
|
// Filters title with clear action
|
||||||
@ -386,24 +400,24 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hidden tags
|
// Hidden tags
|
||||||
if (drawerData.tags != null && drawerData.tags.isNotEmpty() && appSettingsService.getHiddenTags().isNotEmpty()) {
|
if (tags.isNotEmpty() && appSettingsService.getHiddenTags().isNotEmpty()) {
|
||||||
secondaryItem(
|
secondaryItem(
|
||||||
withDivider = true,
|
withDivider = true,
|
||||||
R.string.drawer_item_hidden_tags,
|
R.string.drawer_item_hidden_tags,
|
||||||
DRAWER_ID_HIDDEN_TAGS
|
DRAWER_ID_HIDDEN_TAGS
|
||||||
)
|
)
|
||||||
handleHiddenTags(drawerData.tags)
|
handleHiddenTags(tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tags
|
// Tags
|
||||||
secondaryItem(withDivider = true, R.string.drawer_item_tags, DRAWER_ID_TAGS)
|
secondaryItem(withDivider = true, R.string.drawer_item_tags, DRAWER_ID_TAGS)
|
||||||
if (drawerData.tags == null && !loadedFromCache) {
|
if (tags.isEmpty()) {
|
||||||
binding.mainDrawer.itemAdapter.add(
|
binding.mainDrawer.itemAdapter.add(
|
||||||
SecondaryDrawerItem()
|
SecondaryDrawerItem()
|
||||||
.apply { nameRes = R.string.drawer_error_loading_tags; isSelectable = false }
|
.apply { nameRes = R.string.drawer_error_loading_tags; isSelectable = false }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
handleTags(drawerData.tags!!)
|
handleTags(tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sources
|
// Sources
|
||||||
@ -411,15 +425,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
startActivity(Intent(v!!.context, SourcesActivity::class.java))
|
startActivity(Intent(v!!.context, SourcesActivity::class.java))
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
if (drawerData.sources == null && !loadedFromCache) {
|
if (sources.isEmpty()) {
|
||||||
binding.mainDrawer.itemAdapter.add(
|
binding.mainDrawer.itemAdapter.add(
|
||||||
SecondaryDrawerItem().apply {
|
SecondaryDrawerItem().apply {
|
||||||
nameRes = R.string.drawer_error_loading_tags
|
nameRes = R.string.drawer_error_loading_sources
|
||||||
isSelectable = false
|
isSelectable = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
handleSources(drawerData.sources!!)
|
handleSources(sources)
|
||||||
}
|
}
|
||||||
|
|
||||||
// About action
|
// About action
|
||||||
@ -731,29 +745,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
|
|
||||||
private fun reloadBadges() {
|
private fun reloadBadges() {
|
||||||
if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
|
if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
repository.reloadBadges()
|
repository.reloadBadges()
|
||||||
reloadBadgeContent()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reloadBadgeContent() {
|
|
||||||
if (appSettingsService.isDisplayUnreadCountEnabled()) {
|
|
||||||
tabNewBadge
|
|
||||||
.setText(repository.badgeUnread.toString())
|
|
||||||
.maybeShow()
|
|
||||||
}
|
|
||||||
if (appSettingsService.isDisplayAllCountEnabled()) {
|
|
||||||
tabArchiveBadge
|
|
||||||
.setText(repository.badgeAll.toString())
|
|
||||||
.maybeShow()
|
|
||||||
tabStarredBadge
|
|
||||||
.setText(repository.badgeStarred.toString())
|
|
||||||
.maybeShow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reloadTagsBadges() {
|
private fun reloadTagsBadges() {
|
||||||
tagsBadge.forEach {
|
tagsBadge.forEach {
|
||||||
binding.mainDrawer.updateBadge(it.key, StringHolder(it.value.toString()))
|
binding.mainDrawer.updateBadge(it.key, StringHolder(it.value.toString()))
|
||||||
@ -875,10 +872,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
|
|||||||
|
|
||||||
private fun maxItemNumber(): Int =
|
private fun maxItemNumber(): Int =
|
||||||
when (elementsShown) {
|
when (elementsShown) {
|
||||||
ItemType.UNREAD -> repository.badgeUnread
|
ItemType.UNREAD -> repository.badgeUnread.value
|
||||||
ItemType.ALL -> repository.badgeAll
|
ItemType.ALL -> repository.badgeAll.value
|
||||||
ItemType.STARRED -> repository.badgeStarred
|
ItemType.STARRED -> repository.badgeStarred.value
|
||||||
else -> repository.badgeUnread // if !elementsShown then unread are fetched.
|
else -> repository.badgeUnread.value // if !elementsShown then unread are fetched.
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) {
|
private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) {
|
||||||
|
@ -93,12 +93,15 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun goToMain() {
|
private fun goToMain() {
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
repository.updateApiVersion()
|
||||||
|
}
|
||||||
val intent = Intent(this, HomeActivity::class.java)
|
val intent = Intent(this, HomeActivity::class.java)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preferenceError(t: Throwable) {
|
private fun preferenceError() {
|
||||||
appSettingsService.resetLoginInformation()
|
appSettingsService.resetLoginInformation()
|
||||||
|
|
||||||
binding.urlView.error = getString(R.string.wrong_infos)
|
binding.urlView.error = getString(R.string.wrong_infos)
|
||||||
@ -163,11 +166,10 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
|||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val result = repository.login()
|
val result = repository.login()
|
||||||
if (result) {
|
if (result) {
|
||||||
repository.updateApiVersion()
|
|
||||||
goToMain()
|
goToMain()
|
||||||
} else {
|
} else {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
preferenceError(Exception("Not success"))
|
preferenceError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,21 @@ package bou.amine.apps.readerforselfossv2.android
|
|||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate.*
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import bou.amine.apps.readerforselfossv2.DI.networkModule
|
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.android.viewmodel.AppViewModel
|
||||||
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
|
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
|
||||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
|
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
|
||||||
@ -28,6 +32,7 @@ import io.github.aakira.napier.DebugAntilog
|
|||||||
import io.github.aakira.napier.Napier
|
import io.github.aakira.napier.Napier
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.kodein.di.*
|
import org.kodein.di.*
|
||||||
|
|
||||||
@ -37,7 +42,7 @@ class MyApp : MultiDexApplication(), DIAware {
|
|||||||
import(networkModule)
|
import(networkModule)
|
||||||
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
|
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
|
||||||
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
|
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
|
||||||
bind<Repository>() with singleton { Repository(instance(), instance(), connectivityStatus, instance()) }
|
bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) }
|
||||||
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
|
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
|
||||||
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
|
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
|
||||||
}
|
}
|
||||||
@ -46,6 +51,10 @@ class MyApp : MultiDexApplication(), DIAware {
|
|||||||
private val viewModel: AppViewModel by instance()
|
private val viewModel: AppViewModel by instance()
|
||||||
private val connectivityStatus: ConnectivityStatus by instance()
|
private val connectivityStatus: ConnectivityStatus by instance()
|
||||||
private val driverFactory: DriverFactory by instance()
|
private val driverFactory: DriverFactory by instance()
|
||||||
|
private val appSettingsService : AppSettingsService by instance()
|
||||||
|
|
||||||
|
// TODO: handle with the "previous" way
|
||||||
|
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@ -75,7 +84,6 @@ class MyApp : MultiDexApplication(), DIAware {
|
|||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNotificationChannels() {
|
private fun handleNotificationChannels() {
|
||||||
@ -128,6 +136,19 @@ class MyApp : MultiDexApplication(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
if (appSettingsService.getCurrentTheme() == MODE_NIGHT_FOLLOW_SYSTEM) {
|
||||||
|
var mode = when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
|
||||||
|
Configuration.UI_MODE_NIGHT_YES -> MODE_NIGHT_YES
|
||||||
|
else -> MODE_NIGHT_NO
|
||||||
|
}
|
||||||
|
setDefaultNightMode(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
class AppLifeCycleObserver(val connectivityStatus: ConnectivityStatus, val repository: Repository) : DefaultLifecycleObserver {
|
class AppLifeCycleObserver(val connectivityStatus: ConnectivityStatus, val repository: Repository) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
override fun onResume(owner: LifecycleOwner) {
|
override fun onResume(owner: LifecycleOwner) {
|
||||||
|
@ -36,9 +36,9 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
|||||||
|
|
||||||
private fun showMenuItem(willAddToFavorite: Boolean) {
|
private fun showMenuItem(willAddToFavorite: Boolean) {
|
||||||
if (willAddToFavorite) {
|
if (willAddToFavorite) {
|
||||||
toolbarMenu.findItem(R.id.star).icon.setTint(Color.WHITE)
|
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.WHITE)
|
||||||
} else {
|
} else {
|
||||||
toolbarMenu.findItem(R.id.star).icon.setTint(Color.RED)
|
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.RED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,20 +56,13 @@ class SourcesActivity : AppCompatActivity(), DIAware {
|
|||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
val response = repository.getSources()
|
val response = repository.getSources()
|
||||||
if (response != null) {
|
if (response.isNotEmpty()) {
|
||||||
items = response
|
items = response
|
||||||
val mAdapter = SourcesListAdapter(
|
val mAdapter = SourcesListAdapter(
|
||||||
this@SourcesActivity, items
|
this@SourcesActivity, items
|
||||||
)
|
)
|
||||||
binding.recyclerView.adapter = mAdapter
|
binding.recyclerView.adapter = mAdapter
|
||||||
mAdapter.notifyDataSetChanged()
|
mAdapter.notifyDataSetChanged()
|
||||||
if (items.isEmpty()) {
|
|
||||||
Toast.makeText(
|
|
||||||
this@SourcesActivity,
|
|
||||||
R.string.nothing_here,
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
this@SourcesActivity,
|
this@SourcesActivity,
|
||||||
|
@ -10,9 +10,12 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import bou.amine.apps.readerforselfossv2.android.R
|
import bou.amine.apps.readerforselfossv2.android.R
|
||||||
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
|
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
|
||||||
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
|
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.*
|
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
|
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
|
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
|
||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
@ -59,7 +62,7 @@ class ItemCardAdapter(
|
|||||||
|
|
||||||
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
|
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
|
||||||
|
|
||||||
binding.sourceTitleAndDate.text = itm.sourceAndDateText(repository.dateUtils)
|
binding.sourceTitleAndDate.text = itm.sourceAndDateText()
|
||||||
|
|
||||||
if (!appSettingsService.isFullHeightCardsEnabled()) {
|
if (!appSettingsService.isFullHeightCardsEnabled()) {
|
||||||
binding.itemImage.maxHeight = imageMaxHeight
|
binding.itemImage.maxHeight = imageMaxHeight
|
||||||
@ -108,13 +111,11 @@ class ItemCardAdapter(
|
|||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
repository.unstarr(item)
|
repository.unstarr(item)
|
||||||
}
|
}
|
||||||
item.starred = false
|
|
||||||
binding.favButton.isSelected = false
|
binding.favButton.isSelected = false
|
||||||
} else {
|
} else {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
repository.starr(item)
|
repository.starr(item)
|
||||||
}
|
}
|
||||||
item.starred = true
|
|
||||||
binding.favButton.isSelected = true
|
binding.favButton.isSelected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ class ItemListAdapter(
|
|||||||
|
|
||||||
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
|
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
|
||||||
|
|
||||||
binding.sourceTitleAndDate.text = itm.sourceAndDateText(repository.dateUtils)
|
binding.sourceTitleAndDate.text = itm.sourceAndDateText()
|
||||||
|
|
||||||
if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
|
if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
|
|||||||
updateItems(this.items)
|
updateItems(this.items)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unmarkSnackbar(position: Int) {
|
private fun unmarkSnackbar(item: SelfossModel.Item, position: Int) {
|
||||||
val s = Snackbar
|
val s = Snackbar
|
||||||
.make(
|
.make(
|
||||||
app.findViewById(R.id.coordLayout),
|
app.findViewById(R.id.coordLayout),
|
||||||
@ -37,7 +37,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
|
|||||||
)
|
)
|
||||||
.setAction(R.string.undo_string) {
|
.setAction(R.string.undo_string) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
unreadItemAtIndex(position, false)
|
unreadItemAtIndex(item, position, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
|
|||||||
s.show()
|
s.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun markSnackbar(position: Int) {
|
private fun markSnackbar(item: SelfossModel.Item, position: Int) {
|
||||||
val s = Snackbar
|
val s = Snackbar
|
||||||
.make(
|
.make(
|
||||||
app.findViewById(R.id.coordLayout),
|
app.findViewById(R.id.coordLayout),
|
||||||
@ -55,7 +55,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
|
|||||||
Snackbar.LENGTH_LONG
|
Snackbar.LENGTH_LONG
|
||||||
)
|
)
|
||||||
.setAction(R.string.undo_string) {
|
.setAction(R.string.undo_string) {
|
||||||
readItemAtIndex(position)
|
readItemAtIndex(item, position, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val view = s.view
|
val view = s.view
|
||||||
@ -66,37 +66,36 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
|
|||||||
|
|
||||||
fun handleItemAtIndex(position: Int) {
|
fun handleItemAtIndex(position: Int) {
|
||||||
if (items[position].unread) {
|
if (items[position].unread) {
|
||||||
readItemAtIndex(position)
|
readItemAtIndex(items[position], position)
|
||||||
} else {
|
} else {
|
||||||
unreadItemAtIndex(position)
|
unreadItemAtIndex(items[position], position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readItemAtIndex(position: Int, showSnackbar: Boolean = true) {
|
private fun readItemAtIndex(item: SelfossModel.Item, position: Int, showSnackbar: Boolean = true) {
|
||||||
val i = items[position]
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
repository.markAsRead(i)
|
repository.markAsRead(item)
|
||||||
}
|
}
|
||||||
if (repository.displayedItems == ItemType.UNREAD) {
|
if (repository.displayedItems == ItemType.UNREAD) {
|
||||||
items.remove(i)
|
items.remove(item)
|
||||||
notifyItemRemoved(position)
|
notifyItemRemoved(position)
|
||||||
updateItems(items)
|
updateItems(items)
|
||||||
} else {
|
} else {
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
}
|
}
|
||||||
if (showSnackbar) {
|
if (showSnackbar) {
|
||||||
unmarkSnackbar(position)
|
unmarkSnackbar(item, position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unreadItemAtIndex(position: Int, showSnackbar: Boolean = true) {
|
private fun unreadItemAtIndex(item: SelfossModel.Item, position: Int, showSnackbar: Boolean = true) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
repository.unmarkAsRead(items[position])
|
repository.unmarkAsRead(item)
|
||||||
|
|
||||||
}
|
}
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
if (showSnackbar) {
|
if (showSnackbar) {
|
||||||
markSnackbar(position)
|
markSnackbar(item, position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,9 +61,13 @@ class SourcesListAdapter(
|
|||||||
binding.sourceTitle.text = itm.title.getHtmlDecoded()
|
binding.sourceTitle.text = itm.title.getHtmlDecoded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getItemId(position: Int) = position.toLong()
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int) = position
|
||||||
|
|
||||||
override fun getItemCount(): Int = items.size
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
inner class ViewHolder(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
inner class ViewHolder(private val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
handleClickListeners()
|
handleClickListeners()
|
||||||
@ -74,13 +78,13 @@ class SourcesListAdapter(
|
|||||||
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
|
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
|
||||||
|
|
||||||
deleteBtn.setOnClickListener {
|
deleteBtn.setOnClickListener {
|
||||||
val (id) = items[adapterPosition]
|
val (id) = items[bindingAdapterPosition]
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val successfullyDeletedSource = repository.deleteSource(id)
|
val successfullyDeletedSource = repository.deleteSource(id)
|
||||||
if (successfullyDeletedSource) {
|
if (successfullyDeletedSource) {
|
||||||
items.removeAt(adapterPosition)
|
items.removeAt(bindingAdapterPosition)
|
||||||
notifyItemRemoved(adapterPosition)
|
notifyItemRemoved(bindingAdapterPosition)
|
||||||
notifyItemRangeChanged(adapterPosition, itemCount)
|
notifyItemRangeChanged(bindingAdapterPosition, itemCount)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
app,
|
app,
|
||||||
|
@ -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>
|
|
||||||
}
|
|
@ -52,11 +52,13 @@ override fun doWork(): Result {
|
|||||||
|
|
||||||
repository.handleDBActions()
|
repository.handleDBActions()
|
||||||
|
|
||||||
|
val apiItems = repository.tryToCacheItemsAndGetNewOnes()
|
||||||
if (appSettingsService.isNotifyNewItemsEnabled()) {
|
if (appSettingsService.isNotifyNewItemsEnabled()) {
|
||||||
launch {
|
launch {
|
||||||
handleNewItemsNotification(repository.tryToCacheItemsAndGetNewOnes(), notificationManager)
|
handleNewItemsNotification(apiItems, notificationManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
apiItems.map { it.preloadImages(context) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Result.success()
|
return Result.success()
|
||||||
@ -66,6 +68,7 @@ override fun doWork(): Result {
|
|||||||
newItems: List<SelfossModel.Item>?,
|
newItems: List<SelfossModel.Item>?,
|
||||||
notificationManager: NotificationManager
|
notificationManager: NotificationManager
|
||||||
) {
|
) {
|
||||||
|
// TODO: Check if this coroutine is actually required
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val apiItems = newItems.orEmpty()
|
val apiItems = newItems.orEmpty()
|
||||||
|
|
||||||
@ -102,7 +105,6 @@ override fun doWork(): Result {
|
|||||||
notificationManager.notify(2, newItemsNotification.build())
|
notificationManager.notify(2, newItemsNotification.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
apiItems.map { it.preloadImages(context) }
|
|
||||||
Timer("", false).schedule(4000) {
|
Timer("", false).schedule(4000) {
|
||||||
notificationManager.cancel(1)
|
notificationManager.cancel(1)
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,6 @@ import androidx.core.widget.NestedScrollView
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import bou.amine.apps.readerforselfossv2.android.ImageActivity
|
import bou.amine.apps.readerforselfossv2.android.ImageActivity
|
||||||
import bou.amine.apps.readerforselfossv2.android.R
|
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.databinding.FragmentArticleBinding
|
||||||
import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
|
import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
|
||||||
import bou.amine.apps.readerforselfossv2.android.model.toModel
|
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.android.utils.shareLink
|
||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
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.service.AppSettingsService
|
||||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||||
import bou.amine.apps.readerforselfossv2.utils.getImages
|
import bou.amine.apps.readerforselfossv2.utils.getImages
|
||||||
@ -49,9 +48,6 @@ import org.kodein.di.DI
|
|||||||
import org.kodein.di.DIAware
|
import org.kodein.di.DIAware
|
||||||
import org.kodein.di.android.x.closestDI
|
import org.kodein.di.android.x.closestDI
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.net.MalformedURLException
|
import java.net.MalformedURLException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -81,6 +77,9 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
private var font = ""
|
private var font = ""
|
||||||
private var staticBar = false
|
private var staticBar = false
|
||||||
|
|
||||||
|
private val mercuryApi : MercuryApi by instance()
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@ -101,7 +100,7 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
contentText = item.content
|
contentText = item.content
|
||||||
contentTitle = item.title.getHtmlDecoded()
|
contentTitle = item.title.getHtmlDecoded()
|
||||||
contentImage = item.getThumbnail(repository.baseUrl)
|
contentImage = item.getThumbnail(repository.baseUrl)
|
||||||
contentSource = item.sourceAndDateText(repository.dateUtils)
|
contentSource = item.sourceAndDateText()
|
||||||
allImages = item.getImages()
|
allImages = item.getImages()
|
||||||
|
|
||||||
fontSize = appSettingsService.getFontSize()
|
fontSize = appSettingsService.getFontSize()
|
||||||
@ -249,26 +248,21 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
private fun getContentFromMercury() {
|
private fun getContentFromMercury() {
|
||||||
if (repository.isNetworkAvailable()) {
|
if (repository.isNetworkAvailable()) {
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
val parser = MercuryApi()
|
|
||||||
|
|
||||||
parser.parseUrl(url).enqueue(
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
object : Callback<ParsedContent> {
|
val response = mercuryApi.query(url)
|
||||||
override fun onResponse(
|
if (response.success) {
|
||||||
call: Call<ParsedContent>,
|
|
||||||
response: Response<ParsedContent>
|
|
||||||
) {
|
|
||||||
// TODO: clean all the following after finding the mercury content issue
|
|
||||||
try {
|
try {
|
||||||
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
|
if (response.data != null && response.data!!.content != null && !response.data!!.content.isNullOrEmpty()) {
|
||||||
try {
|
try {
|
||||||
binding.titleView.text = response.body()!!.title
|
binding.titleView.text = response.data!!.title
|
||||||
if (typeface != null) {
|
if (typeface != null) {
|
||||||
binding.titleView.typeface = typeface
|
binding.titleView.typeface = typeface
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Note: Mercury may return relative urls... If it does the url val will not be changed.
|
// Note: Mercury may return relative urls... If it does the url val will not be changed.
|
||||||
URL(response.body()!!.url)
|
URL(response.data!!.url)
|
||||||
url = response.body()!!.url
|
url = response.data!!.url
|
||||||
} catch (e: MalformedURLException) {
|
} catch (e: MalformedURLException) {
|
||||||
// Mercury returned a relative url. We do nothing.
|
// Mercury returned a relative url. We do nothing.
|
||||||
}
|
}
|
||||||
@ -276,20 +270,20 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
contentText = response.body()!!.content.orEmpty()
|
contentText = response.data!!.content.orEmpty()
|
||||||
htmlToWebview()
|
htmlToWebview()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
|
if (response.data!!.lead_image_url != null && !response.data!!.lead_image_url.isNullOrEmpty() && context != null) {
|
||||||
binding.imageView.visibility = View.VISIBLE
|
binding.imageView.visibility = View.VISIBLE
|
||||||
try {
|
try {
|
||||||
Glide
|
Glide
|
||||||
.with(requireContext())
|
.with(requireContext())
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(
|
.load(
|
||||||
response.body()!!.lead_image_url.orEmpty()
|
response.data!!.lead_image_url.orEmpty()
|
||||||
)
|
)
|
||||||
.apply(RequestOptions.fitCenterTransform())
|
.apply(RequestOptions.fitCenterTransform())
|
||||||
.into(binding.imageView)
|
.into(binding.imageView)
|
||||||
@ -323,14 +317,10 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
if (context != null) {
|
if (context != null) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
openInBrowserAfterFailing()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(
|
|
||||||
call: Call<ParsedContent>,
|
|
||||||
t: Throwable
|
|
||||||
) = openInBrowserAfterFailing()
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,6 +344,7 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
binding.webcontent.settings.javaScriptEnabled = false
|
binding.webcontent.settings.javaScriptEnabled = false
|
||||||
|
|
||||||
binding.webcontent.webViewClient = object : WebViewClient() {
|
binding.webcontent.webViewClient = object : WebViewClient() {
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
|
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
|
||||||
if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
|
if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
|
||||||
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||||
@ -361,6 +352,7 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
|
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
|
||||||
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
||||||
if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US).contains(".jpeg")) {
|
if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US).contains(".jpeg")) {
|
||||||
@ -387,7 +379,7 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
|
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
|
||||||
override fun onSingleTapUp(e: MotionEvent?): Boolean {
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||||
return performClick()
|
return performClick()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -408,6 +400,7 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
val fontName = when (font) {
|
val fontName = when (font) {
|
||||||
getString(R.string.open_sans_font_id) -> "Open Sans"
|
getString(R.string.open_sans_font_id) -> "Open Sans"
|
||||||
getString(R.string.roboto_font_id) -> "Roboto"
|
getString(R.string.roboto_font_id) -> "Roboto"
|
||||||
|
getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.model
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
fun SelfossModel.Item.toParcelable() : ParecelableItem =
|
fun SelfossModel.Item.toParcelable() : ParecelableItem =
|
||||||
ParecelableItem(
|
ParecelableItem(
|
||||||
@ -34,17 +33,17 @@ fun ParecelableItem.toModel() : SelfossModel.Item =
|
|||||||
this.tags.split(",")
|
this.tags.split(",")
|
||||||
)
|
)
|
||||||
data class ParecelableItem(
|
data class ParecelableItem(
|
||||||
@SerializedName("id") val id: Int,
|
val id: Int,
|
||||||
@SerializedName("datetime") val datetime: String,
|
val datetime: String,
|
||||||
@SerializedName("title") val title: String,
|
val title: String,
|
||||||
@SerializedName("content") val content: String,
|
val content: String,
|
||||||
@SerializedName("unread") var unread: Boolean,
|
var unread: Boolean,
|
||||||
@SerializedName("starred") var starred: Boolean,
|
var starred: Boolean,
|
||||||
@SerializedName("thumbnail") val thumbnail: String?,
|
val thumbnail: String?,
|
||||||
@SerializedName("icon") val icon: String?,
|
val icon: String?,
|
||||||
@SerializedName("link") val link: String,
|
val link: String,
|
||||||
@SerializedName("sourcetitle") val sourcetitle: String,
|
val sourcetitle: String,
|
||||||
@SerializedName("tags") val tags: String
|
val tags: String
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
7
androidApp/src/main/res/font/source_code_pro_medium.xml
Normal file
7
androidApp/src/main/res/font/source_code_pro_medium.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
app:fontProviderAuthority="com.google.android.gms.fonts"
|
||||||
|
app:fontProviderPackage="com.google.android.gms"
|
||||||
|
app:fontProviderQuery="name=Source Code Pro&weight=500"
|
||||||
|
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
|
||||||
|
</font-family>
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Thème sombre</string>
|
<string name="mode_dark">Thème sombre</string>
|
||||||
<string name="mode_system">Utiliser les paramètres système</string>
|
<string name="mode_system">Utiliser les paramètres système</string>
|
||||||
<string name="mode_light">Thème clair</string>
|
<string name="mode_light">Thème clair</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">深色模式</string>
|
<string name="mode_dark">深色模式</string>
|
||||||
<string name="mode_system">遵循系统设置</string>
|
<string name="mode_system">遵循系统设置</string>
|
||||||
<string name="mode_light">浅色模式</string>
|
<string name="mode_light">浅色模式</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -132,4 +132,5 @@
|
|||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
<string name="mode_system">Follow the system setting</string>
|
<string name="mode_system">Follow the system setting</string>
|
||||||
<string name="mode_light">Light mode</string>
|
<string name="mode_light">Light mode</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<string-array name="ModeValues">
|
<string-array name="ModeValues">
|
||||||
<item>1</item> <!--MODE_NIGHT_NO-->
|
<item>1</item> <!--MODE_NIGHT_NO-->
|
||||||
<item>2</item> <!--MODE_NIGHT_YES-->
|
<item>2</item> <!--MODE_NIGHT_YES-->
|
||||||
<item>0</item> <!--MODE_NIGHT_AUTO_TIME-->
|
<item>-1</item> <!--MODE_NIGHT_FOLLOW_SYSTEM-->
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="Voice">
|
<string-array name="Voice">
|
||||||
|
@ -3,5 +3,6 @@
|
|||||||
<array name="preloaded_fonts" translatable="false">
|
<array name="preloaded_fonts" translatable="false">
|
||||||
<item>@font/open_sans</item>
|
<item>@font/open_sans</item>
|
||||||
<item>@font/roboto</item>
|
<item>@font/roboto</item>
|
||||||
|
<item>@font/source_code_pro_medium</item>
|
||||||
</array>
|
</array>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -4,5 +4,6 @@
|
|||||||
<item></item>
|
<item></item>
|
||||||
<item>@string/open_sans_font_id</item>
|
<item>@string/open_sans_font_id</item>
|
||||||
<item>@string/roboto_font_id</item>
|
<item>@string/roboto_font_id</item>
|
||||||
|
<item>@string/source_code_pro_font_id</item>
|
||||||
</array>
|
</array>
|
||||||
</resources>
|
</resources>
|
@ -4,5 +4,6 @@
|
|||||||
<item>Systems</item>
|
<item>Systems</item>
|
||||||
<item>Open Sans</item>
|
<item>Open Sans</item>
|
||||||
<item>Roboto</item>
|
<item>Roboto</item>
|
||||||
|
<item>Source Code Pro</item>
|
||||||
</array>
|
</array>
|
||||||
</resources>
|
</resources>
|
@ -63,6 +63,7 @@
|
|||||||
<string name="card_height_off">Card height will be fixed</string>
|
<string name="card_height_off">Card height will be fixed</string>
|
||||||
<string name="source_code">Source code</string>
|
<string name="source_code">Source code</string>
|
||||||
<string name="drawer_error_loading_tags">Error loading tags…</string>
|
<string name="drawer_error_loading_tags">Error loading tags…</string>
|
||||||
|
<string name="drawer_error_loading_sources">Error loading sources…</string>
|
||||||
<string name="drawer_item_filters">Filters</string>
|
<string name="drawer_item_filters">Filters</string>
|
||||||
<string name="drawer_action_clear">clear</string>
|
<string name="drawer_action_clear">clear</string>
|
||||||
<string name="drawer_item_tags">Tags</string>
|
<string name="drawer_item_tags">Tags</string>
|
||||||
@ -109,7 +110,7 @@
|
|||||||
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>
|
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>
|
||||||
<string name="pref_periodic_refresh_minutes_title"><![CDATA[Sync interval ( >= 15 minutes)]]></string>
|
<string name="pref_periodic_refresh_minutes_title"><![CDATA[Sync interval ( >= 15 minutes)]]></string>
|
||||||
<string name="pref_switch_refresh_when_charging">Only refresh when phone is charging</string>
|
<string name="pref_switch_refresh_when_charging">Only refresh when phone is charging</string>
|
||||||
<string name="loading_notification_title">Loading ...</string>
|
<string name="loading_notification_title">Loading …</string>
|
||||||
<string name="loading_notification_text">Selfoss is syncing your articles</string>
|
<string name="loading_notification_text">Selfoss is syncing your articles</string>
|
||||||
<string name="notification_channel_sync">Sync notification</string>
|
<string name="notification_channel_sync">Sync notification</string>
|
||||||
<string name="new_items_channel_sync">New items notification</string>
|
<string name="new_items_channel_sync">New items notification</string>
|
||||||
@ -124,6 +125,7 @@
|
|||||||
<string name="reader_text_align_left">Align left</string>
|
<string name="reader_text_align_left">Align left</string>
|
||||||
<string name="reader_text_align_justify">Justify</string>
|
<string name="reader_text_align_justify">Justify</string>
|
||||||
<string name="settings_reader_font">Reader font</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="open_sans_font_id" translatable="false">open_sans</string>
|
||||||
<string name="roboto_font_id" translatable="false">roboto</string>
|
<string name="roboto_font_id" translatable="false">roboto</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||||
|
33
androidApp/src/test/kotlin/DatesTest.kt
Normal file
33
androidApp/src/test/kotlin/DatesTest.kt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package bou.amine.apps.readerforselfossv2.repository
|
||||||
|
|
||||||
|
import bou.amine.apps.readerforselfossv2.utils.DateUtils
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toInstant
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DatesTest {
|
||||||
|
|
||||||
|
private val v3Date = "2013-04-07T13:43:00+01:00"
|
||||||
|
private val v4Date = "2013-04-07 13:43:00"
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun v3_date_should_be_parsed() {
|
||||||
|
val date = DateUtils.parseDate(v3Date)
|
||||||
|
val expected = LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.of("UTC+1")) .toEpochMilliseconds()
|
||||||
|
|
||||||
|
assertEquals(date, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun v4_date_should_be_parsed() {
|
||||||
|
val date = DateUtils.parseDate(v4Date)
|
||||||
|
val expected =
|
||||||
|
LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
|
||||||
|
.toEpochMilliseconds()
|
||||||
|
|
||||||
|
assertEquals(date, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1370
androidApp/src/test/kotlin/RepositoryTest.kt
Normal file
1370
androidApp/src/test/kotlin/RepositoryTest.kt
Normal file
File diff suppressed because it is too large
Load Diff
57
androidApp/src/test/kotlin/TestUtils.kt
Normal file
57
androidApp/src/test/kotlin/TestUtils.kt
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package bou.amine.apps.readerforselfossv2.repository
|
||||||
|
|
||||||
|
import bou.amine.apps.readerforselfossv2.dao.ITEM
|
||||||
|
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||||
|
|
||||||
|
|
||||||
|
fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> {
|
||||||
|
return listOf(
|
||||||
|
ITEM(
|
||||||
|
id = item.id,
|
||||||
|
datetime = item.datetime,
|
||||||
|
title = item.title,
|
||||||
|
content = item.content,
|
||||||
|
unread = item.unread,
|
||||||
|
starred = item.starred,
|
||||||
|
thumbnail = item.thumbnail,
|
||||||
|
icon = item.icon,
|
||||||
|
link = item.link,
|
||||||
|
sourcetitle = item.sourcetitle,
|
||||||
|
tags = item.tags
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<SelfossModel.Item> {
|
||||||
|
return listOf(
|
||||||
|
SelfossModel.Item(
|
||||||
|
id = item.id.toInt(),
|
||||||
|
datetime = item.datetime,
|
||||||
|
title = item.title,
|
||||||
|
content = item.content,
|
||||||
|
unread = item.unread,
|
||||||
|
starred = item.starred,
|
||||||
|
thumbnail = item.thumbnail,
|
||||||
|
icon = item.icon,
|
||||||
|
link = item.link,
|
||||||
|
sourcetitle = item.sourcetitle,
|
||||||
|
tags = item.tags.split(',')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeItemParameters {
|
||||||
|
var id = "20"
|
||||||
|
var datetime = "2022-09-09T03:32:01-04:00"
|
||||||
|
val title = "Etica della ricerca sotto i riflettori."
|
||||||
|
val content =
|
||||||
|
"<p><strong>Luigi Campanella, già Presidente SCI</strong></p>\n<p>L’etica della scienza è di certo ambito di cui continuiamo a scoprire nuovi aspetti e risvolti.</p>\n<p>L’ultimo è quello delle intelligenze artificiali capaci di creare opere complesse basate su immagini e parole memorizzate con il rischio di fake news e di contenuti disturbanti.</p>\n<p>Per evitare che ciò accada si sta procedendo filtrando secondo criteri di autocensura i dati da cui l’intelligenza artificiale parte.</p>\n<p>Comincia ad intravedersi un futuro prossimo di competizione fra autori umani ed artificiali nel quale sarà importante, quando i loro prodotti saranno indistinguibili, dichiararne l’origine.</p>\n<p>Come si comprende, si conferma che gli aspetti etici dell’innovazione e della ricerca si diversificato sempre di più.</p>\n<p>La biologia molecolare e la genetica già in passato hanno posto all’attenzione comune aspetti di etica della scienza che hanno indotto a nuove riflessioni circa i limiti delle ricerche.</p>\n<p>L’argomento, sempre attuale, torna sulle prime pagine a seguito della pubblicazione di una ricerca della Università di Cambridge che ha sviluppato una struttura cellulare di un topo con un cuore che batte regolarmente.</p>\n<img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image002-1.png?w=481\" alt=\"\" width=\"697\" height=\"430\" /><img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image003-1.png?w=906\" alt=\"\" /><p>Magdalena Zernicka-Goetz</p>\n<img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image004.jpg?w=474\" alt=\"\" width=\"622\" height=\"465\" /><p>Gianluca Amadei</p>\n<p>Del gruppo fa parte anche uno scienziato italiano Gianluca Amadei,che dinnanzi alle obiezioni di natura etica sulla realizzazione della vita artificiale si è affrettato a sostenere che non è creare nuove vite il fine primario della ricerca, ma quello di salvare quelle esistenti, di dare contributi essenziali alla medicina citando il caso del fallimento tuttora non interpretato di alcune gravidanze e di superare la sperimentazione animale, così contribuendo positivamente alla soluzione di un altro dilemma etico.</p>\n<p>L’embrione sintetico ha ovviamente come primo traguardo il contributo ai trapianti oggi drammaticamente carenti nell’offerta rispetto alla domanda, con attese fino a 4 anni per i trapianti di cuore ed a 2 anni per quelli di fegato. Il lavoro dovrebbe adesso continuare presso l’Ateneo di Padova per creare nuovi organi e nuovi farmaci.</p>"
|
||||||
|
var unread = true
|
||||||
|
var starred = true
|
||||||
|
val thumbnail = null
|
||||||
|
val icon = "ba79e238383ce83c23a169929c8906ef.png"
|
||||||
|
val link =
|
||||||
|
"https://ilblogdellasci.wordpress.com/2022/09/09/etica-della-ricerca-sotto-i-riflettori/"
|
||||||
|
var sourcetitle = "La Chimica e la Società"
|
||||||
|
var tags = "Chimica, Testing"
|
||||||
|
}
|
@ -1,21 +1,20 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
|
||||||
gradlePluginPortal()
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
|
|
||||||
classpath("com.android.tools.build:gradle:7.3.0")
|
|
||||||
|
|
||||||
// sonarquve
|
|
||||||
classpath("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513")
|
|
||||||
|
|
||||||
// SqlDelight
|
// SqlDelight
|
||||||
classpath("com.squareup.sqldelight:gradle-plugin:1.5.3")
|
classpath("com.squareup.sqldelight:gradle-plugin:1.5.4")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
//trick: for the same plugin versions in all sub-modules
|
||||||
|
id("com.android.application").version("7.3.1").apply(false)
|
||||||
|
id("com.android.library").version("7.3.1").apply(false)
|
||||||
|
kotlin("android").version("1.7.20").apply(false)
|
||||||
|
kotlin("multiplatform").version("1.7.20").apply(false)
|
||||||
|
id("org.sonarqube").version("3.4.0.2513").apply(false)
|
||||||
|
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
|
||||||
|
}
|
||||||
|
|
||||||
apply(plugin = "org.sonarqube")
|
apply(plugin = "org.sonarqube")
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
@ -27,6 +26,7 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tasks.register("clean", Delete::class) {
|
tasks.register("clean", Delete::class) {
|
||||||
delete(rootProject.buildDir)
|
delete(rootProject.buildDir)
|
||||||
}
|
}
|
@ -11,14 +11,26 @@
|
|||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
#Tue Mar 22 16:50:00 CET 2022
|
#Tue Mar 22 16:50:00 CET 2022
|
||||||
|
#Gradle
|
||||||
|
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||||
|
|
||||||
|
#Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
kotlin.mpp.enableCInteropCommonization=true
|
|
||||||
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
#Android
|
||||||
kotlin.native.enableDependencyPropagation=false
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
kotlin.native.enableDependencyPropagation=false
|
||||||
|
#android.nonTransitiveRClass=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
|
||||||
|
|
||||||
|
#MPP
|
||||||
|
kotlin.mpp.enableCInteropCommonization=true
|
||||||
kotlin.mpp.enableGranularSourceSetsMetadata=true
|
kotlin.mpp.enableGranularSourceSetsMetadata=true
|
||||||
|
|
||||||
|
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
ignoreGitVersion=false
|
ignoreGitVersion=false
|
||||||
|
kotlin.native.cacheKind.iosX64=none
|
||||||
pushCache=true
|
pushCache=true
|
||||||
|
@ -2,12 +2,9 @@ import SwiftUI
|
|||||||
import shared
|
import shared
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
let greet = Greeting().greeting()
|
|
||||||
|
|
||||||
let toto = SelfossApi().getItems()
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(greet)
|
Text("ototot")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,14 @@ pluginManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
buildCache {
|
buildCache {
|
||||||
remote<HttpBuildCache> {
|
remote<HttpBuildCache> {
|
||||||
url = uri("http://18.0.0.7:3071/cache/")
|
url = uri("http://18.0.0.7:3071/cache/")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
object SqlDelight {
|
object SqlDelight {
|
||||||
const val runtime = "com.squareup.sqldelight:runtime:1.5.3"
|
const val runtime = "com.squareup.sqldelight:runtime:1.5.4"
|
||||||
const val android = "com.squareup.sqldelight:android-driver:1.5.3"
|
const val android = "com.squareup.sqldelight:android-driver:1.5.4"
|
||||||
const val native = "com.squareup.sqldelight:native-driver:1.5.3"
|
const val native = "com.squareup.sqldelight:native-driver:1.5.4"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ kotlin {
|
|||||||
listOf(
|
listOf(
|
||||||
iosX64(),
|
iosX64(),
|
||||||
iosArm64(),
|
iosArm64(),
|
||||||
//iosSimulatorArm64() sure all ios dependencies support this target
|
// iosSimulatorArm64()
|
||||||
).forEach {
|
).forEach {
|
||||||
it.binaries.framework {
|
it.binaries.framework {
|
||||||
baseName = "shared"
|
baseName = "shared"
|
||||||
@ -40,14 +40,11 @@ kotlin {
|
|||||||
implementation("org.kodein.di:kodein-di:7.12.0")
|
implementation("org.kodein.di:kodein-di:7.12.0")
|
||||||
|
|
||||||
//Settings
|
//Settings
|
||||||
implementation("com.russhwolf:multiplatform-settings-no-arg:0.9")
|
implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC")
|
||||||
|
|
||||||
//Logging
|
//Logging
|
||||||
implementation("io.github.aakira:napier:2.6.1")
|
implementation("io.github.aakira:napier:2.6.1")
|
||||||
|
|
||||||
// Network information
|
|
||||||
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
|
|
||||||
|
|
||||||
// Sql
|
// Sql
|
||||||
implementation(SqlDelight.runtime)
|
implementation(SqlDelight.runtime)
|
||||||
}
|
}
|
||||||
@ -61,6 +58,7 @@ kotlin {
|
|||||||
val androidMain by getting {
|
val androidMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("io.ktor:ktor-client-okhttp:2.1.1")
|
implementation("io.ktor:ktor-client-okhttp:2.1.1")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
||||||
|
|
||||||
// Sql
|
// Sql
|
||||||
implementation(SqlDelight.android)
|
implementation(SqlDelight.android)
|
||||||
@ -81,9 +79,9 @@ kotlin {
|
|||||||
iosArm64Main.dependsOn(this)
|
iosArm64Main.dependsOn(this)
|
||||||
// iosSimulatorArm64Main.dependsOn(this)
|
// iosSimulatorArm64Main.dependsOn(this)
|
||||||
|
|
||||||
// Sql
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(SqlDelight.native)
|
implementation(SqlDelight.native)
|
||||||
|
implementation("io.ktor:ktor-client-ios:2.1.1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val iosX64Test by getting
|
val iosX64Test by getting
|
||||||
@ -93,20 +91,17 @@ kotlin {
|
|||||||
dependsOn(commonTest)
|
dependsOn(commonTest)
|
||||||
iosX64Test.dependsOn(this)
|
iosX64Test.dependsOn(this)
|
||||||
iosArm64Test.dependsOn(this)
|
iosArm64Test.dependsOn(this)
|
||||||
dependencies {
|
|
||||||
implementation("io.ktor:ktor-client-ios:2.1.1")
|
|
||||||
}
|
|
||||||
// iosSimulatorArm64Test.dependsOn(this)
|
// iosSimulatorArm64Test.dependsOn(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 31
|
compileSdk = 32
|
||||||
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
|
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 31
|
targetSdk = 32
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
@ -115,10 +110,11 @@ android {
|
|||||||
namespace = "bou.amine.apps.readerforselfossv2"
|
namespace = "bou.amine.apps.readerforselfossv2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
sqldelight {
|
sqldelight {
|
||||||
database("ReaderForSelfossDB") {
|
database("ReaderForSelfossDB") {
|
||||||
packageName = "bou.amine.apps.readerforselfossv2.dao"
|
packageName = "bou.amine.apps.readerforselfossv2.dao"
|
||||||
sourceFolders = listOf("sqldelight")
|
sourceFolders = listOf("sqldelight")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.utils
|
package bou.amine.apps.readerforselfossv2.utils
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
import kotlinx.datetime.*
|
||||||
import java.time.Instant
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
actual class DateUtils actual constructor(actual val appSettingsService: AppSettingsService) {
|
|
||||||
|
|
||||||
|
actual class DateUtils {
|
||||||
|
actual companion object {
|
||||||
actual fun parseDate(dateString: String): Long {
|
actual fun parseDate(dateString: String): Long {
|
||||||
|
return try {
|
||||||
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
|
Instant.parse(dateString).toEpochMilliseconds()
|
||||||
|
} catch (e: Exception) {
|
||||||
return if (appSettingsService.getApiVersion() >= 4) {
|
LocalDateTime.parse(dateString.replace(" ", "T")).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
|
||||||
OffsetDateTime.parse(dateString).toInstant().toEpochMilli()
|
|
||||||
} else {
|
|
||||||
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(
|
|
||||||
ZoneOffset.UTC).toEpochMilli()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,9 +20,10 @@ actual class DateUtils actual constructor(actual val appSettingsService: AppSett
|
|||||||
|
|
||||||
return " " + DateUtils.getRelativeTimeSpanString(
|
return " " + DateUtils.getRelativeTimeSpanString(
|
||||||
date,
|
date,
|
||||||
Instant.now().toEpochMilli(),
|
Clock.System.now().toEpochMilliseconds(),
|
||||||
DateUtils.MINUTE_IN_MILLIS,
|
DateUtils.MINUTE_IN_MILLIS,
|
||||||
DateUtils.FORMAT_ABBREV_RELATIVE
|
DateUtils.FORMAT_ABBREV_RELATIVE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.DI
|
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.rest.SelfossApi
|
||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
@ -10,4 +11,5 @@ import org.kodein.di.singleton
|
|||||||
val networkModule by DI.Module {
|
val networkModule by DI.Module {
|
||||||
bind<AppSettingsService>() with singleton { AppSettingsService() }
|
bind<AppSettingsService>() with singleton { AppSettingsService() }
|
||||||
bind<SelfossApi>() with singleton { SelfossApi(instance()) }
|
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
|
val unread: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class SuccessResponse(val success: Boolean) {
|
|
||||||
val isSuccess: Boolean
|
|
||||||
get() = success
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Stats(
|
class Stats(
|
||||||
val total: Int,
|
val total: Int,
|
||||||
@ -108,8 +102,8 @@ class SelfossModel {
|
|||||||
return stringUrl
|
return stringUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sourceAndDateText(dateUtils: DateUtils): String =
|
fun sourceAndDateText(): String =
|
||||||
this.sourcetitle.getHtmlDecoded() + dateUtils.parseRelativeDate(this.datetime)
|
this.sourcetitle.getHtmlDecoded() + DateUtils.parseRelativeDate(this.datetime)
|
||||||
|
|
||||||
fun toggleStar(): Item {
|
fun toggleStar(): Item {
|
||||||
this.starred = !this.starred
|
this.starred = !this.starred
|
||||||
@ -138,7 +132,11 @@ class SelfossModel {
|
|||||||
object BooleanSerializer : KSerializer<Boolean> {
|
object BooleanSerializer : KSerializer<Boolean> {
|
||||||
override fun deserialize(decoder: Decoder): Boolean {
|
override fun deserialize(decoder: Decoder): Boolean {
|
||||||
val json = ((decoder as JsonDecoder).decodeJsonElement()).jsonPrimitive
|
val json = ((decoder as JsonDecoder).decodeJsonElement()).jsonPrimitive
|
||||||
return json.booleanOrNull ?: json.int == 1
|
return if (json.booleanOrNull != null) {
|
||||||
|
json.boolean
|
||||||
|
} else {
|
||||||
|
json.int == 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val descriptor: SerialDescriptor
|
override val descriptor: SerialDescriptor
|
||||||
@ -148,16 +146,4 @@ class SelfossModel {
|
|||||||
TODO("Not yet implemented")
|
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,23 +3,23 @@ package bou.amine.apps.readerforselfossv2.repository
|
|||||||
import bou.amine.apps.readerforselfossv2.dao.*
|
import bou.amine.apps.readerforselfossv2.dao.*
|
||||||
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
|
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
|
||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
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.rest.SelfossApi
|
||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
import bou.amine.apps.readerforselfossv2.utils.*
|
import bou.amine.apps.readerforselfossv2.utils.*
|
||||||
import com.github.ln_12.library.ConnectivityStatus
|
|
||||||
import io.github.aakira.napier.Napier
|
import io.github.aakira.napier.Napier
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class Repository(private val api: SelfossApi, private val appSettingsService: AppSettingsService, connectivityStatus: ConnectivityStatus, private val db: ReaderForSelfossDB) {
|
class Repository(private val api: SelfossApi, private val appSettingsService: AppSettingsService, val isConnectionAvailable: MutableStateFlow<Boolean>, private val db: ReaderForSelfossDB) {
|
||||||
|
|
||||||
var items = ArrayList<SelfossModel.Item>()
|
var items = ArrayList<SelfossModel.Item>()
|
||||||
val isConnectionAvailable = connectivityStatus.isNetworkConnected
|
|
||||||
var connectionMonitored = false
|
var connectionMonitored = false
|
||||||
|
|
||||||
var baseUrl = appSettingsService.getBaseUrl()
|
var baseUrl = appSettingsService.getBaseUrl()
|
||||||
lateinit var dateUtils: DateUtils
|
|
||||||
|
|
||||||
var displayedItems = ItemType.UNREAD
|
var displayedItems = ItemType.UNREAD
|
||||||
|
|
||||||
@ -29,25 +29,19 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
|
|
||||||
var offlineOverride = false
|
var offlineOverride = false
|
||||||
|
|
||||||
var badgeUnread = 0
|
private val _badgeUnread = MutableStateFlow(0)
|
||||||
set(value) {field = if (value < 0) { 0 } else { value } }
|
val badgeUnread = _badgeUnread.asStateFlow()
|
||||||
var badgeAll = 0
|
private val _badgeAll = MutableStateFlow(0)
|
||||||
set(value) {field = if (value < 0) { 0 } else { value } }
|
val badgeAll = _badgeAll.asStateFlow()
|
||||||
var badgeStarred = 0
|
private val _badgeStarred = MutableStateFlow(0)
|
||||||
set(value) {field = if (value < 0) { 0 } else { value } }
|
val badgeStarred = _badgeStarred.asStateFlow()
|
||||||
|
|
||||||
init {
|
private var fetchedSources = false
|
||||||
// TODO: Dispatchers.IO not available in KMM, an alternative solution should be found
|
private var fetchedTags = false
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
updateApiVersion()
|
|
||||||
dateUtils = DateUtils(appSettingsService)
|
|
||||||
reloadBadges()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
|
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
|
||||||
// TODO: Use the updatedSince parameter
|
// 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
|
var fromDB = false
|
||||||
if (isNetworkAvailable()) {
|
if (isNetworkAvailable()) {
|
||||||
fetchedItems = api.getItems(
|
fetchedItems = api.getItems(
|
||||||
@ -61,12 +55,19 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
} else {
|
} else {
|
||||||
if (appSettingsService.isItemCachingEnabled()) {
|
if (appSettingsService.isItemCachingEnabled()) {
|
||||||
fromDB = true
|
fromDB = true
|
||||||
fetchedItems = SelfossModel.StatusAndData.succes(
|
var dbItems = getDBItems().filter {
|
||||||
getDBItems().filter {
|
|
||||||
displayedItems == ItemType.ALL ||
|
displayedItems == ItemType.ALL ||
|
||||||
(it.unread && displayedItems == ItemType.UNREAD) ||
|
(it.unread && displayedItems == ItemType.UNREAD) ||
|
||||||
(it.starred && displayedItems == ItemType.STARRED)
|
(it.starred && displayedItems == ItemType.STARRED)
|
||||||
}.map { it.toView() }
|
}
|
||||||
|
if (tagFilter != null) {
|
||||||
|
dbItems = dbItems.filter { it.tags.split(',').contains(tagFilter!!.tag) }
|
||||||
|
}
|
||||||
|
if (sourceFilter != null) {
|
||||||
|
dbItems = dbItems.filter { it.sourcetitle == sourceFilter!!.title }
|
||||||
|
}
|
||||||
|
fetchedItems = StatusAndData.succes(
|
||||||
|
dbItems.map { it.toView() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,14 +75,14 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
if (fetchedItems.success && fetchedItems.data != null) {
|
if (fetchedItems.success && fetchedItems.data != null) {
|
||||||
items = ArrayList(fetchedItems.data!!)
|
items = ArrayList(fetchedItems.data!!)
|
||||||
if (fromDB) {
|
if (fromDB) {
|
||||||
items.sortByDescending { dateUtils.parseDate(it.datetime) }
|
items.sortByDescending { DateUtils.parseDate(it.datetime) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
|
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()) {
|
if (isNetworkAvailable()) {
|
||||||
val offset = items.size
|
val offset = items.size
|
||||||
fetchedItems = api.getItems(
|
fetchedItems = api.getItems(
|
||||||
@ -105,9 +106,9 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
val items = api.getItems(
|
val items = api.getItems(
|
||||||
itemType.type,
|
itemType.type,
|
||||||
0,
|
0,
|
||||||
tagFilter?.tag,
|
null,
|
||||||
sourceFilter?.id?.toLong(),
|
null,
|
||||||
searchFilter,
|
null,
|
||||||
null,
|
null,
|
||||||
200
|
200
|
||||||
)
|
)
|
||||||
@ -126,37 +127,45 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
if (isNetworkAvailable()) {
|
if (isNetworkAvailable()) {
|
||||||
val response = api.stats()
|
val response = api.stats()
|
||||||
if (response.success && response.data != null) {
|
if (response.success && response.data != null) {
|
||||||
badgeUnread = response.data.unread
|
_badgeUnread.value = response.data.unread
|
||||||
badgeAll = response.data.total
|
_badgeAll.value = response.data.total
|
||||||
badgeStarred = response.data.starred
|
_badgeStarred.value = response.data.starred
|
||||||
success = true
|
success = true
|
||||||
}
|
}
|
||||||
} else {
|
} else if (appSettingsService.isItemCachingEnabled()) {
|
||||||
// TODO: do this differently, because it's not efficient
|
// TODO: do this differently, because it's not efficient
|
||||||
val dbItems = getDBItems()
|
val dbItems = getDBItems()
|
||||||
badgeUnread = dbItems.filter { item -> item.unread }.size
|
_badgeUnread.value = dbItems.filter { item -> item.unread }.size
|
||||||
badgeStarred = dbItems.filter { item -> item.starred }.size
|
_badgeStarred.value = dbItems.filter { item -> item.starred }.size
|
||||||
badgeAll = items.size
|
_badgeAll.value = dbItems.size
|
||||||
|
success = true
|
||||||
}
|
}
|
||||||
return success
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getTags(): List<SelfossModel.Tag>? {
|
suspend fun getTags(): List<SelfossModel.Tag> {
|
||||||
return if (isNetworkAvailable()) {
|
val isDatabaseEnabled = appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
|
||||||
|
return if (isNetworkAvailable() && !fetchedTags) {
|
||||||
val apiTags = api.tags()
|
val apiTags = api.tags()
|
||||||
if (apiTags.success && apiTags.data != null && (appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled())) {
|
if (apiTags.success && apiTags.data != null && isDatabaseEnabled) {
|
||||||
resetDBTagsWithData(apiTags.data)
|
resetDBTagsWithData(apiTags.data)
|
||||||
|
if (!appSettingsService.isUpdateSourcesEnabled()) {
|
||||||
|
fetchedTags = true
|
||||||
}
|
}
|
||||||
apiTags.data
|
}
|
||||||
} else {
|
apiTags.data ?: emptyList()
|
||||||
|
} else if (isDatabaseEnabled) {
|
||||||
getDBTags().map { it.toView() }
|
getDBTags().map { it.toView() }
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSpouts(): Map<String, SelfossModel.Spout>? {
|
// TODO: Add tests
|
||||||
|
suspend fun getSpouts(): Map<String, SelfossModel.Spout> {
|
||||||
return if (isNetworkAvailable()) {
|
return if (isNetworkAvailable()) {
|
||||||
val spouts = api.spouts()
|
val spouts = api.spouts()
|
||||||
return if (spouts.success && spouts.data != null) {
|
if (spouts.success && spouts.data != null) {
|
||||||
spouts.data
|
spouts.data
|
||||||
} else {
|
} else {
|
||||||
emptyMap() // TODO: do something here
|
emptyMap() // TODO: do something here
|
||||||
@ -166,18 +175,25 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSources(): ArrayList<SelfossModel.Source>? {
|
suspend fun getSources(): ArrayList<SelfossModel.Source> {
|
||||||
return if (isNetworkAvailable()) {
|
val isDatabaseEnabled = appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
|
||||||
|
return if (isNetworkAvailable() && !fetchedSources) {
|
||||||
val apiSources = api.sources()
|
val apiSources = api.sources()
|
||||||
if (apiSources.success && apiSources.data != null && (appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled())) {
|
if (apiSources.success && apiSources.data != null && isDatabaseEnabled) {
|
||||||
resetDBSourcesWithData(apiSources.data)
|
resetDBSourcesWithData(apiSources.data)
|
||||||
|
if (!appSettingsService.isUpdateSourcesEnabled()) {
|
||||||
|
fetchedSources = true
|
||||||
}
|
}
|
||||||
apiSources.data
|
}
|
||||||
} else {
|
apiSources.data ?: ArrayList()
|
||||||
|
} else if (isDatabaseEnabled) {
|
||||||
ArrayList(getDBSources().map { it.toView() })
|
ArrayList(getDBSources().map { it.toView() })
|
||||||
|
} else {
|
||||||
|
ArrayList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add tests
|
||||||
suspend fun markAsRead(item: SelfossModel.Item): Boolean {
|
suspend fun markAsRead(item: SelfossModel.Item): Boolean {
|
||||||
val success = markAsReadById(item.id)
|
val success = markAsReadById(item.id)
|
||||||
|
|
||||||
@ -189,14 +205,14 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
|
|
||||||
private suspend fun markAsReadById(id: Int): Boolean {
|
private suspend fun markAsReadById(id: Int): Boolean {
|
||||||
return if (isNetworkAvailable()) {
|
return if (isNetworkAvailable()) {
|
||||||
api.markAsRead(id.toString())?.isSuccess
|
api.markAsRead(id.toString()).isSuccess
|
||||||
} else {
|
} else {
|
||||||
insertDBAction(id.toString(), read = true)
|
insertDBAction(id.toString(), read = true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add tests
|
||||||
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
|
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
|
||||||
val success = unmarkAsReadById(item.id)
|
val success = unmarkAsReadById(item.id)
|
||||||
|
|
||||||
@ -208,13 +224,14 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
|
|
||||||
private suspend fun unmarkAsReadById(id: Int): Boolean {
|
private suspend fun unmarkAsReadById(id: Int): Boolean {
|
||||||
return if (isNetworkAvailable()) {
|
return if (isNetworkAvailable()) {
|
||||||
api.unmarkAsRead(id.toString())?.isSuccess
|
api.unmarkAsRead(id.toString()).isSuccess
|
||||||
} else {
|
} else {
|
||||||
insertDBAction(id.toString(), unread = true)
|
insertDBAction(id.toString(), unread = true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add tests
|
||||||
suspend fun starr(item: SelfossModel.Item): Boolean {
|
suspend fun starr(item: SelfossModel.Item): Boolean {
|
||||||
val success = starrById(item.id)
|
val success = starrById(item.id)
|
||||||
|
|
||||||
@ -226,13 +243,14 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
|
|
||||||
private suspend fun starrById(id: Int): Boolean {
|
private suspend fun starrById(id: Int): Boolean {
|
||||||
return if (isNetworkAvailable()) {
|
return if (isNetworkAvailable()) {
|
||||||
api.starr(id.toString())?.isSuccess
|
api.starr(id.toString()).isSuccess
|
||||||
} else {
|
} else {
|
||||||
insertDBAction(id.toString(), starred = true)
|
insertDBAction(id.toString(), starred = true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add tests
|
||||||
suspend fun unstarr(item: SelfossModel.Item): Boolean {
|
suspend fun unstarr(item: SelfossModel.Item): Boolean {
|
||||||
val success = unstarrById(item.id)
|
val success = unstarrById(item.id)
|
||||||
|
|
||||||
@ -244,17 +262,18 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
|
|
||||||
private suspend fun unstarrById(id: Int): Boolean {
|
private suspend fun unstarrById(id: Int): Boolean {
|
||||||
return if (isNetworkAvailable()) {
|
return if (isNetworkAvailable()) {
|
||||||
api.unstarr(id.toString())?.isSuccess
|
api.unstarr(id.toString()).isSuccess
|
||||||
} else {
|
} else {
|
||||||
insertDBAction(id.toString(), starred = true)
|
insertDBAction(id.toString(), starred = true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add tests
|
||||||
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
|
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
|
||||||
var success = false
|
var success = false
|
||||||
|
|
||||||
if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() })?.isSuccess) {
|
if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() }).isSuccess) {
|
||||||
success = true
|
success = true
|
||||||
for (item in items) {
|
for (item in items) {
|
||||||
markAsReadLocally(item)
|
markAsReadLocally(item)
|
||||||
@ -266,7 +285,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
private fun markAsReadLocally(item: SelfossModel.Item) {
|
private fun markAsReadLocally(item: SelfossModel.Item) {
|
||||||
if (item.unread) {
|
if (item.unread) {
|
||||||
item.unread = false
|
item.unread = false
|
||||||
badgeUnread -= 1
|
_badgeUnread.value -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
@ -277,7 +296,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
private fun unmarkAsReadLocally(item: SelfossModel.Item) {
|
private fun unmarkAsReadLocally(item: SelfossModel.Item) {
|
||||||
if (!item.unread) {
|
if (!item.unread) {
|
||||||
item.unread = true
|
item.unread = true
|
||||||
badgeUnread += 1
|
_badgeUnread.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
@ -288,7 +307,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
private fun starrLocally(item: SelfossModel.Item) {
|
private fun starrLocally(item: SelfossModel.Item) {
|
||||||
if (!item.starred) {
|
if (!item.starred) {
|
||||||
item.starred = true
|
item.starred = true
|
||||||
badgeStarred += 1
|
_badgeStarred.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
@ -299,7 +318,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
private fun unstarrLocally(item: SelfossModel.Item) {
|
private fun unstarrLocally(item: SelfossModel.Item) {
|
||||||
if (item.starred) {
|
if (item.starred) {
|
||||||
item.starred = false
|
item.starred = false
|
||||||
badgeStarred -= 1
|
_badgeStarred.value -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
@ -323,7 +342,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
tags,
|
tags,
|
||||||
filter,
|
filter,
|
||||||
appSettingsService.getApiVersion()
|
appSettingsService.getApiVersion()
|
||||||
)?.isSuccess == true
|
).isSuccess == true
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@ -333,17 +352,15 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
var success = false
|
var success = false
|
||||||
if (isNetworkAvailable()) {
|
if (isNetworkAvailable()) {
|
||||||
val response = api.deleteSource(id)
|
val response = api.deleteSource(id)
|
||||||
if (response != null) {
|
|
||||||
success = response.isSuccess
|
success = response.isSuccess
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return success
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateRemote(): Boolean {
|
suspend fun updateRemote(): Boolean {
|
||||||
return if (isNetworkAvailable()) {
|
return if (isNetworkAvailable()) {
|
||||||
api.update()?.equals("finished")
|
api.update().data.equals("finished")
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -354,10 +371,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
if (isNetworkAvailable()) {
|
if (isNetworkAvailable()) {
|
||||||
try {
|
try {
|
||||||
val response = api.login()
|
val response = api.login()
|
||||||
result = response?.isSuccess == true
|
result = response.isSuccess == true
|
||||||
if (result) {
|
|
||||||
updateApiVersion()
|
|
||||||
}
|
|
||||||
} catch (cause: Throwable) {
|
} catch (cause: Throwable) {
|
||||||
Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote")
|
Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote")
|
||||||
}
|
}
|
||||||
@ -390,9 +404,9 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
private fun deleteDBAction(action: ACTION) =
|
private fun deleteDBAction(action: ACTION) =
|
||||||
db.actionsQueries.deleteAction(action.id)
|
db.actionsQueries.deleteAction(action.id)
|
||||||
|
|
||||||
fun getDBTags(): List<TAG> = db.tagsQueries.tags().executeAsList()
|
private fun getDBTags(): List<TAG> = db.tagsQueries.tags().executeAsList()
|
||||||
|
|
||||||
fun getDBSources(): List<SOURCE> = db.sourcesQueries.sources().executeAsList()
|
private fun getDBSources(): List<SOURCE> = db.sourcesQueries.sources().executeAsList()
|
||||||
|
|
||||||
private fun resetDBTagsWithData(tagEntities: List<SelfossModel.Tag>) {
|
private fun resetDBTagsWithData(tagEntities: List<SelfossModel.Tag>) {
|
||||||
db.tagsQueries.deleteAllTags()
|
db.tagsQueries.deleteAllTags()
|
||||||
@ -430,8 +444,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
private fun updateDBItem(item: SelfossModel.Item) =
|
private fun updateDBItem(item: SelfossModel.Item) =
|
||||||
db.itemsQueries.updateItem(item.datetime, item.title.getHtmlDecoded(), item.content, item.unread, item.starred, item.thumbnail, item.icon, item.link, item.sourcetitle, item.tags.joinToString(","), item.id.toString())
|
db.itemsQueries.updateItem(item.datetime, item.title.getHtmlDecoded(), item.content, item.unread, item.starred, item.thumbnail, item.icon, item.link, item.sourcetitle, item.tags.joinToString(","), item.id.toString())
|
||||||
|
|
||||||
|
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> {
|
||||||
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item>? {
|
|
||||||
try {
|
try {
|
||||||
val newItems = getMaxItemsForBackground(ItemType.UNREAD)
|
val newItems = getMaxItemsForBackground(ItemType.UNREAD)
|
||||||
val allItems = getMaxItemsForBackground(ItemType.ALL)
|
val allItems = getMaxItemsForBackground(ItemType.ALL)
|
||||||
@ -444,6 +457,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
|
|||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add tests
|
||||||
suspend fun handleDBActions() {
|
suspend fun handleDBActions() {
|
||||||
|
|
||||||
val actions: List<ACTION> = getDBActions()
|
val actions: List<ACTION> = getDBActions()
|
||||||
|
@ -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
|
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 bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
@ -66,7 +66,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
|
|||||||
client = createHttpClient()
|
client = createHttpClient()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun login(): SelfossModel.SuccessResponse =
|
suspend fun login(): SuccessResponse =
|
||||||
maybeResponse(client.get(url("/login")) {
|
maybeResponse(client.get(url("/login")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
parameter("password", appSettingsService.getPassword())
|
||||||
@ -80,7 +80,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
|
|||||||
search: String?,
|
search: String?,
|
||||||
updatedSince: String?,
|
updatedSince: String?,
|
||||||
items: Int? = null
|
items: Int? = null
|
||||||
): SelfossModel.StatusAndData<List<SelfossModel.Item>> =
|
): StatusAndData<List<SelfossModel.Item>> =
|
||||||
bodyOrFailure(client.get(url("/items")) {
|
bodyOrFailure(client.get(url("/items")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
parameter("password", appSettingsService.getPassword())
|
||||||
@ -93,64 +93,64 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
|
|||||||
parameter("offset", offset)
|
parameter("offset", offset)
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun stats(): SelfossModel.StatusAndData<SelfossModel.Stats> =
|
suspend fun stats(): StatusAndData<SelfossModel.Stats> =
|
||||||
bodyOrFailure(client.get(url("/stats")) {
|
bodyOrFailure(client.get(url("/stats")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
parameter("password", appSettingsService.getPassword())
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun tags(): SelfossModel.StatusAndData<List<SelfossModel.Tag>> =
|
suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
|
||||||
bodyOrFailure(client.get(url("/tags")) {
|
bodyOrFailure(client.get(url("/tags")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
parameter("password", appSettingsService.getPassword())
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun update(): SelfossModel.StatusAndData<String> =
|
suspend fun update(): StatusAndData<String> =
|
||||||
bodyOrFailure(client.get(url("/update")) {
|
bodyOrFailure(client.get(url("/update")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
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")) {
|
bodyOrFailure(client.get(url("/sources/spouts")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
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")) {
|
bodyOrFailure(client.get(url("/sources/list")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
parameter("password", appSettingsService.getPassword())
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun version(): SelfossModel.StatusAndData<SelfossModel.ApiVersion> =
|
suspend fun version(): StatusAndData<SelfossModel.ApiVersion> =
|
||||||
bodyOrFailure(client.get(url("/api/about")))
|
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")) {
|
maybeResponse(client.post(url("/mark/$id")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
parameter("password", appSettingsService.getPassword())
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun unmarkAsRead(id: String): SelfossModel.SuccessResponse =
|
suspend fun unmarkAsRead(id: String): SuccessResponse =
|
||||||
maybeResponse(client.post(url("/unmark/$id")) {
|
maybeResponse(client.post(url("/unmark/$id")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
parameter("password", appSettingsService.getPassword())
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun starr(id: String): SelfossModel.SuccessResponse =
|
suspend fun starr(id: String): SuccessResponse =
|
||||||
maybeResponse(client.post(url("/starr/$id")) {
|
maybeResponse(client.post(url("/starr/$id")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
parameter("password", appSettingsService.getPassword())
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun unstarr(id: String): SelfossModel.SuccessResponse =
|
suspend fun unstarr(id: String): SuccessResponse =
|
||||||
maybeResponse(client.post(url("/unstarr/$id")) {
|
maybeResponse(client.post(url("/unstarr/$id")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
parameter("password", appSettingsService.getPassword())
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun markAllAsRead(ids: List<String>): SelfossModel.SuccessResponse =
|
suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
|
||||||
maybeResponse(client.submitForm(
|
maybeResponse(client.submitForm(
|
||||||
url = url("/mark"),
|
url = url("/mark"),
|
||||||
formParameters = Parameters.build {
|
formParameters = Parameters.build {
|
||||||
@ -167,7 +167,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
|
|||||||
tags: String,
|
tags: String,
|
||||||
filter: String,
|
filter: String,
|
||||||
version: Int
|
version: Int
|
||||||
): SelfossModel.SuccessResponse =
|
): SuccessResponse =
|
||||||
maybeResponse(
|
maybeResponse(
|
||||||
if (version > 1) {
|
if (version > 1) {
|
||||||
createSource2(title, url, spout, tags, filter)
|
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")) {
|
maybeResponse(client.delete(url("/source/$id")) {
|
||||||
parameter("username", appSettingsService.getUserName())
|
parameter("username", appSettingsService.getUserName())
|
||||||
parameter("password", appSettingsService.getPassword())
|
parameter("password", appSettingsService.getPassword())
|
||||||
})
|
})
|
||||||
|
|
||||||
suspend fun maybeResponse(r: HttpResponse): SelfossModel.SuccessResponse {
|
|
||||||
return if (r.status.isSuccess()) {
|
|
||||||
r.body()
|
|
||||||
} else {
|
|
||||||
SelfossModel.SuccessResponse(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse): SelfossModel.StatusAndData<T> {
|
|
||||||
return if (r.status.isSuccess()) {
|
|
||||||
SelfossModel.StatusAndData.succes(r.body())
|
|
||||||
} else {
|
|
||||||
SelfossModel.StatusAndData.error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -35,6 +35,7 @@ class AppSettingsService {
|
|||||||
private var _fontSize: Int? = null
|
private var _fontSize: Int? = null
|
||||||
private var _staticBar: Boolean? = null
|
private var _staticBar: Boolean? = null
|
||||||
private var _font: String = ""
|
private var _font: String = ""
|
||||||
|
private var _theme: Int? = null
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -54,8 +55,8 @@ class AppSettingsService {
|
|||||||
return _apiVersion
|
return _apiVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshApiVersion() {
|
private fun refreshApiVersion() {
|
||||||
_apiVersion = settings.getInt("apiVersionMajor", -1)
|
_apiVersion = settings.getInt(API_VERSION_MAJOR, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBaseUrl(): String {
|
fun getBaseUrl(): String {
|
||||||
@ -86,8 +87,8 @@ class AppSettingsService {
|
|||||||
return _itemsNumber!!
|
return _itemsNumber!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshItemsNumber() {
|
private fun refreshItemsNumber() {
|
||||||
_itemsNumber = settings.getString("prefer_api_items_number", "20").toInt()
|
_itemsNumber = settings.getString(API_ITEMS_NUMBER, "20").toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApiTimeout(): Long {
|
fun getApiTimeout(): Long {
|
||||||
@ -98,24 +99,24 @@ class AppSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshApiTimeout() {
|
private fun refreshApiTimeout() {
|
||||||
val settingsTimeout = settings.getLong("api_timeout", HttpTimeout.INFINITE_TIMEOUT_MS)
|
val settingsTimeout = settings.getLong(API_TIMEOUT, HttpTimeout.INFINITE_TIMEOUT_MS)
|
||||||
_apiTimeout = if (settingsTimeout > 0) settingsTimeout else HttpTimeout.INFINITE_TIMEOUT_MS
|
_apiTimeout = if (settingsTimeout > 0) settingsTimeout else HttpTimeout.INFINITE_TIMEOUT_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshBaseUrl() {
|
private fun refreshBaseUrl() {
|
||||||
_baseUrl = settings.getString("url", "")
|
_baseUrl = settings.getString(BASE_URL, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshUsername() {
|
private fun refreshUsername() {
|
||||||
_userName = settings.getString("login", "")
|
_userName = settings.getString(LOGIN, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshPassword() {
|
private fun refreshPassword() {
|
||||||
_password = settings.getString("password", "")
|
_password = settings.getString(PASSWORD, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshArticleViewerEnabled() {
|
private fun refreshArticleViewerEnabled() {
|
||||||
_articleViewer = settings.getBoolean("prefer_article_viewer", true)
|
_articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isArticleViewerEnabled(): Boolean {
|
fun isArticleViewerEnabled(): Boolean {
|
||||||
@ -125,7 +126,7 @@ class AppSettingsService {
|
|||||||
return _articleViewer == true
|
return _articleViewer == true
|
||||||
}
|
}
|
||||||
private fun refreshShouldBeCardViewEnabled() {
|
private fun refreshShouldBeCardViewEnabled() {
|
||||||
_shouldBeCardView = settings.getBoolean("card_view_active", false)
|
_shouldBeCardView = settings.getBoolean(CARD_VIEW_ACTIVE, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCardViewEnabled(): Boolean {
|
fun isCardViewEnabled(): Boolean {
|
||||||
@ -135,7 +136,7 @@ class AppSettingsService {
|
|||||||
return _shouldBeCardView == true
|
return _shouldBeCardView == true
|
||||||
}
|
}
|
||||||
private fun refreshDisplayUnreadCountEnabled() {
|
private fun refreshDisplayUnreadCountEnabled() {
|
||||||
_displayUnreadCount = settings.getBoolean("display_unread_count", true)
|
_displayUnreadCount = settings.getBoolean(DISPLAY_UNREAD_COUNT, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDisplayUnreadCountEnabled(): Boolean {
|
fun isDisplayUnreadCountEnabled(): Boolean {
|
||||||
@ -145,7 +146,7 @@ class AppSettingsService {
|
|||||||
return _displayUnreadCount == true
|
return _displayUnreadCount == true
|
||||||
}
|
}
|
||||||
private fun refreshDisplayAllCountEnabled() {
|
private fun refreshDisplayAllCountEnabled() {
|
||||||
_displayAllCount = settings.getBoolean("display_other_count", false)
|
_displayAllCount = settings.getBoolean(DISPLAY_OTHER_COUNT, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDisplayAllCountEnabled(): Boolean {
|
fun isDisplayAllCountEnabled(): Boolean {
|
||||||
@ -155,7 +156,7 @@ class AppSettingsService {
|
|||||||
return _displayAllCount == true
|
return _displayAllCount == true
|
||||||
}
|
}
|
||||||
private fun refreshFullHeightCardsEnabled() {
|
private fun refreshFullHeightCardsEnabled() {
|
||||||
_fullHeightCards = settings.getBoolean("full_height_cards", false)
|
_fullHeightCards = settings.getBoolean(FULL_HEIGHT_CARDS, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isFullHeightCardsEnabled(): Boolean {
|
fun isFullHeightCardsEnabled(): Boolean {
|
||||||
@ -165,7 +166,7 @@ class AppSettingsService {
|
|||||||
return _fullHeightCards == true
|
return _fullHeightCards == true
|
||||||
}
|
}
|
||||||
private fun refreshUpdateSourcesEnabled() {
|
private fun refreshUpdateSourcesEnabled() {
|
||||||
_updateSources = settings.getBoolean("update_sources", true)
|
_updateSources = settings.getBoolean(UPDATE_SOURCES, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isUpdateSourcesEnabled(): Boolean {
|
fun isUpdateSourcesEnabled(): Boolean {
|
||||||
@ -175,7 +176,7 @@ class AppSettingsService {
|
|||||||
return _updateSources == true
|
return _updateSources == true
|
||||||
}
|
}
|
||||||
private fun refreshPeriodicRefreshEnabled() {
|
private fun refreshPeriodicRefreshEnabled() {
|
||||||
_periodicRefresh = settings.getBoolean("periodic_refresh", false)
|
_periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPeriodicRefreshEnabled(): Boolean {
|
fun isPeriodicRefreshEnabled(): Boolean {
|
||||||
@ -186,7 +187,7 @@ class AppSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshRefreshWhenChargingOnlyEnabled() {
|
private fun refreshRefreshWhenChargingOnlyEnabled() {
|
||||||
_refreshWhenChargingOnly = settings.getBoolean("refresh_when_charging", false)
|
_refreshWhenChargingOnly = settings.getBoolean(REFRESH_WHEN_CHARGING, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isRefreshWhenChargingOnlyEnabled(): Boolean {
|
fun isRefreshWhenChargingOnlyEnabled(): Boolean {
|
||||||
@ -197,22 +198,22 @@ class AppSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshRefreshMinutes() {
|
private fun refreshRefreshMinutes() {
|
||||||
_refreshMinutes = settings.getString("periodic_refresh_minutes", "360").toLong()
|
_refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
|
||||||
if (_refreshMinutes <= 15) {
|
if (_refreshMinutes <= 15) {
|
||||||
_refreshMinutes = 15
|
_refreshMinutes = 15
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRefreshMinutes(): Long {
|
fun getRefreshMinutes(): Long {
|
||||||
if (_refreshMinutes != null) {
|
if (_refreshMinutes != 360L) {
|
||||||
refreshRefreshMinutes()
|
refreshRefreshMinutes()
|
||||||
}
|
}
|
||||||
return _refreshMinutes
|
return _refreshMinutes
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshHiddenTags() {
|
private fun refreshHiddenTags() {
|
||||||
if (settings.getString("hidden_tags", "").isNotEmpty()) {
|
if (settings.getString(HIDDEN_TAGS, "").isNotEmpty()) {
|
||||||
_hiddenTags = settings.getString("hidden_tags", "").replace("\\s".toRegex(), "").split(",")
|
_hiddenTags = settings.getString(HIDDEN_TAGS, "").replace("\\s".toRegex(), "").split(",")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +225,7 @@ class AppSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshInfiniteLoadingEnabled() {
|
private fun refreshInfiniteLoadingEnabled() {
|
||||||
_infiniteLoading = settings.getBoolean("infinite_loading", false)
|
_infiniteLoading = settings.getBoolean(INFINITE_LOADING, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isInfiniteLoadingEnabled(): Boolean {
|
fun isInfiniteLoadingEnabled(): Boolean {
|
||||||
@ -235,7 +236,7 @@ class AppSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshItemCachingEnabled() {
|
private fun refreshItemCachingEnabled() {
|
||||||
_itemsCaching = settings.getBoolean("items_caching", false)
|
_itemsCaching = settings.getBoolean(ITEMS_CACHING, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isItemCachingEnabled(): Boolean {
|
fun isItemCachingEnabled(): Boolean {
|
||||||
@ -246,7 +247,7 @@ class AppSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshNotifyNewItemsEnabled() {
|
private fun refreshNotifyNewItemsEnabled() {
|
||||||
_notifyNewItems = settings.getBoolean("notify_new_items", false)
|
_notifyNewItems = settings.getBoolean(NOTIFY_NEW_ITEMS, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isNotifyNewItemsEnabled(): Boolean {
|
fun isNotifyNewItemsEnabled(): Boolean {
|
||||||
@ -258,7 +259,7 @@ class AppSettingsService {
|
|||||||
|
|
||||||
|
|
||||||
private fun refreshMarkOnScrollEnabled() {
|
private fun refreshMarkOnScrollEnabled() {
|
||||||
_markOnScroll = settings.getBoolean("mark_on_scroll", false)
|
_markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isMarkOnScrollEnabled(): Boolean {
|
fun isMarkOnScrollEnabled(): Boolean {
|
||||||
@ -270,7 +271,7 @@ class AppSettingsService {
|
|||||||
|
|
||||||
|
|
||||||
private fun refreshActiveAllignment() {
|
private fun refreshActiveAllignment() {
|
||||||
_activeAlignment = settings.getInt("text_align", JUSTIFY)
|
_activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActiveAllignment(): Int {
|
fun getActiveAllignment(): Int {
|
||||||
@ -281,12 +282,12 @@ class AppSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun changeAllignment(allignment: Int) {
|
fun changeAllignment(allignment: Int) {
|
||||||
settings.putInt("text_align", allignment)
|
settings.putInt(TEXT_ALIGN, allignment)
|
||||||
_activeAlignment = allignment
|
_activeAlignment = allignment
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshFontSize() {
|
private fun refreshFontSize() {
|
||||||
_fontSize = settings.getString("reader_font_size", "16").toInt()
|
_fontSize = settings.getString(READER_FONT_SIZE, "16").toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFontSize(): Int {
|
fun getFontSize(): Int {
|
||||||
@ -297,7 +298,7 @@ class AppSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshStaticBarEnabled() {
|
private fun refreshStaticBarEnabled() {
|
||||||
_staticBar = settings.getBoolean("reader_static_bar", false)
|
_staticBar = settings.getBoolean(READER_STATIC_BAR, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isStaticBarEnabled(): Boolean {
|
fun isStaticBarEnabled(): Boolean {
|
||||||
@ -308,16 +309,27 @@ class AppSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshFont() {
|
private fun refreshFont() {
|
||||||
_font = settings.getString("reader_font", "")
|
_font = settings.getString(READER_FONT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFont(): String {
|
fun getFont(): String {
|
||||||
if (_font != null) {
|
if (_font.isEmpty()) {
|
||||||
refreshFont()
|
refreshFont()
|
||||||
}
|
}
|
||||||
return _font
|
return _font
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun refreshCurrentTheme() {
|
||||||
|
_theme = settings.getString(CURRENT_THEME, "-1").toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentTheme(): Int {
|
||||||
|
if (_theme == null) {
|
||||||
|
refreshCurrentTheme()
|
||||||
|
}
|
||||||
|
return _theme ?: -1
|
||||||
|
}
|
||||||
|
|
||||||
fun refreshApiSettings() {
|
fun refreshApiSettings() {
|
||||||
refreshPassword()
|
refreshPassword()
|
||||||
refreshUsername()
|
refreshUsername()
|
||||||
@ -346,6 +358,7 @@ class AppSettingsService {
|
|||||||
refreshFontSize()
|
refreshFontSize()
|
||||||
refreshFont()
|
refreshFont()
|
||||||
refreshStaticBarEnabled()
|
refreshStaticBarEnabled()
|
||||||
|
refreshCurrentTheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshLoginInformation(
|
fun refreshLoginInformation(
|
||||||
@ -353,21 +366,21 @@ class AppSettingsService {
|
|||||||
login: String,
|
login: String,
|
||||||
password: String
|
password: String
|
||||||
) {
|
) {
|
||||||
settings.putString("url", url)
|
settings.putString(BASE_URL, url)
|
||||||
settings.putString("login", login)
|
settings.putString(LOGIN, login)
|
||||||
settings.putString("password", password)
|
settings.putString(PASSWORD, password)
|
||||||
refreshApiSettings()
|
refreshApiSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetLoginInformation() {
|
fun resetLoginInformation() {
|
||||||
settings.remove("url")
|
settings.remove(BASE_URL)
|
||||||
settings.remove("login")
|
settings.remove(LOGIN)
|
||||||
settings.remove("password")
|
settings.remove(PASSWORD)
|
||||||
refreshApiSettings()
|
refreshApiSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateApiVersion(apiMajorVersion: Int) {
|
fun updateApiVersion(apiMajorVersion: Int) {
|
||||||
settings.putInt("apiVersionMajor", apiMajorVersion)
|
settings.putInt(API_VERSION_MAJOR, apiMajorVersion)
|
||||||
refreshApiVersion()
|
refreshApiVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +391,7 @@ class AppSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun disableArticleViewer() {
|
fun disableArticleViewer() {
|
||||||
settings.putBoolean("prefer_article_viewer", false)
|
settings.putBoolean(PREFER_ARTICLE_VIEWER, false)
|
||||||
refreshArticleViewerEnabled()
|
refreshArticleViewerEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,5 +409,55 @@ class AppSettingsService {
|
|||||||
const val JUSTIFY = 1
|
const val JUSTIFY = 1
|
||||||
|
|
||||||
const val ALIGN_LEFT = 2
|
const val ALIGN_LEFT = 2
|
||||||
|
|
||||||
|
const val API_VERSION_MAJOR = "apiVersionMajor"
|
||||||
|
|
||||||
|
const val API_ITEMS_NUMBER = "prefer_api_items_number"
|
||||||
|
|
||||||
|
const val API_TIMEOUT = "api_timeout"
|
||||||
|
|
||||||
|
const val BASE_URL = "url"
|
||||||
|
|
||||||
|
const val LOGIN = "login"
|
||||||
|
|
||||||
|
const val PASSWORD = "password"
|
||||||
|
|
||||||
|
const val PREFER_ARTICLE_VIEWER = "prefer_article_viewer"
|
||||||
|
|
||||||
|
const val CARD_VIEW_ACTIVE = "card_view_active"
|
||||||
|
|
||||||
|
const val DISPLAY_UNREAD_COUNT = "display_unread_count"
|
||||||
|
|
||||||
|
const val DISPLAY_OTHER_COUNT = "display_other_count"
|
||||||
|
|
||||||
|
const val FULL_HEIGHT_CARDS = "full_height_cards"
|
||||||
|
|
||||||
|
const val UPDATE_SOURCES = "update_sources"
|
||||||
|
|
||||||
|
const val PERIODIC_REFRESH = "periodic_refresh"
|
||||||
|
|
||||||
|
const val REFRESH_WHEN_CHARGING = "refresh_when_charging"
|
||||||
|
|
||||||
|
const val READER_FONT = "reader_font"
|
||||||
|
|
||||||
|
const val READER_STATIC_BAR = "reader_static_bar"
|
||||||
|
|
||||||
|
const val READER_FONT_SIZE = "reader_font_size"
|
||||||
|
|
||||||
|
const val TEXT_ALIGN = "text_align"
|
||||||
|
|
||||||
|
const val MARK_ON_SCROLL = "mark_on_scroll"
|
||||||
|
|
||||||
|
const val NOTIFY_NEW_ITEMS = "notify_new_items"
|
||||||
|
|
||||||
|
const val PERIODIC_REFRESH_MINUTES = "periodic_refresh_minutes"
|
||||||
|
|
||||||
|
const val HIDDEN_TAGS = "hidden_tags"
|
||||||
|
|
||||||
|
const val INFINITE_LOADING = "infinite_loading"
|
||||||
|
|
||||||
|
const val ITEMS_CACHING = "items_caching"
|
||||||
|
|
||||||
|
const val CURRENT_THEME = "currentMode"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,27 +0,0 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.service
|
|
||||||
|
|
||||||
import bou.amine.apps.readerforselfossv2.utils.DateUtils
|
|
||||||
|
|
||||||
class SearchService(val dateUtils: DateUtils) {
|
|
||||||
var displayedItems: String = "unread"
|
|
||||||
set(value) {
|
|
||||||
field = when (value) {
|
|
||||||
"all" -> "all"
|
|
||||||
"unread" -> "unread"
|
|
||||||
"read" -> "read"
|
|
||||||
"starred" -> "starred"
|
|
||||||
else -> "all"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var position = 0
|
|
||||||
var searchFilter: String? = null
|
|
||||||
var sourceIDFilter: Long? = null
|
|
||||||
var sourceFilter: String? = null
|
|
||||||
var tagFilter: String? = null
|
|
||||||
var itemsCaching = false
|
|
||||||
|
|
||||||
var badgeUnread = -1
|
|
||||||
var badgeAll = -1
|
|
||||||
var badgeStarred = -1
|
|
||||||
}
|
|
@ -1,16 +1,9 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.utils
|
package bou.amine.apps.readerforselfossv2.utils
|
||||||
|
|
||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
expect class DateUtils() {
|
||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
companion object {
|
||||||
|
|
||||||
|
|
||||||
fun SelfossModel.Item.parseDate(dateUtils: DateUtils): Long =
|
|
||||||
dateUtils.parseDate(this.datetime)
|
|
||||||
|
|
||||||
expect class DateUtils constructor(appSettingsService: AppSettingsService) {
|
|
||||||
val appSettingsService: AppSettingsService // This is needed because of https://stackoverflow.com/a/65249085
|
|
||||||
|
|
||||||
fun parseDate(dateString: String): Long
|
fun parseDate(dateString: String): Long
|
||||||
|
|
||||||
fun parseRelativeDate(dateString: String): String
|
fun parseRelativeDate(dateString: String): String
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -65,6 +65,6 @@ fun SelfossModel.Item.toEntity(): ITEM =
|
|||||||
this.thumbnail,
|
this.thumbnail,
|
||||||
this.icon,
|
this.icon,
|
||||||
this.link,
|
this.link,
|
||||||
this.title.getHtmlDecoded(),
|
this.sourcetitle.getHtmlDecoded(),
|
||||||
this.tags.joinToString(",")
|
this.tags.joinToString(",")
|
||||||
)
|
)
|
@ -1,8 +1,7 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.utils
|
package bou.amine.apps.readerforselfossv2.utils
|
||||||
|
|
||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
actual class DateUtils {
|
||||||
|
actual companion object {
|
||||||
actual class DateUtils actual constructor(actual val appSettingsService: AppSettingsService) {
|
|
||||||
actual fun parseDate(dateString: String): Long {
|
actual fun parseDate(dateString: String): Long {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
@ -10,5 +9,5 @@ actual class DateUtils actual constructor(actual val appSettingsService: AppSett
|
|||||||
actual fun parseRelativeDate(dateString: String): String {
|
actual fun parseRelativeDate(dateString: String): String {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,12 +0,0 @@
|
|||||||
package bou.amine.apps.readerforselfossv2
|
|
||||||
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class IosGreetingTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testExample() {
|
|
||||||
assertTrue(Greeting().greeting().contains("iOS"), "Check iOS is mentioned")
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,8 @@ package bou.amine.apps.readerforselfossv2.utils
|
|||||||
|
|
||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
|
|
||||||
actual class DateUtils actual constructor(actual val appSettingsService: AppSettingsService) {
|
actual class DateUtils {
|
||||||
|
actual companion object {
|
||||||
actual fun parseDate(dateString: String): Long {
|
actual fun parseDate(dateString: String): Long {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
@ -10,5 +11,6 @@ actual class DateUtils actual constructor(actual val appSettingsService: AppSett
|
|||||||
actual fun parseRelativeDate(dateString: String): String {
|
actual fun parseRelativeDate(dateString: String): String {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
Reference in New Issue
Block a user