chore: code style.
Some checks failed
Check PR code / Lint (pull_request) Successful in 59s
Check PR code / build (pull_request) Failing after 9m25s

This commit is contained in:
Amine Bouabdallaoui 2025-01-11 14:12:24 +01:00
parent 5035392aff
commit 6b5f6cbbe0
60 changed files with 910 additions and 795 deletions

27
.editorconfig Normal file
View File

@ -0,0 +1,27 @@
root = true
[*]
insert_final_newline = true
[{*.kt,*.kts}]
[*.{kt,kts}]
# Disable wildcard imports entirely
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
end_of_line = lf
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
ij_kotlin_packages_to_use_import_on_demand = unset
indent_size = 4
indent_style = space
ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = unset
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
ktlint_code_style = ktlint_official
ktlint_function_signature_body_expression_wrapping = multiline
ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2
ktlint_ignore_back_ticked_identifier = false
max_line_length = 140

View File

@ -20,7 +20,7 @@ jobs:
- name: Install detekt
run: 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
- name: Linting...
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' || true
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
- name: Detecting...
run: ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
build:

View File

@ -20,65 +20,72 @@ import org.hamcrest.Matchers.hasToString
fun performLogin(someUrl: String? = null) {
onView(withId(R.id.urlView)).perform(click()).perform(
typeTextIntoFocusedView(
if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888"
)
if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888",
),
)
onView(withId(R.id.signInButton)).perform(click())
}
fun loginAndInitHome() {
performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
onView(withText("OK")).perform(click())
}
fun changeAndCancelSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) {
fun changeAndCancelSetting(
oldValue: String,
newValue: String,
openSettingItem: () -> Unit,
) {
openSettingItem()
onView(
withId(android.R.id.edit)
withId(android.R.id.edit),
).perform(replaceText(newValue))
onView(
withId(android.R.id.button2)
withId(android.R.id.button2),
).perform(click())
openSettingItem()
onView(
withId(android.R.id.edit)
withId(android.R.id.edit),
).check(matches(withText(oldValue)))
onView(
withText(newValue)
withText(newValue),
).check(doesNotExist())
onView(
withId(android.R.id.button2)
withId(android.R.id.button2),
).perform(click())
}
fun changeAndSaveSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) {
fun changeAndSaveSetting(
oldValue: String,
newValue: String,
openSettingItem: () -> Unit,
) {
openSettingItem()
onView(
withId(android.R.id.edit)
withId(android.R.id.edit),
).perform(replaceText(newValue))
onView(
withId(android.R.id.button1)
withId(android.R.id.button1),
).perform(click())
openSettingItem()
onView(
withId(android.R.id.edit)
withId(android.R.id.edit),
).check(matches(withText(newValue)))
if (oldValue.isNotEmpty()) {
onView(
withText(oldValue)
withText(oldValue),
).check(doesNotExist())
}
onView(
withId(android.R.id.button2)
withId(android.R.id.button2),
).perform(click())
}
fun testPreferencesFromArray(
context: Context,
@ArrayRes arrayRes: Int,
openSettingItem: () -> Unit
openSettingItem: () -> Unit,
) {
openSettingItem()
context.resources.getStringArray(arrayRes).forEach { res ->
@ -90,7 +97,10 @@ fun testPreferencesFromArray(
}
}
fun testAddSourceWithUrl(url: String, sourceName: String) {
fun testAddSourceWithUrl(
url: String,
sourceName: String,
) {
onView(withId(R.id.fab))
.perform(click())
onView(withId(R.id.nameInput))

View File

@ -25,8 +25,9 @@ import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.hamcrest.TypeSafeMatcher
fun withError(@StringRes id: Int): TypeSafeMatcher<View?> {
fun withError(
@StringRes id: Int,
): TypeSafeMatcher<View?> {
return object : TypeSafeMatcher<View?>() {
override fun matchesSafely(view: View?): Boolean {
if (view == null) {
@ -52,7 +53,9 @@ fun isPopupWindow(): Matcher<Root> {
return isPlatformPopup()
}
fun withDrawable(@DrawableRes id: Int) = object : TypeSafeMatcher<View>() {
fun withDrawable(
@DrawableRes id: Int,
) = object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("ImageView with drawable same as drawable with id $id")
}
@ -68,43 +71,49 @@ fun withDrawable(@DrawableRes id: Int) = object : TypeSafeMatcher<View>() {
}
}
fun hasBottombarItemText(@StringRes id: Int): Matcher<View>? {
fun hasBottombarItemText(
@StringRes id: Int,
): Matcher<View>? {
return allOf(
withResourceName("fixed_bottom_navigation_icon"),
withParent(
allOf(
withResourceName("fixed_bottom_navigation_icon_container"),
hasSibling(withText(id))
)
)
hasSibling(withText(id)),
),
),
)
}
fun withSettingsCheckboxWidget(@StringRes id: Int): Matcher<View>? {
fun withSettingsCheckboxWidget(
@StringRes id: Int,
): Matcher<View>? {
return allOf(
withId(android.R.id.switch_widget),
withParent(
withSettingsCheckboxFrame(id)
)
withSettingsCheckboxFrame(id),
),
)
}
fun withSettingsCheckboxFrame(@StringRes id: Int): Matcher<View>? {
fun withSettingsCheckboxFrame(
@StringRes id: Int,
): Matcher<View>? {
return allOf(
withId(android.R.id.widget_frame),
hasSibling(
allOf(
withClassName(Matchers.equalTo(RelativeLayout::class.java.name)),
withChild(
withText(id)
)
)
)
withText(id),
),
),
),
)
}
fun openMenu() {
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>()
ApplicationProvider.getApplicationContext<Context>(),
)
}

View File

@ -23,7 +23,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class HomeActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -36,13 +35,13 @@ class HomeActivityTest {
fun testMenu() {
onView(withId(R.id.action_search)).check(matches(isDisplayed())).check(
matches(
isClickable()
)
isClickable(),
),
)
onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check(
matches(
isClickable()
)
isClickable(),
),
)
openMenu()
onView(withText(R.string.readAll)).check(matches(isDisplayed()))
@ -57,19 +56,19 @@ class HomeActivityTest {
fun testMenuActions() {
onView(withId(R.id.action_search)).perform(click())
onView(
withId(R.id.search_src_text)
withId(R.id.search_src_text),
).check(matches(isFocused()))
onView(isRoot()).perform(ViewActions.pressBack())
onView(withId(R.id.action_filter)).perform(click())
onView(
withText(R.string.filter_item_sources)
withText(R.string.filter_item_sources),
).check(matches(isDisplayed()))
onView(
withText(R.string.filter_item_tags)
withText(R.string.filter_item_tags),
).check(matches(isDisplayed()))
onView(
withId(R.id.floatingActionButton2)
withId(R.id.floatingActionButton2),
).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack())
@ -107,14 +106,13 @@ class HomeActivityTest {
fun testEmptyView() {
onView(withId(R.id.emptyText)).check(matches(isDisplayed()))
onView(
hasBottombarItemText(R.string.tab_new)
hasBottombarItemText(R.string.tab_new),
).check(matches(isDisplayed())).check(matches(isSelected()))
onView(
hasBottombarItemText(R.string.tab_read)
hasBottombarItemText(R.string.tab_read),
).check(matches(isDisplayed())).check(matches(not(isSelected())))
onView(
hasBottombarItemText(R.string.tab_favs)
hasBottombarItemText(R.string.tab_favs),
).check(matches(isDisplayed())).check(matches(not(isSelected())))
}
}

View File

@ -23,7 +23,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class LoginActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -52,11 +51,11 @@ class LoginActivityTest {
onView(withId(R.id.urlView)).check(matches(isDisplayed()))
onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked()))
.check(
matches(isClickable())
matches(isClickable()),
)
onView(withId(R.id.withLogin)).check(matches(isDisplayed()))
.check(matches(isNotChecked())).check(
matches(isClickable())
matches(isClickable()),
)
}

View File

@ -26,11 +26,9 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityGeneralTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -38,7 +36,7 @@ class SettingsActivityGeneralTest {
fun init() {
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext()
ApplicationProvider.getApplicationContext(),
)
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_general)).perform(click())
@ -48,68 +46,75 @@ class SettingsActivityGeneralTest {
fun testGeneral() {
onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
onView(
withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title)
withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title),
).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
isDisplayed(),
not(isChecked()),
),
),
)
onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check(
matches(
allOf(
isDisplayed(), isChecked()
)
)
isDisplayed(),
isChecked(),
),
),
)
onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
isEnabled()
)
isEnabled(),
),
)
onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxWidget(R.string.card_height_title)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(
matches(
not(isEnabled())
)
not(isEnabled()),
),
)
onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check(
matches(
allOf(
isDisplayed(), isChecked()
)
)
isDisplayed(),
isChecked(),
),
),
)
onView(withId(R.id.settings)).perform(swipeUp())
onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
isDisplayed(),
not(isChecked()),
),
),
)
}
@ -120,25 +125,25 @@ class SettingsActivityGeneralTest {
// Value check
onView(
withId(android.R.id.edit)
withId(android.R.id.edit),
).perform(replaceText("AVC"))
.check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+
onView(
withId(android.R.id.edit)
withId(android.R.id.edit),
).perform(replaceText("-1"))
.check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+
onView(
withId(android.R.id.edit)
withId(android.R.id.edit),
).perform(replaceText("300"))
.check(matches(withText("")))
onView(
withId(android.R.id.edit)
withId(android.R.id.edit),
).perform(typeTextIntoFocusedView("300"))
.check(matches(withText("30")))
onView(
withId(android.R.id.edit)
withId(android.R.id.edit),
).perform(replaceText("10"))
.check(matches(withText("10")))
onView(isRoot()).perform(ViewActions.pressBack())
@ -157,14 +162,14 @@ class SettingsActivityGeneralTest {
// article viewer settings
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
isEnabled()
)
isEnabled(),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
not(isEnabled())
)
not(isEnabled()),
),
)
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled())))

