Compare commits

...

12 Commits

Author SHA1 Message Date
f7a29d66ca Perform network connectivity checks in the repository 2022-08-17 17:58:16 +02:00
davidoskky
734b0b7112 Add multiplatform connectivity check 2022-08-17 17:47:34 +02:00
Amine Louveau
dec620a409 Merge pull request 'Changed ids to items.' (#29) from id-to-int into master
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/29
2022-08-17 14:31:11 +00:00
aminecmi
4d29ee0b92 Last fixes. 2022-08-17 16:16:11 +02:00
aminecmi
33333ca998 This may work. 2022-08-17 14:52:03 +02:00
aminecmi
8d87eef0fc More fixes. 2022-08-17 14:24:28 +02:00
aminecmi
5a26513ed7 These params need to be here too. 2022-08-17 14:06:56 +02:00
aminecmi
5b7f5225d8 Can't be detached because of a lock file. 2022-08-17 14:03:32 +02:00
aminecmi
03f53bf9c9 Detached scan 2022-08-17 11:04:44 +02:00
aminecmi
e06e6d580d Detached scan. 2022-08-17 11:00:02 +02:00
aminecmi
63e8649512 Fixing issues with build. 2022-08-17 10:50:04 +02:00
aminecmi
6260c3fc06 Fixes and drone build should work. 2022-08-17 10:43:56 +02:00
26 changed files with 321 additions and 236 deletions

View File

@ -1,20 +1,21 @@
kind: pipeline kind: pipeline
type: docker type: docker
name: android
steps: steps:
- name: build
image: mingc/android-build-box:latest
commands:
- ./gradlew build
- name: code-analysis - name: code-analysis
image: mingc/android-build-box:latest image: mingc/android-build-box:latest
failure: ignore failure: ignore
commands: commands:
- ls -la - ls -la
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.sources=. -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN - ./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\""
environment: environment:
SONAR_HOST_URL: SONAR_HOST_URL:
from_secret: sonarScannerHostUrl from_secret: sonarScannerHostUrl
SONAR_LOGIN: SONAR_LOGIN:
from_secret: sonarScannerLogin from_secret: sonarScannerLogin
- name: build
image: mingc/android-build-box:latest
commands:
- ./gradlew :androidApp:build -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\""

View File

@ -1,5 +1,7 @@
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
val ignoreGitVersion: String by project
plugins { plugins {
id("com.android.application") id("com.android.application")
kotlin("android") kotlin("android")
@ -32,11 +34,19 @@ fun gitVersion(): String {
} }
fun versionCodeFromGit(): Int { fun versionCodeFromGit(): Int {
if (ignoreGitVersion == "true") {
// don't care
return 1
}
println("version code " + gitVersion()) println("version code " + gitVersion())
return gitVersion().toInt() return gitVersion().toInt()
} }
fun versionNameFromGit(): String { fun versionNameFromGit(): String {
if (ignoreGitVersion == "true") {
// don't care
return "1"
}
println("version name " + gitVersion()) println("version name " + gitVersion())
return gitVersion() return gitVersion()
} }

View File

@ -1,32 +0,0 @@
// TODO
//package bou.amine.apps.readerforselfossv2.android.utils
//
//import bou.amine.apps.readerforselfossv2.android.utils.Config
//import bou.amine.apps.readerforselfossv2.android.utils.parseDate
//import org.junit.Test
//
//class DateUtilsTest {
//
// @Test
// fun parseDateV4() {
//
// Config.apiVersion = 4
// val dateString = "2013-04-07T13:43:00+01:00"
//
// val milliseconds = parseDate(dateString).toEpochMilli()
// val correctMilliseconds : Long = 1365338580000
//
// assert(milliseconds == correctMilliseconds)
// }
//
// @Test
// fun parseDateV1() {
// Config.apiVersion = 0
// val dateString = "2013-04-07 13:43:00"
//
// val milliseconds = parseDate(dateString).toEpochMilli()
// val correctMilliseconds = 1365342180000
//
// assert(milliseconds == correctMilliseconds)
// }
//}

View File

@ -1074,7 +1074,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items.map { it.id }) val success = repository.markAllAsRead(items)
if (success) { if (success) {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
@ -1153,10 +1153,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
actions.forEach { action -> actions.forEach { action ->
when { when {
action.read -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action) action.read -> doAndReportOnFail(repository.markAsReadById(action.articleId.toInt()), action)
action.unread -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action) action.unread -> doAndReportOnFail(repository.unmarkAsReadById(action.articleId.toInt()), action)
action.starred -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action) action.starred -> doAndReportOnFail(repository.starrById(action.articleId.toInt()), action)
action.unstarred -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action) action.unstarred -> doAndReportOnFail(repository.unstarrById(action.articleId.toInt()), action)
} }
} }
} }

