Compare commits

..

11 Commits

Author SHA1 Message Date
0aac2cba0b Android gradle plugin
All checks were successful
continuous-integration/drone/pr Build is passing
2023-09-07 11:55:15 +02:00
42e1c1ebbd Downgrade gradle version
Some checks failed
continuous-integration/drone/pr Build is failing
2023-08-22 16:49:48 +02:00
a67a2a0927 Use forEachIndexed to support older apis
Some checks failed
continuous-integration/drone/pr Build is failing
2023-08-21 23:25:35 +02:00
24beee070c Remove unused indexed function call
Some checks failed
continuous-integration/drone/pr Build is failing
2023-08-19 11:56:54 +02:00
1e6bc57751 Revert to Java version 11 2023-08-19 11:56:54 +02:00
85d966f54b Fix naming and reintroduce the xmlns
Some checks failed
continuous-integration/drone/pr Build is failing
2023-08-10 21:56:07 +02:00
3fff8eeeb6 Handle most HTTP client creation in common code 2023-08-10 21:43:13 +02:00
5eb5a5658f Add translations
All checks were successful
continuous-integration/drone/pr Build is passing
2023-07-15 13:48:23 +02:00
3646658cb1 Fix R8 compilation problem
Some checks failed
continuous-integration/drone/pr Build is failing
2023-07-15 13:25:51 +02:00
b793301706 Add a login switch to disable SSL verification
Some checks failed
continuous-integration/drone/pr Build is failing
2023-07-13 14:55:48 +02:00
313042955c Upgrade to gradle 8 2023-07-13 14:55:48 +02:00
68 changed files with 1483 additions and 1944 deletions

View File

@@ -3,38 +3,35 @@ type: docker
name: test name: test
steps: steps:
- name: Lint
failure: ignore
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- echo "Install linters..."
- curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
- curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip && unzip detekt-cli-1.23.1.zip
- echo "---------------------------------------------------------"
- echo "Linting..."
- ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' || true
- echo "---------------------------------------------------------"
- echo "Detecting..."
- ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
- echo "---------------------------------------------------------"
command_timeout: 1m
- name: BuildAndTest - name: BuildAndTest
image: mingc/android-build-box:latest image: mingc/android-build-box:latest
commands: commands:
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Configure gradle..." - echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Configure java..." - echo "Building..."
- . ~/.bash_profile - ./gradlew build -x test
- jenv global 17.0
- java --version
- date
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Building and testing..." - echo "Testing..."
- ./gradlew build
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- ./gradlew koverMergedXmlReport
environment:
TZ: Europe/Paris
SONAR_HOST_URL:
from_secret: sonarScannerHostUrl
SONAR_LOGIN:
from_secret: sonarScannerLogin
- name: Analyse
image: kytay/sonar-node-plugin
settings:
sonar_host:
from_secret: sonarScannerHostUrl
sonar_token:
from_secret: sonarScannerLogin
use_node_version: 16.18.1
sonar_debug: true
sonar_project_settings: ./sonar-project.properties
trigger: trigger:
event: event:
- push - push
@@ -53,24 +50,20 @@ steps:
- git fetch --tags -p - git fetch --tags -p
- PREV=$(git describe --tags --abbrev=0) - PREV=$(git describe --tags --abbrev=0)
- ./build.sh --publish --from-ci - ./build.sh --publish --from-ci
- git remote add pushing https://$GITEA_USR:$GITEA_PASS@gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform.git
- VER=$(git describe --tags --abbrev=0) - VER=$(git describe --tags --abbrev=0)
- CHANGELOG=$(git log $PREV..HEAD --pretty="- %s") - CHANGELOG=$(git log $PREV..HEAD --pretty="- %s")
- echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md - echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md
- git add CHANGELOG.md - git add CHANGELOG.md
- git commit -m "Changelog for $VER [CI SKIP]" - git commit -m "Changelog for $VER [CI SKIP]"
- git push pushing master
- git push pushing --tags
environment: environment:
TZ: Europe/Paris TZ: Europe/Paris
GITEA_USR:
- name: git-push from_secret: giteaUsr
image: appleboy/drone-git-push GITEA_PASS:
settings: from_secret: giteaPass
branch: master
remote:
from_secret: remoteUrl
followtags: true
ssh_key:
from_secret: privateKey
skip_verify: true
- name: scpFiles - name: scpFiles
image: appleboy/drone-scp image: appleboy/drone-scp
@@ -114,10 +107,10 @@ steps:
- git fetch --tags - git fetch --tags
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Configure gradle..." - echo "Configure gradle..."
- mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties - mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=false\npushCache=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Generate APK" - echo "Generate APK"
- ./gradlew :androidApp:assembleGithubConfigRelease - ./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

View File

@@ -1,11 +1,3 @@
**v123102841**
- chore: cleaning ci steps and upgrading dependencies.
- feat: Self signed ssl support.
- Changelog for v123061811 [CI SKIP]
--------------------------------------------------------------------
**v123061811** **v123061811**
- feat: Added confirmation dialog for disconnect item menu. - feat: Added confirmation dialog for disconnect item menu.

View File