View File

@ -21,11 +21,9 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityOfflineTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -38,7 +36,7 @@ class SettingsActivityOfflineTest {
}
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext()
ApplicationProvider.getApplicationContext(),
)
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_offline)).perform(click())
@ -49,58 +47,63 @@ class SettingsActivityOfflineTest {
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_items_caching)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
matches(
isEnabled()
)
isEnabled(),
),
)
onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
matches(
allOf(isNotEnabled(), isDisplayed())
)
allOf(isNotEnabled(), isDisplayed()),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_refresh_when_charging)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches(
isNotEnabled()
)
isNotEnabled(),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_notify_new_items)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches(
isNotEnabled()
)
isNotEnabled(),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).check(
matches(
allOf(
isDisplayed(), isChecked()
)
)
isDisplayed(),
isChecked(),
),
),
)
}
@ -111,50 +114,50 @@ class SettingsActivityOfflineTest {
onView(withText(R.string.pref_switch_items_caching_on)).check(matches(isDisplayed()))
onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
matches(
isEnabled()
)
isEnabled(),
),
)
onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
matches(
isNotEnabled()
)
isNotEnabled(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches(
isNotEnabled()
)
isNotEnabled(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches(
isNotEnabled()
)
isNotEnabled(),
),
)
onView(withText(R.string.pref_switch_periodic_refresh_off)).check(
matches(
isDisplayed()
)
isDisplayed(),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).perform(click())
onView(withText(R.string.pref_switch_periodic_refresh_on)).check(
matches(
isDisplayed()
)
isDisplayed(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_periodic_refresh_minutes_title)).check(
matches(
isEnabled()
)
isEnabled(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches(
isEnabled()
)
isEnabled(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches(
isEnabled()
)
isEnabled(),
),
)
changeAndCancelSetting("360", "123") {
onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click())

View File

@ -19,11 +19,9 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityReaderTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -36,7 +34,7 @@ class SettingsActivityReaderTest {
}
loginAndInitHome()
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext()
ApplicationProvider.getApplicationContext(),
)
onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_viewer)).perform(click())
@ -47,11 +45,12 @@ class SettingsActivityReaderTest {
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check(
matches(
allOf(
isDisplayed(), not(
isChecked()
)
)
)
isDisplayed(),
not(
isChecked(),
),
),
),
)
onView(withText(R.string.pref_content_reader_font_size)).check(matches(isDisplayed()))
onView(withText(R.string.settings_reader_font)).check(matches(isDisplayed()))
@ -61,14 +60,14 @@ class SettingsActivityReaderTest {
fun testReaderActions() {
onView(withText(R.string.pref_switch_actions_pager_scroll_off)).check(
matches(
isDisplayed()
)
isDisplayed(),
),
)
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).perform(click())
onView(withText(R.string.pref_switch_actions_pager_scroll_on)).check(
matches(
isDisplayed()
)
isDisplayed(),
),
)
onView(withText(R.string.pref_content_reader_font_size)).perform(click())

View File

