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
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
image: mingc/android-build-box:latest
commands:
- echo "---------------------------------------------------------"
- 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 "Configure java..."
- . ~/.bash_profile
- jenv global 17.0
- java --version
- date
- echo "Building..."
- ./gradlew build -x test
- echo "---------------------------------------------------------"
- echo "Building and testing..."
- ./gradlew build
- echo "Testing..."
- 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:
event:
- push
@@ -53,24 +50,20 @@ steps:
- git fetch --tags -p
- PREV=$(git describe --tags --abbrev=0)
- ./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)
- CHANGELOG=$(git log $PREV..HEAD --pretty="- %s")
- echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md
- git add CHANGELOG.md
- git commit -m "Changelog for $VER [CI SKIP]"
- git push pushing master
- git push pushing --tags
environment:
TZ: Europe/Paris
- name: git-push
image: appleboy/drone-git-push
settings:
branch: master
remote:
from_secret: remoteUrl
followtags: true
ssh_key:
from_secret: privateKey
skip_verify: true
GITEA_USR:
from_secret: giteaUsr
GITEA_PASS:
from_secret: giteaPass
- name: scpFiles
image: appleboy/drone-scp
@@ -114,10 +107,10 @@ steps:
- git fetch --tags
- echo "---------------------------------------------------------"
- 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 "Generate APK"
- ./gradlew :androidApp:assembleGithubConfigRelease
- ./gradlew :androidApp:assembleGithubConfigRelease -P pushCache=false
- echo "---------------------------------------------------------"
- echo "Get 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**
- feat: Added confirmation dialog for disconnect item menu.

View File