View File

@ -13,6 +13,7 @@ import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.NetworkStatus
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
@ -25,7 +26,7 @@ class MyApp : MultiDexApplication(), DIAware {
override val di by DI.lazy { override val di by DI.lazy {
import(networkModule) import(networkModule)
bind<Repository>() with singleton { Repository(instance(), instance()) } bind<Repository>() with singleton { Repository(instance(), instance(), NetworkStatus(applicationContext)) }
} }
private lateinit var config: Config private lateinit var config: Config

View File

@ -112,7 +112,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
private fun readItem(item: SelfossModel.Item) { private fun readItem(item: SelfossModel.Item) {
if (markOnScroll) { if (markOnScroll) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(item.id) repository.markAsRead(item)
// TODO: Handle failure // TODO: Handle failure
} }
} }
@ -207,13 +207,13 @@ class ReaderActivity : AppCompatActivity(), DIAware {
R.id.star -> { R.id.star -> {
if (allItems[binding.pager.currentItem].starred) { if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(allItems[binding.pager.currentItem].id) repository.unstarr(allItems[binding.pager.currentItem])
// TODO: Handle failure // TODO: Handle failure
} }
afterUnsave() afterUnsave()
} else { } else {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.starr(allItems[binding.pager.currentItem].id) repository.starr(allItems[binding.pager.currentItem])
// TODO: Handle failure // TODO: Handle failure
} }
afterSave() afterSave()

View File

@ -113,14 +113,14 @@ class ItemCardAdapter(
if (c.isNetworkAvailable()) { if (c.isNetworkAvailable()) {
if (item.starred) { if (item.starred) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(item.id) repository.unstarr(item)
// TODO: Handle failure // TODO: Handle failure
} }
item.starred = false item.starred = false
binding.favButton.isSelected = false binding.favButton.isSelected = false
} else { } else {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.starr(item.id) repository.starr(item)
// TODO: Handle failure // TODO: Handle failure
} }
item.starred = true item.starred = true

View File