@ -20,7 +20,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class SettingsActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
lateinit var context: Context
@ -35,10 +34,8 @@ class SettingsActivityTest {
onView(withText(R.string.title_activity_settings)).perform(click())
}
@Test
fun testAllSettings() {
onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_viewer)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed()))
@ -48,14 +45,13 @@ class SettingsActivityTest {
matches(
allOf(
isDisplayed(),
not(isSelected())
)
)
not(isSelected()),
),
),
)
onView(withText(R.string.action_about)).check(matches(isDisplayed()))
}
@Test
fun testThemes() {
testPreferencesFromArray(context, R.array.ModeTitles) {
@ -63,7 +59,6 @@ class SettingsActivityTest {
}
}
@Test
fun testExperimentail() {
onView(withText(R.string.pref_header_experimental)).perform(click())
@ -75,13 +70,11 @@ class SettingsActivityTest {
}
}
@Test
fun testBugReports() {
onView(withText(R.string.pref_switch_disable_acra)).perform(click())
}
@Test
fun testLinks() {
onView(withText(R.string.pref_header_links)).perform(click())
@ -91,7 +84,6 @@ class SettingsActivityTest {
onView(withText(R.string.translation)).check(matches(isDisplayed()))
}
@Test
fun testAbout() {
onView(withText(R.string.action_about)).perform(click())

View File

@ -41,11 +41,10 @@ class SourcesActivityTest {
fun addSource() {
testAddSourceWithUrl(
"https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10",
sourceName
sourceName,
)
}
@Test
fun addSourceCheckContent() {
testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName)
@ -54,7 +53,7 @@ class SourcesActivityTest {
onView(withText(R.string.menu_home_refresh)).perform(click())
onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
onView(
withId(android.R.id.button1)
withId(android.R.id.button1),
).perform(click())
Thread.sleep(10000)
onView(withId(R.id.swipeRefreshLayout)).perform(swipeDown())
@ -74,7 +73,6 @@ class SourcesActivityTest {
onView(withText(sourceName)).check(doesNotExist())
}
private fun goToSources() {
openMenu()
onView(withText(R.string.menu_home_sources))

View File

@ -35,7 +35,7 @@ import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem
@ -49,7 +49,10 @@ import org.kodein.di.instance
import java.security.MessageDigest
import java.util.concurrent.TimeUnit
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware {
class HomeActivity :
AppCompatActivity(),
SearchView.OnQueryTextListener,
DIAware {
private var items: ArrayList<SelfossModel.Item> = ArrayList()
private var elementsShown: ItemType = ItemType.UNREAD
@ -171,7 +174,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getElementsAccordingToTab()
}
} else {
Toast.makeText(
Toast
.makeText(
this@HomeActivity,
"Found null when swiping at positon $position.",
Toast.LENGTH_LONG,
@ -200,15 +204,18 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
tabNewBadge =
TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setHideOnSelect(false)
.hide(false)
tabArchiveBadge =
TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setHideOnSelect(false)
.hide(false)
tabStarredBadge =
TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setHideOnSelect(false)
.hide(false)
if (appSettingsService.isDisplayUnreadCountEnabled()) {
lifecycleScope.launch {
@ -236,14 +243,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
BottomNavigationItem(
R.drawable.ic_tab_fiber_new_black_24dp,
getString(R.string.tab_new),
)
.setBadgeItem(tabNewBadge)
).setBadgeItem(tabNewBadge)
val tabArchive =
BottomNavigationItem(
R.drawable.ic_tab_archive_black_24dp,
getString(R.string.tab_read),
)
.setBadgeItem(tabArchiveBadge)
).setBadgeItem(tabArchiveBadge)
val tabStarred =
BottomNavigationItem(
R.drawable.ic_tab_favorite_black_24dp,
@ -425,17 +430,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
}
private fun getLastVisibleItem(): Int {
return when (val manager = binding.recyclerView.layoutManager) {
private fun getLastVisibleItem(): Int =
when (val manager = binding.recyclerView.layoutManager) {
is StaggeredGridLayoutManager ->
manager.findLastCompletelyVisibleItemPositions(
manager
.findLastCompletelyVisibleItemPositions(
null,
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0
}
}
private fun mayBeEmpty() =
if (items.isEmpty()) {
@ -577,7 +582,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
messageRes: Int,
doFn: () -> Unit,
) {
AlertDialog.Builder(this@HomeActivity)
AlertDialog
.Builder(this@HomeActivity)
.setMessage(messageRes)
.setTitle(titleRes)
.setPositiveButton(android.R.string.ok) { _, _ -> doFn() }
@ -589,7 +595,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.issue_tracker -> {
baseContext.openUrlInBrowser(AppSettingsService.trackerUrl)
baseContext.openUrlInBrowser(AppSettingsService.TRACKER_URL)
return true
}
@ -606,14 +612,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote()
if (updatedRemote) {
Toast.makeText(
Toast
.makeText(
this@HomeActivity,
R.string.refresh_success_response,
Toast.LENGTH_LONG,
)
.show()
).show()
} else {
Toast.makeText(
Toast
.makeText(
this@HomeActivity,
R.string.refresh_failer_message,
Toast.LENGTH_SHORT,
@ -633,7 +640,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items)
if (success) {
Toast.makeText(
Toast
.makeText(
this@HomeActivity,
R.string.all_posts_read,
Toast.LENGTH_SHORT,
@ -642,7 +650,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getElementsAccordingToTab()
} else {
Toast.makeText(
Toast
.makeText(
this@HomeActivity,
R.string.all_posts_not_read,
Toast.LENGTH_SHORT,
@ -651,7 +660,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleListResult()
binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement()
}
}
}
@ -661,7 +669,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
R.id.action_disconnect -> {
needsConfirmation(
R.string.confirm_disconnect_title,
R.string.confirm_disconnect_description
R.string.confirm_disconnect_description,
) {
runBlocking {
repository.logout()
@ -702,7 +710,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun handleRecurringTask() {
if (appSettingsService.isPeriodicRefreshEnabled()) {
val myConstraints =
Constraints.Builder()
Constraints
.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
.setRequiresStorageNotLow(true)
@ -711,18 +720,18 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val backgroundWork =
PeriodicWorkRequestBuilder<LoadingWorker>(
appSettingsService.getRefreshMinutes(),
TimeUnit.MINUTES
)
.setConstraints(myConstraints)
TimeUnit.MINUTES,
).setConstraints(myConstraints)
.addTag("selfoss-loading")
.build()
WorkManager.getInstance(
WorkManager
.getInstance(
baseContext,
).enqueueUniquePeriodicWork(
"selfoss-loading",
ExistingPeriodicWorkPolicy.KEEP,
backgroundWork
backgroundWork,
)
}
}

View File

@ -30,7 +30,9 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
class LoginActivity : AppCompatActivity(), DIAware {
class LoginActivity :
AppCompatActivity(),
DIAware {
private var inValidCount: Int = 0
private var isWithLogin = false
@ -108,7 +110,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
repository.updateApiInformation()
ACRA.errorReporter.putCustomData(
"SELFOSS_API_VERSION",
appSettingsService.getApiVersion().toString()
appSettingsService.getApiVersion().toString(),
)
CountingIdlingResourceSingleton.decrement()
}
@ -151,7 +153,8 @@ class LoginActivity : AppCompatActivity(), DIAware {
repository.updateApiInformation()
} catch (e: Exception) {
if (e.message?.startsWith("No transformation found") == true) {
Toast.makeText(
Toast
.makeText(
applicationContext,
R.string.application_selfoss_only,
Toast.LENGTH_LONG,
@ -270,7 +273,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
return when (item.itemId) {
R.id.issue_tracker -> {
val browserIntent =
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.TRACKER_URL))
startActivity(browserIntent)
return true
}
@ -280,9 +283,9 @@ class LoginActivity : AppCompatActivity(), DIAware {
.withAboutIconShown(true)
.withAboutVersionShown(true)
.withAboutSpecial2("Bug reports")
.withAboutSpecial2Description(AppSettingsService.trackerUrl)
.withAboutSpecial2Description(AppSettingsService.TRACKER_URL)
.withAboutSpecial1("Project Page")
.withAboutSpecial1Description(AppSettingsService.sourceUrl)
.withAboutSpecial1Description(AppSettingsService.SOURCE_URL)
.start(this)
true
}

View File

@ -9,11 +9,11 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.di.networkModule
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.github.ln_12.library.ConnectivityStatus
@ -36,7 +36,9 @@ import org.kodein.di.bind
import org.kodein.di.instance
import org.kodein.di.singleton
class MyApp : MultiDexApplication(), DIAware {
class MyApp :
MultiDexApplication(),
DIAware {
override val di by DI.lazy {
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) }
import(networkModule)
@ -89,7 +91,8 @@ class MyApp : MultiDexApplication(), DIAware {
R.string.network_connectivity_lost
}
Toast.makeText(
Toast
.makeText(
applicationContext,
toastMessage,
Toast.LENGTH_SHORT,
@ -151,13 +154,13 @@ class MyApp : MultiDexApplication(), DIAware {
val name = getString(R.string.notification_channel_sync)
val importance = NotificationManager.IMPORTANCE_LOW
val mChannel = NotificationChannel(AppSettingsService.syncChannelId, name, importance)
val mChannel = NotificationChannel(AppSettingsService.SYNC_CHANNEL_ID, name, importance)
val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel =
NotificationChannel(
AppSettingsService.newItemsChannelId,
AppSettingsService.NEW_ITEMS_CHANNEL_ID,
newItemsChannelname,
newItemsChannelimportance,
)

View File

@ -103,8 +103,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
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(

View File

@ -49,7 +49,10 @@ class ItemCardAdapter(
return ViewHolder(binding)
}
private fun handleClickListeners(holderBinding: CardItemBinding, position: Int) {
private fun handleClickListeners(
holderBinding: CardItemBinding,
position: Int,
) {
holderBinding.favButton.setOnClickListener {
val item = items[position]
if (item.starred) {
@ -96,7 +99,8 @@ class ItemCardAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = try {
binding.sourceTitleAndDate.text =
try {
itm.sourceAuthorAndDate()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")

View File

@ -53,7 +53,8 @@ class ItemListAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = try {
binding.sourceTitleAndDate.text =
try {
itm.sourceAuthorAndDate()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")

View File

@ -11,14 +11,16 @@ import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.DIAware
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware {
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
RecyclerView.Adapter<VH>(),
DIAware {
abstract val items: ArrayList<SelfossModel.Item>
abstract val repository: Repository
abstract val binding: ViewBinding
@ -45,8 +47,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
app.findViewById(R.id.coordLayout),
R.string.marked_as_read,
Snackbar.LENGTH_LONG,
)
.setAction(R.string.undo_string) {
).setAction(R.string.undo_string) {
unreadItemAtIndex(item, position, false)
}
@ -66,8 +67,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
app.findViewById(R.id.coordLayout),
R.string.marked_as_unread,
Snackbar.LENGTH_LONG,
)
.setAction(R.string.undo_string) {
).setAction(R.string.undo_string) {
readItemAtIndex(item, position, false)
}
@ -77,7 +77,10 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show()
}
protected fun handleLinkOpening(holderBinding: ViewBinding, position: Int) {
protected fun handleLinkOpening(
holderBinding: ViewBinding,
position: Int,
) {
holderBinding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl(

View File

@ -23,11 +23,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.DIAware
import org.kodein.di.instance
import java.util.*
import java.util.Timer
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()
@ -40,12 +42,13 @@ class LoadingWorker(val context: Context, params: WorkerParameters) :
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification =
NotificationCompat.Builder(applicationContext, AppSettingsService.syncChannelId)
NotificationCompat
.Builder(applicationContext, AppSettingsService.SYNC_CHANNEL_ID)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(AppSettingsService.syncChannelId)
.setChannelId(AppSettingsService.SYNC_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build())
@ -87,19 +90,18 @@ class LoadingWorker(val context: Context, params: WorkerParameters) :
PendingIntent.getActivity(context, 0, intent, pflags)
val newItemsNotification =
NotificationCompat.Builder(
NotificationCompat
.Builder(
applicationContext,
AppSettingsService.newItemsChannelId,
)
.setContentTitle(context.getString(R.string.new_items_notification_title))
AppSettingsService.NEW_ITEMS_CHANNEL_ID,
).setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(
context.getString(
R.string.new_items_notification_text,
newSize,
),
)
.setPriority(PRIORITY_DEFAULT)
.setChannelId(AppSettingsService.newItemsChannelId)
).setPriority(PRIORITY_DEFAULT)
.setChannelId(AppSettingsService.NEW_ITEMS_CHANNEL_ID)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)

View File

@ -115,7 +115,8 @@ class ArticleFragment : Fragment(), DIAware {
contentText = item.content
contentTitle = item.title.getHtmlDecoded()
contentImage = item.getThumbnail(repository.baseUrl)
contentSource = try {
contentSource =
try {
item.sourceAuthorAndDate()
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("Article Fragment parse date")
@ -337,7 +338,10 @@ class ArticleFragment : Fragment(), DIAware {
view: WebView?,
url: String,
): Boolean {
return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
return if (context != null &&
url.isUrlValid() &&
binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) {
requireContext().openUrlInBrowser(url)
true
} else {
@ -422,7 +426,6 @@ class ArticleFragment : Fragment(), DIAware {
context.theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
context.theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context issue when setting attributes, but context wasn't null before")
}
@ -458,7 +461,7 @@ class ArticleFragment : Fragment(), DIAware {
binding.webcontent.setOnTouchListener { _, event ->
gestureDetector.onTouchEvent(
event
event,
)
}

View File

@ -64,15 +64,14 @@ class SettingsActivity :
outState.putCharSequence(TITLE_TAG, title)
}
override fun onSupportNavigateUp(): Boolean {
return if (supportFragmentManager.popBackStackImmediate()) {
override fun onSupportNavigateUp(): Boolean =
if (supportFragmentManager.popBackStackImmediate()) {
supportActionBar?.title = getText(R.string.title_activity_settings)
false
} else {
super.onBackPressed()
true
}
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
@ -81,7 +80,8 @@ class SettingsActivity :
// Instantiate the new Fragment
val args = pref.extras
val fragment =
supportFragmentManager.fragmentFactory.instantiate(
supportFragmentManager.fragmentFactory
.instantiate(
classLoader,
pref.fragment.toString(),
).apply {
@ -89,7 +89,8 @@ class SettingsActivity :
setTargetFragment(caller, 0)
}
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, fragment)
.addToBackStack(null)
.commit()
@ -108,7 +109,7 @@ class SettingsActivity :
preferenceManager.findPreference<Preference>(CURRENT_THEME)?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(
newValue.toString().toInt()
newValue.toString().toInt(),
) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true
}
@ -144,10 +145,11 @@ class SettingsActivity :
val input: Int = (dest.toString() + source.toString()).toInt()
if (input in 1..200) return@InputFilter null
} catch (nfe: NumberFormatException) {
Toast.makeText(
Toast
.makeText(
activity,
R.string.items_number_should_be_number,
Toast.LENGTH_LONG
Toast.LENGTH_LONG,
).show()
}
""
@ -234,19 +236,19 @@ class SettingsActivity :
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.trackerUrl)
openUrl(AppSettingsService.TRACKER_URL)
true
}
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.sourceUrl)
openUrl(AppSettingsService.SOURCE_URL)
false
}
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.translationUrl)
openUrl(AppSettingsService.TRANSLATION_URL)
false
}
}

View File

@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.testing
import androidx.test.espresso.idling.CountingIdlingResource
object CountingIdlingResourceSingleton {
private const val RESOURCE = "GLOBAL"
@JvmField

View File

@ -2,7 +2,6 @@ package bou.amine.apps.readerforselfossv2.android.testing
import android.os.Build
class TestingHelper {
fun isUnitTest(): Boolean {
var device = Build.DEVICE

View File

@ -18,7 +18,6 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.openItemUrl(
currentItem: Int,
linkDecoded: String,
@ -42,8 +41,7 @@ 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()
@ -61,7 +59,6 @@ fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) {
}
fun Context.openUrlInBrowserAsNewTask(url: String) {
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(url)
@ -80,7 +77,6 @@ fun Context.mayBeStartActivity(intent: Intent) {
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, getString(R.string.no_browser), Toast.LENGTH_SHORT).show()
}
}
class LinkOnTouchListener : View.OnTouchListener {

View File

@ -8,22 +8,19 @@ import org.acra.config.CoreConfiguration
import org.acra.config.ReportingAdministrator
import org.acra.data.CrashReportData
@AutoService(ReportingAdministrator::class)
class AcraReportingAdministrator : ReportingAdministrator {
override fun shouldStartCollecting(
context: Context,
config: CoreConfiguration,
reportBuilder: ReportBuilder
): Boolean {
return reportBuilder.exception !is DeadSystemException && (reportBuilder.exception != null && reportBuilder.exception!!::class.simpleName != "CannotDeliverBroadcastException")
}
reportBuilder: ReportBuilder,
): Boolean =
reportBuilder.exception !is DeadSystemException &&
(reportBuilder.exception != null && reportBuilder.exception!!::class.simpleName != "CannotDeliverBroadcastException")
override fun shouldSendReport(
context: Context,
config: CoreConfiguration,
crashReportData: CrashReportData
): Boolean {
return crashReportData.get("BRAND") != "redroid"
}
crashReportData: CrashReportData,
): Boolean = crashReportData.get("BRAND") != "redroid"
}

View File

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

View File

@ -11,13 +11,17 @@ fun dialogMessage(): String {
return latestDialog.findViewById<TextView>(android.R.id.message)?.text.toString()
}
fun Menu.assertClickable(@IdRes id: Int) {
fun Menu.assertClickable(
@IdRes id: Int,
) {
this.assertVisible(id)
val item = this.findItem(id)
assertTrue(item.isEnabled)
}
fun Menu.assertVisible(@IdRes id: Int) {
fun Menu.assertVisible(
@IdRes id: Int,
) {
val item = this.findItem(id)
assertTrue(item.isVisible)
}

View File

@ -11,10 +11,8 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
@RunWith(RobotElectriqueRunnerclass::class)
@RunWith(RobotElectriqueRunner::class)
class LoginActivityTest {
@Test
fun login_shouldDisplay() {
Robolectric.buildActivity(LoginActivity::class.java).use { controller ->

View File

@ -3,10 +3,8 @@ package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
class RobotElectriqueRunnerclass(testClass: Class<*>?) :
RobolectricTestRunner(testClass) {
override fun buildGlobalConfig(): Config {
return Config.Builder().setSdk(25, 30, 33).build()
}
class RobotElectriqueRunner(
testClass: Class<*>?,
) : RobolectricTestRunner(testClass) {
override fun buildGlobalConfig(): Config = Config.Builder().setSdk(25, 30, 33).build()
}

View File

@ -42,14 +42,15 @@ private const val FEED_URL = "https://test.com/feed"
private const val TAGS = "Test, New"
private const val NUMBER_ARTICLES = 100
private const val NUMBER_UNREAD = 50
private const val NUMBER_STARRED = 20
class RepositoryTest {
private val db = mockk<ReaderForSelfossDB>(relaxed = true)
private val appSettingsService = mockk<AppSettingsService>()
private val api = mockk<SelfossApi>()
private val NUMBER_ARTICLES = 100
private val NUMBER_UNREAD = 50
private val NUMBER_STARRED = 20
private lateinit var repository: Repository
private fun initializeRepository(
@ -77,10 +78,11 @@ class RepositoryTest {
coEvery { api.apiInformation() } returns
StatusAndData(
success = true,
data = SelfossModel.ApiInformation(
data =
SelfossModel.ApiInformation(
"2.19-ba1e8e3",
"4.0.0",
SelfossModel.ApiConfiguration(false, true)
SelfossModel.ApiConfiguration(false, true),
),
)
coEvery { api.stats() } returns
@ -135,10 +137,11 @@ class RepositoryTest {
coEvery { api.apiInformation() } returns
StatusAndData(
success = true,
data = SelfossModel.ApiInformation(
data =
SelfossModel.ApiInformation(
"2.19-ba1e8e3",
"4.0.0",
SelfossModel.ApiConfiguration(true, true)
SelfossModel.ApiConfiguration(true, true),
),
)
every { appSettingsService.getUserName() } returns ""
@ -155,10 +158,11 @@ class RepositoryTest {
coEvery { api.apiInformation() } returns
StatusAndData(
success = true,
data = SelfossModel.ApiInformation(
data =
SelfossModel.ApiInformation(
"2.19-ba1e8e3",
"4.0.0",
SelfossModel.ApiConfiguration(true, true)
SelfossModel.ApiConfiguration(true, true),
),
)
every { appSettingsService.getUserName() } returns "username"
@ -175,10 +179,11 @@ class RepositoryTest {
coEvery { api.apiInformation() } returns
StatusAndData(
success = true,
data = SelfossModel.ApiInformation(
data =
SelfossModel.ApiInformation(
"2.19-ba1e8e3",
"4.0.0",
SelfossModel.ApiConfiguration(true, false)
SelfossModel.ApiConfiguration(true, false),
),
)
every { appSettingsService.getUserName() } returns ""
@ -195,10 +200,11 @@ class RepositoryTest {
coEvery { api.apiInformation() } returns
StatusAndData(
success = true,
data = SelfossModel.ApiInformation(
data =
SelfossModel.ApiInformation(
"2.19-ba1e8e3",
"4.0.0",
SelfossModel.ApiConfiguration(false, true)
SelfossModel.ApiConfiguration(false, true),
),
)
every { appSettingsService.getUserName() } returns ""

View File

@ -3,8 +3,8 @@ package bou.amine.apps.readerforselfossv2.tests.repository
import bou.amine.apps.readerforselfossv2.dao.ITEM
import bou.amine.apps.readerforselfossv2.model.SelfossModel
fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> {
return listOf(
fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> =
listOf(
ITEM(
id = item.id,
datetime = item.datetime,
@ -20,10 +20,9 @@ fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<I
author = item.author,
),
)
}
fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<SelfossModel.Item> {
return listOf(
fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<SelfossModel.Item> =
listOf(
SelfossModel.Item(
id = item.id.toInt(),
datetime = item.datetime,
@ -39,14 +38,13 @@ fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<S
author = item.author,
),
)
}
class FakeItemParameters {
var id = "20"
var datetime = "2022-09-09T03:32:01-04:00"
val title = "Etica della ricerca sotto i riflettori."
val content =
"<p><strong>Luigi Campanella, già Presidente SCI</strong></p>\n<p>Letica della scienza è di certo ambito di cui continuiamo a scoprire nuovi aspetti e risvolti.</p>\n<p>Lultimo è quello delle intelligenze artificiali capaci di creare opere complesse basate su immagini e parole memorizzate con il rischio di fake news e di contenuti disturbanti.</p>\n<p>Per evitare che ciò accada si sta procedendo filtrando secondo criteri di autocensura i dati da cui lintelligenza artificiale parte.</p>\n<p>Comincia ad intravedersi un futuro prossimo di competizione fra autori umani ed artificiali nel quale sarà importante, quando i loro prodotti saranno indistinguibili, dichiararne lorigine.</p>\n<p>Come si comprende, si conferma che gli aspetti etici dellinnovazione e della ricerca si diversificato sempre di più.</p>\n<p>La biologia molecolare e la genetica già in passato hanno posto allattenzione comune aspetti di etica della scienza che hanno indotto a nuove riflessioni circa i limiti delle ricerche.</p>\n<p>Largomento, sempre attuale, torna sulle prime pagine a seguito della pubblicazione di una ricerca della Università di Cambridge che ha sviluppato una struttura cellulare di un topo con un cuore che batte regolarmente.</p>\n<img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image002-1.png?w=481\" alt=\"\" width=\"697\" height=\"430\" /><img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image003-1.png?w=906\" alt=\"\" /><p>Magdalena Zernicka-Goetz</p>\n<img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image004.jpg?w=474\" alt=\"\" width=\"622\" height=\"465\" /><p>Gianluca Amadei</p>\n<p>Del gruppo fa parte anche uno scienziato italiano Gianluca Amadei,che dinnanzi alle obiezioni di natura etica sulla realizzazione della vita artificiale si è affrettato a sostenere che non è creare nuove vite il fine primario della ricerca, ma quello di salvare quelle esistenti, di dare contributi essenziali alla medicina citando il caso del fallimento tuttora non interpretato di alcune gravidanze e di superare la sperimentazione animale, così contribuendo positivamente alla soluzione di un altro dilemma etico.</p>\n<p>Lembrione sintetico ha ovviamente come primo traguardo il contributo ai trapianti oggi drammaticamente carenti nellofferta rispetto alla domanda, con attese fino a 4 anni per i trapianti di cuore ed a 2 anni per quelli di fegato. Il lavoro dovrebbe adesso continuare presso lAteneo di Padova per creare nuovi organi e nuovi farmaci.</p>"
"<p><strong>Luigi Campanella, già Presidente SCI</strong></p>\n<p>Letica della scienza è di certo ambito di cui continuiamo</p>"
var unread = true
var starred = true
val thumbnail = null

View File

@ -9,7 +9,7 @@ actual class DriverFactory(private val context: Context) {
return AndroidSqliteDriver(
ReaderForSelfossDB.Schema,
context,
"ReaderForSelfossV2-android.db"
"ReaderForSelfossV2-android.db",
)
}
}

View File

@ -8,16 +8,18 @@ class NaiveTrustManager : X509TrustManager {
override fun checkClientTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
) {}
) {
}
override fun checkServerTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
) {}
) {
}
override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
}
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
config.https.trustManager = NaiveTrustManager()
}

View File

@ -1,8 +1,7 @@
package bou.amine.apps.readerforselfossv2.utils
import android.text.format.DateUtils
import io.github.aakira.napier.Napier
import kotlinx.datetime.*
import kotlinx.datetime.Clock
actual class DateUtils {
actual companion object {

View File

@ -4,19 +4,13 @@ import android.net.Uri
import android.text.Html
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import org.jsoup.Jsoup
import java.util.*
import java.util.Locale
actual fun String.getHtmlDecoded(): String {
return Html.fromHtml(this).toString()
}
actual fun String.getHtmlDecoded(): String = Html.fromHtml(this).toString()
actual fun SelfossModel.Item.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
actual fun SelfossModel.Item.getIcon(baseUrl: String): String = constructUrl(baseUrl, "favicons", icon)
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
return constructUrl(baseUrl, "thumbnails", thumbnail)
}
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String = constructUrl(baseUrl, "thumbnails", thumbnail)
actual fun SelfossModel.Item.getImages(): ArrayList<String> {
val allImages = ArrayList<String>()
@ -34,16 +28,14 @@ actual fun SelfossModel.Item.getImages(): ArrayList<String> {
return allImages
}
actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
actual fun SelfossModel.Source.getIcon(baseUrl: String): String = constructUrl(baseUrl, "favicons", icon)
actual fun constructUrl(
baseUrl: String,
path: String,
file: String?,
): String {
return if (file == null || file == "null" || file.isEmpty()) {
): String =
if (file == null || file == "null" || file.isEmpty()) {
""
} else {
val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
@ -51,4 +43,3 @@ actual fun constructUrl(
baseUriBuilder.toString()
}
}

View File

@ -1,4 +1,4 @@
package bou.amine.apps.readerforselfossv2.DI
package bou.amine.apps.readerforselfossv2.di
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.rest.SelfossApi

View File

@ -7,7 +7,7 @@ class MercuryModel {
class ParsedContent(
val title: String? = null,
val content: String? = null,
val lead_image_url: String? = null, // NOSONAR
val lead_image_url: String? = null,
val url: String? = null,
val error: Boolean? = null,
val message: String? = null,

View File

@ -3,19 +3,20 @@ package bou.amine.apps.readerforselfossv2.model
import kotlinx.serialization.Serializable
@Serializable
class SuccessResponse(val success: Boolean) {
class SuccessResponse(
val success: Boolean,
) {
val isSuccess: Boolean
get() = success
}
class StatusAndData<T>(val success: Boolean, val data: T? = null) {
class StatusAndData<T>(
val success: Boolean,
val data: T? = null,
) {
companion object {
fun <T> succes(d: T): StatusAndData<T> {
return StatusAndData(true, d)
}
fun <T> succes(d: T): StatusAndData<T> = StatusAndData(true, d)
fun <T> error(): StatusAndData<T> {
return StatusAndData(false)
}
fun <T> error(): StatusAndData<T> = StatusAndData(false)
}
}

View File

@ -141,7 +141,7 @@ class SelfossModel {
}
if (stringUrl.isEmptyOrNullOrNullString()) {
throw Exception("Link ${link} was translated to ${stringUrl}, but was empty. Handle this.")
throw Exception("Link $link was translated to $stringUrl, but was empty. Handle this.")
}
return stringUrl
@ -172,12 +172,11 @@ 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())) {
override fun deserialize(decoder: Decoder): List<String> =
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)
@ -188,7 +187,7 @@ class SelfossModel {
) {
encoder.encodeCollection(
PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING),
value.size
value.size,
) { this.toString() }
}
}
@ -204,9 +203,10 @@ class SelfossModel {
}
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor(
get() =
PrimitiveSerialDescriptor(
"BooleanOrIntForSomeSelfossVersions",
PrimitiveKind.BOOLEAN
PrimitiveKind.BOOLEAN,
)
override fun serialize(

View File

@ -1,12 +1,21 @@
package bou.amine.apps.readerforselfossv2.repository
import bou.amine.apps.readerforselfossv2.dao.*
import bou.amine.apps.readerforselfossv2.dao.ACTION
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ITEM
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.dao.SOURCE
import bou.amine.apps.readerforselfossv2.dao.TAG
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.*
import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.toEntity
import bou.amine.apps.readerforselfossv2.utils.toParsedDate
import bou.amine.apps.readerforselfossv2.utils.toView
import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -27,26 +36,26 @@ class Repository(
var displayedItems = ItemType.UNREAD
private var _tagFilter = MutableStateFlow<SelfossModel.Tag?>(null)
var tagFilter = _tagFilter.asStateFlow()
private var _sourceFilter = MutableStateFlow<SelfossModel.Source?>(null)
var sourceFilter = _sourceFilter.asStateFlow()
private var tagFilterFlow = MutableStateFlow<SelfossModel.Tag?>(null)
var tagFilter = tagFilterFlow.asStateFlow()
private var sourceFilterFlow = MutableStateFlow<SelfossModel.Source?>(null)
var sourceFilter = sourceFilterFlow.asStateFlow()
var searchFilter: String? = null
var offlineOverride = false
private val _badgeUnread = MutableStateFlow(0)
val badgeUnread = _badgeUnread.asStateFlow()
private val _badgeAll = MutableStateFlow(0)
val badgeAll = _badgeAll.asStateFlow()
private val _badgeStarred = MutableStateFlow(0)
val badgeStarred = _badgeStarred.asStateFlow()
private val badgeUnreadFlow = MutableStateFlow(0)
val badgeUnread = badgeUnreadFlow.asStateFlow()
private val badgeAllFlow = MutableStateFlow(0)
val badgeAll = badgeAllFlow.asStateFlow()
private val badgeStarredFlow = MutableStateFlow(0)
val badgeStarred = badgeStarredFlow.asStateFlow()
private var fetchedTags = false
private var fetchedSources = false
private var _readerItems = ArrayList<SelfossModel.Item>()
private var _selectedSource: SelfossModel.SourceDetail? = null
private var readerItems = ArrayList<SelfossModel.Item>()
private var selectedSource: SelfossModel.SourceDetail? = null
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
@ -135,17 +144,17 @@ class Repository(
if (isNetworkAvailable()) {
val response = api.stats()
if (response.success && response.data != null) {
_badgeUnread.value = response.data.unread ?: 0
_badgeAll.value = response.data.total
_badgeStarred.value = response.data.starred ?: 0
badgeUnreadFlow.value = response.data.unread ?: 0
badgeAllFlow.value = response.data.total
badgeStarredFlow.value = response.data.starred ?: 0
success = true
}
} else if (appSettingsService.isItemCachingEnabled()) {
// TODO: do this differently, because it's not efficient
val dbItems = getDBItems()
_badgeUnread.value = dbItems.filter { item -> item.unread }.size
_badgeStarred.value = dbItems.filter { item -> item.starred }.size
_badgeAll.value = dbItems.size
badgeUnreadFlow.value = dbItems.filter { item -> item.unread }.size
badgeStarredFlow.value = dbItems.filter { item -> item.starred }.size
badgeAllFlow.value = dbItems.size
success = true
}
return success
@ -170,8 +179,8 @@ class Repository(
}
}
suspend fun getSpouts(): Map<String, SelfossModel.Spout> {
return if (isNetworkAvailable()) {
suspend fun getSpouts(): Map<String, SelfossModel.Spout> =
if (isNetworkAvailable()) {
val spouts = api.spouts()
if (spouts.success && spouts.data != null) {
spouts.data
@ -181,7 +190,6 @@ class Repository(
} else {
throw NetworkUnavailableException()
}
}
suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> {
var sources = ArrayList<SelfossModel.Source>()
@ -234,14 +242,13 @@ class Repository(
return success
}
private suspend fun markAsReadById(id: Int): Boolean {
return if (isNetworkAvailable()) {
private suspend fun markAsReadById(id: Int): Boolean =
if (isNetworkAvailable()) {
api.markAsRead(id.toString()).isSuccess
} else {
insertDBAction(id.toString(), read = true)
true
}
}
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
val success = unmarkAsReadById(item.id)
@ -252,14 +259,13 @@ class Repository(
return success
}
private suspend fun unmarkAsReadById(id: Int): Boolean {
return if (isNetworkAvailable()) {
private suspend fun unmarkAsReadById(id: Int): Boolean =
if (isNetworkAvailable()) {
api.unmarkAsRead(id.toString()).isSuccess
} else {
insertDBAction(id.toString(), unread = true)
true
}
}
suspend fun starr(item: SelfossModel.Item): Boolean {
val success = starrById(item.id)
@ -270,14 +276,13 @@ class Repository(
return success
}
private suspend fun starrById(id: Int): Boolean {
return if (isNetworkAvailable()) {
private suspend fun starrById(id: Int): Boolean =
if (isNetworkAvailable()) {
api.starr(id.toString()).isSuccess
} else {
insertDBAction(id.toString(), starred = true)
true
}
}
suspend fun unstarr(item: SelfossModel.Item): Boolean {
val success = unstarrById(item.id)
@ -288,14 +293,13 @@ class Repository(
return success
}
private suspend fun unstarrById(id: Int): Boolean {
return if (isNetworkAvailable()) {
private suspend fun unstarrById(id: Int): Boolean =
if (isNetworkAvailable()) {
api.unstarr(id.toString()).isSuccess
} else {
insertDBAction(id.toString(), starred = true)
true
}
}
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false
@ -312,7 +316,7 @@ class Repository(
private fun markAsReadLocally(item: SelfossModel.Item) {
if (item.unread) {
item.unread = false
_badgeUnread.value -= 1
badgeUnreadFlow.value -= 1
}
CoroutineScope(Dispatchers.Main).launch {
@ -323,7 +327,7 @@ class Repository(
private fun unmarkAsReadLocally(item: SelfossModel.Item) {
if (!item.unread) {
item.unread = true
_badgeUnread.value += 1
badgeUnreadFlow.value += 1
}
CoroutineScope(Dispatchers.Main).launch {
@ -334,7 +338,7 @@ class Repository(
private fun starrLocally(item: SelfossModel.Item) {
if (!item.starred) {
item.starred = true
_badgeStarred.value += 1
badgeStarredFlow.value += 1
}
CoroutineScope(Dispatchers.Main).launch {
@ -345,7 +349,7 @@ class Repository(
private fun unstarrLocally(item: SelfossModel.Item) {
if (item.starred) {
item.starred = false
_badgeStarred.value -= 1
badgeStarredFlow.value -= 1
}
CoroutineScope(Dispatchers.Main).launch {
@ -361,7 +365,8 @@ class Repository(
): Boolean {
var response = false
if (isNetworkAvailable()) {
response = api.createSourceForVersion(
response = api
.createSourceForVersion(
title,
url,
spout,
@ -407,13 +412,12 @@ class Repository(
return success
}
suspend fun updateRemote(): Boolean {
return if (isNetworkAvailable()) {
suspend fun updateRemote(): Boolean =
if (isNetworkAvailable()) {
api.update().data.equals("finished")
} else {
false
}
}
suspend fun login(): Boolean {
var result = false
@ -422,7 +426,7 @@ class Repository(
val response = api.login()
result = response.isSuccess == true
} catch (cause: Throwable) {
Napier.e("login failed", cause, tag = "RepositoryImpl.login")
Napier.e("login failed", cause, tag = "Repository.login")
}
}
return result
@ -436,7 +440,7 @@ class Repository(
// a random rss feed, that would throw a NoTransformationFoundException
fetchFailed = !api.getItemsWithoutCatch().success
} catch (e: Throwable) {
Napier.e("checkIfFetchFails failed", e, tag = "RepositoryImpl.shouldBeSelfossInstance")
Napier.e("checkIfFetchFails failed", e, tag = "Repository.shouldBeSelfossInstance")
}
}
@ -448,10 +452,10 @@ class Repository(
try {
val response = api.logout()
if (!response.isSuccess) {
Napier.e("Couldn't logout.", tag = "RepositoryImpl.logout")
Napier.e("Couldn't logout.", tag = "Repository.logout")
}
} catch (cause: Throwable) {
Napier.e("logout failed", cause, tag = "RepositoryImpl.logout")
Napier.e("logout failed", cause, tag = "Repository.logout")
}
appSettingsService.clearAll()
} else {
@ -578,16 +582,19 @@ class Repository(
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()),
@ -607,34 +614,30 @@ class Repository(
}
fun setTagFilter(tag: SelfossModel.Tag?) {
_tagFilter.value = tag
tagFilterFlow.value = tag
}
fun setSourceFilter(source: SelfossModel.Source?) {
_sourceFilter.value = source
sourceFilterFlow.value = source
}
fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) {
_readerItems = readerItems
this.readerItems = readerItems
}
fun getReaderItems(): ArrayList<SelfossModel.Item> {
return _readerItems
}
fun getReaderItems(): ArrayList<SelfossModel.Item> = readerItems
fun migrate(driverFactory: DriverFactory) {
ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
}
fun setSelectedSource(source: SelfossModel.SourceDetail) {
_selectedSource = source
selectedSource = source
}
fun unsetSelectedSource() {
_selectedSource = null
selectedSource = null
}
fun getSelectedSource(): SelfossModel.SourceDetail? {
return _selectedSource
}
fun getSelectedSource(): SelfossModel.SourceDetail? = selectedSource
}

View File

@ -3,23 +3,28 @@ package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.model.SuccessResponse
import io.github.aakira.napier.Napier
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.delete
import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.url
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.http.isSuccess
suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse {
return if (r != null && r.status === HttpStatusCode.NotFound) {
suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse =
if (r != null && r.status === HttpStatusCode.NotFound) {
SuccessResponse(true)
} else {
maybeResponse(r)
}
}
suspend fun maybeResponse(r: HttpResponse?): SuccessResponse {
return if (r != null && r.status.isSuccess()) {
suspend fun maybeResponse(r: HttpResponse?): SuccessResponse =
if (r != null && r.status.isSuccess()) {
r.body()
} else {
if (r != null) {
@ -27,7 +32,6 @@ suspend fun maybeResponse(r: HttpResponse?): SuccessResponse {
}
SuccessResponse(false)
}
}
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> {
try {

View File

@ -33,16 +33,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
expect fun setupInsecureHTTPEngine(config: CIOEngineConfig)
expect fun setupInsecureHttpEngine(config: CIOEngineConfig)
class SelfossApi(private val appSettingsService: AppSettingsService) {
class SelfossApi(
private val appSettingsService: AppSettingsService,
) {
var client = createHttpClient()
fun createHttpClient() =
HttpClient(CIO) {
if (appSettingsService.getSelfSigned()) {
engine {
setupInsecureHTTPEngine(this)
setupInsecureHttpEngine(this)
}
}
install(HttpCache)
@ -105,12 +107,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
private fun hasLoginInfo() =
appSettingsService.getUserName().isNotEmpty() &&
appSettingsService.getPassword()
appSettingsService
.getPassword()
.isNotEmpty()
suspend fun login(): SuccessResponse =
if (appSettingsService.getUserName().isNotEmpty() &&
appSettingsService.getPassword()
appSettingsService
.getPassword()
.isNotEmpty()
) {
if (shouldHavePostLogin()) {
@ -127,8 +131,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client.tryToGet(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -150,8 +156,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client.tryToPost(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -168,8 +176,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
},
)
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()) {
@ -181,8 +188,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
private suspend fun maybeLogoutIfAvailable() =
responseOrSuccessIf404(
client.tryToGet(url("/logout")) {
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -202,8 +211,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
private suspend fun doLogout() =
maybeResponse(
client.tryToDelete(url("/api/session/current")) {
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -242,8 +253,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("updatedsince", updatedSince)
parameter("items", items ?: appSettingsService.getItemsNumber())
parameter("offset", offset)
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -269,8 +282,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
}
parameter("type", "all")
parameter("items", 1)
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -294,8 +309,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -319,8 +336,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -344,8 +363,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -369,8 +390,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -394,8 +417,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -419,8 +444,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -440,8 +467,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
bodyOrFailure(
client.tryToGet(url("/api/about")) {
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -465,8 +494,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -490,8 +521,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -515,8 +548,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -540,8 +575,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -571,8 +608,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
ids.map { append("ids[]", it) }
},
block = {
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -625,8 +664,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
append(tagsParamName, tags)
},
block = {
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -680,8 +721,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
append(tagsParamName, tags)
},
block = {
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
@ -705,8 +748,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName()
.isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(

View File

@ -2,7 +2,9 @@ package bou.amine.apps.readerforselfossv2.service
import com.russhwolf.settings.Settings
class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
class AppSettingsService(
acraSenderServiceProcess: Boolean = false,
) {
val settings: Settings =
if (acraSenderServiceProcess) {
ACRASettings()
@ -11,37 +13,37 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
}
// Api related
private var _apiVersion: Int = -1
private var _publicAccess: Boolean? = null
private var _selfSigned: Boolean? = null
private var _baseUrl: String = ""
private var _userName: String = ""
private var _basicUserName: String = ""
private var _password: String = ""
private var _basicPassword: String = ""
private var apiVersion: Int = -1
private var publicAccess: Boolean? = null
private var selfSigned: Boolean? = null
private var baseUrl: String = ""
private var userName: String = ""
private var basicUserName: String = ""
private var password: String = ""
private var basicPassword: String = ""
// User settings related
private var _itemsCaching: Boolean? = null
private var _articleViewer: Boolean? = null
private var _shouldBeCardView: Boolean? = null
private var _displayUnreadCount: Boolean? = null
private var _displayAllCount: Boolean? = null
private var _fullHeightCards: Boolean? = null
private var _updateSources: Boolean? = null
private var _periodicRefresh: Boolean? = null
private var _refreshWhenChargingOnly: Boolean? = null
private var _infiniteLoading: Boolean? = null
private var _notifyNewItems: Boolean? = null
private var _itemsNumber: Int? = null
private var _apiTimeout: Long? = null
private var _refreshMinutes: Long = 360
private var _markOnScroll: Boolean? = null
private var _activeAlignment: Int? = null
private var itemsCaching: Boolean? = null
private var articleViewer: Boolean? = null
private var shouldBeCardView: Boolean? = null
private var displayUnreadCount: Boolean? = null
private var displayAllCount: Boolean? = null
private var fullHeightCards: Boolean? = null
private var updateSources: Boolean? = null
private var periodicRefresh: Boolean? = null
private var refreshWhenChargingOnly: Boolean? = null
private var infiniteLoading: Boolean? = null
private var notifyNewItems: Boolean? = null
private var itemsNumber: Int? = null
private var apiTimeout: Long? = null
private var refreshMinutes: Long = 360
private var markOnScroll: Boolean? = null
private var activeAlignment: Int? = null
private var _fontSize: Int? = null
private var _staticBar: Boolean? = null
private var _font: String = ""
private var _theme: Int? = null
private var fontSize: Int? = null
private var staticBar: Boolean? = null
private var font: String = ""
private var theme: Int? = null
init {
refreshApiSettings()
@ -49,11 +51,11 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
}
fun getApiVersion(): Int {
if (_apiVersion == -1) {
if (apiVersion == -1) {
refreshApiVersion()
return _apiVersion
return apiVersion
}
return _apiVersion
return apiVersion
}
fun updateApiVersion(apiMajorVersion: Int) {
@ -62,14 +64,14 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
}
private fun refreshApiVersion() {
_apiVersion = settings.getInt(API_VERSION_MAJOR, -1)
apiVersion = settings.getInt(API_VERSION_MAJOR, -1)
}
fun getPublicAccess(): Boolean {
if (_publicAccess == null) {
if (publicAccess == null) {
refreshPublicAccess()
}
return _publicAccess!!
return publicAccess!!
}
fun updatePublicAccess(publicAccess: Boolean) {
@ -78,14 +80,14 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
}
private fun refreshPublicAccess() {
_publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false)
publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false)
}
fun getSelfSigned(): Boolean {
if (_selfSigned == null) {
if (selfSigned == null) {
refreshSelfSigned()
}
return _selfSigned!!
return selfSigned!!
}
fun updateSelfSigned(selfSigned: Boolean) {
@ -94,53 +96,53 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
}
private fun refreshSelfSigned() {
_selfSigned = settings.getBoolean(API_SELF_SIGNED, false)
selfSigned = settings.getBoolean(API_SELF_SIGNED, false)
}
fun getBaseUrl(): String {
if (_baseUrl.isEmpty()) {
if (baseUrl.isEmpty()) {
refreshBaseUrl()
}
return _baseUrl
return baseUrl
}
fun getUserName(): String {
if (_userName.isEmpty()) {
if (userName.isEmpty()) {
refreshUsername()
}
return _userName
return userName
}
fun getPassword(): String {
if (_password.isEmpty()) {
if (password.isEmpty()) {
refreshPassword()
}
return _password
return password
}
fun getBasicUserName(): String {
if (_basicUserName.isEmpty()) {
if (basicUserName.isEmpty()) {
refreshBasicUsername()
}
return _basicUserName
return basicUserName
}
fun getBasicPassword(): String {
if (_basicPassword.isEmpty()) {
if (basicPassword.isEmpty()) {
refreshBasicPassword()
}
return _basicPassword
return basicPassword
}
fun getItemsNumber(): Int {
if (_itemsNumber == null) {
if (itemsNumber == null) {
refreshItemsNumber()
}
return _itemsNumber!!
return itemsNumber!!
}
private fun refreshItemsNumber() {
_itemsNumber =
itemsNumber =
try {
settings.getString(API_ITEMS_NUMBER, "20").toInt()
} catch (e: Exception) {
@ -150,16 +152,16 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
}
fun getApiTimeout(): Long {
if (_apiTimeout == null) {
if (apiTimeout == null) {
refreshApiTimeout()
}
return _apiTimeout!!
return apiTimeout!!
}
private fun secToMs(n: Long) = n * 1000
private fun refreshApiTimeout() {
_apiTimeout =
apiTimeout =
secToMs(
try {
val settingsTimeout = settings.getString(API_TIMEOUT, "60")
@ -177,229 +179,229 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
}
private fun refreshBaseUrl() {
_baseUrl = settings.getString(BASE_URL, "")
baseUrl = settings.getString(BASE_URL, "")
}
private fun refreshUsername() {
_userName = settings.getString(LOGIN, "")
userName = settings.getString(LOGIN, "")
}
private fun refreshPassword() {
_password = settings.getString(PASSWORD, "")
password = settings.getString(PASSWORD, "")
}
private fun refreshBasicUsername() {
_basicUserName = settings.getString(BASIC_LOGIN, "")
basicUserName = settings.getString(BASIC_LOGIN, "")
}
private fun refreshBasicPassword() {
_basicPassword = settings.getString(BASIC_PASSWORD, "")
basicPassword = settings.getString(BASIC_PASSWORD, "")
}
private fun refreshArticleViewerEnabled() {
_articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
}
fun isArticleViewerEnabled(): Boolean {
if (_articleViewer != null) {
if (articleViewer != null) {
refreshArticleViewerEnabled()
}
return _articleViewer == true
return articleViewer == true
}
private fun refreshShouldBeCardViewEnabled() {
_shouldBeCardView = settings.getBoolean(CARD_VIEW_ACTIVE, false)
shouldBeCardView = settings.getBoolean(CARD_VIEW_ACTIVE, false)
}
fun isCardViewEnabled(): Boolean {
if (_shouldBeCardView != null) {
if (shouldBeCardView != null) {
refreshShouldBeCardViewEnabled()
}
return _shouldBeCardView == true
return shouldBeCardView == true
}
private fun refreshDisplayUnreadCountEnabled() {
_displayUnreadCount = settings.getBoolean(DISPLAY_UNREAD_COUNT, true)
displayUnreadCount = settings.getBoolean(DISPLAY_UNREAD_COUNT, true)
}
fun isDisplayUnreadCountEnabled(): Boolean {
if (_displayUnreadCount != null) {
if (displayUnreadCount != null) {
refreshDisplayUnreadCountEnabled()
}
return _displayUnreadCount == true
return displayUnreadCount == true
}
private fun refreshDisplayAllCountEnabled() {
_displayAllCount = settings.getBoolean(DISPLAY_OTHER_COUNT, false)
displayAllCount = settings.getBoolean(DISPLAY_OTHER_COUNT, false)
}
fun isDisplayAllCountEnabled(): Boolean {
if (_displayAllCount != null) {
if (displayAllCount != null) {
refreshDisplayAllCountEnabled()
}
return _displayAllCount == true
return displayAllCount == true
}
private fun refreshFullHeightCardsEnabled() {
_fullHeightCards = settings.getBoolean(FULL_HEIGHT_CARDS, false)
fullHeightCards = settings.getBoolean(FULL_HEIGHT_CARDS, false)
}
fun isFullHeightCardsEnabled(): Boolean {
if (_fullHeightCards != null) {
if (fullHeightCards != null) {
refreshFullHeightCardsEnabled()
}
return _fullHeightCards == true
return fullHeightCards == true
}
private fun refreshUpdateSourcesEnabled() {
_updateSources = settings.getBoolean(UPDATE_SOURCES, true)
updateSources = settings.getBoolean(UPDATE_SOURCES, true)
}
fun isUpdateSourcesEnabled(): Boolean {
if (_updateSources != null) {
if (updateSources != null) {
refreshUpdateSourcesEnabled()
}
return _updateSources == true
return updateSources == true
}
private fun refreshPeriodicRefreshEnabled() {
_periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false)
periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false)
}
fun isPeriodicRefreshEnabled(): Boolean {
if (_periodicRefresh != null) {
if (periodicRefresh != null) {
refreshPeriodicRefreshEnabled()
}
return _periodicRefresh == true
return periodicRefresh == true
}
private fun refreshRefreshWhenChargingOnlyEnabled() {
_refreshWhenChargingOnly = settings.getBoolean(REFRESH_WHEN_CHARGING, false)
refreshWhenChargingOnly = settings.getBoolean(REFRESH_WHEN_CHARGING, false)
}
fun isRefreshWhenChargingOnlyEnabled(): Boolean {
if (_refreshWhenChargingOnly != null) {
if (refreshWhenChargingOnly != null) {
refreshRefreshWhenChargingOnlyEnabled()
}
return _refreshWhenChargingOnly == true
return refreshWhenChargingOnly == true
}
private fun refreshRefreshMinutes() {
_refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
if (_refreshMinutes <= 15) {
_refreshMinutes = 15
refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
if (refreshMinutes <= 15) {
refreshMinutes = 15
}
}
fun getRefreshMinutes(): Long {
if (_refreshMinutes != 360L) {
if (refreshMinutes != 360L) {
refreshRefreshMinutes()
}
return _refreshMinutes
return refreshMinutes
}
private fun refreshInfiniteLoadingEnabled() {
_infiniteLoading = settings.getBoolean(INFINITE_LOADING, false)
infiniteLoading = settings.getBoolean(INFINITE_LOADING, false)
}
fun isInfiniteLoadingEnabled(): Boolean {
if (_infiniteLoading != null) {
if (infiniteLoading != null) {
refreshInfiniteLoadingEnabled()
}
return _infiniteLoading == true
return infiniteLoading == true
}
private fun refreshItemCachingEnabled() {
_itemsCaching = settings.getBoolean(ITEMS_CACHING, false)
itemsCaching = settings.getBoolean(ITEMS_CACHING, false)
}
fun isItemCachingEnabled(): Boolean {
if (_itemsCaching != null) {
if (itemsCaching != null) {
refreshItemCachingEnabled()
}
return _itemsCaching == true
return itemsCaching == true
}
private fun refreshNotifyNewItemsEnabled() {
_notifyNewItems = settings.getBoolean(NOTIFY_NEW_ITEMS, false)
notifyNewItems = settings.getBoolean(NOTIFY_NEW_ITEMS, false)
}
fun isNotifyNewItemsEnabled(): Boolean {
if (_notifyNewItems != null) {
if (notifyNewItems != null) {
refreshNotifyNewItemsEnabled()
}
return _notifyNewItems == true
return notifyNewItems == true
}
private fun refreshMarkOnScrollEnabled() {
_markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false)
markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false)
}
fun isMarkOnScrollEnabled(): Boolean {
if (_markOnScroll != null) {
if (markOnScroll != null) {
refreshMarkOnScrollEnabled()
}
return _markOnScroll == true
return markOnScroll == true
}
private fun refreshActiveAllignment() {
_activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY)
activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY)
}
fun getActiveAllignment(): Int {
if (_activeAlignment != null) {
if (activeAlignment != null) {
refreshActiveAllignment()
}
return _activeAlignment ?: JUSTIFY
return activeAlignment ?: JUSTIFY
}
fun changeAllignment(allignment: Int) {
settings.putInt(TEXT_ALIGN, allignment)
_activeAlignment = allignment
activeAlignment = allignment
}
private fun refreshFontSize() {
_fontSize = settings.getString(READER_FONT_SIZE, "16").toInt()
fontSize = settings.getString(READER_FONT_SIZE, "16").toInt()
}
fun getFontSize(): Int {
if (_fontSize != null) {
if (fontSize != null) {
refreshFontSize()
}
return _fontSize ?: 16
return fontSize ?: 16
}
private fun refreshStaticBarEnabled() {
_staticBar = settings.getBoolean(READER_STATIC_BAR, false)
staticBar = settings.getBoolean(READER_STATIC_BAR, false)
}
fun isStaticBarEnabled(): Boolean {
if (_staticBar != null) {
if (staticBar != null) {
refreshStaticBarEnabled()
}
return _staticBar == true
return staticBar == true
}
private fun refreshFont() {
_font = settings.getString(READER_FONT, "")
font = settings.getString(READER_FONT, "")
}
fun getFont(): String {
if (_font.isEmpty()) {
if (font.isEmpty()) {
refreshFont()
}
return _font
return font
}
private fun refreshCurrentTheme() {
_theme = settings.getString(CURRENT_THEME, "-1").toInt()
theme = settings.getString(CURRENT_THEME, "-1").toInt()
}
fun getCurrentTheme(): Int {
if (_theme == null) {
if (theme == null) {
refreshCurrentTheme()
}
return _theme ?: -1
return theme ?: -1
}
fun refreshApiSettings() {
@ -478,15 +480,15 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
}
companion object {
const val translationUrl = "https://crwd.in/readerforselfoss"
const val TRANSLATION_URL = "https://crwd.in/readerforselfoss"
const val sourceUrl = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform"
const val SOURCE_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform"
const val trackerUrl = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
const val TRACKER_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
const val syncChannelId = "sync-channel-id"
const val SYNC_CHANNEL_ID = "sync-channel-id"
const val newItemsChannelId = "new-items-channel-id"
const val NEW_ITEMS_CHANNEL_ID = "new-items-channel-id"
const val JUSTIFY = 1

View File

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

View File

@ -14,7 +14,6 @@ class DatesTest {
private val oldVersionDate = "2013-05-07 13:46:00"
private val oldVersionDateVariant = "2021-03-21 10:32:00.000000"
@Test
fun new_version_date_should_be_parsed() {
val date = newVersionDate.toParsedDate()
@ -24,6 +23,7 @@ class DatesTest {
assertEquals(expected, date)
}
@Test
fun new_version_date2_should_be_parsed() {
val date = newVersionDate2.toParsedDate()

View File

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

View File

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