@@ -12,7 +12,7 @@ plugins {
} }
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String { fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
val result: String = ByteArrayOutputStream().use { outputStream -> var result: String = ByteArrayOutputStream().use { outputStream ->
project.exec { project.exec {
commandLine = cmd.split(" ") commandLine = cmd.split(" ")
standardOutput = outputStream standardOutput = outputStream
@@ -25,7 +25,7 @@ fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
fun gitVersion(): String { fun gitVersion(): String {
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true) val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
val process = if (maybeTagOfCurrentCommit.isEmpty()) { var process = if (maybeTagOfCurrentCommit.isEmpty()) {
println("No tag on current commit. Will take the latest one.") println("No tag on current commit. Will take the latest one.")
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1") execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
} else { } else {
@@ -57,22 +57,23 @@ android {
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
// Flag to enable support for the new language APIs // Flag to enable support for the new language APIs
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_11
} }
// For Kotlin projects // For Kotlin projects
kotlinOptions { kotlinOptions {
jvmTarget = "17" jvmTarget = "11"
} }
compileSdk = 34 compileSdk = 33
buildToolsVersion = "33.0.0"
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
} }
defaultConfig { defaultConfig {
applicationId = "bou.amine.apps.readerforselfossv2.android" applicationId = "bou.amine.apps.readerforselfossv2.android"
minSdk = 25 minSdk = 21
targetSdk = 34 targetSdk = 33
versionCode = versionCodeFromGit() versionCode = versionCodeFromGit()
versionName = versionNameFromGit() versionName = versionNameFromGit()
@@ -85,7 +86,7 @@ android {
// tests // tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
packaging { packagingOptions {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
@@ -114,25 +115,25 @@ dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
implementation(project(":shared")) implementation(project(":shared"))
implementation("com.google.android.material:material:1.9.0") implementation("com.google.android.material:material:1.5.0")
implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.appcompat:appcompat:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
implementation("androidx.preference:preference-ktx:1.2.1") implementation("androidx.preference:preference-ktx:1.1.1")
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs"))) implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
// Android Support // Android Support
implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.appcompat:appcompat:1.4.1")
implementation("com.google.android.material:material:1.9.0") implementation("com.google.android.material:material:1.5.0")
implementation("androidx.recyclerview:recyclerview:1.3.1") implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
implementation("androidx.legacy:legacy-support-v4:1.0.0") implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.vectordrawable:vectordrawable:1.2.0-beta01") implementation("androidx.vectordrawable:vectordrawable:1.2.0-alpha02")
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.annotation:annotation:1.7.0") implementation("androidx.annotation:annotation:1.3.0")
implementation("androidx.work:work-runtime-ktx:2.8.1") implementation("androidx.work:work-runtime-ktx:2.7.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("org.jsoup:jsoup:1.15.4") implementation("org.jsoup:jsoup:1.14.3")
//multidex //multidex
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
@@ -153,7 +154,7 @@ dependencies {
// Pager // Pager
implementation("me.relex:circleindicator:2.1.6") implementation("me.relex:circleindicator:2.1.6")
implementation("androidx.viewpager2:viewpager2:1.1.0-beta02") implementation("androidx.viewpager2:viewpager2:1.1.0-beta01")
//Dependency Injection //Dependency Injection
implementation("org.kodein.di:kodein-di:7.14.0") implementation("org.kodein.di:kodein-di:7.14.0")
@@ -169,7 +170,7 @@ dependencies {
//PhotoView //PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
@@ -182,7 +183,7 @@ dependencies {
//test //test
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.12.0") testImplementation("io.mockk:mockk:1.12.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
implementation("ch.acra:acra-http:$acraVersion") implementation("ch.acra:acra-http:$acraVersion")

View File

@@ -43,7 +43,9 @@ import org.kodein.di.instance
import java.security.MessageDigest import java.security.MessageDigest
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware { class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware {
private var items: ArrayList<SelfossModel.Item> = ArrayList() private var items: ArrayList<SelfossModel.Item> = ArrayList()
private var elementsShown: ItemType = ItemType.UNREAD private var elementsShown: ItemType = ItemType.UNREAD
@@ -61,14 +63,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private var fromTabShortcut: Boolean = false private var fromTabShortcut: Boolean = false
private val settingsLauncher = private val settingsLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
appSettingsService.refreshUserSettings() appSettingsService.refreshUserSettings()
} }
override val di by closestDI() override val di by closestDI()
private val repository: Repository by instance() private val repository : Repository by instance()
private val appSettingsService: AppSettingsService by instance() private val appSettingsService : AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -90,6 +92,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleSwipeRefreshLayout() handleSwipeRefreshLayout()
if (appSettingsService.isItemCachingEnabled()) { if (appSettingsService.isItemCachingEnabled()) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
repository.tryToCacheItemsAndGetNewOnes() repository.tryToCacheItemsAndGetNewOnes()
@@ -101,7 +104,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.swipeRefreshLayout.setColorSchemeResources( binding.swipeRefreshLayout.setColorSchemeResources(
R.color.refresh_progress_1, R.color.refresh_progress_1,
R.color.refresh_progress_2, R.color.refresh_progress_2,
R.color.refresh_progress_3, R.color.refresh_progress_3
) )
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
repository.offlineOverride = false repository.offlineOverride = false
@@ -112,8 +115,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
} }
val swipeDirs = val swipeDirs = if (appSettingsService.getPublicAccess()) {
if (appSettingsService.getPublicAccess()) {
0 0
} else { } else {
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
@@ -122,31 +124,28 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val simpleItemTouchCallback = val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback( object : ItemTouchHelper.SimpleCallback(
0, 0,
swipeDirs, swipeDirs
) { ) {
override fun getSwipeDirs( override fun getSwipeDirs(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder
): Int = ): Int =
if (elementsShown == ItemType.STARRED) { if (elementsShown == ItemType.STARRED) {
0 0
} else { } else {
super.getSwipeDirs( super.getSwipeDirs(
recyclerView, recyclerView,
viewHolder, viewHolder
) )
} }
override fun onMove( override fun onMove(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder
): Boolean = false ): Boolean = false
override fun onSwiped( override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {
viewHolder: RecyclerView.ViewHolder,
swipeDir: Int,
) {
val position = viewHolder.bindingAdapterPosition val position = viewHolder.bindingAdapterPosition
val i = items.elementAtOrNull(position) val i = items.elementAtOrNull(position)
@@ -163,7 +162,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
"Found null when swiping at positon $position.", "Found null when swiping at positon $position.",
Toast.LENGTH_LONG, Toast.LENGTH_LONG
).show() ).show()
} }
} }
@@ -172,10 +171,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView) ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
} }
private fun updateBottomBarBadgeCount( private fun updateBottomBarBadgeCount(badge: TextBadgeItem, count: Int) {
badge: TextBadgeItem,
count: Int,
) {
if (count > 0) { if (count > 0) {
badge badge
.setText(count.toString()) .setText(count.toString())
@@ -186,16 +182,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
private fun handleBottomBar() { private fun handleBottomBar() {
tabNewBadge =
TextBadgeItem() tabNewBadge = TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false).hide(false)
tabArchiveBadge = tabArchiveBadge = TextBadgeItem()
TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false).hide(false)
tabStarredBadge = tabStarredBadge = TextBadgeItem()
TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false).hide(false)
@@ -224,19 +218,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val tabNew = val tabNew =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_fiber_new_black_24dp, R.drawable.ic_tab_fiber_new_black_24dp,
getString(R.string.tab_new), getString(R.string.tab_new)
) )
.setBadgeItem(tabNewBadge) .setBadgeItem(tabNewBadge)
val tabArchive = val tabArchive =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_archive_black_24dp, R.drawable.ic_tab_archive_black_24dp,
getString(R.string.tab_read), getString(R.string.tab_read)
) )
.setBadgeItem(tabArchiveBadge) .setBadgeItem(tabArchiveBadge)
val tabStarred = val tabStarred =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_favorite_black_24dp, R.drawable.ic_tab_favorite_black_24dp,
getString(R.string.tab_favs), getString(R.string.tab_favs)
).setActiveColorResource(R.color.pink) ).setActiveColorResource(R.color.pink)
.setBadgeItem(tabStarredBadge) .setBadgeItem(tabStarredBadge)
@@ -277,6 +271,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getElementsAccordingToTab() getElementsAccordingToTab()
} }
private fun handleGDPRDialog(GDPRShown: Boolean) { private fun handleGDPRDialog(GDPRShown: Boolean) {
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256") val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(appSettingsService.getBaseUrl().toByteArray()) messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
@@ -286,7 +281,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
alertDialog.setMessage(getString(R.string.gdpr_dialog_message)) alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
alertDialog.setButton( alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL,
"OK", "OK"
) { dialog, _ -> ) { dialog, _ ->
appSettingsService.settings.putBoolean("GDPR_shown", true) appSettingsService.settings.putBoolean("GDPR_shown", true)
dialog.dismiss() dialog.dismiss()
@@ -303,19 +298,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
when (currentManager) { when (currentManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
if (!appSettingsService.isCardViewEnabled()) { if (!appSettingsService.isCardViewEnabled()) {
layoutManager = layoutManager = GridLayoutManager(
GridLayoutManager(
this, this,
calculateNoOfColumns(), calculateNoOfColumns()
) )
binding.recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
} }
is GridLayoutManager -> is GridLayoutManager ->
if (appSettingsService.isCardViewEnabled()) { if (appSettingsService.isCardViewEnabled()) {
layoutManager = layoutManager = StaggeredGridLayoutManager(
StaggeredGridLayoutManager(
calculateNoOfColumns(), calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL, StaggeredGridLayoutManager.VERTICAL
) )
layoutManager.gapStrategy = layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
@@ -324,17 +317,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
else -> else ->
if (currentManager == null) { if (currentManager == null) {
if (!appSettingsService.isCardViewEnabled()) { if (!appSettingsService.isCardViewEnabled()) {
layoutManager = layoutManager = GridLayoutManager(
GridLayoutManager(
this, this,
calculateNoOfColumns(), calculateNoOfColumns()
) )
binding.recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
} else { } else {
layoutManager = layoutManager = StaggeredGridLayoutManager(
StaggeredGridLayoutManager(
calculateNoOfColumns(), calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL, StaggeredGridLayoutManager.VERTICAL
) )
layoutManager.gapStrategy = layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
@@ -345,11 +336,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
private fun handleBottomBarActions() { private fun handleBottomBarActions() {
binding.bottomBar.setTabSelectedListener( binding.bottomBar.setTabSelectedListener(object : BottomNavigationBar.OnTabSelectedListener {
object : BottomNavigationBar.OnTabSelectedListener {
override fun onTabUnselected(position: Int) = Unit override fun onTabUnselected(position: Int) = Unit
override fun onTabReselected(position: Int) { override fun onTabReselected(position: Int) {
when (val layoutManager = binding.recyclerView.adapter) { when (val layoutManager = binding.recyclerView.adapter) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPositions(null)[0] == 0) { if (layoutManager.findFirstCompletelyVisibleItemPositions(null)[0] == 0) {
@@ -377,8 +368,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
fetchOnEmptyList() fetchOnEmptyList()
} }
}, })
)
} }
fun fetchOnEmptyList() { fun fetchOnEmptyList() {
@@ -388,13 +378,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
private fun handleInfiniteScroll() { private fun handleInfiniteScroll() {
recyclerViewScrollListener = recyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
object : RecyclerView.OnScrollListener() { override fun onScrolled(localRecycler: RecyclerView, dx: Int, dy: Int) {
override fun onScrolled(
localRecycler: RecyclerView,
dx: Int,
dy: Int,
) {
if (dy > 0) { if (dy > 0) {
val lastVisibleItem = getLastVisibleItem() val lastVisibleItem = getLastVisibleItem()
@@ -409,11 +394,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener) binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
} }
private fun getLastVisibleItem(): Int { private fun getLastVisibleItem() : Int {
return when (val manager = binding.recyclerView.layoutManager) { return when (val manager = binding.recyclerView.layoutManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
manager.findLastCompletelyVisibleItemPositions( null
null,
).last() ).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition() is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0 else -> 0
@@ -427,9 +411,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.emptyText.visibility = View.GONE binding.emptyText.visibility = View.GONE
} }
fun getElementsAccordingToTab(appendResults: Boolean = false) { fun getElementsAccordingToTab(
offset = appendResults: Boolean = false
if (appendResults && items.size > 0) { ) {
offset = if (appendResults && items.size > 0) {
items.size - 1 items.size - 1
} else { } else {
0 0
@@ -439,15 +424,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getItems(appendResults, elementsShown) getItems(appendResults, elementsShown)
} }
private fun getItems( private fun getItems(appendResults: Boolean, itemType: ItemType) {
appendResults: Boolean,
itemType: ItemType,
) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
repository.displayedItems = itemType repository.displayedItems = itemType
items = items = if (appendResults) {
if (appendResults) {
repository.getOlderItems() repository.getOlderItems()
} else { } else {
repository.getNewerItems() repository.getNewerItems()
@@ -460,8 +441,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun handleListResult(appendResults: Boolean = false) { private fun handleListResult(appendResults: Boolean = false) {
if (appendResults) { if (appendResults) {
val oldManager = binding.recyclerView.layoutManager val oldManager = binding.recyclerView.layoutManager
firstVisible = firstVisible = when (oldManager) {
when (oldManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPositions(null).last() oldManager.findFirstCompletelyVisibleItemPositions(null).last()
is GridLayoutManager -> is GridLayoutManager ->
@@ -491,8 +471,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.recyclerView.addItemDecoration( binding.recyclerView.addItemDecoration(
DividerItemDecoration( DividerItemDecoration(
this@HomeActivity, this@HomeActivity,
DividerItemDecoration.VERTICAL, DividerItemDecoration.VERTICAL
), )
) )
} }
binding.recyclerView.adapter = recyclerAdapter binding.recyclerView.adapter = recyclerAdapter
@@ -549,11 +529,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
return true return true
} }
private fun needsConfirmation( private fun needsConfirmation(titleRes: Int, messageRes: Int, doFn: () -> Unit) {
titleRes: Int,
messageRes: Int,
doFn: () -> Unit,
) {
AlertDialog.Builder(this@HomeActivity) AlertDialog.Builder(this@HomeActivity)
.setMessage(messageRes) .setMessage(messageRes)
.setTitle(titleRes) .setTitle(titleRes)
@@ -578,15 +554,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
if (updatedRemote) { if (updatedRemote) {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.refresh_success_response, R.string.refresh_success_response, Toast.LENGTH_LONG
Toast.LENGTH_LONG,
) )
.show() .show()
} else { } else {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.refresh_failer_message, R.string.refresh_failer_message,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT
).show() ).show()
} }
} }
@@ -604,7 +579,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.all_posts_read, R.string.all_posts_read,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT
).show() ).show()
tabNewBadge.removeBadge() tabNewBadge.removeBadge()
@@ -613,7 +588,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.all_posts_not_read, R.string.all_posts_not_read,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT
).show() ).show()
} }
handleListResult() handleListResult()
@@ -660,8 +635,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun handleRecurringTask() { private fun handleRecurringTask() {
if (appSettingsService.isPeriodicRefreshEnabled()) { if (appSettingsService.isPeriodicRefreshEnabled()) {
val myConstraints = val myConstraints = Constraints.Builder()
Constraints.Builder()
.setRequiresBatteryNotLow(true) .setRequiresBatteryNotLow(true)
.setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled()) .setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
.setRequiresStorageNotLow(true) .setRequiresStorageNotLow(true)
@@ -673,9 +647,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
.addTag("selfoss-loading") .addTag("selfoss-loading")
.build() .build()
WorkManager.getInstance( WorkManager.getInstance(baseContext).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork)
baseContext,
).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork)
} }
} }
} }

View File

@@ -11,8 +11,8 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivityImageBindin
import bou.amine.apps.readerforselfossv2.android.fragments.ImageFragment import bou.amine.apps.readerforselfossv2.android.fragments.ImageFragment
class ImageActivity : AppCompatActivity() { class ImageActivity : AppCompatActivity() {
private lateinit var allImages: ArrayList<String> private lateinit var allImages : ArrayList<String>
private var position: Int = 0 private var position : Int = 0
private lateinit var binding: ActivityImageBinding private lateinit var binding: ActivityImageBinding
@@ -32,41 +32,24 @@ class ImageActivity : AppCompatActivity() {
binding.pager.adapter = ScreenSlidePagerAdapter(this) binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(position, false) binding.pager.setCurrentItem(position, false)
val transitionListener = val transitionListener = object : MotionLayout.TransitionListener {
object : MotionLayout.TransitionListener { override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {
override fun onTransitionStarted( // Nothing
motionLayout: MotionLayout?, }
startId: Int,
endId: Int, override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float
) { ) {
// Nothing // Nothing
} }
override fun onTransitionChange( override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
motionLayout: MotionLayout?,
startId: Int,
endId: Int,
progress: Float,
) {
// Nothing
}
override fun onTransitionCompleted(
motionLayout: MotionLayout?,
currentId: Int,
) {
if (motionLayout?.currentState == binding.root.endState) { if (motionLayout?.currentState == binding.root.endState) {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
overridePendingTransition(0, 0) overridePendingTransition(0, 0)
} }
} }
override fun onTransitionTrigger( override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {
motionLayout: MotionLayout?,
triggerId: Int,
positive: Boolean,
progress: Float,
) {
// Nothing // Nothing
} }
} }
@@ -85,6 +68,7 @@ class ImageActivity : AppCompatActivity() {
} }
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allImages.size override fun getItemCount(): Int = allImages.size
override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position]) override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])

View File

@@ -28,7 +28,9 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class LoginActivity : AppCompatActivity(), DIAware { class LoginActivity : AppCompatActivity(), DIAware {
private var inValidCount: Int = 0 private var inValidCount: Int = 0
private var isWithLogin = false private var isWithLogin = false
@@ -38,6 +40,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance() private val appSettingsService: AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -66,6 +69,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
} }
private fun handleActions() { private fun handleActions() {
binding.passwordView.setOnEditorActionListener( binding.passwordView.setOnEditorActionListener(
TextView.OnEditorActionListener { _, id, _ -> TextView.OnEditorActionListener { _, id, _ ->
if (id == R.id.loginView || id == EditorInfo.IME_NULL) { if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
@@ -73,7 +77,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
return@OnEditorActionListener true return@OnEditorActionListener true
} }
false false
}, }
) )
binding.signInButton.setOnClickListener { attemptLogin() } binding.signInButton.setOnClickListener { attemptLogin() }
@@ -94,7 +98,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
alertDialog.setMessage(getString(R.string.base_url_error)) alertDialog.setMessage(getString(R.string.base_url_error))
alertDialog.setButton( alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL,
"OK", "OK"
) { dialog, _ -> dialog.dismiss() } ) { dialog, _ -> dialog.dismiss() }
alertDialog.show() alertDialog.show()
} }
@@ -119,6 +123,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
} }
private fun attemptLogin() { private fun attemptLogin() {
// Reset errors. // Reset errors.
binding.urlView.error = null binding.urlView.error = null
binding.loginView.error = null binding.loginView.error = null
@@ -150,7 +155,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
Toast.makeText( Toast.makeText(
applicationContext, applicationContext,
R.string.application_selfoss_only, R.string.application_selfoss_only,
Toast.LENGTH_LONG, Toast.LENGTH_LONG
).show() ).show()
} }
preferenceError() preferenceError()
@@ -164,7 +169,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
private fun failLoginDetails( private fun failLoginDetails(
password: String, password: String,
login: String, login: String
) { ) {
var lastFocusedView: View? = null var lastFocusedView: View? = null
var cancel = false var cancel = false
@@ -197,7 +202,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
alertDialog.setMessage(getString(R.string.text_wrong_url)) alertDialog.setMessage(getString(R.string.text_wrong_url))
alertDialog.setButton( alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL, AlertDialog.BUTTON_NEUTRAL,
"OK", "OK"
) { dialog, _ -> dialog.dismiss() } ) { dialog, _ -> dialog.dismiss() }
alertDialog.show() alertDialog.show()
inValidCount = 0 inValidCount = 0
@@ -206,10 +211,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
maybeCancelAndFocusView(cancel, focusView) maybeCancelAndFocusView(cancel, focusView)
} }
private fun maybeCancelAndFocusView( private fun maybeCancelAndFocusView(cancel: Boolean, focusView: View?) {
cancel: Boolean,
focusView: View?,
) {
if (cancel) { if (cancel) {
focusView?.requestFocus() focusView?.requestFocus()
} }
@@ -223,13 +225,12 @@ class LoginActivity : AppCompatActivity(), DIAware {
.animate() .animate()
.setDuration(shortAnimTime.toLong()) .setDuration(shortAnimTime.toLong())
.alpha( .alpha(
if (show) 0F else 1F, if (show) 0F else 1F
).setListener( ).setListener(object : AnimatorListenerAdapter() {
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
} }
}, }
) )
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
@@ -237,13 +238,12 @@ class LoginActivity : AppCompatActivity(), DIAware {
.animate() .animate()
.setDuration(shortAnimTime.toLong()) .setDuration(shortAnimTime.toLong())
.alpha( .alpha(
if (show) 1F else 0F, if (show) 1F else 0F
).setListener( ).setListener(object : AnimatorListenerAdapter() {
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
} }
}, }
) )
} }

View File

@@ -8,6 +8,7 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)

View File

@@ -32,18 +32,18 @@ import org.acra.sender.HttpSender
import org.kodein.di.* import org.kodein.di.*
class MyApp : MultiDexApplication(), DIAware { class MyApp : MultiDexApplication(), DIAware {
override val di by DI.lazy { override val di by DI.lazy {
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) } bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) }
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 bind<Repository>() with singleton {
singleton {
Repository( Repository(
instance(), instance(),
instance(), instance(),
isConnectionAvailable, isConnectionAvailable,
instance(), instance()
) )
} }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
@@ -70,14 +70,13 @@ class MyApp : MultiDexApplication(), DIAware {
ProcessLifecycleOwner.get().lifecycle.addObserver( ProcessLifecycleOwner.get().lifecycle.addObserver(
AppLifeCycleObserver( AppLifeCycleObserver(
connectivityStatus, connectivityStatus,
repository, repository
), )
) )
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
viewModel.networkAvailableProvider.collect { networkAvailable -> viewModel.networkAvailableProvider.collect { networkAvailable ->
val toastMessage = val toastMessage = if (networkAvailable) {
if (networkAvailable) {
repository.handleDBActions() repository.handleDBActions()
R.string.network_connectivity_retrieved R.string.network_connectivity_retrieved
} else { } else {
@@ -87,7 +86,7 @@ class MyApp : MultiDexApplication(), DIAware {
Toast.makeText( Toast.makeText(
applicationContext, applicationContext,
toastMessage, toastMessage,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT
).show() ).show()
} }
} }
@@ -101,8 +100,7 @@ class MyApp : MultiDexApplication(), DIAware {
initAcra { initAcra {
reportFormat = StringFormat.JSON reportFormat = StringFormat.JSON
reportContent = reportContent = listOf(
listOf(
ReportField.REPORT_ID, ReportField.REPORT_ID,
ReportField.INSTALLATION_ID, ReportField.INSTALLATION_ID,
ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_CODE,
@@ -123,16 +121,16 @@ class MyApp : MultiDexApplication(), DIAware {
ReportField.USER_COMMENT, ReportField.USER_COMMENT,
ReportField.USER_CRASH_DATE, ReportField.USER_CRASH_DATE,
ReportField.USER_EMAIL, ReportField.USER_EMAIL,
ReportField.CUSTOM_DATA, ReportField.CUSTOM_DATA
) )
toast { toast {
// required //required
text = getString(R.string.crash_toast_text) text = getString(R.string.crash_toast_text)
length = Toast.LENGTH_SHORT length = Toast.LENGTH_SHORT
} }
httpSender { httpSender {
uri = uri =
"https://bugs.amine-louveau.fr/report" // best guess, you may need to adjust this "https://bugs.amine-louveau.fr/report" /*best guess, you may need to adjust this*/
basicAuthLogin = "qMEscjj89Gwt6cPR" basicAuthLogin = "qMEscjj89Gwt6cPR"
basicAuthPassword = "Yo58QFlGzFaWlBzP" basicAuthPassword = "Yo58QFlGzFaWlBzP"
httpMethod = HttpSender.Method.POST httpMethod = HttpSender.Method.POST
@@ -150,11 +148,10 @@ class MyApp : MultiDexApplication(), DIAware {
val newItemsChannelname = getString(R.string.new_items_channel_sync) val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = val newItemsChannelmChannel = NotificationChannel(
NotificationChannel(
AppSettingsService.newItemsChannelId, AppSettingsService.newItemsChannelId,
newItemsChannelname, newItemsChannelname,
newItemsChannelimportance, newItemsChannelimportance
) )
notificationManager.createNotificationChannel(mChannel) notificationManager.createNotificationChannel(mChannel)
@@ -166,11 +163,9 @@ class MyApp : MultiDexApplication(), DIAware {
val oldHandler = Thread.getDefaultUncaughtExceptionHandler() val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, e -> Thread.setDefaultUncaughtExceptionHandler { thread, e ->
if (e is NoClassDefFoundError && if (e is NoClassDefFoundError && e.stackTrace.asList().any {
e.stackTrace.asList().any {
it.toString().contains("android.view.ViewDebug") it.toString().contains("android.view.ViewDebug")
} }) {
) {
// Nothing // Nothing
} else { } else {
oldHandler.uncaughtException(thread, e) oldHandler.uncaughtException(thread, e)
@@ -180,8 +175,9 @@ class MyApp : MultiDexApplication(), DIAware {
class AppLifeCycleObserver( class AppLifeCycleObserver(
val connectivityStatus: ConnectivityStatus, val connectivityStatus: ConnectivityStatus,
val repository: Repository, val repository: Repository
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {
super.onResume(owner) super.onResume(owner)
repository.connectionMonitored = true repository.connectionMonitored = true

View File

@@ -23,6 +23,7 @@ import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class ReaderActivity : AppCompatActivity(), DIAware { class ReaderActivity : AppCompatActivity(), DIAware {
private var currentItem: Int = 0 private var currentItem: Int = 0
private lateinit var toolbarMenu: Menu private lateinit var toolbarMenu: Menu
@@ -101,15 +102,15 @@ class ReaderActivity : AppCompatActivity(), DIAware {
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
FragmentStateAdapter(fa) { FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allItems.size override fun getItemCount(): Int = allItems.size
override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position]) override fun createFragment(position: Int): Fragment =
ArticleFragment.newInstance(allItems[position])
} }
override fun onKeyDown( override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
keyCode: Int,
event: KeyEvent?,
): Boolean {
return when (keyCode) { return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> { KeyEvent.KEYCODE_VOLUME_DOWN -> {
val currentFragment = val currentFragment =
@@ -151,8 +152,10 @@ class ReaderActivity : AppCompatActivity(), DIAware {
canFavorite() canFavorite()
} }
binding.pager.registerOnPageChangeCallback( binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
super.onPageSelected(position) super.onPageSelected(position)
@@ -163,7 +166,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
} }
readItem(allItems[position]) readItem(allItems[position])
} }
}, }
) )
} }

View File

@@ -18,10 +18,11 @@ import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class SourcesActivity : AppCompatActivity(), DIAware { class SourcesActivity : AppCompatActivity(), DIAware {
private lateinit var binding: ActivitySourcesBinding private lateinit var binding: ActivitySourcesBinding
override val di by closestDI() override val di by closestDI()
private val repository: Repository by instance() private val repository : Repository by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
binding = ActivitySourcesBinding.inflate(layoutInflater) binding = ActivitySourcesBinding.inflate(layoutInflater)
@@ -57,10 +58,8 @@ class SourcesActivity : AppCompatActivity(), DIAware {
val response = repository.getSourcesDetails() val response = repository.getSourcesDetails()
if (response.isNotEmpty()) { if (response.isNotEmpty()) {
items = response items = response
val mAdapter = val mAdapter = SourcesListAdapter(
SourcesListAdapter( this@SourcesActivity, items
this@SourcesActivity,
items,
) )
binding.recyclerView.adapter = mAdapter binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged() mAdapter.notifyDataSetChanged()
@@ -68,7 +67,7 @@ class SourcesActivity : AppCompatActivity(), DIAware {
Toast.makeText( Toast.makeText(
this@SourcesActivity, this@SourcesActivity,
R.string.cant_get_sources, R.string.cant_get_sources,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT
).show() ).show()
} }
} }

View File

@@ -21,7 +21,9 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class UpsertSourceActivity : AppCompatActivity(), DIAware { class UpsertSourceActivity : AppCompatActivity(), DIAware {
private var existingSource: SelfossModel.SourceDetail? = null private var existingSource: SelfossModel.SourceDetail? = null
private var mSpoutsValue: String? = null private var mSpoutsValue: String? = null
@@ -56,6 +58,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.title = resources.getString(title) supportActionBar?.title = resources.getString(title)
maybeGetDetailsFromIntentSharing(intent) maybeGetDetailsFromIntentSharing(intent)
binding.saveBtn.setOnClickListener { binding.saveBtn.setOnClickListener {
@@ -85,14 +88,8 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
private fun handleSpoutsSpinner() { private fun handleSpoutsSpinner() {
val spoutsKV = HashMap<String, String>() val spoutsKV = HashMap<String, String>()
binding.spoutsSpinner.onItemSelectedListener = binding.spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
object : AdapterView.OnItemSelectedListener { override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) {
override fun onItemSelected(
adapterView: AdapterView<*>,
view: View?,
i: Int,
l: Long,
) {
if (view != null) { if (view != null) {
val spoutName = (view as TextView).text.toString() val spoutName = (view as TextView).text.toString()
mSpoutsValue = spoutsKV[spoutName] mSpoutsValue = spoutsKV[spoutName]
@@ -104,11 +101,12 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
} }
} }
fun handleSpoutFailure(networkIssue: Boolean = false) { fun handleSpoutFailure(networkIssue: Boolean = false) {
Toast.makeText( Toast.makeText(
this@UpsertSourceActivity, this@UpsertSourceActivity,
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts, if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT
).show() ).show()
binding.progress.visibility = View.GONE binding.progress.visibility = View.GONE
} }
@@ -129,7 +127,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
ArrayAdapter( ArrayAdapter(
this@UpsertSourceActivity, this@UpsertSourceActivity,
android.R.layout.simple_spinner_item, android.R.layout.simple_spinner_item,
itemsStrings, itemsStrings
) )
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spoutsSpinner.adapter = spinnerArrayAdapter binding.spoutsSpinner.adapter = spinnerArrayAdapter
@@ -146,7 +144,9 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
} }
} }
private fun maybeGetDetailsFromIntentSharing(intent: Intent) { private fun maybeGetDetailsFromIntentSharing(
intent: Intent
) {
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) { if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
binding.sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT)) binding.sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
binding.nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE)) binding.nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
@@ -172,14 +172,13 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
} }
else -> { else -> {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val successfullyAddedSource = val successfullyAddedSource = if (existingSource != null) {
if (existingSource != null) {
repository.updateSource( repository.updateSource(
existingSource!!.id, existingSource!!.id,
binding.nameInput.text.toString(), binding.nameInput.text.toString(),
url, url,
mSpoutsValue!!, mSpoutsValue!!,
binding.tags.text.toString(), binding.tags.text.toString()
) )
} else { } else {
repository.createSource( repository.createSource(
@@ -195,7 +194,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
Toast.makeText( Toast.makeText(
this@UpsertSourceActivity, this@UpsertSourceActivity,
R.string.cant_create_source, R.string.cant_create_source,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT
).show() ).show()
} }
} }

View File

@@ -32,7 +32,7 @@ import org.kodein.di.instance
class ItemCardAdapter( class ItemCardAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<SelfossModel.Item>, override var items: ArrayList<SelfossModel.Item>,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit, override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() { ) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private val imageMaxHeight: Int = private val imageMaxHeight: Int =
@@ -42,18 +42,12 @@ class ItemCardAdapter(
override val repository: Repository by instance() override val repository: Repository by instance()
override val appSettingsService: AppSettingsService by instance() override val appSettingsService: AppSettingsService by instance()
override fun onCreateViewHolder( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
parent: ViewGroup,
viewType: Int,
): ViewHolder {
val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding) return ViewHolder(binding)
} }
override fun onBindViewHolder( override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder: ViewHolder,
position: Int,
) {
with(holder) { with(holder) {
val itm = items[position] val itm = items[position]
@@ -103,6 +97,7 @@ class ItemCardAdapter(
} }
private fun handleClickListeners() { private fun handleClickListeners() {
binding.favButton.setOnClickListener { binding.favButton.setOnClickListener {
val item = items[bindingAdapterPosition] val item = items[bindingAdapterPosition]
if (item.starred) { if (item.starred) {
@@ -135,7 +130,7 @@ class ItemCardAdapter(
bindingAdapterPosition, bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(), items[bindingAdapterPosition].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(), appSettingsService.isArticleViewerEnabled(),
app, app
) )
} }
} }

View File

@@ -23,26 +23,20 @@ import org.kodein.di.instance
class ItemListAdapter( class ItemListAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<SelfossModel.Item>, override var items: ArrayList<SelfossModel.Item>,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit, override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemListAdapter.ViewHolder>() { ) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val c: Context = app.baseContext private val c: Context = app.baseContext
override val di: DI by closestDI(app) override val di: DI by closestDI(app)
override val repository: Repository by instance() override val repository : Repository by instance()
override val appSettingsService: AppSettingsService by instance() override val appSettingsService : AppSettingsService by instance()
override fun onCreateViewHolder( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
parent: ViewGroup,
viewType: Int,
): ViewHolder {
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding) return ViewHolder(binding)
} }
override fun onBindViewHolder( override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder: ViewHolder,
position: Int,
) {
with(holder) { with(holder) {
val itm = items[position] val itm = items[position]
@@ -55,6 +49,7 @@ class ItemListAdapter(
binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate() binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate()
if (itm.getThumbnail(repository.baseUrl).isEmpty()) { if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded()) binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else { } else {
@@ -69,6 +64,7 @@ class ItemListAdapter(
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) { inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
init { init {
handleLinkOpening() handleLinkOpening()
} }
@@ -80,7 +76,7 @@ class ItemListAdapter(
bindingAdapterPosition, bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(), items[bindingAdapterPosition].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(), appSettingsService.isArticleViewerEnabled(),
app, app
) )
} }
} }

View File

@@ -28,16 +28,12 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
updateItems(this.items) updateItems(this.items)
} }
private fun unmarkSnackbar( private fun unmarkSnackbar(item: SelfossModel.Item, position: Int) {
item: SelfossModel.Item, val s = Snackbar
position: Int,
) {
val s =
Snackbar
.make( .make(
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
R.string.marked_as_read, R.string.marked_as_read,
Snackbar.LENGTH_LONG, Snackbar.LENGTH_LONG
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
unreadItemAtIndex(item, position, false) unreadItemAtIndex(item, position, false)
@@ -49,16 +45,12 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show() s.show()
} }
private fun markSnackbar( private fun markSnackbar(item: SelfossModel.Item, position: Int) {
item: SelfossModel.Item, val s = Snackbar
position: Int,
) {
val s =
Snackbar
.make( .make(
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
R.string.marked_as_unread, R.string.marked_as_unread,
Snackbar.LENGTH_LONG, Snackbar.LENGTH_LONG
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
readItemAtIndex(item, position, false) readItemAtIndex(item, position, false)
@@ -78,11 +70,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
} }
} }
private fun readItemAtIndex( private fun readItemAtIndex(item: SelfossModel.Item, position: Int, showSnackbar: Boolean = true) {
item: SelfossModel.Item,
position: Int,
showSnackbar: Boolean = true,
) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(item) repository.markAsRead(item)
} }
@@ -98,13 +86,10 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
} }
} }
private fun unreadItemAtIndex( private fun unreadItemAtIndex(item: SelfossModel.Item, position: Int, showSnackbar: Boolean = true) {
item: SelfossModel.Item,
position: Int,
showSnackbar: Boolean = true,
) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(item) repository.unmarkAsRead(item)
} }
notifyItemChanged(position) notifyItemChanged(position)
if (showSnackbar) { if (showSnackbar) {
@@ -112,13 +97,11 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
} }
} }
fun addItemAtIndex( fun addItemAtIndex(item: SelfossModel.Item, position: Int) {
item: SelfossModel.Item,
position: Int,
) {
items.add(position, item) items.add(position, item)
notifyItemInserted(position) notifyItemInserted(position)
updateItems(items) updateItems(items)
} }
fun addItemsAtEnd(newItems: List<SelfossModel.Item>) { fun addItemsAtEnd(newItems: List<SelfossModel.Item>) {
@@ -126,5 +109,6 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
items.addAll(newItems) items.addAll(newItems)
notifyItemRangeInserted(oldSize, newItems.size) notifyItemRangeInserted(oldSize, newItems.size)
updateItems(items) updateItems(items)
} }
} }

View File

@@ -28,26 +28,20 @@ import org.kodein.di.instance
class SourcesListAdapter( class SourcesListAdapter(
private val app: Activity, private val app: Activity,
private val items: ArrayList<SelfossModel.SourceDetail>, private val items: ArrayList<SelfossModel.SourceDetail>
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware { ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private lateinit var binding: SourceListItemBinding private lateinit var binding: SourceListItemBinding
override val di: DI by closestDI(app) override val di: DI by closestDI(app)
private val repository: Repository by instance() private val repository : Repository by instance()
override fun onCreateViewHolder( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
parent: ViewGroup,
viewType: Int,
): ViewHolder {
binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding.root) return ViewHolder(binding.root)
} }
override fun onBindViewHolder( override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder: ViewHolder,
position: Int,
) {
val itm = items[position] val itm = items[position]
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
@@ -73,11 +67,13 @@ class SourcesListAdapter(
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
inner class ViewHolder(private val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) { inner class ViewHolder(private val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
init { init {
handleClickListeners() handleClickListeners()
} }
private fun handleClickListeners() { private fun handleClickListeners() {
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn) val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener { deleteBtn.setOnClickListener {
@@ -92,7 +88,7 @@ class SourcesListAdapter(
Toast.makeText( Toast.makeText(
app, app,
R.string.can_delete_source, R.string.can_delete_source,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT
).show() ).show()
} }
} }
@@ -103,6 +99,7 @@ class SourcesListAdapter(
repository.setSelectedSource(source) repository.setSelectedSource(source)
app.startActivity(Intent(app, UpsertSourceActivity::class.java)) app.startActivity(Intent(app, UpsertSourceActivity::class.java))
} }
} }
} }

View File

@@ -26,15 +26,16 @@ import org.kodein.di.instance
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
class LoadingWorker(val context: Context, params: WorkerParameters) : class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params),
Worker(context, params),
DIAware { DIAware {
override val di by lazy { (applicationContext as MyApp).di } override val di by lazy { (applicationContext as MyApp).di }
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance() private val appSettingsService: AppSettingsService by instance()
override fun doWork(): Result { override fun doWork(): Result {
if (appSettingsService.isPeriodicRefreshEnabled() && isNetworkAccessible(context)) { if (appSettingsService.isPeriodicRefreshEnabled() && isNetworkAccessible(context)) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val notificationManager = val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -66,19 +67,19 @@ class LoadingWorker(val context: Context, params: WorkerParameters) :
private fun handleNewItemsNotification( private fun handleNewItemsNotification(
newItems: List<SelfossModel.Item>?, newItems: List<SelfossModel.Item>?,
notificationManager: NotificationManager, notificationManager: NotificationManager
) { ) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val apiItems = newItems.orEmpty() val apiItems = newItems.orEmpty()
val newSize = apiItems.filter { it.unread }.size val newSize = apiItems.filter { it.unread }.size
if (newSize > 0) { if (newSize > 0) {
val intent =
Intent(context, MainActivity::class.java).apply { val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
val pflags = val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
} else { } else {
0 0
@@ -89,14 +90,14 @@ class LoadingWorker(val context: Context, params: WorkerParameters) :
val newItemsNotification = val newItemsNotification =
NotificationCompat.Builder( NotificationCompat.Builder(
applicationContext, applicationContext,
AppSettingsService.newItemsChannelId, AppSettingsService.newItemsChannelId
) )
.setContentTitle(context.getString(R.string.new_items_notification_title)) .setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText( .setContentText(
context.getString( context.getString(
R.string.new_items_notification_text, R.string.new_items_notification_text,
newSize, newSize
), )
) )
.setPriority(PRIORITY_DEFAULT) .setPriority(PRIORITY_DEFAULT)
.setChannelId(AppSettingsService.newItemsChannelId) .setChannelId(AppSettingsService.newItemsChannelId)

View File

@@ -57,6 +57,7 @@ import java.net.URL
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
private const val IMAGE_JPG = "image/jpg" private const val IMAGE_JPG = "image/jpg"
class ArticleFragment : Fragment(), DIAware { class ArticleFragment : Fragment(), DIAware {
@@ -83,6 +84,7 @@ class ArticleFragment : Fragment(), DIAware {
private val mercuryApi: MercuryApi by instance() private val mercuryApi: MercuryApi by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -94,7 +96,7 @@ class ArticleFragment : Fragment(), DIAware {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle?, savedInstanceState: Bundle?
): View { ): View {
try { try {
binding = FragmentArticleBinding.inflate(inflater, container, false) binding = FragmentArticleBinding.inflate(inflater, container, false)
@@ -144,8 +146,9 @@ class ArticleFragment : Fragment(), DIAware {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show() if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
} }
} }
}, }
) )
} catch (e: InflateException) { } catch (e: InflateException) {
e.sendSilentlyWithAcraWithName("webview not available") e.sendSilentlyWithAcraWithName("webview not available")
if (context != null) { if (context != null) {
@@ -153,7 +156,7 @@ class ArticleFragment : Fragment(), DIAware {
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton( .setPositiveButton(
android.R.string.ok, android.R.string.ok
) { _, _ -> ) { _, _ ->
appSettingsService.disableArticleViewer() appSettingsService.disableArticleViewer()
requireActivity().finish() requireActivity().finish()
@@ -208,8 +211,7 @@ class ArticleFragment : Fragment(), DIAware {
when (item.itemId) { when (item.itemId) {
R.id.share_action -> requireActivity().shareLink(url, contentTitle) R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item) R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
R.id.unread_action -> R.id.unread_action -> if (context != null) {
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) repository.markAsRead(this@ArticleFragment.item)
@@ -218,7 +220,7 @@ class ArticleFragment : Fragment(), DIAware {
Toast.makeText( Toast.makeText(
context, context,
R.string.marked_as_read, R.string.marked_as_read,
Toast.LENGTH_LONG, Toast.LENGTH_LONG
).show() ).show()
} else { } else {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
@@ -228,7 +230,7 @@ class ArticleFragment : Fragment(), DIAware {
Toast.makeText( Toast.makeText(
context, context,
R.string.marked_as_unread, R.string.marked_as_unread,
Toast.LENGTH_LONG, Toast.LENGTH_LONG
).show() ).show()
} }
} }
@@ -239,14 +241,13 @@ class ArticleFragment : Fragment(), DIAware {
override fun onItemLongClick(item: MenuItem?) { override fun onItemLongClick(item: MenuItem?) {
// We do nothing // We do nothing
} }
}, }
) )
return floatingToolbar return floatingToolbar
} }
private fun refreshAlignment() { private fun refreshAlignment() {
textAlignment = textAlignment = when (appSettingsService.getActiveAllignment()) {
when (appSettingsService.getActiveAllignment()) {
1 -> "justify" 1 -> "justify"
2 -> "left" 2 -> "left"
else -> "justify" else -> "justify"
@@ -301,7 +302,7 @@ class ArticleFragment : Fragment(), DIAware {
.with(requireContext()) .with(requireContext())
.asBitmap() .asBitmap()
.load( .load(
lead_image_url, lead_image_url
) )
.apply(RequestOptions.fitCenterTransform()) .apply(RequestOptions.fitCenterTransform())
.into(binding.imageView) .into(binding.imageView)
@@ -311,13 +312,9 @@ class ArticleFragment : Fragment(), DIAware {
} }
private fun handleImageLoading() { private fun handleImageLoading() {
binding.webcontent.webViewClient = binding.webcontent.webViewClient = object : WebViewClient() {
object : WebViewClient() {
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading( override fun shouldOverrideUrlLoading(view: WebView?, url: String): Boolean {
view: WebView?,
url: String,
): Boolean {
return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
try { try {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
@@ -331,13 +328,9 @@ class ArticleFragment : Fragment(), DIAware {
} }
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun shouldInterceptRequest( override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
view: WebView,
url: String,
): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url.lowercase(Locale.US).contains(".jpg") || if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US)
url.lowercase(Locale.US)
.contains(".jpeg") .contains(".jpeg")
) { ) {
try { try {
@@ -346,7 +339,7 @@ class ArticleFragment : Fragment(), DIAware {
return WebResourceResponse( return WebResourceResponse(
IMAGE_JPG, IMAGE_JPG,
"UTF-8", "UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG), getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)
) )
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing // Do nothing
@@ -358,7 +351,7 @@ class ArticleFragment : Fragment(), DIAware {
return WebResourceResponse( return WebResourceResponse(
IMAGE_JPG, IMAGE_JPG,
"UTF-8", "UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.PNG), getBitmapInputStream(image, Bitmap.CompressFormat.PNG)
) )
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing // Do nothing
@@ -370,7 +363,7 @@ class ArticleFragment : Fragment(), DIAware {
return WebResourceResponse( return WebResourceResponse(
IMAGE_JPG, IMAGE_JPG,
"UTF-8", "UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP), getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)
) )
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing // Do nothing
@@ -387,6 +380,7 @@ class ArticleFragment : Fragment(), DIAware {
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily) val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs) val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
binding.webcontent.settings.standardFontFamily = a.getString(0) binding.webcontent.settings.standardFontFamily = a.getString(0)
binding.webcontent.visibility = View.VISIBLE binding.webcontent.visibility = View.VISIBLE
@@ -403,14 +397,11 @@ class ArticleFragment : Fragment(), DIAware {
handleImageLoading() handleImageLoading()
val gestureDetector = val gestureDetector =
GestureDetector( GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
activity,
object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean { override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick() return performClick()
} }
}, })
)
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) } binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
@@ -426,20 +417,18 @@ class ArticleFragment : Fragment(), DIAware {
e.sendSilentlyWithAcraWithName("htmlToWebview > $url") e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
} }
val fontName = val fontName = when (font) {
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" getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
else -> "" else -> ""
} }
val fontLinkAndStyle = val fontLinkAndStyle = if (font.isNotEmpty()) {
if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${ """<link href="https://fonts.googleapis.com/css?family=${
fontName.replace( fontName.replace(
" ", " ",
"+", "+"
) )
}" rel="stylesheet"> }" rel="stylesheet">
|<style> |<style>
@@ -468,7 +457,7 @@ class ArticleFragment : Fragment(), DIAware {
| color: ${ | color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and resources.getColor(R.color.colorAccent), 0xFFFFFF and resources.getColor(R.color.colorAccent)
) )
} !important; } !important;
| } | }
@@ -484,7 +473,7 @@ class ArticleFragment : Fragment(), DIAware {
| background-color: ${ | background-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data, 0xFFFFFF and colorSurface.data
) )
}; };
| } | }
@@ -492,13 +481,13 @@ class ArticleFragment : Fragment(), DIAware {
| background-color: ${ | background-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data, 0xFFFFFF and colorSurface.data
) )
} !important; } !important;
| border-color: ${ | border-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data, 0xFFFFFF and colorSurface.data
) )
} !important; } !important;
| padding: 0 !important; | padding: 0 !important;
@@ -513,7 +502,7 @@ class ArticleFragment : Fragment(), DIAware {
| background-color: ${ | background-color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and colorSurface.data, 0xFFFFFF and colorSurface.data
) )
}; };
| } | }
@@ -522,11 +511,10 @@ class ArticleFragment : Fragment(), DIAware {
|</head> |</head>
|<body> |<body>
| $contentText | $contentText
|</body> |</body>""".trimMargin(),
""".trimMargin(),
"text/html", "text/html",
"utf-8", "utf-8",
null, null
) )
} }
} }
@@ -547,13 +535,16 @@ class ArticleFragment : Fragment(), DIAware {
requireContext().openInBrowserAsNewTask(this@ArticleFragment.item) requireContext().openInBrowserAsNewTask(this@ArticleFragment.item)
} else { } else {
Exception("openInBrowserAfterFailing context is null").sendSilentlyWithAcraWithName("openInBrowserAfterFailing > $context") Exception("openInBrowserAfterFailing context is null").sendSilentlyWithAcraWithName("openInBrowserAfterFailing > $context")
} }
} }
companion object { companion object {
private const val ARG_ITEMS = "items" private const val ARG_ITEMS = "items"
fun newInstance(item: SelfossModel.Item): ArticleFragment { fun newInstance(
item: SelfossModel.Item
): ArticleFragment {
val fragment = ArticleFragment() val fragment = ArticleFragment()
val args = Bundle() val args = Bundle()
args.putParcelable(ARG_ITEMS, item.toParcelable()) args.putParcelable(ARG_ITEMS, item.toParcelable())
@@ -563,11 +554,10 @@ class ArticleFragment : Fragment(), DIAware {
} }
fun performClick(): Boolean { fun performClick(): Boolean {
if (allImages != null && ( if (allImages != null && (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE)
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
)
) { ) {
val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra) val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
val intent = Intent(activity, ImageActivity::class.java) val intent = Intent(activity, ImageActivity::class.java)
@@ -578,4 +568,6 @@ class ArticleFragment : Fragment(), DIAware {
} }
return false return false
} }
} }

View File

@@ -32,7 +32,9 @@ 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
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
private lateinit var binding: FilterFragmentBinding private lateinit var binding: FilterFragmentBinding
override val di: DI by closestDI() override val di: DI by closestDI()
private val repository: Repository by instance() private val repository: Repository by instance()
@@ -42,17 +44,18 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle?, savedInstanceState: Bundle?
): View { ): View {
binding = binding =
FilterFragmentBinding.inflate( FilterFragmentBinding.inflate(
inflater, inflater,
container, container,
false, false
) )
val context: Context? = context val context: Context? = context
if (context == null) { if (context == null) {
dismiss() dismiss()
Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView") Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
@@ -74,7 +77,9 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
return binding.root return binding.root
} }
private suspend fun handleSourceChips(context: Context) { private suspend fun handleSourceChips(
context: Context
) {
val sourceGroup = binding.sourcesGroup val sourceGroup = binding.sourcesGroup
repository.getSourcesDetailsOrStats().forEachIndexed { _, source -> repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
@@ -83,11 +88,10 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
Glide.with(context) Glide.with(context)
.load(source.getIcon(repository.baseUrl)) .load(source.getIcon(repository.baseUrl))
.into( .into(object : ViewTarget<Chip?, Drawable?>(c) {
object : ViewTarget<Chip?, Drawable?>(c) {
override fun onResourceReady( override fun onResourceReady(
resource: Drawable, resource: Drawable,
transition: Transition<in Drawable?>?, transition: Transition<in Drawable?>?
) { ) {
try { try {
c.chipIcon = resource c.chipIcon = resource
@@ -95,8 +99,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
e.sendSilentlyWithAcraWithName("sources > onResourceReady") e.sendSilentlyWithAcraWithName("sources > onResourceReady")
} }
} }
},
) })
c.text = source.title.getHtmlDecoded() c.text = source.title.getHtmlDecoded()
@@ -117,6 +121,7 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
repository.setTagFilter(null) repository.setTagFilter(null)
} }
if (repository.sourceFilter.value?.equals(source) == true) { if (repository.sourceFilter.value?.equals(source) == true) {
c.isCloseIconVisible = true c.isCloseIconVisible = true
selectedChip = c selectedChip = c
@@ -132,7 +137,9 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
} }
} }
private suspend fun handleTagChips(context: Context) { private suspend fun handleTagChips(
context: Context,
) {
val tagGroup = binding.tagsGroup val tagGroup = binding.tagsGroup
val tags = repository.getTags() val tags = repository.getTags()
@@ -145,8 +152,7 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
if (tag.color.isNotEmpty()) { if (tag.color.isNotEmpty()) {
try { try {
val gd = GradientDrawable() val gd = GradientDrawable()
val gdColor = val gdColor = try {
try {
Color.parseColor(tag.color) Color.parseColor(tag.color)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
e.sendSilentlyWithAcraWithName("color issue " + tag.color) e.sendSilentlyWithAcraWithName("color issue " + tag.color)
@@ -191,4 +197,6 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
companion object { companion object {
const val TAG = "FilterModalBottomSheet" const val TAG = "FilterModalBottomSheet"
} }
} }

View File

@@ -11,7 +11,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
class ImageFragment : Fragment() { class ImageFragment : Fragment() {
private lateinit var imageUrl: String
private lateinit var imageUrl : String
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
private var _binding: FragmentImageBinding? = null private var _binding: FragmentImageBinding? = null
private val binding get() = _binding private val binding get() = _binding
@@ -22,11 +23,7 @@ class ImageFragment : Fragment() {
imageUrl = requireArguments().getString("imageUrl")!! imageUrl = requireArguments().getString("imageUrl")!!
} }
override fun onCreateView( override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
_binding = FragmentImageBinding.inflate(inflater, container, false) _binding = FragmentImageBinding.inflate(inflater, container, false)
val view = binding?.root val view = binding?.root
@@ -48,7 +45,9 @@ class ImageFragment : Fragment() {
companion object { companion object {
private const val ARG_IMAGE = "imageUrl" private const val ARG_IMAGE = "imageUrl"
fun newInstance(imageUrl: String): ImageFragment { fun newInstance(
imageUrl : String
): ImageFragment {
val fragment = ImageFragment() val fragment = ImageFragment()
val args = Bundle() val args = Bundle()
args.putString(ARG_IMAGE, imageUrl) args.putString(ARG_IMAGE, imageUrl)

View File

@@ -9,20 +9,21 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
fun SelfossModel.Item.preloadImages(context: Context): Boolean { fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages() val imageUrls = this.getImages()
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000) val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
try { try {
for (url in imageUrls) { for (url in imageUrls) {
if (URLUtil.isValidUrl(url)) { if ( URLUtil.isValidUrl(url)) {
Glide.with(context).asBitmap() Glide.with(context).asBitmap()
.apply(glideOptions) .apply(glideOptions)
.load(url).submit() .load(url).submit()
} }
} }
} catch (e: Error) { } catch (e : Error) {
e.sendSilentlyWithAcraWithName("preloadImages") e.sendSilentlyWithAcraWithName("preloadImages")
return false return false
} }

View File

@@ -4,7 +4,7 @@ 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
fun SelfossModel.Item.toParcelable(): ParecelableItem = fun SelfossModel.Item.toParcelable() : ParecelableItem =
ParecelableItem( ParecelableItem(
this.id, this.id,
this.datetime, this.datetime,
@@ -17,10 +17,9 @@ fun SelfossModel.Item.toParcelable(): ParecelableItem =
this.link, this.link,
this.sourcetitle, this.sourcetitle,
this.tags.joinToString(","), this.tags.joinToString(","),
this.author, this.author
) )
fun ParecelableItem.toModel() : SelfossModel.Item =
fun ParecelableItem.toModel(): SelfossModel.Item =
SelfossModel.Item( SelfossModel.Item(
this.id, this.id,
this.datetime, this.datetime,
@@ -33,9 +32,8 @@ fun ParecelableItem.toModel(): SelfossModel.Item =
this.link, this.link,
this.sourcetitle, this.sourcetitle,
this.tags.split(","), this.tags.split(","),
this.author, this.author
) )
data class ParecelableItem( data class ParecelableItem(
val id: Int, val id: Int,
val datetime: String, val datetime: String,
@@ -48,14 +46,13 @@ data class ParecelableItem(
val link: String, val link: String,
val sourcetitle: String, val sourcetitle: String,
val tags: String, val tags: String,
val author: String?, val author: String?
) : Parcelable { ) : Parcelable {
companion object { companion object {
@JvmField @JvmField
val CREATOR: Parcelable.Creator<ParecelableItem> = val CREATOR: Parcelable.Creator<ParecelableItem> = object : Parcelable.Creator<ParecelableItem> {
object : Parcelable.Creator<ParecelableItem> {
override fun createFromParcel(source: Parcel): ParecelableItem = ParecelableItem(source) override fun createFromParcel(source: Parcel): ParecelableItem = ParecelableItem(source)
override fun newArray(size: Int): Array<ParecelableItem?> = arrayOfNulls(size) override fun newArray(size: Int): Array<ParecelableItem?> = arrayOfNulls(size)
} }
} }
@@ -72,15 +69,12 @@ data class ParecelableItem(
link = source.readString().orEmpty(), link = source.readString().orEmpty(),
sourcetitle = source.readString().orEmpty(), sourcetitle = source.readString().orEmpty(),
tags = source.readString().orEmpty(), tags = source.readString().orEmpty(),
author = source.readString().orEmpty(), author = source.readString().orEmpty()
) )
override fun describeContents() = 0 override fun describeContents() = 0
override fun writeToParcel( override fun writeToParcel(dest: Parcel, flags: Int) {
dest: Parcel,
flags: Int,
) {
dest.writeInt(id) dest.writeInt(id)
dest.writeString(datetime) dest.writeString(datetime)
dest.writeString(title) dest.writeString(title)

View File

@@ -24,10 +24,8 @@ import org.kodein.di.android.closestDI
private const val TITLE_TAG = "settingsActivityTitle" private const val TITLE_TAG = "settingsActivityTitle"
class SettingsActivity : class SettingsActivity : AppCompatActivity(),
AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, DIAware {
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
DIAware {
override val di by closestDI() override val di by closestDI()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -74,14 +72,13 @@ class SettingsActivity :
override fun onPreferenceStartFragment( override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat, caller: PreferenceFragmentCompat,
pref: Preference, pref: Preference
): Boolean { ): Boolean {
// Instantiate the new Fragment // Instantiate the new Fragment
val args = pref.extras val args = pref.extras
val fragment = val fragment = supportFragmentManager.fragmentFactory.instantiate(
supportFragmentManager.fragmentFactory.instantiate(
classLoader, classLoader,
pref.fragment.toString(), pref.fragment
).apply { ).apply {
arguments = args arguments = args
setTargetFragment(caller, 0) setTargetFragment(caller, 0)
@@ -97,20 +94,15 @@ class SettingsActivity :
} }
class MainPreferenceFragment : PreferenceFragmentCompat() { class MainPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences( override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_main, rootKey) setPreferencesFromResource(R.xml.pref_main, rootKey)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯ AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true true
} }
preferenceManager.findPreference<Preference>("action_about")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("action_about")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
Preference.OnPreferenceClickListener { _ ->
context?.let { context?.let {
LibsBuilder() LibsBuilder()
.withAboutIconShown(true) .withAboutIconShown(true)
@@ -123,17 +115,13 @@ class SettingsActivity :
} }
class GeneralPreferenceFragment : PreferenceFragmentCompat() { class GeneralPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences( override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_general, rootKey) setPreferencesFromResource(R.xml.pref_general, rootKey)
val editTextPreference = preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number") val editTextPreference = preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number")
editTextPreference?.setOnBindEditTextListener { editText -> editTextPreference?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.filters = editText.filters = arrayOf(
arrayOf(
InputFilter { source, _, _, dest, _, _ -> InputFilter { source, _, _, dest, _, _ ->
try { try {
val input: Int = (dest.toString() + source.toString()).toInt() val input: Int = (dest.toString() + source.toString()).toInt()
@@ -143,42 +131,26 @@ class SettingsActivity :
Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show() Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show()
} }
"" ""
}, }
) )
} }
} }
} }
class ArticleViewerPreferenceFragment : PreferenceFragmentCompat() { class ArticleViewerPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences( override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_viewer, rootKey) setPreferencesFromResource(R.xml.pref_viewer, rootKey)
val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size") val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size")
fontSize?.setOnBindEditTextListener { editText -> fontSize?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.addTextChangedListener { editText.addTextChangedListener { object : TextWatcher {
object : TextWatcher { override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
override fun beforeTextChanged(
charSequence: CharSequence,
i: Int,
i1: Int,
i2: Int,
) {
// We do nothing // We do nothing
} }
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
override fun onTextChanged(
charSequence: CharSequence,
i: Int,
i1: Int,
i2: Int,
) {
// We do nothing // We do nothing
} }
override fun afterTextChanged(editable: Editable) { override fun afterTextChanged(editable: Editable) {
try { try {
editText.textSize = editable.toString().toInt().toFloat() editText.textSize = editable.toString().toInt().toFloat()
@@ -186,10 +158,8 @@ class SettingsActivity :
e.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > afterTextChanged") e.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > afterTextChanged")
} }
} }
} } }
} editText.filters = arrayOf(
editText.filters =
arrayOf(
InputFilter { source, _, _, dest, _, _ -> InputFilter { source, _, _, dest, _, _ ->
try { try {
val input = (dest.toString() + source.toString()).toInt() val input = (dest.toString() + source.toString()).toInt()
@@ -198,30 +168,23 @@ class SettingsActivity :
nfe.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > filters") nfe.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > filters")
} }
"" ""
}, }
) )
} }
} }
} }
class OfflinePreferenceFragment : PreferenceFragmentCompat() { class OfflinePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences( override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_offline, rootKey) setPreferencesFromResource(R.xml.pref_offline, rootKey)
} }
} }
class ThemePreferenceFragment : PreferenceFragmentCompat() { class ThemePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences( override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_theme, rootKey) setPreferencesFromResource(R.xml.pref_theme, rootKey)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯ AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true true
} }
@@ -234,26 +197,20 @@ class SettingsActivity :
startActivity(browserIntent) startActivity(browserIntent)
} }
override fun onCreatePreferences( override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_links, rootKey) setPreferencesFromResource(R.xml.pref_links, rootKey)
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.trackerUrl)) openUrl(Uri.parse(AppSettingsService.trackerUrl))
true true
} }
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.sourceUrl)) openUrl(Uri.parse(AppSettingsService.sourceUrl))
false false
} }
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.translationUrl)) openUrl(Uri.parse(AppSettingsService.translationUrl))
false false
} }
@@ -261,10 +218,7 @@ class SettingsActivity :
} }
class ExperimentalPreferenceFragment : PreferenceFragmentCompat() { class ExperimentalPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences( override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_experimental, rootKey) setPreferencesFromResource(R.xml.pref_experimental, rootKey)
} }
} }

View File

@@ -5,10 +5,7 @@ import android.content.Intent
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
fun Context.shareLink( fun Context.shareLink(itemUrl: String, itemTitle: String) {
itemUrl: String,
itemTitle: String,
) {
val sendIntent = Intent() val sendIntent = Intent()
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
sendIntent.action = Intent.ACTION_SEND sendIntent.action = Intent.ACTION_SEND
@@ -18,7 +15,7 @@ fun Context.shareLink(
startActivity( startActivity(
Intent.createChooser( Intent.createChooser(
sendIntent, sendIntent,
getString(R.string.share), getString(R.string.share)
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
) )
} }

View File

@@ -12,19 +12,16 @@ import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
import kotlin.math.abs import kotlin.math.abs
class CircleImageView class CircleImageView @JvmOverloads constructor(
@JvmOverloads
constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0, defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) { ) : RelativeLayout(context, attrs, defStyleAttr) {
val view: View val view: View
val imageView: ShapeableImageView val imageView: ShapeableImageView
val textView: TextView val textView: TextView
private val colorScheme = private val colorScheme = listOf(
listOf(
-0x1a8c8d, -0x1a8c8d,
-0xf9d6e, -0xf9d6e,
-0x459738, -0x459738,
@@ -41,7 +38,7 @@ class CircleImageView
-0x2ab1, -0x2ab1,
-0x48b3, -0x48b3,
-0x5e7781, -0x5e7781,
-0x6f5b52, -0x6f5b52
) )
init { init {
@@ -62,4 +59,4 @@ class CircleImageView
private fun colorFromIdentifier(key: String): Int { private fun colorFromIdentifier(key: String): Int {
return colorScheme[abs(key.hashCode()) % colorScheme.size] return colorScheme[abs(key.hashCode()) % colorScheme.size]
} }
} }

View File

@@ -21,13 +21,14 @@ fun Context.openItemUrl(
currentItem: Int, currentItem: Int,
linkDecoded: String, linkDecoded: String,
articleViewer: Boolean, articleViewer: Boolean,
app: Activity, app: Activity
) { ) {
if (!linkDecoded.isUrlValid()) { if (!linkDecoded.isUrlValid()) {
Toast.makeText( Toast.makeText(
this, this,
this.getString(R.string.cant_open_invalid_url), this.getString(R.string.cant_open_invalid_url),
Toast.LENGTH_LONG, Toast.LENGTH_LONG
).show() ).show()
} else { } else {
if (articleViewer) { if (articleViewer) {
@@ -43,7 +44,8 @@ fun Context.openItemUrl(
} }
} }
fun String.isUrlValid(): Boolean = this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches() fun String.isUrlValid(): Boolean =
this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
fun String.isBaseUrlInvalid(): Boolean { fun String.isBaseUrlInvalid(): Boolean {
val baseUrl = this.toHttpUrlOrNull() val baseUrl = this.toHttpUrlOrNull()
@@ -64,10 +66,7 @@ fun Context.openInBrowserAsNewTask(i: SelfossModel.Item) {
} }
class LinkOnTouchListener : View.OnTouchListener { class LinkOnTouchListener : View.OnTouchListener {
override fun onTouch( override fun onTouch(v: View?, event: MotionEvent?): Boolean {
v: View?,
event: MotionEvent?,
): Boolean {
var ret = false var ret = false
val widget: TextView = v as TextView val widget: TextView = v as TextView
val text: CharSequence = widget.text val text: CharSequence = widget.text

View File

@@ -8,4 +8,5 @@ fun TextBadgeItem.removeBadge(): TextBadgeItem {
return this return this
} }
fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this fun TextBadgeItem.maybeShow(): TextBadgeItem =
if (this.isHidden) this.show() else this

View File

@@ -10,30 +10,22 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
fun Context.bitmapCenterCrop( fun Context.bitmapCenterCrop(url: String, iv: ImageView) =
url: String, Glide.with(this)
iv: ImageView,
) = Glide.with(this)
.asBitmap() .asBitmap()
.load(url) .load(url)
.apply(RequestOptions.centerCropTransform()) .apply(RequestOptions.centerCropTransform())
.into(iv) .into(iv)
fun Context.circularDrawable( fun Context.circularDrawable(url: String, view: CircleImageView) {
url: String, view.textView.text =""
view: CircleImageView,
) {
view.textView.text = ""
Glide.with(this) Glide.with(this)
.load(url) .load(url)
.into(view.imageView) .into(view.imageView)
} }
fun getBitmapInputStream( fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream {
bitmap: Bitmap,
compressFormat: Bitmap.CompressFormat,
): InputStream {
val byteArrayOutputStream = ByteArrayOutputStream() val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(compressFormat, 80, byteArrayOutputStream) bitmap.compress(compressFormat, 80, byteArrayOutputStream)
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray() val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()

View File

@@ -19,8 +19,7 @@ class AppViewModel(private val repository: Repository) : ViewModel() {
if (isConnected && !wasConnected && repository.connectionMonitored) { if (isConnected && !wasConnected && repository.connectionMonitored) {
_networkAvailableProvider.emit(true) _networkAvailableProvider.emit(true)
wasConnected = true wasConnected = true
} else if (!isConnected && wasConnected && repository.connectionMonitored) } else if (!isConnected && wasConnected && repository.connectionMonitored){
{
_networkAvailableProvider.emit(false) _networkAvailableProvider.emit(false)
wasConnected = false wasConnected = false
} }

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Lettore RSS per Selfoss"</string> <string name="app_name">"Lettore RSS per Selfoss"</string>
<string name="title_activity_login">"Accedi"</string> <string name="title_activity_login">"Accedi"</string>
<string name="prompt_password">"Password"</string> <string name="prompt_password">"Password"</string>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
</resources>

View File

@@ -8,48 +8,39 @@ import kotlinx.datetime.toInstant
import org.junit.Test import org.junit.Test
class DatesTest { class DatesTest {
private val newVersionDateVariant = "2022-12-24T17:00:08+00"
private val newVersionDate = "2013-04-07T13:43:00+01:00" private val v3Date = "2013-04-07T13:43:00+01:00"
private val oldVersionDate = "2013-05-07 13:46:00" private val v4Date = "2013-04-07 13:43:00"
private val oldVersionDateVariant = "2021-03-21 10:32:00.000000" private val bug1Date = "2022-12-24T17:00:08+00"
@Test @Test
fun new_version_date_should_be_parsed() { fun v3_date_should_be_parsed() {
val date = DateUtils.parseDate(newVersionDate) val date = DateUtils.parseDate(v3Date)
val expected =
LocalDateTime(2013, 4, 7, 14, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(date, expected)
}
@Test
fun v4_date_should_be_parsed() {
val date = DateUtils.parseDate(v4Date)
val expected = val expected =
LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault()) LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(date, expected)
} }
@Test @Test
fun old_version_date_should_be_parsed() { fun bug1_date_should_be_parsed() {
val date = DateUtils.parseDate(oldVersionDate) val date = DateUtils.parseDate(bug1Date)
val expected = val expected =
LocalDateTime(2013, 5, 7, 13, 46, 0, 0).toInstant(TimeZone.currentSystemDefault()) LocalDateTime(2022, 12, 24, 18, 0, 8, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(date, expected)
} }
@Test
fun old_version_variant_date_should_be_parsed() {
val date = DateUtils.parseDate(oldVersionDateVariant)
val expected =
LocalDateTime(2021, 3, 21, 10, 32, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(expected, date)
}
@Test
fun new_version_variant_date_should_be_parsed() {
val date = DateUtils.parseDate(newVersionDateVariant)
val expected =
LocalDateTime(2022, 12, 24, 17, 0, 8, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(expected, date)
}
} }

View File

@@ -42,11 +42,11 @@ class RepositoryTest {
private val NUMBER_STARRED = 20 private val NUMBER_STARRED = 20
private lateinit var repository: Repository private lateinit var repository: Repository
private fun initializeRepository( private fun initializeRepository(
isConnectionAvailable: MutableStateFlow<Boolean> = isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(
MutableStateFlow( true
true, )
),
) { ) {
repository = Repository(api, appSettingsService, isConnectionAvailable, db) repository = Repository(api, appSettingsService, isConnectionAvailable, db)
@@ -64,15 +64,13 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns StatusAndData(
StatusAndData(
success = true, success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)), data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true))
) )
coEvery { api.stats() } returns coEvery { api.stats() } returns StatusAndData(
StatusAndData(
success = true, success = true,
data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED), data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED)
) )
every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit
@@ -118,10 +116,9 @@ class RepositoryTest {
@Test @Test
fun get_public_access() { fun get_public_access() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns StatusAndData(
StatusAndData(
success = true, success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)), data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true))
) )
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
@@ -134,10 +131,9 @@ class RepositoryTest {
@Test @Test
fun get_public_access_username_not_empty() { fun get_public_access_username_not_empty() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns StatusAndData(
StatusAndData(
success = true, success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)), data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true))
) )
every { appSettingsService.getUserName() } returns "username" every { appSettingsService.getUserName() } returns "username"
@@ -150,10 +146,9 @@ class RepositoryTest {
@Test @Test
fun get_public_access_no_auth() { fun get_public_access_no_auth() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns StatusAndData(
StatusAndData(
success = true, success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, false)), data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, false))
) )
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
@@ -166,10 +161,9 @@ class RepositoryTest {
@Test @Test
fun get_public_access_disabled() { fun get_public_access_disabled() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns StatusAndData(
StatusAndData(
success = true, success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)), data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true))
) )
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
@@ -188,7 +182,7 @@ class RepositoryTest {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = generateTestApiItem(itemParameters), data = generateTestApiItem(itemParameters)
) )
initializeRepository() initializeRepository()
@@ -270,7 +264,7 @@ class RepositoryTest {
itemParameter3.tags = "Other, Tag" itemParameter3.tags = "Other, Tag"
itemParameter3.id = "3" itemParameter3.id = "3"
coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems( coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
itemParameter1, itemParameter1
) + ) +
generateTestDBItems(itemParameter2) + generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3) generateTestDBItems(itemParameter3)
@@ -298,7 +292,7 @@ class RepositoryTest {
itemParameter3.sourcetitle = "Other" itemParameter3.sourcetitle = "Other"
itemParameter3.id = "3" itemParameter3.id = "3"
coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems( coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
itemParameter1, itemParameter1
) + ) +
generateTestDBItems(itemParameter2) + generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3) generateTestDBItems(itemParameter3)
@@ -306,8 +300,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
repository.setSourceFilter( repository.setSourceFilter(SelfossModel.SourceDetail(
SelfossModel.SourceDetail(
1, 1,
"Test", "Test",
null, null,
@@ -315,9 +308,8 @@ class RepositoryTest {
SPOUT, SPOUT,
"", "",
IMAGE_URL, IMAGE_URL,
SelfossModel.SourceParams("url"), SelfossModel.SourceParams("url")
), ))
)
runBlocking { runBlocking {
repository.getNewerItems() repository.getNewerItems()
} }
@@ -600,15 +592,13 @@ class RepositoryTest {
} }
private fun prepareTags(): Pair<List<SelfossModel.Tag>, List<TAG>> { private fun prepareTags(): Pair<List<SelfossModel.Tag>, List<TAG>> {
val tags = val tags = listOf(
listOf(
SelfossModel.Tag("test", "red", 6), SelfossModel.Tag("test", "red", 6),
SelfossModel.Tag("second", "yellow", 0), SelfossModel.Tag("second", "yellow", 0)
) )
val tagsDB = val tagsDB = listOf(
listOf(
TAG("test_DB", "red", 6), TAG("test_DB", "red", 6),
TAG("second_DB", "yellow", 0), TAG("second_DB", "yellow", 0)
) )
coEvery { api.tags() } returns StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
@@ -631,8 +621,7 @@ class RepositoryTest {
} }
private fun prepareSources(): Pair<ArrayList<SelfossModel.SourceDetail>, List<SOURCE>> { private fun prepareSources(): Pair<ArrayList<SelfossModel.SourceDetail>, List<SOURCE>> {
val sources = val sources = arrayListOf(
arrayListOf(
SelfossModel.SourceDetail( SelfossModel.SourceDetail(
1, 1,
"First source", "First source",
@@ -641,7 +630,7 @@ class RepositoryTest {
SPOUT, SPOUT,
"", "",
IMAGE_URL_2, IMAGE_URL_2,
SelfossModel.SourceParams("url"), SelfossModel.SourceParams("url")
), ),
SelfossModel.SourceDetail( SelfossModel.SourceDetail(
2, 2,
@@ -651,11 +640,10 @@ class RepositoryTest {
SPOUT, SPOUT,
"", "",
IMAGE_URL, IMAGE_URL,
SelfossModel.SourceParams("url"), SelfossModel.SourceParams("url")
),
) )
val sourcesDB = )
listOf( val sourcesDB = listOf(
SOURCE( SOURCE(
"1", "1",
"First DB source", "First DB source",
@@ -663,7 +651,7 @@ class RepositoryTest {
SPOUT, SPOUT,
"", "",
IMAGE_URL_2, IMAGE_URL_2,
"url", "url"
), ),
SOURCE( SOURCE(
"2", "2",
@@ -672,8 +660,8 @@ class RepositoryTest {
SPOUT, SPOUT,
"", "",
IMAGE_URL, IMAGE_URL,
"url", "url"
), )
) )
coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources) coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources)
@@ -808,8 +796,7 @@ class RepositoryTest {
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = response = repository.createSource(
repository.createSource(
"test", "test",
FEED_URL, FEED_URL,
SPOUT, SPOUT,
@@ -836,12 +823,11 @@ class RepositoryTest {
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = response = repository.createSource(
repository.createSource(
"test", "test",
FEED_URL, FEED_URL,
SPOUT, SPOUT,
TAGS, TAGS
) )
} }
@@ -850,7 +836,7 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any(), any()
) )
} }
assertSame(false, response) assertSame(false, response)
@@ -864,12 +850,11 @@ class RepositoryTest {
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = response = repository.createSource(
repository.createSource(
"test", "test",
FEED_URL, FEED_URL,
SPOUT, SPOUT,
TAGS, TAGS
) )
} }
@@ -931,10 +916,9 @@ class RepositoryTest {
@Test @Test
fun update_remote() { fun update_remote() {
coEvery { api.update() } returns coEvery { api.update() } returns StatusAndData(
StatusAndData(
success = true, success = true,
data = "finished", data = "finished"
) )
initializeRepository() initializeRepository()
@@ -949,10 +933,9 @@ class RepositoryTest {
@Test @Test
fun update_remote_but_response_fails() { fun update_remote_but_response_fails() {
coEvery { api.update() } returns coEvery { api.update() } returns StatusAndData(
StatusAndData(
success = false, success = false,
data = "unallowed access", data = "unallowed access"
) )
initializeRepository() initializeRepository()
@@ -967,10 +950,9 @@ class RepositoryTest {
@Test @Test
fun update_remote_with_unallowed_access() { fun update_remote_with_unallowed_access() {
coEvery { api.update() } returns coEvery { api.update() } returns StatusAndData(
StatusAndData(
success = true, success = true,
data = "unallowed access", data = "unallowed access"
) )
initializeRepository() initializeRepository()
@@ -985,10 +967,9 @@ class RepositoryTest {
@Test @Test
fun update_remote_without_connection() { fun update_remote_without_connection() {
coEvery { api.update() } returns coEvery { api.update() } returns StatusAndData(
StatusAndData(
success = true, success = true,
data = "undocumented...", data = "undocumented..."
) )
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
@@ -1056,7 +1037,7 @@ class RepositoryTest {
appSettingsService.refreshLoginInformation( appSettingsService.refreshLoginInformation(
BASE_URL, BASE_URL,
"login", "login",
"password", "password"
) )
} }
} }
@@ -1076,10 +1057,9 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any(), any()
) )
} returnsMany } returnsMany listOf(
listOf(
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter2)), StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
@@ -1133,8 +1113,8 @@ class RepositoryTest {
SPOUT, SPOUT,
"", "",
IMAGE_URL_2, IMAGE_URL_2,
SelfossModel.SourceParams("url"), SelfossModel.SourceParams("url")
), )
) )
repository.searchFilter = "search" repository.searchFilter = "search"
} }

View File

@@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.repository
import bou.amine.apps.readerforselfossv2.dao.ITEM import bou.amine.apps.readerforselfossv2.dao.ITEM
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> { fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> {
return listOf( return listOf(
ITEM( ITEM(
@@ -17,8 +18,8 @@ fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<I
link = item.link, link = item.link,
sourcetitle = item.sourcetitle, sourcetitle = item.sourcetitle,
tags = item.tags, tags = item.tags,
author = item.author, author = item.author
), )
) )
} }
@@ -36,8 +37,8 @@ fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<S
link = item.link, link = item.link,
sourcetitle = item.sourcetitle, sourcetitle = item.sourcetitle,
tags = item.tags.split(','), tags = item.tags.split(','),
author = item.author, author = item.author
), )
) )
} }

View File

@@ -1,14 +1,14 @@
buildscript { buildscript {
dependencies { dependencies {
// SqlDelight // SqlDelight
classpath("com.squareup.sqldelight:gradle-plugin:1.5.5") classpath("com.squareup.sqldelight:gradle-plugin:1.5.4")
} }
} }
plugins { plugins {
//trick: for the same plugin versions in all sub-modules //trick: for the same plugin versions in all sub-modules
id("com.android.application").version("8.1.2").apply(false) id("com.android.application").version("8.1.0").apply(false)
id("com.android.library").version("8.1.2").apply(false) id("com.android.library").version("8.1.0").apply(false)
id("org.jetbrains.kotlin.android").version("1.9.10").apply(false) id("org.jetbrains.kotlin.android").version("1.9.10").apply(false)
kotlin("multiplatform").version("1.9.10").apply(false) kotlin("multiplatform").version("1.9.10").apply(false)
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false) id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
@@ -20,6 +20,7 @@ allprojects {
// maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")} // maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
google() google()
mavenCentral() mavenCentral()
jcenter()
maven { url = uri("https://www.jitpack.io") } maven { url = uri("https://www.jitpack.io") }
} }
} }

View File

@@ -26,3 +26,4 @@ org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
ignoreGitVersion=false ignoreGitVersion=false
kotlin.native.cacheKind.iosX64=none kotlin.native.cacheKind.iosX64=none
pushCache=true

View File

@@ -1,6 +1,6 @@
#Thu Jul 13 11:41:19 CEST 2023 #Thu Jul 13 11:41:19 CEST 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -1,3 +1,5 @@
val pushCache: String by settings
pluginManagement { pluginManagement {
repositories { repositories {
// maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")} // maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
@@ -15,6 +17,17 @@ dependencyResolutionManagement {
} }
} }
buildCache {
remote<HttpBuildCache> {
url = uri("http://18.0.0.7:3071/cache/")
isAllowInsecureProtocol = true
isAllowUntrustedServer = true
isUseExpectContinue = true
isPush = (pushCache == "true")
}
}
rootProject.name = "ReaderForSelfossV2" rootProject.name = "ReaderForSelfossV2"
include(":androidApp") include(":androidApp")
include(":shared") include(":shared")

View File

@@ -17,6 +17,7 @@ plugins {
kotlin { kotlin {
androidTarget() androidTarget()
jvmToolchain(11)
listOf( listOf(
iosX64(), iosX64(),
@@ -37,12 +38,12 @@ kotlin {
implementation("io.ktor:ktor-client-logging:$ktorVersion") implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-client-auth:$ktorVersion") implementation("io.ktor:ktor-client-auth:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("org.jsoup:jsoup:1.15.4") implementation("org.jsoup:jsoup:1.15.4")
//Dependency Injection //Dependency Injection
implementation("org.kodein.di:kodein-di:7.14.0") implementation("org.kodein.di:kodein-di:7.12.0")
//Settings //Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC") implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC")
@@ -63,7 +64,6 @@ kotlin {
val androidMain by getting { val androidMain by getting {
dependencies { dependencies {
implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("io.ktor:ktor-client-okhttp:2.2.4")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
// Sql // Sql
@@ -103,14 +103,14 @@ kotlin {
} }
android { android {
compileSdk = 34 compileSdk = 33
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig { defaultConfig {
minSdk = 25 minSdk = 21
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_11
} }
namespace = "bou.amine.apps.readerforselfossv2" namespace = "bou.amine.apps.readerforselfossv2"
} }

View File

@@ -5,15 +5,9 @@ import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
class NaiveTrustManager : X509TrustManager { class NaiveTrustManager : X509TrustManager {
override fun checkClientTrusted( override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
chain: Array<out X509Certificate>?,
authType: String?,
) {}
override fun checkServerTrusted( override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
chain: Array<out X509Certificate>?,
authType: String?,
) {}
override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf() override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
} }

View File

@@ -3,39 +3,30 @@ package bou.amine.apps.readerforselfossv2.utils
import android.text.format.DateUtils import android.text.format.DateUtils
import kotlinx.datetime.* import kotlinx.datetime.*
actual class DateUtils { actual class DateUtils {
actual companion object { actual companion object {
// Possible formats are
// yyyy-mm-dd hh:mm:ss format
private val oldVersionFormat = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(.()\\d*)?".toRegex()
// yyyy-MM-dd'T'HH:mm:ss[.SSS]XXX (RFC3339)
private val newVersionFormat = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{2}(:\\d{2})?".toRegex()
// We may need to consider moving the formatting to platform specific code, even if the tests are doubled
// For now, we handle this in a hacky way, because kotlin only accepts iso formats
actual fun parseDate(dateString: String): Long { actual fun parseDate(dateString: String): Long {
var isoDateString: String = return try {
if (dateString.matches(oldVersionFormat)) { Instant.parse(dateString).toEpochMilliseconds()
dateString.replace(" ", "T") } catch (e: Exception) {
} else if (dateString.matches(newVersionFormat)) { var str = dateString.replace(" ", "T")
dateString.split("+")[0] if (str.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{2}".toRegex())) {
} else { str = str.split("+")[0]
throw Exception("Unrecognized format for $dateString") }
LocalDateTime.parse(str).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
} }
return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
} }
actual fun parseRelativeDate(dateString: String): String { actual fun parseRelativeDate(dateString: String): String {
val date = parseDate(dateString) val date = parseDate(dateString)
return " " + return " " + DateUtils.getRelativeTimeSpanString(
DateUtils.getRelativeTimeSpanString(
date, date,
Clock.System.now().toEpochMilliseconds(), Clock.System.now().toEpochMilliseconds(),
DateUtils.MINUTE_IN_MILLIS, DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE, DateUtils.FORMAT_ABBREV_RELATIVE
) )
} }
} }

View File

@@ -21,13 +21,13 @@ actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
actual fun SelfossModel.Item.getImages(): ArrayList<String> { actual fun SelfossModel.Item.getImages(): ArrayList<String> {
val allImages = ArrayList<String>() val allImages = ArrayList<String>()
for (image in Jsoup.parse(content).getElementsByTag("img")) { for ( image in Jsoup.parse(content).getElementsByTag("img")) {
val url = image.attr("src") val url = image.attr("src")
if (url.lowercase(Locale.US).contains(".jpg") || if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US).contains(".jpeg") || url.lowercase(Locale.US).contains(".jpeg") ||
url.lowercase(Locale.US).contains(".png") || url.lowercase(Locale.US).contains(".png") ||
url.lowercase(Locale.US).contains(".webp") url.lowercase(Locale.US).contains(".webp"))
) { {
allImages.add(url) allImages.add(url)
} }
} }
@@ -38,11 +38,7 @@ actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon) return constructUrl(baseUrl, "favicons", icon)
} }
actual fun constructUrl( actual fun constructUrl(baseUrl: String, path: String, file: String?): String {
baseUrl: String,
path: String,
file: String?,
): String {
return if (file == null || file == "null" || file.isEmpty()) { return if (file == null || file == "null" || file.isEmpty()) {
"" ""
} else { } else {

View File

@@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.DI
import bou.amine.apps.readerforselfossv2.rest.MercuryApi 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 org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.bind import org.kodein.di.bind
import org.kodein.di.instance import org.kodein.di.instance

View File

@@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
class MercuryModel { class MercuryModel {
@Serializable @Serializable
class ParsedContent( class ParsedContent(
val title: String? = null, val title: String? = null,
@@ -11,6 +12,6 @@ class MercuryModel {
val url: String? = null, val url: String? = null,
val error: Boolean? = null, val error: Boolean? = null,
val message: String? = null, val message: String? = null,
val failed: Boolean? = null, val failed: Boolean? = null
) )
} }

View File

@@ -13,31 +13,32 @@ import kotlinx.serialization.encoding.encodeCollection
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
class SelfossModel { class SelfossModel {
@Serializable @Serializable
data class Tag( data class Tag(
val tag: String, val tag: String,
val color: String, val color: String,
val unread: Int, val unread: Int
) )
@Serializable @Serializable
class Stats( class Stats(
val total: Int, val total: Int,
val unread: Int? = null, val unread: Int? = null,
val starred: Int? = null, val starred: Int? = null
) )
@Serializable @Serializable
data class Spout( data class Spout(
val name: String, val name: String,
val description: String, val description: String
) )
@Serializable @Serializable
data class ApiInformation( data class ApiInformation(
val version: String? = null, val version: String? = null,
val apiversion: String? = null, val apiversion: String? = null,
val configuration: ApiConfiguration? = null, val configuration: ApiConfiguration? = null
) { ) {
fun getApiMajorVersion(): Int { fun getApiMajorVersion(): Int {
var versionNumber = 0 var versionNumber = 0
@@ -55,7 +56,7 @@ class SelfossModel {
@Serializable(with = BooleanSerializer::class) @Serializable(with = BooleanSerializer::class)
val publicMode: Boolean? = null, val publicMode: Boolean? = null,
@Serializable(with = BooleanSerializer::class) @Serializable(with = BooleanSerializer::class)
val authEnabled: Boolean? = null, val authEnabled: Boolean? = null
) { ) {
fun isAuthEnabled() = authEnabled ?: true fun isAuthEnabled() = authEnabled ?: true
@@ -76,7 +77,7 @@ class SelfossModel {
override var title: String, override var title: String,
override var unread: Int? = null, override var unread: Int? = null,
override var error: String? = null, override var error: String? = null,
override var icon: String? = null, override var icon: String? = null
) : Source ) : Source
@Serializable @Serializable
@@ -89,14 +90,13 @@ class SelfossModel {
var spout: String? = null, var spout: String? = null,
override var error: String? = null, override var error: String? = null,
override var icon: String? = null, override var icon: String? = null,
var params: SourceParams? = null, var params: SourceParams? = null
) : Source ) : Source
@Serializable @Serializable
data class SourceParams( data class SourceParams(
val url: String? = null, val url: String? = null
) )
@Serializable @Serializable
data class Item( data class Item(
val id: Int, val id: Int,
@@ -113,12 +113,11 @@ class SelfossModel {
val sourcetitle: String, val sourcetitle: String,
@Serializable(with = TagsListSerializer::class) @Serializable(with = TagsListSerializer::class)
val tags: List<String>, val tags: List<String>,
val author: String? = null, val author: String? = null
) { ) {
fun getLinkDecoded(): String { fun getLinkDecoded(): String {
var stringUrl: String var stringUrl: String
stringUrl = stringUrl = if (link.contains("//news.google.com/news/") && link.contains("&amp;url=")) {
if (link.contains("//news.google.com/news/") && link.contains("&amp;url=")) {
link.substringAfter("&amp;url=") link.substringAfter("&amp;url=")
} else { } else {
this.link.replace("&amp;", "&") this.link.replace("&amp;", "&")
@@ -152,22 +151,21 @@ class SelfossModel {
} }
} }
// TODO: this seems to be super slow. // TODO: this seems to be super slow.
object TagsListSerializer : KSerializer<List<String>> { object TagsListSerializer : KSerializer<List<String>> {
override fun deserialize(decoder: Decoder): List<String> { override fun deserialize(decoder: Decoder): List<String> {
return when (val json = ((decoder as JsonDecoder).decodeJsonElement())) { return when(val json = ((decoder as JsonDecoder).decodeJsonElement())) {
is JsonArray -> json.toList().map { it.toString().replace("^\"|\"$".toRegex(), "") } is JsonArray -> json.toList().map { it.toString().replace("^\"|\"$".toRegex(), "") }
else -> json.toString().split(",") else -> json.toString().split(",")
} }
} }
override val descriptor: SerialDescriptor override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING) get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING)
override fun serialize( override fun serialize(encoder: Encoder, value: List<String>) {
encoder: Encoder,
value: List<String>,
) {
encoder.encodeCollection(PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING), value.size) { this.toString() } encoder.encodeCollection(PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING), value.size) { this.toString() }
} }
} }
@@ -185,10 +183,7 @@ class SelfossModel {
override val descriptor: SerialDescriptor override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("b", PrimitiveKind.BOOLEAN) get() = PrimitiveSerialDescriptor("b", PrimitiveKind.BOOLEAN)
override fun serialize( override fun serialize(encoder: Encoder, value: Boolean) {
encoder: Encoder,
value: Boolean,
) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View File

@@ -19,8 +19,9 @@ class Repository(
private val api: SelfossApi, private val api: SelfossApi,
private val appSettingsService: AppSettingsService, private val appSettingsService: AppSettingsService,
val isConnectionAvailable: MutableStateFlow<Boolean>, val isConnectionAvailable: MutableStateFlow<Boolean>,
private val db: ReaderForSelfossDB, private val db: ReaderForSelfossDB
) { ) {
var items = ArrayList<SelfossModel.Item>() var items = ArrayList<SelfossModel.Item>()
var connectionMonitored = false var connectionMonitored = false
@@ -52,18 +53,16 @@ class Repository(
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
fetchedItems = fetchedItems = api.getItems(
api.getItems(
displayedItems.type, displayedItems.type,
offset = 0, offset = 0,
tagFilter.value?.tag, tagFilter.value?.tag,
sourceFilter.value?.id?.toLong(), sourceFilter.value?.id?.toLong(),
searchFilter, searchFilter,
null, null
) )
} else if (appSettingsService.isItemCachingEnabled()) { } else if (appSettingsService.isItemCachingEnabled()) {
var dbItems = 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)
@@ -76,9 +75,8 @@ class Repository(
} }
val itemsList = ArrayList(dbItems.map { it.toView() }) val itemsList = ArrayList(dbItems.map { it.toView() })
itemsList.sortByDescending { DateUtils.parseDate(it.datetime) } itemsList.sortByDescending { DateUtils.parseDate(it.datetime) }
fetchedItems = fetchedItems = StatusAndData.succes(
StatusAndData.succes( itemsList
itemsList,
) )
} }
@@ -92,14 +90,13 @@ class Repository(
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val offset = items.size val offset = items.size
fetchedItems = fetchedItems = api.getItems(
api.getItems(
displayedItems.type, displayedItems.type,
offset, offset,
tagFilter.value?.tag, tagFilter.value?.tag,
sourceFilter.value?.id?.toLong(), sourceFilter.value?.id?.toLong(),
searchFilter, searchFilter,
null, null
) )
} // When using the db cache, we load everything the first time, so there should be nothing more to load. } // When using the db cache, we load everything the first time, so there should be nothing more to load.
@@ -111,15 +108,14 @@ class Repository(
private suspend fun getMaxItemsForBackground(itemType: ItemType): List<SelfossModel.Item> { private suspend fun getMaxItemsForBackground(itemType: ItemType): List<SelfossModel.Item> {
return if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
val items = val items = api.getItems(
api.getItems(
itemType.type, itemType.type,
0, 0,
null, null,
null, null,
null, null,
null, null,
200, 200
) )
return if (items.success && items.data != null) { return if (items.success && items.data != null) {
items.data items.data
@@ -378,7 +374,7 @@ class Repository(
title: String, title: String,
url: String, url: String,
spout: String, spout: String,
tags: String, tags: String
): Boolean { ): Boolean {
var response = false var response = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
@@ -388,10 +384,7 @@ class Repository(
return response return response
} }
suspend fun deleteSource( suspend fun deleteSource(id: Int, title: String): Boolean {
id: Int,
title: String,
): Boolean {
var success = false var success = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val response = api.deleteSource(id) val response = api.deleteSource(id)
@@ -463,11 +456,7 @@ class Repository(
} }
} }
fun refreshLoginInformation( fun refreshLoginInformation(url: String, login: String, password: String) {
url: String,
login: String,
password: String,
) {
appSettingsService.refreshLoginInformation(url, login, password) appSettingsService.refreshLoginInformation(url, login, password)
baseUrl = url baseUrl = url
api.refreshLoginInformation() api.refreshLoginInformation()
@@ -485,10 +474,9 @@ class Repository(
// Check if we're accessing the instance in public mode // Check if we're accessing the instance in public mode
// This happens when auth and public mode are enabled but // This happens when auth and public mode are enabled but
// no credentials are provided to login // no credentials are provided to login
if (appSettingsService.getUserName().isEmpty() && if (appSettingsService.getUserName().isEmpty()
fetchedInformation.data.getApiConfiguration().isAuthEnabled() && && fetchedInformation.data.getApiConfiguration().isAuthEnabled()
fetchedInformation.data.getApiConfiguration().isPublicModeEnabled() && fetchedInformation.data.getApiConfiguration().isPublicModeEnabled()) {
) {
appSettingsService.updatePublicAccess(true) appSettingsService.updatePublicAccess(true)
} }
} }
@@ -497,9 +485,11 @@ class Repository(
fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride
private fun getDBActions(): List<ACTION> = db.actionsQueries.actions().executeAsList() private fun getDBActions(): List<ACTION> =
db.actionsQueries.actions().executeAsList()
private fun deleteDBAction(action: ACTION) = db.actionsQueries.deleteAction(action.id) private fun deleteDBAction(action: ACTION) =
db.actionsQueries.deleteAction(action.id)
private fun getDBTags(): List<TAG> = db.tagsQueries.tags().executeAsList() private fun getDBTags(): List<TAG> = db.tagsQueries.tags().executeAsList()
@@ -540,8 +530,9 @@ class Repository(
read: Boolean = false, read: Boolean = false,
unread: Boolean = false, unread: Boolean = false,
starred: Boolean = false, starred: Boolean = false,
unstarred: Boolean = false, unstarred: Boolean = false
) = db.actionsQueries.insertAction(articleid, read, unread, starred, unstarred) ) =
db.actionsQueries.insertAction(articleid, read, unread, starred, unstarred)
private fun updateDBItem(item: SelfossModel.Item) = private fun updateDBItem(item: SelfossModel.Item) =
db.itemsQueries.updateItem( db.itemsQueries.updateItem(
@@ -556,7 +547,7 @@ class Repository(
item.sourcetitle, item.sourcetitle,
item.tags.joinToString(","), item.tags.joinToString(","),
item.author, item.author,
item.id.toString(), item.id.toString()
) )
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> { suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> {
@@ -573,38 +564,32 @@ class Repository(
} }
suspend fun handleDBActions() { suspend fun handleDBActions() {
val actions: List<ACTION> = getDBActions() val actions: List<ACTION> = getDBActions()
actions.forEach { action -> actions.forEach { action ->
when { when {
action.read -> action.read -> doAndReportOnFail(
doAndReportOnFail(
markAsReadById(action.articleid.toInt()), markAsReadById(action.articleid.toInt()),
action, action
) )
action.unread -> action.unread -> doAndReportOnFail(
doAndReportOnFail(
unmarkAsReadById(action.articleid.toInt()), unmarkAsReadById(action.articleid.toInt()),
action, action
) )
action.starred -> action.starred -> doAndReportOnFail(
doAndReportOnFail(
starrById(action.articleid.toInt()), starrById(action.articleid.toInt()),
action, action
) )
action.unstarred -> action.unstarred -> doAndReportOnFail(
doAndReportOnFail(
unstarrById(action.articleid.toInt()), unstarrById(action.articleid.toInt()),
action, action
) )
} }
} }
} }
private fun doAndReportOnFail( private fun doAndReportOnFail(result: Boolean, action: ACTION) {
result: Boolean,
action: ACTION,
) {
if (result) { if (result) {
deleteDBAction(action) deleteDBAction(action)
} }

View File

@@ -11,23 +11,21 @@ import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class MercuryApi() { class MercuryApi() {
var client = createHttpClient() var client = createHttpClient()
private fun createHttpClient(): HttpClient { private fun createHttpClient(): HttpClient {
return HttpClient { return HttpClient {
install(ContentNegotiation) { install(ContentNegotiation) {
install(HttpCache) install(HttpCache)
json( json(Json {
Json {
prettyPrint = true prettyPrint = true
isLenient = true isLenient = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
}, })
)
} }
install(Logging) { install(Logging) {
logger = logger = object : Logger {
object : Logger {
override fun log(message: String) { override fun log(message: String) {
Napier.d(message, tag = "LogMercuryCalls") Napier.d(message, tag = "LogMercuryCalls")
} }
@@ -39,9 +37,7 @@ class MercuryApi() {
} }
suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> = suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> =
bodyOrFailure( bodyOrFailure(client.get("https://amine-louveau.fr/parser.php") {
client.get("https://amine-louveau.fr/parser.php") {
parameter("link", url) parameter("link", url)
}, })
)
} }

View File

@@ -10,6 +10,7 @@ import io.ktor.client.request.forms.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse { suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse {
return if (r != null && r.status === HttpStatusCode.NotFound) { return if (r != null && r.status === HttpStatusCode.NotFound) {
SuccessResponse(true) SuccessResponse(true)
@@ -39,7 +40,7 @@ suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T>
inline fun tryToRequest( inline fun tryToRequest(
requestType: String, requestType: String,
fn: () -> HttpResponse, fn: () -> HttpResponse
): HttpResponse? { ): HttpResponse? {
var response: HttpResponse? = null var response: HttpResponse? = null
try { try {
@@ -52,42 +53,26 @@ inline fun tryToRequest(
suspend inline fun HttpClient.tryToGet( suspend inline fun HttpClient.tryToGet(
urlString: String, urlString: String,
crossinline block: HttpRequestBuilder.() -> Unit = {}, crossinline block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse? = ): HttpResponse? = tryToRequest("Get") { return this.get { url(urlString); block() } }
tryToRequest("Get") {
return this.get {
url(urlString)
block()
}
}
suspend inline fun HttpClient.tryToPost( suspend inline fun HttpClient.tryToPost(
urlString: String, urlString: String,
block: HttpRequestBuilder.() -> Unit = {}, block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse? = ): HttpResponse? = tryToRequest("Post") { return this.post { url(urlString); block() } }
tryToRequest("Post") {
return this.post {
url(urlString)
block()
}
}
suspend inline fun HttpClient.tryToDelete( suspend inline fun HttpClient.tryToDelete(
urlString: String, urlString: String,
block: HttpRequestBuilder.() -> Unit = {}, block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse? = ): HttpResponse? = tryToRequest("Delete") { return this.delete { url(urlString); block() } }
tryToRequest("Delete") {
return this.delete {
url(urlString)
block()
}
}
suspend fun HttpClient.tryToSubmitForm( suspend fun HttpClient.tryToSubmitForm(
url: String, url: String,
formParameters: Parameters = Parameters.Empty, formParameters: Parameters = Parameters.Empty,
encodeInQuery: Boolean = false, encodeInQuery: Boolean = false,
block: HttpRequestBuilder.() -> Unit = {}, block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse? = ): HttpResponse? =
tryToRequest("SubmitForm") { tryToRequest("SubmitForm") {
return this.submitForm(formParameters, encodeInQuery) { return this.submitForm(formParameters, encodeInQuery) {

View File

@@ -35,10 +35,10 @@ import kotlinx.serialization.json.Json
expect fun setupInsecureHTTPEngine(config: CIOEngineConfig) expect fun setupInsecureHTTPEngine(config: CIOEngineConfig)
class SelfossApi(private val appSettingsService: AppSettingsService) { fun createHttpClient(
var client = createHttpClient() appSettingsService: AppSettingsService,
api: SelfossApi
fun createHttpClient() = ) =
HttpClient(CIO) { HttpClient(CIO) {
if (appSettingsService.getSelfSigned()) { if (appSettingsService.getSelfSigned()) {
engine { engine {
@@ -47,18 +47,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
install(ContentNegotiation) { install(ContentNegotiation) {
install(HttpCache) install(HttpCache)
json( json(Json {
Json {
prettyPrint = true prettyPrint = true
isLenient = true isLenient = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
explicitNulls = false })
},
)
} }
install(Logging) { install(Logging) {
logger = logger = object : Logger {
object : Logger {
override fun log(message: String) { override fun log(message: String) {
Napier.d(message, tag = "LogApiCalls") Napier.d(message, tag = "LogApiCalls")
} }
@@ -72,13 +68,13 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
install(HttpRequestRetry) { install(HttpRequestRetry) {
maxRetries = 2 maxRetries = 2
retryIf { _, response -> retryIf { _, response ->
response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo() response.status == HttpStatusCode.Forbidden && api.shouldHavePostLogin() && api.hasLoginInfo()
} }
modifyRequest { modifyRequest {
Napier.i("Will modify", tag = "HttpSend") Napier.i("Will modify", tag = "HttpSend")
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
Napier.i("Will login", tag = "HttpSend") Napier.i("Will login", tag = "HttpSend")
login() api.login()
Napier.i("Did login", tag = "HttpSend") Napier.i("Did login", tag = "HttpSend")
} }
} }
@@ -86,11 +82,16 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
expectSuccess = false expectSuccess = false
} }
fun url(path: String) = "${appSettingsService.getBaseUrl()}$path" class SelfossApi(private val appSettingsService: AppSettingsService) {
var client = createHttpClient(appSettingsService, this)
fun url(path: String) =
"${appSettingsService.getBaseUrl()}$path"
fun refreshLoginInformation() { fun refreshLoginInformation() {
appSettingsService.refreshApiSettings() appSettingsService.refreshApiSettings()
client = createHttpClient() client = createHttpClient(appSettingsService, this)
} }
fun constructBasicAuthValue(credentials: BasicAuthCredentials): String { fun constructBasicAuthValue(credentials: BasicAuthCredentials): String {
@@ -101,16 +102,13 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
// Api version was introduces after the POST login, so when there is a version, it should be available // Api version was introduces after the POST login, so when there is a version, it should be available
private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1 fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
fun hasLoginInfo() =
private fun hasLoginInfo() = appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword()
appSettingsService.getUserName().isNotEmpty() &&
appSettingsService.getPassword()
.isNotEmpty() .isNotEmpty()
suspend fun login(): SuccessResponse = suspend fun login(): SuccessResponse =
if (appSettingsService.getUserName().isNotEmpty() && if (appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword()
appSettingsService.getPassword()
.isNotEmpty() .isNotEmpty()
) { ) {
if (shouldHavePostLogin()) { if (shouldHavePostLogin()) {
@@ -122,49 +120,30 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
SuccessResponse(true) SuccessResponse(true)
} }
private suspend fun getLogin() = private suspend fun getLogin() = maybeResponse(client.tryToGet(url("/login")) {
maybeResponse(
client.tryToGet(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
private suspend fun postLogin() = private suspend fun postLogin() = maybeResponse(client.tryToPost(url("/login")) {
maybeResponse(
client.tryToPost(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0 private fun shouldHaveNewLogout() =
appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
suspend fun logout(): SuccessResponse = suspend fun logout(): SuccessResponse =
if (shouldHaveNewLogout()) { if (shouldHaveNewLogout()) {
@@ -174,42 +153,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
private suspend fun maybeLogoutIfAvailable() = private suspend fun maybeLogoutIfAvailable() =
responseOrSuccessIf404( responseOrSuccessIf404(client.tryToGet(url("/logout")) {
client.tryToGet(url("/logout")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
private suspend fun doLogout() = private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current")) {
maybeResponse(
client.tryToDelete(url("/api/session/current")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun getItems( suspend fun getItems(
type: String, type: String,
@@ -218,10 +178,9 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
source: Long?, source: Long?,
search: String?, search: String?,
updatedSince: String?, updatedSince: String?,
items: Int? = null, items: Int? = null
): StatusAndData<List<SelfossModel.Item>> = ): StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure( bodyOrFailure(client.tryToGet(url("/items")) {
client.tryToGet(url("/items")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
@@ -235,23 +194,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("offset", offset) parameter("offset", offset)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun getItemsWithoutCatch(): StatusAndData<List<SelfossModel.Item>> = suspend fun getItemsWithoutCatch(): StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure( bodyOrFailure(client.get(url("/items")) {
client.get(url("/items")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
@@ -260,275 +210,166 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("items", 1) parameter("items", 1)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun stats(): StatusAndData<SelfossModel.Stats> = suspend fun stats(): StatusAndData<SelfossModel.Stats> =
bodyOrFailure( bodyOrFailure(client.tryToGet(url("/stats")) {
client.tryToGet(url("/stats")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> = suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
bodyOrFailure( bodyOrFailure(client.tryToGet(url("/tags")) {
client.tryToGet(url("/tags")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun update(): StatusAndData<String> = suspend fun update(): StatusAndData<String> =
bodyOrFailure( bodyOrFailure(client.tryToGet(url("/update")) {
client.tryToGet(url("/update")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> = suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
bodyOrFailure( bodyOrFailure(client.tryToGet(url("/sources/spouts")) {
client.tryToGet(url("/sources/spouts")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> = suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> =
bodyOrFailure( bodyOrFailure(client.tryToGet(url("/sources/stats")) {
client.tryToGet(url("/sources/stats")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> = suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> =
bodyOrFailure( bodyOrFailure(client.tryToGet(url("/sources/list")) {
client.tryToGet(url("/sources/list")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> = suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
bodyOrFailure( bodyOrFailure(client.tryToGet(url("/api/about")) {
client.tryToGet(url("/api/about")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun markAsRead(id: String): SuccessResponse = suspend fun markAsRead(id: String): SuccessResponse =
maybeResponse( maybeResponse(client.tryToPost(url("/mark/$id")) {
client.tryToPost(url("/mark/$id")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun unmarkAsRead(id: String): SuccessResponse = suspend fun unmarkAsRead(id: String): SuccessResponse =
maybeResponse( maybeResponse(client.tryToPost(url("/unmark/$id")) {
client.tryToPost(url("/unmark/$id")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun starr(id: String): SuccessResponse = suspend fun starr(id: String): SuccessResponse =
maybeResponse( maybeResponse(client.tryToPost(url("/starr/$id")) {
client.tryToPost(url("/starr/$id")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun unstarr(id: String): SuccessResponse = suspend fun unstarr(id: String): SuccessResponse =
maybeResponse( maybeResponse(client.tryToPost(url("/unstarr/$id")) {
client.tryToPost(url("/unstarr/$id")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, })
)
suspend fun markAllAsRead(ids: List<String>): SuccessResponse = suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
maybeResponse( maybeResponse(client.tryToSubmitForm(
client.tryToSubmitForm(
url = url("/mark"), url = url("/mark"),
formParameters = formParameters = Parameters.build {
Parameters.build {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName()) append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword()) append("password", appSettingsService.getPassword())
@@ -538,20 +379,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
block = { block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, }
), ))
)
suspend fun createSourceForVersion( suspend fun createSourceForVersion(
title: String, title: String,
@@ -564,7 +397,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
createSource("tags[]", title, url, spout, tags) createSource("tags[]", title, url, spout, tags)
} else { } else {
createSource("tags", title, url, spout, tags) createSource("tags", title, url, spout, tags)
}, }
) )
private suspend fun createSource( private suspend fun createSource(
@@ -572,12 +405,11 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
title: String, title: String,
url: String, url: String,
spout: String, spout: String,
tags: String, tags: String
): HttpResponse? = ): HttpResponse? =
client.tryToSubmitForm( client.tryToSubmitForm(
url = url("/source"), url = url("/source"),
formParameters = formParameters = Parameters.build {
Parameters.build {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName()) append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword()) append("password", appSettingsService.getPassword())
@@ -590,18 +422,11 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
block = { block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, }
) )
suspend fun updateSourceForVersion( suspend fun updateSourceForVersion(
@@ -609,14 +434,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
title: String, title: String,
url: String, url: String,
spout: String, spout: String,
tags: String, tags: String
): SuccessResponse = ): SuccessResponse =
maybeResponse( maybeResponse(
if (appSettingsService.getApiVersion() > 1) { if (appSettingsService.getApiVersion() > 1) {
updateSource(id, "tags[]", title, url, spout, tags) updateSource(id, "tags[]", title, url, spout, tags)
} else { } else {
updateSource(id, "tags", title, url, spout, tags) updateSource(id, "tags", title, url, spout, tags)
}, }
) )
private suspend fun updateSource( private suspend fun updateSource(
@@ -629,8 +454,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
): HttpResponse? = ): HttpResponse? =
client.tryToSubmitForm( client.tryToSubmitForm(
url = url("/source/$id"), url = url("/source/$id"),
formParameters = formParameters = Parameters.build {
Parameters.build {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName()) append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword()) append("password", appSettingsService.getPassword())
@@ -643,23 +467,15 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
block = { block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append( append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
}, }
) )
suspend fun deleteSource(id: Int): SuccessResponse = suspend fun deleteSource(id: Int): SuccessResponse =
maybeResponse( maybeResponse(client.tryToDelete(url("/source/$id")) {
client.tryToDelete(url("/source/$id")) {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
@@ -671,12 +487,11 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
constructBasicAuthValue( constructBasicAuthValue(
BasicAuthCredentials( BasicAuthCredentials(
username = appSettingsService.getBasicUserName(), username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(), password = appSettingsService.getBasicPassword()
), )
), )
) )
} }
} }
}, })
)
} }

View File

@@ -13,93 +13,58 @@ class ACRASettings : Settings {
// Nothing // Nothing
} }
override fun getBoolean( override fun getBoolean(key: String, defaultValue: Boolean): Boolean = false
key: String,
defaultValue: Boolean,
): Boolean = false
override fun getBooleanOrNull(key: String): Boolean? = null override fun getBooleanOrNull(key: String): Boolean? = null
override fun getDouble( override fun getDouble(key: String, defaultValue: Double): Double = 0.0
key: String,
defaultValue: Double,
): Double = 0.0
override fun getDoubleOrNull(key: String): Double? = null override fun getDoubleOrNull(key: String): Double? = null
override fun getFloat( override fun getFloat(key: String, defaultValue: Float): Float = 0.0F
key: String,
defaultValue: Float,
): Float = 0.0F
override fun getFloatOrNull(key: String): Float? = null override fun getFloatOrNull(key: String): Float? = null
override fun getInt( override fun getInt(key: String, defaultValue: Int): Int = 0
key: String,
defaultValue: Int,
): Int = 0
override fun getIntOrNull(key: String): Int? = null override fun getIntOrNull(key: String): Int? = null
override fun getLong( override fun getLong(key: String, defaultValue: Long): Long = 0
key: String,
defaultValue: Long,
): Long = 0
override fun getLongOrNull(key: String): Long? = null override fun getLongOrNull(key: String): Long? = null
override fun getString( override fun getString(key: String, defaultValue: String): String = "0"
key: String,
defaultValue: String,
): String = "0"
override fun getStringOrNull(key: String): String? = null override fun getStringOrNull(key: String): String? = null
override fun hasKey(key: String): Boolean = false override fun hasKey(key: String): Boolean = false
override fun putBoolean( override fun putBoolean(key: String, value: Boolean) {
key: String,
value: Boolean,
) {
// Nothing // Nothing
} }
override fun putDouble( override fun putDouble(key: String, value: Double) {
key: String,
value: Double,
) {
// Nothing // Nothing
} }
override fun putFloat( override fun putFloat(key: String, value: Float) {
key: String,
value: Float,
) {
// Nothing // Nothing
} }
override fun putInt( override fun putInt(key: String, value: Int) {
key: String,
value: Int,
) {
// Nothing // Nothing
} }
override fun putLong( override fun putLong(key: String, value: Long) {
key: String,
value: Long,
) {
// Nothing // Nothing
} }
override fun putString( override fun putString(key: String, value: String) {
key: String,
value: String,
) {
// Nothing // Nothing
} }
override fun remove(key: String) { override fun remove(key: String) {
// Nothing // Nothing
} }
} }

View File

@@ -3,12 +3,7 @@ package bou.amine.apps.readerforselfossv2.service
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
class AppSettingsService(acraSenderServiceProcess: Boolean = false) { class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
val settings: Settings = val settings: Settings = if (acraSenderServiceProcess) { ACRASettings() } else { Settings() }
if (acraSenderServiceProcess) {
ACRASettings()
} else {
Settings()
}
// Api related // Api related
private var _apiVersion: Int = -1 private var _apiVersion: Int = -1
@@ -43,6 +38,7 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
private var _font: String = "" private var _font: String = ""
private var _theme: Int? = null private var _theme: Int? = null
init { init {
refreshApiSettings() refreshApiSettings()
refreshUserSettings() refreshUserSettings()
@@ -56,6 +52,7 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
return _apiVersion return _apiVersion
} }
fun updateApiVersion(apiMajorVersion: Int) { fun updateApiVersion(apiMajorVersion: Int) {
settings.putInt(API_VERSION_MAJOR, apiMajorVersion) settings.putInt(API_VERSION_MAJOR, apiMajorVersion)
refreshApiVersion() refreshApiVersion()
@@ -140,13 +137,13 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
private fun refreshItemsNumber() { private fun refreshItemsNumber() {
_itemsNumber = _itemsNumber = try {
try {
settings.getString(API_ITEMS_NUMBER, "20").toInt() settings.getString(API_ITEMS_NUMBER, "20").toInt()
} catch (e: Exception) { } catch (e: Exception) {
settings.remove(API_ITEMS_NUMBER) settings.remove(API_ITEMS_NUMBER)
20 20
} }
} }
fun getApiTimeout(): Long { fun getApiTimeout(): Long {
@@ -159,9 +156,7 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
private fun secToMs(n: Long) = n * 1000 private fun secToMs(n: Long) = n * 1000
private fun refreshApiTimeout() { private fun refreshApiTimeout() {
_apiTimeout = _apiTimeout = secToMs(try {
secToMs(
try {
val settingsTimeout = settings.getString(API_TIMEOUT, "60") val settingsTimeout = settings.getString(API_TIMEOUT, "60")
if (settingsTimeout.toLong() > 0) { if (settingsTimeout.toLong() > 0) {
settingsTimeout.toLong() settingsTimeout.toLong()
@@ -172,8 +167,7 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} catch (e: Exception) { } catch (e: Exception) {
settings.remove(API_TIMEOUT) settings.remove(API_TIMEOUT)
60 60
}, })
)
} }
private fun refreshBaseUrl() { private fun refreshBaseUrl() {
@@ -206,7 +200,6 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
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)
} }
@@ -217,7 +210,6 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
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)
} }
@@ -228,7 +220,6 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
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)
} }
@@ -239,7 +230,6 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
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)
} }
@@ -250,7 +240,6 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
return _fullHeightCards == true return _fullHeightCards == true
} }
private fun refreshUpdateSourcesEnabled() { private fun refreshUpdateSourcesEnabled() {
_updateSources = settings.getBoolean(UPDATE_SOURCES, true) _updateSources = settings.getBoolean(UPDATE_SOURCES, true)
} }
@@ -261,7 +250,6 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
} }
return _updateSources == true return _updateSources == true
} }
private fun refreshPeriodicRefreshEnabled() { private fun refreshPeriodicRefreshEnabled() {
_periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false) _periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false)
} }
@@ -331,6 +319,7 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
return _notifyNewItems == true return _notifyNewItems == true
} }
private fun refreshMarkOnScrollEnabled() { private fun refreshMarkOnScrollEnabled() {
_markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false) _markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false)
} }
@@ -342,6 +331,7 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
return _markOnScroll == true return _markOnScroll == true
} }
private fun refreshActiveAllignment() { private fun refreshActiveAllignment() {
_activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY) _activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY)
} }
@@ -439,7 +429,7 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
fun refreshLoginInformation( fun refreshLoginInformation(
url: String, url: String,
login: String, login: String,
password: String, password: String
) { ) {
val regex = """\/\/(\D+):(\D+)@""".toRegex() val regex = """\/\/(\D+):(\D+)@""".toRegex()
val matchResult = regex.find(url) val matchResult = regex.find(url)
@@ -547,5 +537,6 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
const val ITEMS_CACHING = "items_caching" const val ITEMS_CACHING = "items_caching"
const val CURRENT_THEME = "currentMode" const val CURRENT_THEME = "currentMode"
} }
} }

View File

@@ -9,7 +9,7 @@ fun TAG.toView(): SelfossModel.Tag =
SelfossModel.Tag( SelfossModel.Tag(
this.name, this.name,
this.color, this.color,
this.unread.toInt(), this.unread.toInt()
) )
fun SOURCE.toView(): SelfossModel.SourceDetail = fun SOURCE.toView(): SelfossModel.SourceDetail =
@@ -21,7 +21,7 @@ fun SOURCE.toView(): SelfossModel.SourceDetail =
this.spout, this.spout,
this.error, this.error,
this.icon, this.icon,
if (this.url != null) SelfossModel.SourceParams(this.url) else null, if (this.url != null) SelfossModel.SourceParams(this.url) else null
) )
fun SelfossModel.SourceDetail.toEntity(): SOURCE = fun SelfossModel.SourceDetail.toEntity(): SOURCE =
@@ -32,14 +32,14 @@ fun SelfossModel.SourceDetail.toEntity(): SOURCE =
this.spout.orEmpty(), this.spout.orEmpty(),
this.error.orEmpty(), this.error.orEmpty(),
this.icon.orEmpty(), this.icon.orEmpty(),
this.params?.url, this.params?.url
) )
fun SelfossModel.Tag.toEntity(): TAG = fun SelfossModel.Tag.toEntity(): TAG =
TAG( TAG(
this.tag, this.tag,
this.color, this.color,
this.unread.toLong(), this.unread.toLong()
) )
fun ITEM.toView(): SelfossModel.Item = fun ITEM.toView(): SelfossModel.Item =
@@ -55,7 +55,7 @@ fun ITEM.toView(): SelfossModel.Item =
this.link, this.link,
this.sourcetitle, this.sourcetitle,
this.tags.split(","), this.tags.split(","),
this.author, this.author
) )
fun SelfossModel.Item.toEntity(): ITEM = fun SelfossModel.Item.toEntity(): ITEM =
@@ -71,5 +71,5 @@ fun SelfossModel.Item.toEntity(): ITEM =
this.link, this.link,
this.sourcetitle.getHtmlDecoded(), this.sourcetitle.getHtmlDecoded(),
this.tags.joinToString(","), this.tags.joinToString(","),
this.author, this.author
) )

View File

@@ -3,8 +3,7 @@ package bou.amine.apps.readerforselfossv2.utils
enum class ItemType(val position: Int, val type: String) { enum class ItemType(val position: Int, val type: String) {
UNREAD(1, "unread"), UNREAD(1, "unread"),
ALL(2, "all"), ALL(2, "all"),
STARRED(3, "starred"), STARRED(3, "starred");
;
companion object { companion object {
fun fromInt(value: Int) = values().first { it.position == value } fun fromInt(value: Int) = values().first { it.position == value }

View File

@@ -12,8 +12,4 @@ expect fun SelfossModel.Item.getImages(): ArrayList<String>
expect fun SelfossModel.Source.getIcon(baseUrl: String): String expect fun SelfossModel.Source.getIcon(baseUrl: String): String
expect fun constructUrl( expect fun constructUrl(baseUrl: String, path: String, file: String?): String
baseUrl: String,
path: String,
file: String?,
): String

View File

@@ -1,6 +1,7 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
fun String?.isEmptyOrNullOrNullString(): Boolean = this == null || this == "null" || this.isEmpty() fun String?.isEmptyOrNullOrNullString(): Boolean =
this == null || this == "null" || this.isEmpty()
fun String.longHash(): Long { fun String.longHash(): Long {
var h = 98764321261L var h = 98764321261L

View File

@@ -1,6 +0,0 @@
package bou.amine.apps.readerforselfossv2.rest
import io.ktor.client.engine.cio.CIOEngineConfig
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
}

View File

@@ -22,10 +22,6 @@ actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
actual fun constructUrl( actual fun constructUrl(baseUrl: String, path: String, file: String?): String {
baseUrl: String,
path: String,
file: String?,
): String {
TODO("Not yet implemented") TODO("Not yet implemented")
} }

View File

@@ -1,6 +0,0 @@
package bou.amine.apps.readerforselfossv2.rest
import io.ktor.client.engine.cio.CIOEngineConfig
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
}

View File

@@ -1,5 +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 class DateUtils {
actual companion object { actual companion object {
actual fun parseDate(dateString: String): Long { actual fun parseDate(dateString: String): Long {
@@ -10,4 +12,5 @@ actual class DateUtils {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
} }

View File

@@ -22,10 +22,6 @@ actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
actual fun constructUrl( actual fun constructUrl(baseUrl: String, path: String, file: String?): String {
baseUrl: String,
path: String,
file: String?,
): String {
TODO("Not yet implemented") TODO("Not yet implemented")
} }