@ -79,7 +79,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
private fun readItemAtIndex(position: Int, showSnackbar: Boolean = true) { private fun readItemAtIndex(position: Int, showSnackbar: Boolean = true) {
val i = items[position] val i = items[position]
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(i.id) repository.markAsRead(i)
} }
if (repository.displayedItems == ItemType.UNREAD) { if (repository.displayedItems == ItemType.UNREAD) {
items.remove(i) items.remove(i)
@ -95,7 +95,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
private fun unreadItemAtIndex(position: Int, showSnackbar: Boolean = true) { private fun unreadItemAtIndex(position: Int, showSnackbar: Boolean = true) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(items[position].id) repository.unmarkAsRead(items[position])
// Todo: SharedItems.unreadItem(app, api, db, items[position]) // Todo: SharedItems.unreadItem(app, api, db, items[position])
// TODO: update db // TODO: update db

View File

@ -76,19 +76,19 @@ override fun doWork(): Result {
actions.forEach { action -> actions.forEach { action ->
when { when {
action.read -> doAndReportOnFail( action.read -> doAndReportOnFail(
repository.markAsRead(action.articleId.toInt()), repository.markAsReadById(action.articleId.toInt()),
action action
) )
action.unread -> doAndReportOnFail( action.unread -> doAndReportOnFail(
repository.unmarkAsRead(action.articleId.toInt()), repository.unmarkAsReadById(action.articleId.toInt()),
action action
) )
action.starred -> doAndReportOnFail( action.starred -> doAndReportOnFail(
repository.starr(action.articleId.toInt()), repository.starrById(action.articleId.toInt()),
action action
) )
action.unstarred -> doAndReportOnFail( action.unstarred -> doAndReportOnFail(
repository.unstarr(action.articleId.toInt()), repository.unstarrById(action.articleId.toInt()),
action action
) )
} }

View File

@ -169,7 +169,7 @@ class ArticleFragment : Fragment(), DIAware {
R.id.unread_action -> if (context != null) { R.id.unread_action -> if (context != null) {
if (this@ArticleFragment.item.unread) { if (this@ArticleFragment.item.unread) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(this@ArticleFragment.item.id) repository.markAsRead(this@ArticleFragment.item)
} }
this@ArticleFragment.item.unread = false this@ArticleFragment.item.unread = false
Toast.makeText( Toast.makeText(
@ -179,7 +179,7 @@ class ArticleFragment : Fragment(), DIAware {
).show() ).show()
} else { } else {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(this@ArticleFragment.item.id) repository.unmarkAsRead(this@ArticleFragment.item)
} }
this@ArticleFragment.item.unread = true this@ArticleFragment.item.unread = true
Toast.makeText( Toast.makeText(

View File

@ -46,16 +46,16 @@ class AppColors(a: Activity) {
colorBackground = if (isDarkTheme) { colorBackground = if (isDarkTheme) {
a.setTheme(R.style.NoBarDark) a.setTheme(R.style.NoBarDark)
R.color.darkBackground a.resources.getColor(R.color.darkBackground)
} else { } else {
a.setTheme(R.style.NoBar) a.setTheme(R.style.NoBar)
R.color.grey_50 a.resources.getColor(R.color.grey_50)
} }
textColor = if (isDarkTheme) { textColor = if (isDarkTheme) {
R.color.white a.resources.getColor(R.color.white)
} else { } else {
R.color.grey_900 a.resources.getColor(R.color.grey_900)
} }
} }
} }

View File

@ -18,3 +18,5 @@ kotlin.native.enableDependencyPropagation=false
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.mpp.enableGranularSourceSetsMetadata=true
org.gradle.parallel=true
ignoreGitVersion=false

View File

@ -36,6 +36,9 @@ kotlin {
//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.1.0")
} }
} }
val commonTest by getting { val commonTest by getting {
@ -86,4 +89,8 @@ android {
minSdk = 21 minSdk = 21
targetSdk = 31 targetSdk = 31
} }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
} }

View File

@ -1,5 +0,0 @@
package bou.amine.apps.readerforselfossv2
actual class Platform actual constructor() {
actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

View File

@ -0,0 +1,35 @@
package bou.amine.apps.readerforselfossv2.utils
import android.annotation.SuppressLint
import android.text.format.DateUtils
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(private val apiMajorVersion: Int) {
actual fun parseDate(dateString: String): Long {
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
return if (apiMajorVersion >= 4) {
OffsetDateTime.parse(dateString).toInstant().toEpochMilli()
} else {
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(
ZoneOffset.UTC).toEpochMilli()
}
}
actual fun parseRelativeDate(dateString: String): String {
val date = parseDate(dateString)
return " " + DateUtils.getRelativeTimeSpanString(
date,
Instant.now().toEpochMilli(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE
)
}
}

View File

@ -0,0 +1,17 @@
package bou.amine.apps.readerforselfossv2.utils
import android.content.Context
import com.github.`ln-12`.library.ConnectivityStatus
actual class NetworkStatus(context: Context) {
private val connectivityStatus = ConnectivityStatus(context)
actual val current = connectivityStatus.isNetworkConnected
actual fun start() {
connectivityStatus.start()
}
actual fun stop() {
connectivityStatus.stop()
}
}

View File

@ -1,12 +0,0 @@
package bou.amine.apps.readerforselfossv2
import org.junit.Assert.assertTrue
import org.junit.Test
class AndroidGreetingTest {
@Test
fun testExample() {
assertTrue("Check Android is mentioned", Greeting().greeting().contains("Android"))
}
}

View File

@ -1,7 +0,0 @@
package bou.amine.apps.readerforselfossv2
class Greeting {
fun greeting(): String {
return "Hello, ${Platform().platform}!"
}
}

View File

@ -1,5 +0,0 @@
package bou.amine.apps.readerforselfossv2
expect class Platform() {
val platform: String
}

View File

@ -5,16 +5,18 @@ import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import bou.amine.apps.readerforselfossv2.utils.DateUtils import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.NetworkStatus
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
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.launch import kotlinx.coroutines.launch
class Repository(private val api: SelfossApi, private val apiDetails: ApiDetailsService) { class Repository(private val api: SelfossApi, private val apiDetails: ApiDetailsService, networkStatus: NetworkStatus) {
val settings = Settings() val settings = Settings()
var items = ArrayList<SelfossModel.Item>() var items = ArrayList<SelfossModel.Item>()
private val isConnectionAvailable = networkStatus.current
var baseUrl = apiDetails.getBaseUrl() var baseUrl = apiDetails.getBaseUrl()
lateinit var dateUtils: DateUtils lateinit var dateUtils: DateUtils
@ -36,6 +38,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
set(value) {field = if (value < 0) { 0 } else { value } } set(value) {field = if (value < 0) { 0 } else { value } }
init { init {
networkStatus.start()
// TODO: Dispatchers.IO not available in KMM, an alternative solution should be found // TODO: Dispatchers.IO not available in KMM, an alternative solution should be found
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
updateApiVersion() updateApiVersion()
@ -45,40 +48,65 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
} }
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// TODO: Check connectivity, use the updatedSince parameter // TODO: Use the updatedSince parameter
val fetchedItems = api.getItems(displayedItems.type, if (isConnectionAvailable.value) {
val fetchedItems = api.getItems(
displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(), settings.getString("prefer_api_items_number", "200").toInt(),
offset = 0, offset = 0,
tagFilter?.tag, tagFilter?.tag,
sourceFilter?.id?.toLong(), sourceFilter?.id?.toLong(),
searchFilter, searchFilter,
null) null
)
if (fetchedItems != null) { if (fetchedItems != null) {
items = ArrayList(fetchedItems) items = ArrayList(fetchedItems)
} }
} else {
// TODO: Provide an error message if the connection is not available.
// TODO: Get items from the database
}
return items return items
} }
suspend fun getOlderItems(): ArrayList<SelfossModel.Item> { suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
// TODO: Check connectivity if (isConnectionAvailable.value) {
val offset = items.size val offset = items.size
val fetchedItems = api.getItems(displayedItems.type, val fetchedItems = api.getItems(
displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(), settings.getString("prefer_api_items_number", "200").toInt(),
offset, offset,
tagFilter?.tag, tagFilter?.tag,
sourceFilter?.id?.toLong(), sourceFilter?.id?.toLong(),
searchFilter, searchFilter,
null) null
)
if (fetchedItems != null) { if (fetchedItems != null) {
appendItems(fetchedItems) appendItems(fetchedItems)
} }
} else {
// TODO: Provide an error message
}
return items return items
} }
suspend fun allItems(itemType: ItemType): List<SelfossModel.Item>? = suspend fun allItems(itemType: ItemType): List<SelfossModel.Item>? {
api.getItems(itemType.type, 200, 0, tagFilter?.tag, sourceFilter?.id?.toLong(), searchFilter, null) return if (isConnectionAvailable.value) {
api.getItems(
itemType.type,
200,
0,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null
)
} else {
null
}
}
private fun appendItems(fetchedItems: List<SelfossModel.Item>) { private fun appendItems(fetchedItems: List<SelfossModel.Item>) {
// TODO: Store in DB if enabled by user // TODO: Store in DB if enabled by user
@ -94,8 +122,8 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
} }
suspend fun reloadBadges(): Boolean { suspend fun reloadBadges(): Boolean {
// TODO: Check connectivity, calculate from DB
var success = false var success = false
if (isConnectionAvailable.value) {
val response = api.stats() val response = api.stats()
if (response != null) { if (response != null) {
badgeUnread = response.unread badgeUnread = response.unread
@ -103,72 +131,121 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
badgeStarred = response.starred badgeStarred = response.starred
success = true success = true
} }
} else {
// TODO: Compute badges from database
}
return success return success
} }
suspend fun getTags(): List<SelfossModel.Tag>? { suspend fun getTags(): List<SelfossModel.Tag>? {
// TODO: Check success, store in DB // TODO: Store in DB
return api.tags() return if (isConnectionAvailable.value) {
api.tags()
} else {
// TODO: Compute from database
null
}
} }
suspend fun getSpouts(): Map<String, SelfossModel.Spout>? { suspend fun getSpouts(): Map<String, SelfossModel.Spout>? {
// TODO: Check success, store in DB // TODO: Store in DB
return api.spouts() return if (isConnectionAvailable.value) {
api.spouts()
} else {
// TODO: Compute from database
null
}
} }
suspend fun getSources(): ArrayList<SelfossModel.Source>? { suspend fun getSources(): ArrayList<SelfossModel.Source>? {
// TODO: Check success // TODO: Store in DB
return api.sources() return if (isConnectionAvailable.value) {
api.sources()
} else {
// TODO: Compute from database
null
}
} }
suspend fun markAsRead(id: Int): Boolean { suspend fun markAsRead(item: SelfossModel.Item): Boolean {
val success = markAsReadById(item.id)
if (success) {
markAsReadLocally(item)
}
return success
}
suspend fun markAsReadById(id: Int): Boolean {
var success = false
if (isConnectionAvailable.value) {
success = api.markAsRead(id.toString())?.isSuccess == true
}
return success
}
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
// TODO: Check internet connection // TODO: Check internet connection
val success = api.markAsRead(id.toString())?.isSuccess == true val success = unmarkAsReadById(item.id)
if (success) { if (success) {
markAsReadLocally(items.first {it.id == id}) unmarkAsReadLocally(item)
} }
return success return success
} }
suspend fun unmarkAsRead(id: Int): Boolean { suspend fun unmarkAsReadById(id: Int): Boolean {
// TODO: Check internet connection // TODO: Check internet connection
val success = api.unmarkAsRead(id.toString())?.isSuccess == true var success = false
if (isConnectionAvailable.value) {
if (success) { success = api.unmarkAsRead(id.toString())?.isSuccess == true
unmarkAsReadLocally(items.first {it.id == id})
} }
return success return success
} }
suspend fun starr(id: Int): Boolean { suspend fun starr(item: SelfossModel.Item): Boolean {
// TODO: Check success, store in DB val success = starrById(item.id)
val success = api.starr(id.toString())?.isSuccess == true
if (success) { if (success) {
starrLocally(items.first {it.id == id}) starrLocally(item)
} }
return success return success
} }
suspend fun unstarr(id: Int): Boolean { suspend fun starrById(id: Int): Boolean {
// TODO: Check internet connection var success = false
val success = api.unstarr(id.toString())?.isSuccess == true if (isConnectionAvailable.value) {
success = api.starr(id.toString())?.isSuccess == true
if (success) {
unstarrLocally(items.first {it.id == id})
} }
return success return success
} }
suspend fun markAllAsRead(ids: List<Int>): Boolean { suspend fun unstarr(item: SelfossModel.Item): Boolean {
// TODO: Check Internet connectivity, store in DB val success = unstarrById(item.id)
val success = api.markAllAsRead(ids.map { it.toString() })?.isSuccess == true
if (success) { if (success) {
val itemsToMark = items.filter { it.id in ids } unstarrLocally(item)
for (item in itemsToMark) { }
return success
}
suspend fun unstarrById(id: Int): Boolean {
var success = false
if (isConnectionAvailable.value) {
success = api.unstarr(id.toString())?.isSuccess == true
}
return success
}
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false
if (isConnectionAvailable) {
success = api.markAllAsRead(items.map { it.id.toString() })?.isSuccess == true
}
if (success) {
for (item in items) {
markAsReadLocally(item) markAsReadLocally(item)
} }
} }
@ -214,46 +291,52 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
tags: String, tags: String,
filter: String filter: String
): Boolean { ): Boolean {
// TODO: Check connectivity var response = false
val response = api.createSourceForVersion( if (isConnectionAvailable.value) {
response = api.createSourceForVersion(
title, title,
url, url,
spout, spout,
tags, tags,
filter, filter,
apiMajorVersion apiMajorVersion
) )?.isSuccess == true
}
return response != null return response
} }
suspend fun deleteSource(id: Int): Boolean { suspend fun deleteSource(id: Int): Boolean {
// TODO: Check connectivity, store in DB // TODO: Store in DB
var success = false var success = false
if (isConnectionAvailable.value) {
val response = api.deleteSource(id) val response = api.deleteSource(id)
if (response != null) { if (response != null) {
success = response.isSuccess success = response.isSuccess
} }
}
return success return success
} }
suspend fun updateRemote(): Boolean { suspend fun updateRemote(): Boolean {
// TODO: Handle connectivity issues var response = false
val response = api.update() if (isConnectionAvailable.value) {
return response?.isSuccess ?: false response = api.update()?.isSuccess == true
}
return response
} }
suspend fun login(): Boolean { suspend fun login(): Boolean {
var result = false var result = false
if (isConnectionAvailable.value) {
try { try {
val response = api.login() val response = api.login()
if (response != null && response.isSuccess) { result = response?.isSuccess == true
result = true
}
} catch (cause: Throwable) { } catch (cause: Throwable) {
Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote") Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote")
} }
}
return result return result
} }
@ -271,13 +354,14 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
} }
private suspend fun updateApiVersion() { private suspend fun updateApiVersion() {
// TODO: Handle connectivity issues apiMajorVersion = settings.getInt("apiVersionMajor", 0)
if (isConnectionAvailable.value) {
val fetchedVersion = api.version() val fetchedVersion = api.version()
if (fetchedVersion != null) { if (fetchedVersion != null) {
apiMajorVersion = fetchedVersion.getApiMajorVersion() apiMajorVersion = fetchedVersion.getApiMajorVersion()
settings.putInt("apiVersionMajor", apiMajorVersion) settings.putInt("apiVersionMajor", apiMajorVersion)
} else { }
apiMajorVersion = settings.getInt("apiVersionMajor", 0)
} }
} }