@@ -12,7 +12,7 @@ plugins {
}
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
val result: String = ByteArrayOutputStream().use { outputStream ->
var result: String = ByteArrayOutputStream().use { outputStream ->
project.exec {
commandLine = cmd.split(" ")
standardOutput = outputStream
@@ -25,7 +25,7 @@ fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
fun gitVersion(): String {
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.")
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
} else {
@@ -57,22 +57,23 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
// Flag to enable support for the new language APIs
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// For Kotlin projects
kotlinOptions {
jvmTarget = "17"
jvmTarget = "11"
}
compileSdk = 34
compileSdk = 33
buildToolsVersion = "33.0.0"
buildFeatures {
viewBinding = true
}
defaultConfig {
applicationId = "bou.amine.apps.readerforselfossv2.android"
minSdk = 25
targetSdk = 34
minSdk = 21
targetSdk = 33
versionCode = versionCodeFromGit()
versionName = versionNameFromGit()
@@ -85,7 +86,7 @@ android {
// tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
packaging {
packagingOptions {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
@@ -114,25 +115,25 @@ dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
implementation(project(":shared"))
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
implementation("com.google.android.material:material:1.5.0")
implementation("androidx.appcompat:appcompat:1.4.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")))
// Android Support
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("com.google.android.material:material:1.5.0")
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
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.annotation:annotation:1.7.0")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.annotation:annotation:1.3.0")
implementation("androidx.work:work-runtime-ktx:2.7.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("org.jsoup:jsoup:1.15.4")
implementation("org.jsoup:jsoup:1.14.3")
//multidex
implementation("androidx.multidex:multidex:2.0.1")
@@ -153,7 +154,7 @@ dependencies {
// Pager
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
implementation("org.kodein.di:kodein-di:7.14.0")
@@ -169,7 +170,7 @@ dependencies {
//PhotoView
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")
@@ -182,7 +183,7 @@ dependencies {
//test
testImplementation("junit:junit:4.13.2")
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("ch.acra:acra-http:$acraVersion")

View File

@@ -43,7 +43,9 @@ import org.kodein.di.instance
import java.security.MessageDigest
import java.util.concurrent.TimeUnit
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware {
private var items: ArrayList<SelfossModel.Item> = ArrayList()
private var elementsShown: ItemType = ItemType.UNREAD
@@ -61,22 +63,22 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private var fromTabShortcut: Boolean = false
private val settingsLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
appSettingsService.refreshUserSettings()
}
private val settingsLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
appSettingsService.refreshUserSettings()
}
override val di by closestDI()
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
private val repository : Repository by instance()
private val appSettingsService : AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
val view = binding.root
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
repository.offlineOverride = intent.getBooleanExtra("startOffline", false)
if (fromTabShortcut) {
elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
@@ -90,6 +92,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleSwipeRefreshLayout()
if (appSettingsService.isItemCachingEnabled()) {
CoroutineScope(Dispatchers.Main).launch {
repository.tryToCacheItemsAndGetNewOnes()
@@ -101,7 +104,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.swipeRefreshLayout.setColorSchemeResources(
R.color.refresh_progress_1,
R.color.refresh_progress_2,
R.color.refresh_progress_3,
R.color.refresh_progress_3
)
binding.swipeRefreshLayout.setOnRefreshListener {
repository.offlineOverride = false
@@ -112,41 +115,37 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
}
val swipeDirs =
if (appSettingsService.getPublicAccess()) {
0
} else {
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
}
val swipeDirs = if (appSettingsService.getPublicAccess()) {
0
} else {
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
}
val simpleItemTouchCallback =
object : ItemTouchHelper.SimpleCallback(
0,
swipeDirs,
swipeDirs
) {
override fun getSwipeDirs(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
viewHolder: RecyclerView.ViewHolder
): Int =
if (elementsShown == ItemType.STARRED) {
0
} else {
super.getSwipeDirs(
recyclerView,
viewHolder,
viewHolder
)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(
viewHolder: RecyclerView.ViewHolder,
swipeDir: Int,
) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {
val position = viewHolder.bindingAdapterPosition
val i = items.elementAtOrNull(position)
@@ -163,7 +162,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
Toast.makeText(
this@HomeActivity,
"Found null when swiping at positon $position.",
Toast.LENGTH_LONG,
Toast.LENGTH_LONG
).show()
}
}
@@ -172,10 +171,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
}
private fun updateBottomBarBadgeCount(
badge: TextBadgeItem,
count: Int,
) {
private fun updateBottomBarBadgeCount(badge: TextBadgeItem, count: Int) {
if (count > 0) {
badge
.setText(count.toString())
@@ -186,18 +182,16 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
private fun handleBottomBar() {
tabNewBadge =
TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
tabArchiveBadge =
TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
tabStarredBadge =
TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
tabNewBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
tabArchiveBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
tabStarredBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
if (appSettingsService.isDisplayUnreadCountEnabled()) {
lifecycleScope.launch {
@@ -224,19 +218,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val tabNew =
BottomNavigationItem(
R.drawable.ic_tab_fiber_new_black_24dp,
getString(R.string.tab_new),
getString(R.string.tab_new)
)
.setBadgeItem(tabNewBadge)
val tabArchive =
BottomNavigationItem(
R.drawable.ic_tab_archive_black_24dp,
getString(R.string.tab_read),
getString(R.string.tab_read)
)
.setBadgeItem(tabArchiveBadge)
val tabStarred =
BottomNavigationItem(
R.drawable.ic_tab_favorite_black_24dp,
getString(R.string.tab_favs),
getString(R.string.tab_favs)
).setActiveColorResource(R.color.pink)
.setBadgeItem(tabStarredBadge)
@@ -277,6 +271,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getElementsAccordingToTab()
}
private fun handleGDPRDialog(GDPRShown: Boolean) {
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
@@ -286,7 +281,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK",
"OK"
) { dialog, _ ->
appSettingsService.settings.putBoolean("GDPR_shown", true)
dialog.dismiss()
@@ -303,41 +298,37 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
when (currentManager) {
is StaggeredGridLayoutManager ->
if (!appSettingsService.isCardViewEnabled()) {
layoutManager =
GridLayoutManager(
this,
calculateNoOfColumns(),
)
layoutManager = GridLayoutManager(
this,
calculateNoOfColumns()
)
binding.recyclerView.layoutManager = layoutManager
}
is GridLayoutManager ->
if (appSettingsService.isCardViewEnabled()) {
layoutManager =
StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL,
)
layoutManager = StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager
}
else ->
if (currentManager == null) {
if (!appSettingsService.isCardViewEnabled()) {
layoutManager =
GridLayoutManager(
this,
calculateNoOfColumns(),
)
layoutManager = GridLayoutManager(
this,
calculateNoOfColumns()
)
binding.recyclerView.layoutManager = layoutManager
} else {
layoutManager =
StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL,
)
layoutManager = StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager
}
}
@@ -345,40 +336,39 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
private fun handleBottomBarActions() {
binding.bottomBar.setTabSelectedListener(
object : BottomNavigationBar.OnTabSelectedListener {
override fun onTabUnselected(position: Int) = Unit
binding.bottomBar.setTabSelectedListener(object : BottomNavigationBar.OnTabSelectedListener {
override fun onTabUnselected(position: Int) = Unit
override fun onTabReselected(position: Int) {
when (val layoutManager = binding.recyclerView.adapter) {
is StaggeredGridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPositions(null)[0] == 0) {
getElementsAccordingToTab()
} else {
layoutManager.scrollToPositionWithOffset(0, 0)
}
is GridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
getElementsAccordingToTab()
} else {
layoutManager.scrollToPositionWithOffset(0, 0)
}
else -> Unit
}
override fun onTabReselected(position: Int) {
when (val layoutManager = binding.recyclerView.adapter) {
is StaggeredGridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPositions(null)[0] == 0) {
getElementsAccordingToTab()
} else {
layoutManager.scrollToPositionWithOffset(0, 0)
}
is GridLayoutManager ->
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
getElementsAccordingToTab()
} else {
layoutManager.scrollToPositionWithOffset(0, 0)
}
else -> Unit
}
}
override fun onTabSelected(position: Int) {
offset = 0
lastFetchDone = false
override fun onTabSelected(position: Int) {
offset = 0
lastFetchDone = false
elementsShown = ItemType.fromInt(position + 1)
getElementsAccordingToTab()
binding.recyclerView.scrollToPosition(0)
elementsShown = ItemType.fromInt(position + 1)
getElementsAccordingToTab()
binding.recyclerView.scrollToPosition(0)
fetchOnEmptyList()
}
},
)
fetchOnEmptyList()
}
})
}
fun fetchOnEmptyList() {
@@ -388,33 +378,27 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
private fun handleInfiniteScroll() {
recyclerViewScrollListener =
object : RecyclerView.OnScrollListener() {
override fun onScrolled(
localRecycler: RecyclerView,
dx: Int,
dy: Int,
) {
if (dy > 0) {
val lastVisibleItem = getLastVisibleItem()
recyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(localRecycler: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) {
val lastVisibleItem = getLastVisibleItem()
if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) {
getElementsAccordingToTab(appendResults = true)
}
if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) {
getElementsAccordingToTab(appendResults = true)
}
}
}
}
binding.recyclerView.clearOnScrollListeners()
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
}
private fun getLastVisibleItem(): Int {
private fun getLastVisibleItem() : Int {
return when (val manager = binding.recyclerView.layoutManager) {
is StaggeredGridLayoutManager ->
manager.findLastCompletelyVisibleItemPositions(
null,
).last()
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
null
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0
}
@@ -427,31 +411,28 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.emptyText.visibility = View.GONE
}
fun getElementsAccordingToTab(appendResults: Boolean = false) {
offset =
if (appendResults && items.size > 0) {
items.size - 1
} else {
0
}
fun getElementsAccordingToTab(
appendResults: Boolean = false
) {
offset = if (appendResults && items.size > 0) {
items.size - 1
} else {
0
}
firstVisible = if (appendResults) firstVisible else 0
getItems(appendResults, elementsShown)
}
private fun getItems(
appendResults: Boolean,
itemType: ItemType,
) {
private fun getItems(appendResults: Boolean, itemType: ItemType) {
CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = true
repository.displayedItems = itemType
items =
if (appendResults) {
repository.getOlderItems()
} else {
repository.getNewerItems()
}
items = if (appendResults) {
repository.getOlderItems()
} else {
repository.getNewerItems()
}
binding.swipeRefreshLayout.isRefreshing = false
handleListResult()
}
@@ -460,44 +441,43 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun handleListResult(appendResults: Boolean = false) {
if (appendResults) {
val oldManager = binding.recyclerView.layoutManager
firstVisible =
when (oldManager) {
is StaggeredGridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPositions(null).last()
is GridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPosition()
else -> 0
}
firstVisible = when (oldManager) {
is StaggeredGridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPositions(null).last()
is GridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPosition()
else -> 0
}
}
if (recyclerAdapter == null) {
if (appSettingsService.isCardViewEnabled()) {
recyclerAdapter =
ItemCardAdapter(
this,
items,
) {
updateItems(it)
}
ItemCardAdapter(
this,
items,
) {
updateItems(it)
}
} else {
recyclerAdapter =
ItemListAdapter(
this,
items,
) {
updateItems(it)
}
ItemListAdapter(
this,
items,
) {
updateItems(it)
}
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
this@HomeActivity,
DividerItemDecoration.VERTICAL,
),
DividerItemDecoration.VERTICAL
)
)
}
binding.recyclerView.adapter = recyclerAdapter
} else {
(recyclerAdapter as ItemsAdapter<*>).updateAllItems(items)
(recyclerAdapter as ItemsAdapter<*>).updateAllItems(items)
}
reloadBadges()
@@ -549,11 +529,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
return true
}
private fun needsConfirmation(
titleRes: Int,
messageRes: Int,
doFn: () -> Unit,
) {
private fun needsConfirmation(titleRes: Int, messageRes: Int, doFn: () -> Unit) {
AlertDialog.Builder(this@HomeActivity)
.setMessage(messageRes)
.setTitle(titleRes)
@@ -578,15 +554,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
if (updatedRemote) {
Toast.makeText(
this@HomeActivity,
R.string.refresh_success_response,
Toast.LENGTH_LONG,
R.string.refresh_success_response, Toast.LENGTH_LONG
)
.show()
} else {
Toast.makeText(
this@HomeActivity,
R.string.refresh_failer_message,
Toast.LENGTH_SHORT,
Toast.LENGTH_SHORT
).show()
}
}
@@ -604,7 +579,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
Toast.makeText(
this@HomeActivity,
R.string.all_posts_read,
Toast.LENGTH_SHORT,
Toast.LENGTH_SHORT
).show()
tabNewBadge.removeBadge()
@@ -613,7 +588,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
Toast.makeText(
this@HomeActivity,
R.string.all_posts_not_read,
Toast.LENGTH_SHORT,
Toast.LENGTH_SHORT
).show()
}
handleListResult()
@@ -660,12 +635,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun handleRecurringTask() {
if (appSettingsService.isPeriodicRefreshEnabled()) {
val myConstraints =
Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
.setRequiresStorageNotLow(true)
.build()
val myConstraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
.setRequiresStorageNotLow(true)
.build()
val backgroundWork =
PeriodicWorkRequestBuilder<LoadingWorker>(appSettingsService.getRefreshMinutes(), TimeUnit.MINUTES)
@@ -673,9 +647,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
.addTag("selfoss-loading")
.build()
WorkManager.getInstance(
baseContext,
).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork)
WorkManager.getInstance(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
class ImageActivity : AppCompatActivity() {
private lateinit var allImages: ArrayList<String>
private var position: Int = 0
private lateinit var allImages : ArrayList<String>
private var position : Int = 0
private lateinit var binding: ActivityImageBinding
@@ -32,44 +32,27 @@ class ImageActivity : AppCompatActivity() {
binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(position, false)
val transitionListener =
object : MotionLayout.TransitionListener {
override fun onTransitionStarted(
motionLayout: MotionLayout?,
startId: Int,
endId: Int,
) {
// Nothing
}
val transitionListener = object : MotionLayout.TransitionListener {
override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {
// Nothing
}
override fun onTransitionChange(
motionLayout: MotionLayout?,
startId: Int,
endId: Int,
progress: Float,
) {
// Nothing
}
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float
) {
// Nothing
}
override fun onTransitionCompleted(
motionLayout: MotionLayout?,
currentId: Int,
) {
if (motionLayout?.currentState == binding.root.endState) {
onBackPressedDispatcher.onBackPressed()
overridePendingTransition(0, 0)
}
}
override fun onTransitionTrigger(
motionLayout: MotionLayout?,
triggerId: Int,
positive: Boolean,
progress: Float,
) {
// Nothing
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
if (motionLayout?.currentState == binding.root.endState) {
onBackPressedDispatcher.onBackPressed()
overridePendingTransition(0, 0)
}
}
override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {
// Nothing
}
}
binding.root.setTransitionListener(transitionListener)
}
@@ -85,6 +68,7 @@ class ImageActivity : AppCompatActivity() {
}
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allImages.size
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.instance
class LoginActivity : AppCompatActivity(), DIAware {
private var inValidCount: Int = 0
private var isWithLogin = false
@@ -38,6 +40,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -66,6 +69,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
}
private fun handleActions() {
binding.passwordView.setOnEditorActionListener(
TextView.OnEditorActionListener { _, id, _ ->
if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
@@ -73,7 +77,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
return@OnEditorActionListener true
}
false
},
}
)
binding.signInButton.setOnClickListener { attemptLogin() }
@@ -94,7 +98,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
alertDialog.setMessage(getString(R.string.base_url_error))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK",
"OK"
) { dialog, _ -> dialog.dismiss() }
alertDialog.show()
}
@@ -119,6 +123,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
}
private fun attemptLogin() {
// Reset errors.
binding.urlView.error = null
binding.loginView.error = null
@@ -150,7 +155,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
Toast.makeText(
applicationContext,
R.string.application_selfoss_only,
Toast.LENGTH_LONG,
Toast.LENGTH_LONG
).show()
}
preferenceError()
@@ -164,7 +169,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
private fun failLoginDetails(
password: String,
login: String,
login: String
) {
var lastFocusedView: View? = null
var cancel = false
@@ -197,7 +202,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
alertDialog.setMessage(getString(R.string.text_wrong_url))
alertDialog.setButton(
AlertDialog.BUTTON_NEUTRAL,
"OK",
"OK"
) { dialog, _ -> dialog.dismiss() }
alertDialog.show()
inValidCount = 0
@@ -206,10 +211,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
maybeCancelAndFocusView(cancel, focusView)
}
private fun maybeCancelAndFocusView(
cancel: Boolean,
focusView: View?,
) {
private fun maybeCancelAndFocusView(cancel: Boolean, focusView: View?) {
if (cancel) {
focusView?.requestFocus()
}
@@ -223,13 +225,12 @@ class LoginActivity : AppCompatActivity(), DIAware {
.animate()
.setDuration(shortAnimTime.toLong())
.alpha(
if (show) 0F else 1F,
).setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
}
},
if (show) 0F else 1F
).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
}
}
)
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
@@ -237,13 +238,12 @@ class LoginActivity : AppCompatActivity(), DIAware {
.animate()
.setDuration(shortAnimTime.toLong())
.alpha(
if (show) 1F else 0F,
).setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
}
},
if (show) 1F else 0F
).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
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() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
class ImageFragment : Fragment() {
private lateinit var imageUrl: String
private lateinit var imageUrl : String
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
private var _binding: FragmentImageBinding? = null
private val binding get() = _binding
@@ -22,20 +23,16 @@ class ImageFragment : Fragment() {
imageUrl = requireArguments().getString("imageUrl")!!
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentImageBinding.inflate(inflater, container, false)
val view = binding?.root
binding!!.photoView.visibility = View.VISIBLE
Glide.with(requireActivity())
.asBitmap()
.apply(glideOptions)
.load(imageUrl)
.into(binding!!.photoView)
.asBitmap()
.apply(glideOptions)
.load(imageUrl)
.into(binding!!.photoView)
return view
}
@@ -48,7 +45,9 @@ class ImageFragment : Fragment() {
companion object {
private const val ARG_IMAGE = "imageUrl"
fun newInstance(imageUrl: String): ImageFragment {
fun newInstance(
imageUrl : String
): ImageFragment {
val fragment = ImageFragment()
val args = Bundle()
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.request.RequestOptions
fun SelfossModel.Item.preloadImages(context: Context): Boolean {
fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages()
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
try {
for (url in imageUrls) {
if (URLUtil.isValidUrl(url)) {
if ( URLUtil.isValidUrl(url)) {
Glide.with(context).asBitmap()
.apply(glideOptions)
.load(url).submit()
}
}
} catch (e: Error) {
} catch (e : Error) {
e.sendSilentlyWithAcraWithName("preloadImages")
return false
}

View File

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

View File

@@ -24,10 +24,8 @@ import org.kodein.di.android.closestDI
private const val TITLE_TAG = "settingsActivityTitle"
class SettingsActivity :
AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
DIAware {
class SettingsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, DIAware {
override val di by closestDI()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -37,9 +35,9 @@ class SettingsActivity :
setContentView(binding.root)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, MainPreferenceFragment())
.commit()
.beginTransaction()
.replace(R.id.settings, MainPreferenceFragment())
.commit()
} else {
title = savedInstanceState.getCharSequence(TITLE_TAG)
}
@@ -73,67 +71,57 @@ class SettingsActivity :
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference,
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
// Instantiate the new Fragment
val args = pref.extras
val fragment =
supportFragmentManager.fragmentFactory.instantiate(
val fragment = supportFragmentManager.fragmentFactory.instantiate(
classLoader,
pref.fragment.toString(),
).apply {
arguments = args
setTargetFragment(caller, 0)
}
pref.fragment
).apply {
arguments = args
setTargetFragment(caller, 0)
}
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.settings, fragment)
.addToBackStack(null)
.commit()
.replace(R.id.settings, fragment)
.addToBackStack(null)
.commit()
title = pref.title
supportActionBar?.title = title
return true
}
class MainPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_main, rootKey)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true
}
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true
}
preferenceManager.findPreference<Preference>("action_about")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { _ ->
context?.let {
LibsBuilder()
.withAboutIconShown(true)
.withAboutVersionShown(true)
.start(it)
}
true
preferenceManager.findPreference<Preference>("action_about")?.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
context?.let {
LibsBuilder()
.withAboutIconShown(true)
.withAboutVersionShown(true)
.start(it)
}
true
}
}
}
class GeneralPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_general, rootKey)
val editTextPreference = preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number")
editTextPreference?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.filters =
arrayOf(
editText.filters = arrayOf(
InputFilter { source, _, _, dest, _, _ ->
try {
val input: Int = (dest.toString() + source.toString()).toInt()
@@ -143,53 +131,35 @@ class SettingsActivity :
Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show()
}
""
},
)
}
)
}
}
}
class ArticleViewerPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_viewer, rootKey)
val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size")
fontSize?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.addTextChangedListener {
object : TextWatcher {
override fun beforeTextChanged(
charSequence: CharSequence,
i: Int,
i1: Int,
i2: Int,
) {
// We do nothing
}
override fun onTextChanged(
charSequence: CharSequence,
i: Int,
i1: Int,
i2: Int,
) {
// We do nothing
}
override fun afterTextChanged(editable: Editable) {
try {
editText.textSize = editable.toString().toInt().toFloat()
} catch (e: NumberFormatException) {
e.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > afterTextChanged")
}
editText.addTextChangedListener { object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
// We do nothing
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
// We do nothing
}
override fun afterTextChanged(editable: Editable) {
try {
editText.textSize = editable.toString().toInt().toFloat()
} catch (e: NumberFormatException) {
e.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > afterTextChanged")
}
}
}
editText.filters =
arrayOf(
} }
editText.filters = arrayOf(
InputFilter { source, _, _, dest, _, _ ->
try {
val input = (dest.toString() + source.toString()).toInt()
@@ -198,33 +168,26 @@ class SettingsActivity :
nfe.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > filters")
}
""
},
)
}
)
}
}
}
class OfflinePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_offline, rootKey)
}
}
class ThemePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_theme, rootKey)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true
}
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true
}
}
}
@@ -234,37 +197,28 @@ class SettingsActivity :
startActivity(browserIntent)
}
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_links, rootKey)
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.trackerUrl))
true
}
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.trackerUrl))
true
}
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.sourceUrl))
false
}
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.sourceUrl))
false
}
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.translationUrl))
false
}
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.translationUrl))
false
}
}
}
class ExperimentalPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
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.utils.toStringUriWithHttp
fun Context.shareLink(
itemUrl: String,
itemTitle: String,
) {
fun Context.shareLink(itemUrl: String, itemTitle: String) {
val sendIntent = Intent()
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
sendIntent.action = Intent.ACTION_SEND
@@ -18,7 +15,7 @@ fun Context.shareLink(
startActivity(
Intent.createChooser(
sendIntent,
getString(R.string.share),
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
getString(R.string.share)
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
}

View File

@@ -12,54 +12,51 @@ import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
import com.google.android.material.imageview.ShapeableImageView
import kotlin.math.abs
class CircleImageView
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : RelativeLayout(context, attrs, defStyleAttr) {
val view: View
val imageView: ShapeableImageView
val textView: TextView
class CircleImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
val view: View
val imageView: ShapeableImageView
val textView: TextView
private val colorScheme =
listOf(
-0x1a8c8d,
-0xf9d6e,
-0x459738,
-0x6a8a33,
-0x867935,
-0x9b4a0a,
-0xb03c09,
-0xb22f1f,
-0xb24954,
-0x7e387c,
-0x512a7f,
-0x759b,
-0x2b1ea9,
-0x2ab1,
-0x48b3,
-0x5e7781,
-0x6f5b52,
)
private val colorScheme = listOf(
-0x1a8c8d,
-0xf9d6e,
-0x459738,
-0x6a8a33,
-0x867935,
-0x9b4a0a,
-0xb03c09,
-0xb22f1f,
-0xb24954,
-0x7e387c,
-0x512a7f,
-0x759b,
-0x2b1ea9,
-0x2ab1,
-0x48b3,
-0x5e7781,
-0x6f5b52
)
init {
view = LayoutInflater.from(context).inflate(R.layout.circle_image_view, this, true)
imageView = view.findViewById(R.id.circleImage)
textView = view.findViewById(R.id.circleText)
}
fun setBackgroundAndText(text: String) {
val circleDrawable = GradientDrawable()
val color = colorFromIdentifier(text)
circleDrawable.setColor(color)
imageView.setImageDrawable(circleDrawable)
textView.text = text.toTextDrawableString()
}
private fun colorFromIdentifier(key: String): Int {
return colorScheme[abs(key.hashCode()) % colorScheme.size]
}
init {
view = LayoutInflater.from(context).inflate(R.layout.circle_image_view, this, true)
imageView = view.findViewById(R.id.circleImage)
textView = view.findViewById(R.id.circleText)
}
fun setBackgroundAndText(text: String) {
val circleDrawable = GradientDrawable()
val color = colorFromIdentifier(text)
circleDrawable.setColor(color)
imageView.setImageDrawable(circleDrawable)
textView.text = text.toTextDrawableString()
}
private fun colorFromIdentifier(key: String): Int {
return colorScheme[abs(key.hashCode()) % colorScheme.size]
}
}

View File

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

View File

@@ -8,4 +8,5 @@ fun TextBadgeItem.removeBadge(): TextBadgeItem {
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.InputStream
fun Context.bitmapCenterCrop(
url: String,
iv: ImageView,
) = Glide.with(this)
.asBitmap()
.load(url)
.apply(RequestOptions.centerCropTransform())
.into(iv)
fun Context.bitmapCenterCrop(url: String, iv: ImageView) =
Glide.with(this)
.asBitmap()
.load(url)
.apply(RequestOptions.centerCropTransform())
.into(iv)
fun Context.circularDrawable(
url: String,
view: CircleImageView,
) {
view.textView.text = ""
fun Context.circularDrawable(url: String, view: CircleImageView) {
view.textView.text =""
Glide.with(this)
.load(url)
.into(view.imageView)
}
fun getBitmapInputStream(
bitmap: Bitmap,
compressFormat: Bitmap.CompressFormat,
): InputStream {
fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream {
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(compressFormat, 80, byteArrayOutputStream)
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()

View File

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

View File

@@ -1,5 +1,5 @@
<?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="title_activity_login">"Accedi"</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
class DatesTest {
private val newVersionDateVariant = "2022-12-24T17:00:08+00"
private val newVersionDate = "2013-04-07T13:43:00+01:00"
private val oldVersionDate = "2013-05-07 13:46:00"
private val oldVersionDateVariant = "2021-03-21 10:32:00.000000"
private val v3Date = "2013-04-07T13:43:00+01:00"
private val v4Date = "2013-04-07 13:43:00"
private val bug1Date = "2022-12-24T17:00:08+00"
@Test
fun new_version_date_should_be_parsed() {
val date = DateUtils.parseDate(newVersionDate)
fun v3_date_should_be_parsed() {
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 =
LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(expected, date)
assertEquals(date, expected)
}
@Test
fun old_version_date_should_be_parsed() {
val date = DateUtils.parseDate(oldVersionDate)
fun bug1_date_should_be_parsed() {
val date = DateUtils.parseDate(bug1Date)
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()
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 lateinit var repository: Repository
private fun initializeRepository(
isConnectionAvailable: MutableStateFlow<Boolean> =
MutableStateFlow(
true,
),
isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(
true
)
) {
repository = Repository(api, appSettingsService, isConnectionAvailable, db)
@@ -64,16 +64,14 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.apiInformation() } returns
StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)),
)
coEvery { api.stats() } returns
StatusAndData(
success = true,
data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED),
)
coEvery { api.apiInformation() } returns StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true))
)
coEvery { api.stats() } returns StatusAndData(
success = true,
data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED)
)
every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit
every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems()
@@ -103,7 +101,7 @@ class RepositoryTest {
fun get_api_4_date_with_api_1_version_stored() {
every { appSettingsService.getApiVersion() } returns 1
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
every { appSettingsService.updateApiVersion(any()) } returns Unit
initializeRepository()
@@ -118,11 +116,10 @@ class RepositoryTest {
@Test
fun get_public_access() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns
StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)),
)
coEvery { api.apiInformation() } returns StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true))
)
every { appSettingsService.getUserName() } returns ""
initializeRepository()
@@ -134,11 +131,10 @@ class RepositoryTest {
@Test
fun get_public_access_username_not_empty() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns
StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)),
)
coEvery { api.apiInformation() } returns StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true))
)
every { appSettingsService.getUserName() } returns "username"
initializeRepository()
@@ -150,11 +146,10 @@ class RepositoryTest {
@Test
fun get_public_access_no_auth() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns
StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, false)),
)
coEvery { api.apiInformation() } returns StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, false))
)
every { appSettingsService.getUserName() } returns ""
initializeRepository()
@@ -166,11 +161,10 @@ class RepositoryTest {
@Test
fun get_public_access_disabled() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns
StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)),
)
coEvery { api.apiInformation() } returns StatusAndData(
success = true,
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true))
)
every { appSettingsService.getUserName() } returns ""
initializeRepository()
@@ -186,10 +180,10 @@ class RepositoryTest {
val itemParameters = FakeItemParameters()
itemParameters.datetime = "2021-04-23 11:45:32"
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(
success = true,
data = generateTestApiItem(itemParameters),
)
StatusAndData(
success = true,
data = generateTestApiItem(itemParameters)
)
initializeRepository()
runBlocking {
@@ -202,7 +196,7 @@ class RepositoryTest {
@Test
fun get_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
runBlocking {
@@ -217,7 +211,7 @@ class RepositoryTest {
@Test
fun get_all_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
repository.displayedItems = ItemType.ALL
@@ -233,7 +227,7 @@ class RepositoryTest {
@Test
fun get_newer_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
repository.displayedItems = ItemType.STARRED
@@ -270,10 +264,10 @@ class RepositoryTest {
itemParameter3.tags = "Other, Tag"
itemParameter3.id = "3"
coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
itemParameter1,
itemParameter1
) +
generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3)
generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3)
every { appSettingsService.isItemCachingEnabled() } returns true
@@ -298,26 +292,24 @@ class RepositoryTest {
itemParameter3.sourcetitle = "Other"
itemParameter3.id = "3"
coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
itemParameter1,
itemParameter1
) +
generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3)
generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3)
every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(MutableStateFlow(false))
repository.setSourceFilter(
SelfossModel.SourceDetail(
1,
"Test",
null,
listOf("tags"),
SPOUT,
"",
IMAGE_URL,
SelfossModel.SourceParams("url"),
),
)
repository.setSourceFilter(SelfossModel.SourceDetail(
1,
"Test",
null,
listOf("tags"),
SPOUT,
"",
IMAGE_URL,
SelfossModel.SourceParams("url")
))
runBlocking {
repository.getNewerItems()
}
@@ -330,7 +322,7 @@ class RepositoryTest {
@Test
fun get_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
repository.items = ArrayList(generateTestApiItem())
@@ -346,7 +338,7 @@ class RepositoryTest {
@Test
fun get_all_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
repository.items = ArrayList(generateTestApiItem())
@@ -363,7 +355,7 @@ class RepositoryTest {
@Test
fun get_older_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem())
StatusAndData(success = true, data = generateTestApiItem())
initializeRepository()
repository.displayedItems = ItemType.STARRED
@@ -600,16 +592,14 @@ class RepositoryTest {
}
private fun prepareTags(): Pair<List<SelfossModel.Tag>, List<TAG>> {
val tags =
listOf(
SelfossModel.Tag("test", "red", 6),
SelfossModel.Tag("second", "yellow", 0),
)
val tagsDB =
listOf(
TAG("test_DB", "red", 6),
TAG("second_DB", "yellow", 0),
)
val tags = listOf(
SelfossModel.Tag("test", "red", 6),
SelfossModel.Tag("second", "yellow", 0)
)
val tagsDB = listOf(
TAG("test_DB", "red", 6),
TAG("second_DB", "yellow", 0)
)
coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
@@ -631,50 +621,48 @@ class RepositoryTest {
}
private fun prepareSources(): Pair<ArrayList<SelfossModel.SourceDetail>, List<SOURCE>> {
val sources =
arrayListOf(
SelfossModel.SourceDetail(
1,
"First source",
null,
listOf("Test", "second"),
SPOUT,
"",
IMAGE_URL_2,
SelfossModel.SourceParams("url"),
),
SelfossModel.SourceDetail(
2,
"Second source",
null,
listOf("second"),
SPOUT,
"",
IMAGE_URL,
SelfossModel.SourceParams("url"),
),
val sources = arrayListOf(
SelfossModel.SourceDetail(
1,
"First source",
null,
listOf("Test", "second"),
SPOUT,
"",
IMAGE_URL_2,
SelfossModel.SourceParams("url")
),
SelfossModel.SourceDetail(
2,
"Second source",
null,
listOf("second"),
SPOUT,
"",
IMAGE_URL,
SelfossModel.SourceParams("url")
)
val sourcesDB =
listOf(
SOURCE(
"1",
"First DB source",
"Test,second",
SPOUT,
"",
IMAGE_URL_2,
"url",
),
SOURCE(
"2",
"Second source",
"second",
SPOUT,
"",
IMAGE_URL,
"url",
),
)
val sourcesDB = listOf(
SOURCE(
"1",
"First DB source",
"Test,second",
SPOUT,
"",
IMAGE_URL_2,
"url"
),
SOURCE(
"2",
"Second source",
"second",
SPOUT,
"",
IMAGE_URL,
"url"
)
)
coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
@@ -803,18 +791,17 @@ class RepositoryTest {
@Test
fun create_source() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true)
SuccessResponse(true)
initializeRepository()
var response: Boolean
runBlocking {
response =
repository.createSource(
"test",
FEED_URL,
SPOUT,
TAGS,
)
response = repository.createSource(
"test",
FEED_URL,
SPOUT,
TAGS,
)
}
coVerify(exactly = 1) {
@@ -831,18 +818,17 @@ class RepositoryTest {
@Test
fun create_source_but_response_fails() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(false)
SuccessResponse(false)
initializeRepository()
var response: Boolean
runBlocking {
response =
repository.createSource(
"test",
FEED_URL,
SPOUT,
TAGS,
)
response = repository.createSource(
"test",
FEED_URL,
SPOUT,
TAGS
)
}
coVerify(exactly = 1) {
@@ -850,7 +836,7 @@ class RepositoryTest {
any(),
any(),
any(),
any(),
any()
)
}
assertSame(false, response)
@@ -859,18 +845,17 @@ class RepositoryTest {
@Test
fun create_source_without_connection() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true)
SuccessResponse(true)
initializeRepository(MutableStateFlow(false))
var response: Boolean
runBlocking {
response =
repository.createSource(
"test",
FEED_URL,
SPOUT,
TAGS,
)
response = repository.createSource(
"test",
FEED_URL,
SPOUT,
TAGS
)
}
coVerify(exactly = 0) {
@@ -931,11 +916,10 @@ class RepositoryTest {
@Test
fun update_remote() {
coEvery { api.update() } returns
StatusAndData(
success = true,
data = "finished",
)
coEvery { api.update() } returns StatusAndData(
success = true,
data = "finished"
)
initializeRepository()
var response: Boolean
@@ -949,11 +933,10 @@ class RepositoryTest {
@Test
fun update_remote_but_response_fails() {
coEvery { api.update() } returns
StatusAndData(
success = false,
data = "unallowed access",
)
coEvery { api.update() } returns StatusAndData(
success = false,
data = "unallowed access"
)
initializeRepository()
var response: Boolean
@@ -967,11 +950,10 @@ class RepositoryTest {
@Test
fun update_remote_with_unallowed_access() {
coEvery { api.update() } returns
StatusAndData(
success = true,
data = "unallowed access",
)
coEvery { api.update() } returns StatusAndData(
success = true,
data = "unallowed access"
)
initializeRepository()
var response: Boolean
@@ -985,11 +967,10 @@ class RepositoryTest {
@Test
fun update_remote_without_connection() {
coEvery { api.update() } returns
StatusAndData(
success = true,
data = "undocumented...",
)
coEvery { api.update() } returns StatusAndData(
success = true,
data = "undocumented..."
)
initializeRepository(MutableStateFlow(false))
var response: Boolean
@@ -1056,7 +1037,7 @@ class RepositoryTest {
appSettingsService.refreshLoginInformation(
BASE_URL,
"login",
"password",
"password"
)
}
}
@@ -1076,14 +1057,13 @@ class RepositoryTest {
any(),
any(),
any(),
any(),
)
} returnsMany
listOf(
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
any()
)
} returnsMany listOf(
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
)
initializeRepository()
prepareSearch()
@@ -1097,7 +1077,7 @@ class RepositoryTest {
@Test
fun cache_items_but_response_fails() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = false, data = generateTestApiItem())
StatusAndData(success = false, data = generateTestApiItem())
initializeRepository()
prepareSearch()
@@ -1111,7 +1091,7 @@ class RepositoryTest {
@Test
fun cache_items_without_connection() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = false, data = generateTestApiItem())
StatusAndData(success = false, data = generateTestApiItem())
initializeRepository(MutableStateFlow(false))
prepareSearch()
@@ -1133,8 +1113,8 @@ class RepositoryTest {
SPOUT,
"",
IMAGE_URL_2,
SelfossModel.SourceParams("url"),
),
SelfossModel.SourceParams("url")
)
)
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.model.SelfossModel
fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> {
return listOf(
ITEM(
@@ -17,8 +18,8 @@ fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<I
link = item.link,
sourcetitle = item.sourcetitle,
tags = item.tags,
author = item.author,
),
author = item.author
)
)
}
@@ -36,8 +37,8 @@ fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<S
link = item.link,
sourcetitle = item.sourcetitle,
tags = item.tags.split(','),
author = item.author,
),
author = item.author
)
)
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
#Thu Jul 13 11:41:19 CEST 2023
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists

View File

@@ -1,3 +1,5 @@
val pushCache: String by settings
pluginManagement {
repositories {
// 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"
include(":androidApp")
include(":shared")

View File

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

View File

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

View File

@@ -3,40 +3,31 @@ package bou.amine.apps.readerforselfossv2.utils
import android.text.format.DateUtils
import kotlinx.datetime.*
actual class DateUtils {
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 {
var isoDateString: String =
if (dateString.matches(oldVersionFormat)) {
dateString.replace(" ", "T")
} else if (dateString.matches(newVersionFormat)) {
dateString.split("+")[0]
} else {
throw Exception("Unrecognized format for $dateString")
return try {
Instant.parse(dateString).toEpochMilliseconds()
} catch (e: Exception) {
var str = dateString.replace(" ", "T")
if (str.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{2}".toRegex())) {
str = str.split("+")[0]
}
return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
LocalDateTime.parse(str).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
}
}
actual fun parseRelativeDate(dateString: String): String {
val date = parseDate(dateString)
return " " +
DateUtils.getRelativeTimeSpanString(
date,
Clock.System.now().toEpochMilliseconds(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
)
return " " + DateUtils.getRelativeTimeSpanString(
date,
Clock.System.now().toEpochMilliseconds(),
DateUtils.MINUTE_IN_MILLIS,
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> {
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")
if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US).contains(".jpeg") ||
url.lowercase(Locale.US).contains(".png") ||
url.lowercase(Locale.US).contains(".webp")
) {
url.lowercase(Locale.US).contains(".webp"))
{
allImages.add(url)
}
}
@@ -38,11 +38,7 @@ actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
actual fun constructUrl(
baseUrl: String,
path: String,
file: String?,
): String {
actual fun constructUrl(baseUrl: String, path: String, file: String?): String {
return if (file == null || file == "null" || file.isEmpty()) {
""
} 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.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import org.kodein.di.DI
import org.kodein.di.bind
import org.kodein.di.instance

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,62 +35,63 @@ import kotlinx.serialization.json.Json
expect fun setupInsecureHTTPEngine(config: CIOEngineConfig)
class SelfossApi(private val appSettingsService: AppSettingsService) {
var client = createHttpClient()
fun createHttpClient() =
HttpClient(CIO) {
if (appSettingsService.getSelfSigned()) {
engine {
setupInsecureHTTPEngine(this)
}
fun createHttpClient(
appSettingsService: AppSettingsService,
api: SelfossApi
) =
HttpClient(CIO) {
if (appSettingsService.getSelfSigned()) {
engine {
setupInsecureHTTPEngine(this)
}
install(ContentNegotiation) {
install(HttpCache)
json(
Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
explicitNulls = false
},
)
}
install(Logging) {
logger =
object : Logger {
override fun log(message: String) {
Napier.d(message, tag = "LogApiCalls")
}
}
level = LogLevel.INFO
}
install(HttpTimeout) {
requestTimeoutMillis = appSettingsService.getApiTimeout()
}
install(HttpCookies)
install(HttpRequestRetry) {
maxRetries = 2
retryIf { _, response ->
response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo()
}
modifyRequest {
Napier.i("Will modify", tag = "HttpSend")
CoroutineScope(Dispatchers.Main).launch {
Napier.i("Will login", tag = "HttpSend")
login()
Napier.i("Did login", tag = "HttpSend")
}
}
}
expectSuccess = false
}
install(ContentNegotiation) {
install(HttpCache)
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Napier.d(message, tag = "LogApiCalls")
}
}
level = LogLevel.INFO
}
install(HttpTimeout) {
requestTimeoutMillis = appSettingsService.getApiTimeout()
}
install(HttpCookies)
install(HttpRequestRetry) {
maxRetries = 2
retryIf { _, response ->
response.status == HttpStatusCode.Forbidden && api.shouldHavePostLogin() && api.hasLoginInfo()
}
modifyRequest {
Napier.i("Will modify", tag = "HttpSend")
CoroutineScope(Dispatchers.Main).launch {
Napier.i("Will login", tag = "HttpSend")
api.login()
Napier.i("Did login", tag = "HttpSend")
}
}
}
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() {
appSettingsService.refreshApiSettings()
client = createHttpClient()
client = createHttpClient(appSettingsService, this)
}
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
private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
private fun hasLoginInfo() =
appSettingsService.getUserName().isNotEmpty() &&
appSettingsService.getPassword()
.isNotEmpty()
fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
fun hasLoginInfo() =
appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword()
.isNotEmpty()
suspend fun login(): SuccessResponse =
if (appSettingsService.getUserName().isNotEmpty() &&
appSettingsService.getPassword()
if (appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword()
.isNotEmpty()
) {
if (shouldHavePostLogin()) {
@@ -122,49 +120,30 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
SuccessResponse(true)
}
private suspend fun getLogin() =
maybeResponse(
client.tryToGet(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
private suspend fun getLogin() = maybeResponse(client.tryToGet(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
private suspend fun postLogin() =
maybeResponse(
client.tryToPost(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
private suspend fun postLogin() = maybeResponse(client.tryToPost(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(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 =
if (shouldHaveNewLogout()) {
@@ -174,42 +153,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
}
private suspend fun maybeLogoutIfAvailable() =
responseOrSuccessIf404(
client.tryToGet(url("/logout")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
responseOrSuccessIf404(client.tryToGet(url("/logout")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
},
)
}
})
private suspend fun doLogout() =
maybeResponse(
client.tryToDelete(url("/api/session/current")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun getItems(
type: String,
@@ -218,340 +178,213 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
source: Long?,
search: String?,
updatedSince: String?,
items: Int? = null,
items: Int? = null
): StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure(
client.tryToGet(url("/items")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
bodyOrFailure(client.tryToGet(url("/items")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
parameter("type", type)
parameter("tag", tag)
parameter("source", source)
parameter("search", search)
parameter("updatedsince", updatedSince)
parameter("items", items ?: appSettingsService.getItemsNumber())
parameter("offset", offset)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
parameter("type", type)
parameter("tag", tag)
parameter("source", source)
parameter("search", search)
parameter("updatedsince", updatedSince)
parameter("items", items ?: appSettingsService.getItemsNumber())
parameter("offset", offset)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}
})
suspend fun getItemsWithoutCatch(): StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure(
client.get(url("/items")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
bodyOrFailure(client.get(url("/items")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
parameter("type", "all")
parameter("items", 1)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
parameter("type", "all")
parameter("items", 1)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}
})
suspend fun stats(): StatusAndData<SelfossModel.Stats> =
bodyOrFailure(
client.tryToGet(url("/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
bodyOrFailure(client.tryToGet(url("/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}
})
suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
bodyOrFailure(
client.tryToGet(url("/tags")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
bodyOrFailure(client.tryToGet(url("/tags")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}
})
suspend fun update(): StatusAndData<String> =
bodyOrFailure(
client.tryToGet(url("/update")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
bodyOrFailure(client.tryToGet(url("/update")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}
})
suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
bodyOrFailure(
client.tryToGet(url("/sources/spouts")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
bodyOrFailure(client.tryToGet(url("/sources/spouts")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}
})
suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> =
bodyOrFailure(
client.tryToGet(url("/sources/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
bodyOrFailure(client.tryToGet(url("/sources/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}
})
suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> =
bodyOrFailure(
client.tryToGet(url("/sources/list")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
bodyOrFailure(client.tryToGet(url("/sources/list")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}
})
suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
bodyOrFailure(
client.tryToGet(url("/api/about")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
bodyOrFailure(client.tryToGet(url("/api/about")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
},
)
}
})
suspend fun markAsRead(id: String): SuccessResponse =
maybeResponse(
client.tryToPost(url("/mark/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
maybeResponse(client.tryToPost(url("/mark/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}
})
suspend fun unmarkAsRead(id: String): SuccessResponse =
maybeResponse(
client.tryToPost(url("/unmark/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
maybeResponse(client.tryToPost(url("/unmark/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}
})
suspend fun starr(id: String): SuccessResponse =
maybeResponse(
client.tryToPost(url("/starr/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
maybeResponse(client.tryToPost(url("/starr/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}
})
suspend fun unstarr(id: String): SuccessResponse =
maybeResponse(
client.tryToPost(url("/unstarr/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
maybeResponse(client.tryToPost(url("/unstarr/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
maybeResponse(client.tryToSubmitForm(
url = url("/mark"),
formParameters = Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
ids.map { append("ids[]", it) }
},
block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
},
)
suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
maybeResponse(
client.tryToSubmitForm(
url = url("/mark"),
formParameters =
Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
ids.map { append("ids[]", it) }
},
block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
),
)
}
))
suspend fun createSourceForVersion(
title: String,
@@ -564,7 +397,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
createSource("tags[]", title, url, spout, tags)
} else {
createSource("tags", title, url, spout, tags)
},
}
)
private suspend fun createSource(
@@ -572,36 +405,28 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
title: String,
url: String,
spout: String,
tags: String,
tags: String
): HttpResponse? =
client.tryToSubmitForm(
url = url("/source"),
formParameters =
Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
append("title", title)
append("url", url)
append("spout", spout)
append(tagsParamName, tags)
},
formParameters = Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
append("title", title)
append("url", url)
append("spout", spout)
append(tagsParamName, tags)
},
block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
},
}
)
suspend fun updateSourceForVersion(
@@ -609,14 +434,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
title: String,
url: String,
spout: String,
tags: String,
tags: String
): SuccessResponse =
maybeResponse(
if (appSettingsService.getApiVersion() > 1) {
updateSource(id, "tags[]", title, url, spout, tags)
} else {
updateSource(id, "tags", title, url, spout, tags)
},
}
)
private suspend fun updateSource(
@@ -629,54 +454,44 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
): HttpResponse? =
client.tryToSubmitForm(
url = url("/source/$id"),
formParameters =
Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
append("title", title)
append("url", url)
append("spout", spout)
append(tagsParamName, tags)
},
formParameters = Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
append("title", title)
append("url", url)
append("spout", spout)
append(tagsParamName, tags)
},
block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
},
}
)
suspend fun deleteSource(id: Int): SuccessResponse =
maybeResponse(
client.tryToDelete(url("/source/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
maybeResponse(client.tryToDelete(url("/source/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword()
)
)
}
)
}
},
)
}
})
}

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ fun TAG.toView(): SelfossModel.Tag =
SelfossModel.Tag(
this.name,
this.color,
this.unread.toInt(),
this.unread.toInt()
)
fun SOURCE.toView(): SelfossModel.SourceDetail =
@@ -21,7 +21,7 @@ fun SOURCE.toView(): SelfossModel.SourceDetail =
this.spout,
this.error,
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 =
@@ -32,14 +32,14 @@ fun SelfossModel.SourceDetail.toEntity(): SOURCE =
this.spout.orEmpty(),
this.error.orEmpty(),
this.icon.orEmpty(),
this.params?.url,
this.params?.url
)
fun SelfossModel.Tag.toEntity(): TAG =
TAG(
this.tag,
this.color,
this.unread.toLong(),
this.unread.toLong()
)
fun ITEM.toView(): SelfossModel.Item =
@@ -55,7 +55,7 @@ fun ITEM.toView(): SelfossModel.Item =
this.link,
this.sourcetitle,
this.tags.split(","),
this.author,
this.author
)
fun SelfossModel.Item.toEntity(): ITEM =
@@ -71,5 +71,5 @@ fun SelfossModel.Item.toEntity(): ITEM =
this.link,
this.sourcetitle.getHtmlDecoded(),
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) {
UNREAD(1, "unread"),
ALL(2, "all"),
STARRED(3, "starred"),
;
STARRED(3, "starred");
companion object {
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 constructUrl(
baseUrl: String,
path: String,
file: String?,
): String
expect fun constructUrl(baseUrl: String, path: String, file: String?): String

View File

@@ -1,6 +1,7 @@
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 {
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")
}
actual fun constructUrl(
baseUrl: String,
path: String,
file: String?,
): String {
actual fun constructUrl(baseUrl: String, path: String, file: String?): String {
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
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
actual class DateUtils {
actual companion object {
actual fun parseDate(dateString: String): Long {
@@ -10,4 +12,5 @@ actual class DateUtils {
TODO("Not yet implemented")
}
}
}

View File

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