View File

@ -1,7 +1,5 @@
package bou.amine.apps.readerforselfossv2.rest package bou.amine.apps.readerforselfossv2.rest
import android.os.Parcelable
import android.text.Html
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
class SelfossModel { class SelfossModel {
@ -13,7 +11,7 @@ class SelfossModel {
val unread: Int val unread: Int
) { ) {
fun getTitleDecoded(): String { fun getTitleDecoded(): String {
return Html.fromHtml(tag).toString() return tag // TODO Html.fromHtml(tag).toString()
} }
} }

View File

@ -1,42 +1,16 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
//import android.text.format.DateUtils
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
fun SelfossModel.Item.parseDate(dateUtils: bou.amine.apps.readerforselfossv2.utils.DateUtils): Instant =
fun SelfossModel.Item.parseDate(dateUtils: DateUtils): Long =
dateUtils.parseDate(this.datetime) dateUtils.parseDate(this.datetime)
fun SelfossModel.Item.parseRelativeDate(dateUtils: bou.amine.apps.readerforselfossv2.utils.DateUtils): String = fun SelfossModel.Item.parseRelativeDate(dateUtils: DateUtils): String =
dateUtils.parseRelativeDate(this.datetime) dateUtils.parseRelativeDate(this.datetime)
class DateUtils(private val apiMajorVersion: Int) { expect class DateUtils(apiMajorVersion: Int) {
fun parseDate(dateString: String): Instant { fun parseDate(dateString: String): Long
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss" fun parseRelativeDate(dateString: String): String
return if (apiMajorVersion >= 4) {
OffsetDateTime.parse(dateString).toInstant()
} else {
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(ZoneOffset.UTC)
}
}
fun parseRelativeDate(dateString: String): String {
val date = parseDate(dateString)
// TODO:
// return " " + DateUtils.getRelativeTimeSpanString(
// date.toEpochMilli(),
// Instant.now().toEpochMilli(),
// 60000L, // DateUtils.MINUTE_IN_MILLIS,
// 262144 // DateUtils.FORMAT_ABBREV_RELATIVE
// )
return dateString
}
} }

View File

@ -0,0 +1,9 @@
package bou.amine.apps.readerforselfossv2.utils
import kotlinx.coroutines.flow.MutableStateFlow
expect class NetworkStatus {
val current: MutableStateFlow<Boolean>
fun start()
fun stop()
}

View File

@ -1,12 +0,0 @@
package bou.amine.apps.readerforselfossv2
import kotlin.test.Test
import kotlin.test.assertTrue
class CommonGreetingTest {
@Test
fun testExample() {
assertTrue(Greeting().greeting().contains("Hello"), "Check 'Hello' is mentioned")
}
}

View File

@ -1,7 +0,0 @@
package bou.amine.apps.readerforselfossv2
import platform.UIKit.UIDevice
actual class Platform actual constructor() {
actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

View File

@ -0,0 +1,27 @@
package bou.amine.apps.readerforselfossv2.utils
import com.github.`ln-12`.library.ConnectivityStatus
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
actual class NetworkStatus {
private val connectivityStatus: ConnectivityStatus = ConnectivityStatus()
actual val current: MutableStateFlow<Boolean> = connectivityStatus.isNetworkConnected
actual fun start() {
connectivityStatus.start()
}
actual fun stop() {
connectivityStatus.stop()
}
fun getStatus(success: (Boolean) -> Unit) {
MainScope().launch {
connectivityStatus.isNetworkConnected.collect { status ->
success(status)
}
}
}
}