Compare commits

..

2 Commits

Author SHA1 Message Date
6b5f6cbbe0 chore: code style.
Some checks failed
Check PR code / Lint (pull_request) Successful in 59s
Check PR code / build (pull_request) Failing after 9m25s
2025-01-11 14:12:24 +01:00
5035392aff chore: more debug to fix a context issue. 2025-01-11 12:52:55 +01:00
61 changed files with 388 additions and 1362 deletions

View File

@@ -3,34 +3,25 @@ root = true
[*] [*]
insert_final_newline = true insert_final_newline = true
[.editorconfig] [{*.kt,*.kts}]
insert_final_newline = false
ij_kotlin_line_break_after_multiline_when_entry = false
[*.{kt,kts}] [*.{kt,kts}]
# Disable wildcard imports entirely # Disable wildcard imports entirely
ij_kotlin_name_count_to_use_star_import = 2147483647 ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
end_of_line = lf end_of_line = lf
ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
ij_kotlin_indent_before_arrow_on_new_line = false
ij_kotlin_line_break_after_multiline_when_entry = true
ij_kotlin_packages_to_use_import_on_demand = unset ij_kotlin_packages_to_use_import_on_demand = unset
indent_size = 4 indent_size = 4
indent_style = space indent_style = space
ktlint_argument_list_wrapping_ignore_when_parameter_count_greater_or_equal_than = unset ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = unset
ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = 4
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1 ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
ktlint_code_style = ktlint_official ktlint_code_style = ktlint_official
ktlint_enum_entry_name_casing = upper_or_camel_cases
ktlint_function_naming_ignore_when_annotated_with = unset
ktlint_function_signature_body_expression_wrapping = multiline ktlint_function_signature_body_expression_wrapping = multiline
ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2 ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2
ktlint_ignore_back_ticked_identifier = false ktlint_ignore_back_ticked_identifier = false
ktlint_property_naming_constant_naming = screaming_snake_case
max_line_length = 140 max_line_length = 140
[**/build]
ktlint = disabled

View File

@@ -16,13 +16,13 @@ jobs:
java-version: '17' java-version: '17'
cache: gradle cache: gradle
- name: Install klint - name: Install klint
run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/ run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
- name: Install detekt - name: Install detekt
run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7.zip && unzip detekt-cli-1.23.7.zip 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... - name: Linting...
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
- name: Detecting... - name: Detecting...
run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt' run: ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
build: build:
needs: Lint needs: Lint
uses: ./.gitea/workflows/common_build.yml uses: ./.gitea/workflows/common_build.yml

View File

@@ -1,10 +1,3 @@
**v125010111
- Debug trying to fix context issues. (#174)
- Changelog for v125010031
--------------------------------------------------------------------
**v125010031 **v125010031
- Merge pull request 'Bump dependencies' (#173) from upgarde into master - Merge pull request 'Bump dependencies' (#173) from upgarde into master

View File

@@ -104,14 +104,12 @@ fun testAddSourceWithUrl(
onView(withId(R.id.fab)) onView(withId(R.id.fab))
.perform(click()) .perform(click())
onView(withId(R.id.nameInput)) onView(withId(R.id.nameInput))
.perform(click()) .perform(click()).perform(typeTextIntoFocusedView(sourceName))
.perform(typeTextIntoFocusedView(sourceName))
onView(withId(R.id.sourceUri)) onView(withId(R.id.sourceUri))
.perform(click()) .perform(click())
.perform(typeTextIntoFocusedView(url)) .perform(typeTextIntoFocusedView(url))
onView(withId(R.id.tags)) onView(withId(R.id.tags))
.perform(click()) .perform(click()).perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
.perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
onView(withId(R.id.spoutsSpinner)) onView(withId(R.id.spoutsSpinner))
.perform(click()) .perform(click())
onData(hasToString("RSS Feed")).perform(click()) onData(hasToString("RSS Feed")).perform(click())

View File

@@ -30,21 +30,28 @@ fun withError(
): TypeSafeMatcher<View?> { ): TypeSafeMatcher<View?> {
return object : TypeSafeMatcher<View?>() { return object : TypeSafeMatcher<View?>() {
override fun matchesSafely(view: View?): Boolean { override fun matchesSafely(view: View?): Boolean {
if (view != null && (view !is EditText || view.error == null)) { if (view == null) {
return false
}
val context = view.context
if (view !is EditText) {
return false
}
if (view.error == null) {
return false return false
} }
val context = view!!.context
return (view as EditText).error.toString() == context.getString(id) return view.error.toString() == context.getString(id)
} }
override fun describeTo(description: Description?) { override fun describeTo(description: Description?) {
// Nothing
} }
} }
} }
fun isPopupWindow(): Matcher<Root> = isPlatformPopup() fun isPopupWindow(): Matcher<Root> {
return isPlatformPopup()
}
fun withDrawable( fun withDrawable(
@DrawableRes id: Int, @DrawableRes id: Int,
@@ -53,7 +60,6 @@ fun withDrawable(
description.appendText("ImageView with drawable same as drawable with id $id") description.appendText("ImageView with drawable same as drawable with id $id")
} }
@Suppress("detekt:SwallowedException")
override fun matchesSafely(view: View): Boolean { override fun matchesSafely(view: View): Boolean {
val context = view.context val context = view.context
val expectedBitmap = context.getDrawable(id)!!.toBitmap() val expectedBitmap = context.getDrawable(id)!!.toBitmap()
@@ -67,8 +73,8 @@ fun withDrawable(
fun hasBottombarItemText( fun hasBottombarItemText(
@StringRes id: Int, @StringRes id: Int,
): Matcher<View>? = ): Matcher<View>? {
allOf( return allOf(
withResourceName("fixed_bottom_navigation_icon"), withResourceName("fixed_bottom_navigation_icon"),
withParent( withParent(
allOf( allOf(
@@ -77,21 +83,23 @@ fun hasBottombarItemText(
), ),
), ),
) )
}
fun withSettingsCheckboxWidget( fun withSettingsCheckboxWidget(
@StringRes id: Int, @StringRes id: Int,
): Matcher<View>? = ): Matcher<View>? {
allOf( return allOf(
withId(android.R.id.switch_widget), withId(android.R.id.switch_widget),
withParent( withParent(
withSettingsCheckboxFrame(id), withSettingsCheckboxFrame(id),
), ),
) )
}
fun withSettingsCheckboxFrame( fun withSettingsCheckboxFrame(
@StringRes id: Int, @StringRes id: Int,
): Matcher<View>? = ): Matcher<View>? {
allOf( return allOf(
withId(android.R.id.widget_frame), withId(android.R.id.widget_frame),
hasSibling( hasSibling(
allOf( allOf(
@@ -102,6 +110,7 @@ fun withSettingsCheckboxFrame(
), ),
), ),
) )
}
fun openMenu() { fun openMenu() {
openActionBarOverflowOrOptionsMenu( openActionBarOverflowOrOptionsMenu(

View File

@@ -1,5 +1,6 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.app.Activity
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
@@ -25,33 +26,35 @@ class LoginActivityTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
private fun getActivity(): Activity? {
var activity: Activity? = null
activityRule.scenario.onActivity {
activity = it
}
return activity
}
@Before @Before
fun registerIdlingResource() { fun registerIdlingResource() {
IdlingRegistry IdlingRegistry.getInstance()
.getInstance()
.register(CountingIdlingResourceSingleton.countingIdlingResource) .register(CountingIdlingResourceSingleton.countingIdlingResource)
} }
@After @After
fun unregisterIdlingResource() { fun unregisterIdlingResource() {
IdlingRegistry IdlingRegistry.getInstance()
.getInstance()
.unregister(CountingIdlingResourceSingleton.countingIdlingResource) .unregister(CountingIdlingResourceSingleton.countingIdlingResource)
} }
@Test @Test
fun viewIsInitialized() { fun viewIsInitialized() {
onView(withId(R.id.urlView)).check(matches(isDisplayed())) onView(withId(R.id.urlView)).check(matches(isDisplayed()))
onView(withId(R.id.selfSigned)) onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked()))
.check(matches(isDisplayed()))
.check(matches(isNotChecked()))
.check( .check(
matches(isClickable()), matches(isClickable()),
) )
onView(withId(R.id.withLogin)) onView(withId(R.id.withLogin)).check(matches(isDisplayed()))
.check(matches(isDisplayed())) .check(matches(isNotChecked())).check(
.check(matches(isNotChecked()))
.check(
matches(isClickable()), matches(isClickable()),
) )
} }

View File

@@ -42,7 +42,6 @@ class SettingsActivityGeneralTest {
onView(withText(R.string.pref_header_general)).perform(click()) onView(withText(R.string.pref_header_general)).perform(click())
} }
@Suppress("detekt:LongMethod")
@Test @Test
fun testGeneral() { fun testGeneral() {
onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed())) onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
@@ -119,7 +118,6 @@ class SettingsActivityGeneralTest {
) )
} }
@Suppress("detekt:ForbiddenComment")
@Test @Test
fun testGeneralActionsNumberItems() { fun testGeneralActionsNumberItems() {
onView(withText(R.string.pref_api_items_number_title)).perform(click()) onView(withText(R.string.pref_api_items_number_title)).perform(click())

View File

@@ -42,7 +42,6 @@ class SettingsActivityOfflineTest {
onView(withText(R.string.pref_header_offline)).perform(click()) onView(withText(R.string.pref_header_offline)).perform(click())
} }
@Suppress("detekt:LongMethod")
@Test @Test
fun testOffline() { fun testOffline() {
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
@@ -108,7 +107,6 @@ class SettingsActivityOfflineTest {
) )
} }
@Suppress("detekt:LongMethod")
@Test @Test
fun testOfflineActions() { fun testOfflineActions() {
onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed())) onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed()))

View File

@@ -45,7 +45,6 @@ class SourcesActivityTest {
) )
} }
@Suppress("detekt:SwallowedException")
@Test @Test
fun addSourceCheckContent() { fun addSourceCheckContent() {
testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName) testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName)

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.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
import com.ashokvarma.bottomnavigation.BottomNavigationBar import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem import com.ashokvarma.bottomnavigation.TextBadgeItem
@@ -49,8 +49,6 @@ import org.kodein.di.instance
import java.security.MessageDigest import java.security.MessageDigest
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
private const val MIN_WIDTH_CARD_DP = 300
class HomeActivity : class HomeActivity :
AppCompatActivity(), AppCompatActivity(),
SearchView.OnQueryTextListener, SearchView.OnQueryTextListener,
@@ -202,7 +200,6 @@ class HomeActivity :
} }
} }
@Suppress("detekt:LongMethod")
private fun handleBottomBar() { private fun handleBottomBar() {
tabNewBadge = tabNewBadge =
TextBadgeItem() TextBadgeItem()
@@ -285,7 +282,7 @@ class HomeActivity :
handleBottomBarActions() handleBottomBarActions()
handleGdprDialog(appSettingsService.settings.getBoolean("GDPR_shown", false)) handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
handleRecurringTask() handleRecurringTask()
CountingIdlingResourceSingleton.increment() CountingIdlingResourceSingleton.increment()
@@ -297,10 +294,10 @@ class HomeActivity :
getElementsAccordingToTab() getElementsAccordingToTab()
} }
private fun handleGdprDialog(gdprShown: Boolean) { private fun handleGDPRDialog(GDPRShown: Boolean) {
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256") val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(appSettingsService.getBaseUrl().toByteArray()) messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
if (!gdprShown) { if (!GDPRShown) {
val alertDialog = AlertDialog.Builder(this).create() val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.gdpr_dialog_title)) alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
alertDialog.setMessage(getString(R.string.gdpr_dialog_message)) alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
@@ -317,9 +314,13 @@ class HomeActivity :
private fun reloadLayoutManager() { private fun reloadLayoutManager() {
val currentManager = binding.recyclerView.layoutManager val currentManager = binding.recyclerView.layoutManager
val layoutManager: RecyclerView.LayoutManager
fun gridLayoutManager() { // This will only update the layout manager if settings changed
val layoutManager = when (currentManager) {
is StaggeredGridLayoutManager ->
if (!appSettingsService.isCardViewEnabled()) {
layoutManager =
GridLayoutManager( GridLayoutManager(
this, this,
calculateNoOfColumns(), calculateNoOfColumns(),
@@ -327,8 +328,9 @@ class HomeActivity :
binding.recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
} }
fun staggererdGridLayoutManager() { is GridLayoutManager ->
var layoutManager = if (appSettingsService.isCardViewEnabled()) {
layoutManager =
StaggeredGridLayoutManager( StaggeredGridLayoutManager(
calculateNoOfColumns(), calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL, StaggeredGridLayoutManager.VERTICAL,
@@ -338,23 +340,24 @@ class HomeActivity :
binding.recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
} }
when (currentManager) {
is StaggeredGridLayoutManager ->
if (!appSettingsService.isCardViewEnabled()) {
gridLayoutManager()
}
is GridLayoutManager ->
if (appSettingsService.isCardViewEnabled()) {
staggererdGridLayoutManager()
}
else -> else ->
if (currentManager == null) { if (currentManager == null) {
if (!appSettingsService.isCardViewEnabled()) { if (!appSettingsService.isCardViewEnabled()) {
gridLayoutManager() layoutManager =
GridLayoutManager(
this,
calculateNoOfColumns(),
)
binding.recyclerView.layoutManager = layoutManager
} else { } else {
staggererdGridLayoutManager() layoutManager =
StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL,
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager
} }
} }
} }
@@ -479,8 +482,8 @@ class HomeActivity :
} }
private fun handleListResult(appendResults: Boolean = false) { private fun handleListResult(appendResults: Boolean = false) {
val oldManager = binding.recyclerView.layoutManager
if (appendResults) { if (appendResults) {
val oldManager = binding.recyclerView.layoutManager
firstVisible = firstVisible =
when (oldManager) { when (oldManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
@@ -493,13 +496,7 @@ class HomeActivity :
} }
} }
@Suppress("detekt:ComplexCondition") if (recyclerAdapter == null) {
if (recyclerAdapter == null ||
(
(recyclerAdapter is ItemListAdapter && appSettingsService.isCardViewEnabled()) ||
(recyclerAdapter is ItemCardAdapter && !appSettingsService.isCardViewEnabled())
)
) {
if (appSettingsService.isCardViewEnabled()) { if (appSettingsService.isCardViewEnabled()) {
recyclerAdapter = recyclerAdapter =
ItemCardAdapter( ItemCardAdapter(
@@ -546,7 +543,7 @@ class HomeActivity :
private fun calculateNoOfColumns(): Int { private fun calculateNoOfColumns(): Int {
val displayMetrics = resources.displayMetrics val displayMetrics = resources.displayMetrics
val dpWidth = displayMetrics.widthPixels / displayMetrics.density val dpWidth = displayMetrics.widthPixels / displayMetrics.density
return (dpWidth / MIN_WIDTH_CARD_DP).toInt() return (dpWidth / 300).toInt()
} }
override fun onQueryTextChange(p0: String?): Boolean { override fun onQueryTextChange(p0: String?): Boolean {
@@ -595,11 +592,10 @@ class HomeActivity :
.show() .show()
} }
@Suppress("detekt:ReturnCount", "detekt:LongMethod")
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.issue_tracker -> { R.id.issue_tracker -> {
baseContext.openUrlInBrowser(AppSettingsService.BUG_URL) baseContext.openUrlInBrowser(AppSettingsService.TRACKER_URL)
return true return true
} }

View File

@@ -84,9 +84,7 @@ class ImageActivity : AppCompatActivity() {
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private inner class ScreenSlidePagerAdapter( private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
fa: FragmentActivity,
) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allImages.size override fun getItemCount(): Int = allImages.size
override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position]) override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])

View File

@@ -30,8 +30,6 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
private const val MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED = 3
class LoginActivity : class LoginActivity :
AppCompatActivity(), AppCompatActivity(),
DIAware { DIAware {
@@ -136,18 +134,9 @@ class LoginActivity :
binding.passwordView.error = null binding.passwordView.error = null
// Store values at the time of the login attempt. // Store values at the time of the login attempt.
val url = val url = binding.urlView.text.toString().trim()
binding.urlView.text val login = binding.loginView.text.toString().trim()
.toString() val password = binding.passwordView.text.toString().trim()
.trim()
val login =
binding.loginView.text
.toString()
.trim()
val password =
binding.passwordView.text
.toString()
.trim()
failInvalidUrl(url) failInvalidUrl(url)
failLoginDetails(password, login) failLoginDetails(password, login)
@@ -219,7 +208,7 @@ class LoginActivity :
cancel = true cancel = true
binding.urlView.error = getString(R.string.login_url_problem) binding.urlView.error = getString(R.string.login_url_problem)
inValidCount++ inValidCount++
if (inValidCount == MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED) { if (inValidCount == 3) {
val alertDialog = AlertDialog.Builder(this).create() val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.warning_wrong_url)) alertDialog.setTitle(getString(R.string.warning_wrong_url))
alertDialog.setMessage(getString(R.string.text_wrong_url)) alertDialog.setMessage(getString(R.string.text_wrong_url))
@@ -284,7 +273,7 @@ class LoginActivity :
return when (item.itemId) { return when (item.itemId) {
R.id.issue_tracker -> { R.id.issue_tracker -> {
val browserIntent = val browserIntent =
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.BUG_URL)) Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.TRACKER_URL))
startActivity(browserIntent) startActivity(browserIntent)
return true return true
} }
@@ -294,7 +283,7 @@ class LoginActivity :
.withAboutIconShown(true) .withAboutIconShown(true)
.withAboutVersionShown(true) .withAboutVersionShown(true)
.withAboutSpecial2("Bug reports") .withAboutSpecial2("Bug reports")
.withAboutSpecial2Description(AppSettingsService.BUG_URL) .withAboutSpecial2Description(AppSettingsService.TRACKER_URL)
.withAboutSpecial1("Project Page") .withAboutSpecial1("Project Page")
.withAboutSpecial1Description(AppSettingsService.SOURCE_URL) .withAboutSpecial1Description(AppSettingsService.SOURCE_URL)
.start(this) .start(this)

View File

@@ -62,7 +62,6 @@ class MyApp :
private val connectivityStatus: ConnectivityStatus by instance() private val connectivityStatus: ConnectivityStatus by instance()
private val driverFactory: DriverFactory by instance() private val driverFactory: DriverFactory by instance()
@Suppress("detekt:ForbiddenComment")
// TODO: handle with the "previous" way // TODO: handle with the "previous" way
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true) private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
@@ -161,7 +160,7 @@ class MyApp :
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = val newItemsChannelmChannel =
NotificationChannel( NotificationChannel(
AppSettingsService.NEW_ITEMS_CHANNEL, AppSettingsService.NEW_ITEMS_CHANNEL_ID,
newItemsChannelname, newItemsChannelname,
newItemsChannelimportance, newItemsChannelimportance,
) )

View File

@@ -22,9 +22,7 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class ReaderActivity : class ReaderActivity : AppCompatActivity(), DIAware {
AppCompatActivity(),
DIAware {
private var currentItem: Int = 0 private var currentItem: Int = 0
private lateinit var toolbarMenu: Menu private lateinit var toolbarMenu: Menu
@@ -53,7 +51,6 @@ class ReaderActivity :
showMenuItem(false) showMenuItem(false)
} }
@Suppress("detekt:SwallowedException")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityReaderBinding.inflate(layoutInflater) binding = ActivityReaderBinding.inflate(layoutInflater)
@@ -102,9 +99,8 @@ class ReaderActivity :
oldInstanceState.clear() oldInstanceState.clear()
} }
private inner class ScreenSlidePagerAdapter( private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
fa: FragmentActivity, FragmentStateAdapter(fa) {
) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allItems.size override fun getItemCount(): Int = allItems.size
override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position]) override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
@@ -113,8 +109,8 @@ class ReaderActivity :
override fun onKeyDown( override fun onKeyDown(
keyCode: Int, keyCode: Int,
event: KeyEvent?, event: KeyEvent?,
): Boolean = ): Boolean {
when (keyCode) { return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> { KeyEvent.KEYCODE_VOLUME_DOWN -> {
val currentFragment = val currentFragment =
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
@@ -133,6 +129,7 @@ class ReaderActivity :
super.onKeyDown(keyCode, event) super.onKeyDown(keyCode, event)
} }
} }
}
private fun alignmentMenu() { private fun alignmentMenu() {
val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT

View File

@@ -18,9 +18,7 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class SourcesActivity : class SourcesActivity : AppCompatActivity(), DIAware {
AppCompatActivity(),
DIAware {
private lateinit var binding: ActivitySourcesBinding private lateinit var binding: ActivitySourcesBinding
override val di by closestDI() override val di by closestDI()
@@ -70,8 +68,7 @@ class SourcesActivity :
binding.recyclerView.adapter = mAdapter binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged() mAdapter.notifyDataSetChanged()
} else { } else {
Toast Toast.makeText(
.makeText(
this@SourcesActivity, this@SourcesActivity,
R.string.cant_get_sources, R.string.cant_get_sources,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT,

View File

@@ -21,9 +21,7 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class UpsertSourceActivity : class UpsertSourceActivity : AppCompatActivity(), DIAware {
AppCompatActivity(),
DIAware {
private var existingSource: SelfossModel.SourceDetail? = null private var existingSource: SelfossModel.SourceDetail? = null
private var mSpoutsValue: String? = null private var mSpoutsValue: String? = null
@@ -85,7 +83,6 @@ class UpsertSourceActivity :
} }
} }
@Suppress("detekt:SwallowedException")
private fun handleSpoutsSpinner() { private fun handleSpoutsSpinner() {
val spoutsKV = HashMap<String, String>() val spoutsKV = HashMap<String, String>()
binding.spoutsSpinner.onItemSelectedListener = binding.spoutsSpinner.onItemSelectedListener =
@@ -108,8 +105,7 @@ class UpsertSourceActivity :
} }
fun handleSpoutFailure(networkIssue: Boolean = false) { fun handleSpoutFailure(networkIssue: Boolean = false) {
Toast Toast.makeText(
.makeText(
this@UpsertSourceActivity, this@UpsertSourceActivity,
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts, if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT,
@@ -174,7 +170,6 @@ class UpsertSourceActivity :
sourceDetailsUnavailable -> { sourceDetailsUnavailable -> {
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
} }
else -> { else -> {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val successfullyAddedSource = val successfullyAddedSource =
@@ -197,8 +192,7 @@ class UpsertSourceActivity :
if (successfullyAddedSource) { if (successfullyAddedSource) {
finish() finish()
} else { } else {
Toast Toast.makeText(
.makeText(
this@UpsertSourceActivity, this@UpsertSourceActivity,
R.string.cant_create_source, R.string.cant_create_source,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT,

View File

@@ -118,18 +118,16 @@ class ItemCardAdapter(
binding.itemImage.setImageDrawable(null) binding.itemImage.setImageDrawable(null)
} else { } else {
binding.itemImage.visibility = View.VISIBLE binding.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService) c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage)
} }
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded()) binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else { } else {
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage, appSettingsService) c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
} }
} }
} }
inner class ViewHolder( inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
val binding: CardItemBinding,
) : RecyclerView.ViewHolder(binding.root)
} }

View File

@@ -65,15 +65,13 @@ class ItemListAdapter(
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded()) binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else { } else {
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService) c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
} }
} else { } else {
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService) c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage)
} }
} }
} }
inner class ViewHolder( inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
val binding: ListItemBinding,
) : RecyclerView.ViewHolder(binding.root)
} }

View File

@@ -11,7 +11,7 @@ import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

View File

@@ -16,7 +16,6 @@ import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBindi
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon import bou.amine.apps.readerforselfossv2.utils.getIcon
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -30,14 +29,12 @@ import org.kodein.di.instance
class SourcesListAdapter( class SourcesListAdapter(
private val app: Activity, private val app: Activity,
private val items: ArrayList<SelfossModel.SourceDetail>, private val items: ArrayList<SelfossModel.SourceDetail>,
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
DIAware {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private lateinit var binding: SourceListItemBinding private lateinit var binding: SourceListItemBinding
override val di: DI by closestDI(app) override val di: DI by closestDI(app)
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
@@ -64,8 +61,7 @@ class SourcesListAdapter(
notifyItemRemoved(position) notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount) notifyItemRangeChanged(position, itemCount)
} else { } else {
Toast Toast.makeText(
.makeText(
app, app,
R.string.can_delete_source, R.string.can_delete_source,
Toast.LENGTH_SHORT, Toast.LENGTH_SHORT,
@@ -84,7 +80,7 @@ class SourcesListAdapter(
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded()) binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
} else { } else {
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService) c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
} }
if (!itm.error.isNullOrBlank()) { if (!itm.error.isNullOrBlank()) {
@@ -103,7 +99,5 @@ class SourcesListAdapter(
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
inner class ViewHolder( inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView)
val mView: ConstraintLayout,
) : RecyclerView.ViewHolder(mView)
} }

View File

@@ -26,8 +26,6 @@ import org.kodein.di.instance
import java.util.Timer import java.util.Timer
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
private const val NOTIFICATION_DELAY = 4000L
class LoadingWorker( class LoadingWorker(
val context: Context, val context: Context,
params: WorkerParameters, params: WorkerParameters,
@@ -63,7 +61,7 @@ class LoadingWorker(
handleNewItemsNotification(apiItems, notificationManager) handleNewItemsNotification(apiItems, notificationManager)
} }
} }
apiItems.map { it.preloadImages(context, appSettingsService) } apiItems.map { it.preloadImages(context) }
} }
} }
return Result.success() return Result.success()
@@ -95,7 +93,7 @@ class LoadingWorker(
NotificationCompat NotificationCompat
.Builder( .Builder(
applicationContext, applicationContext,
AppSettingsService.NEW_ITEMS_CHANNEL, AppSettingsService.NEW_ITEMS_CHANNEL_ID,
).setContentTitle(context.getString(R.string.new_items_notification_title)) ).setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText( .setContentText(
context.getString( context.getString(
@@ -103,16 +101,16 @@ class LoadingWorker(
newSize, newSize,
), ),
).setPriority(PRIORITY_DEFAULT) ).setPriority(PRIORITY_DEFAULT)
.setChannelId(AppSettingsService.NEW_ITEMS_CHANNEL) .setChannelId(AppSettingsService.NEW_ITEMS_CHANNEL_ID)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true) .setAutoCancel(true)
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp) .setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
Timer("", false).schedule(NOTIFICATION_DELAY) { Timer("", false).schedule(4000) {
notificationManager.notify(2, newItemsNotification.build()) notificationManager.notify(2, newItemsNotification.build())
} }
} }
Timer("", false).schedule(NOTIFICATION_DELAY) { Timer("", false).schedule(4000) {
notificationManager.cancel(1) notificationManager.cancel(1)
} }
} }

View File

@@ -32,9 +32,7 @@ import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
import bou.amine.apps.readerforselfossv2.android.model.toModel import bou.amine.apps.readerforselfossv2.android.model.toModel
import bou.amine.apps.readerforselfossv2.android.model.toParcelable import bou.amine.apps.readerforselfossv2.android.model.toParcelable
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapFitCenter
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.glide.getGlideImageForResource
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
@@ -48,6 +46,9 @@ import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getImages import bou.amine.apps.readerforselfossv2.utils.getImages
import bou.amine.apps.readerforselfossv2.utils.getThumbnail import bou.amine.apps.readerforselfossv2.utils.getThumbnail
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar import com.github.rubensousa.floatingtoolbar.FloatingToolbar
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -64,17 +65,9 @@ import java.util.Locale
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
private const val IMAGE_JPG = "image/jpg" private const val IMAGE_JPG = "image/jpg"
private const val IMAGE_PNG = "image/png"
private const val IMAGE_WEBP = "image/webp"
private const val WHITE_COLOR_HEX = 0xFFFFFF class ArticleFragment : Fragment(), DIAware {
private var fontSize: Int = 16
private const val DEFAULT_FONT_SIZE = 16
class ArticleFragment :
Fragment(),
DIAware {
private var fontSize: Int = DEFAULT_FONT_SIZE
private lateinit var item: SelfossModel.Item private lateinit var item: SelfossModel.Item
private lateinit var url: String private lateinit var url: String
private lateinit var contentText: String private lateinit var contentText: String
@@ -105,7 +98,6 @@ class ArticleFragment :
item = pi.toModel() item = pi.toModel()
} }
@Suppress("detekt:LongMethod")
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -175,8 +167,7 @@ class ArticleFragment :
} catch (e: InflateException) { } catch (e: InflateException) {
e.sendSilentlyWithAcraWithName("webview not available") e.sendSilentlyWithAcraWithName("webview not available")
try { try {
AlertDialog AlertDialog.Builder(requireContext())
.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton( .setPositiveButton(
@@ -184,7 +175,8 @@ class ArticleFragment :
) { _, _ -> ) { _, _ ->
appSettingsService.disableArticleViewer() appSettingsService.disableArticleViewer()
requireActivity().finish() requireActivity().finish()
}.create() }
.create()
.show() .show()
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null") e.sendSilentlyWithAcraWithName("Context required is null")
@@ -209,7 +201,12 @@ class ArticleFragment :
if (!contentImage.isEmptyOrNullOrNullString() && context != null) { if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
requireContext().bitmapFitCenter(contentImage, binding.imageView, appSettingsService) Glide
.with(requireContext())
.asBitmap()
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else { } else {
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
} }
@@ -238,8 +235,7 @@ class ArticleFragment :
repository.markAsRead(this@ArticleFragment.item) repository.markAsRead(this@ArticleFragment.item)
} }
this@ArticleFragment.item.unread = false this@ArticleFragment.item.unread = false
Toast Toast.makeText(
.makeText(
requireContext(), requireContext(),
R.string.marked_as_read, R.string.marked_as_read,
Toast.LENGTH_LONG, Toast.LENGTH_LONG,
@@ -249,8 +245,7 @@ class ArticleFragment :
repository.unmarkAsRead(this@ArticleFragment.item) repository.unmarkAsRead(this@ArticleFragment.item)
} }
this@ArticleFragment.item.unread = true this@ArticleFragment.item.unread = true
Toast Toast.makeText(
.makeText(
context, context,
R.string.marked_as_unread, R.string.marked_as_unread,
Toast.LENGTH_LONG, Toast.LENGTH_LONG,
@@ -281,7 +276,6 @@ class ArticleFragment :
} }
} }
@Suppress("detekt:SwallowedException")
private fun getContentFromMercury() { private fun getContentFromMercury() {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
@@ -320,10 +314,17 @@ class ArticleFragment :
} }
} }
private fun handleLeadImage(leadImageUrl: String?) { private fun handleLeadImage(lead_image_url: String?) {
if (!leadImageUrl.isNullOrEmpty() && context != null) { if (!lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
requireContext().bitmapFitCenter(leadImageUrl, binding.imageView, appSettingsService) Glide
.with(requireContext())
.asBitmap()
.load(
lead_image_url,
)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else { } else {
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
} }
@@ -336,8 +337,8 @@ class ArticleFragment :
override fun shouldOverrideUrlLoading( override fun shouldOverrideUrlLoading(
view: WebView?, view: WebView?,
url: String, url: String,
): Boolean = ): Boolean {
if (context != null && return if (context != null &&
url.isUrlValid() && url.isUrlValid() &&
binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) { ) {
@@ -346,43 +347,63 @@ class ArticleFragment :
} else { } else {
false false
} }
}
@Suppress("detekt:SwallowedException", "detekt:ReturnCount")
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun shouldInterceptRequest( override fun shouldInterceptRequest(
view: WebView, view: WebView,
url: String, url: String,
): WebResourceResponse? { ): WebResourceResponse? {
val (mime: String?, compression: Bitmap.CompressFormat) = val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url if (url.lowercase(Locale.US).contains(".jpg") ||
.lowercase(Locale.US) url.lowercase(Locale.US)
.contains(".jpg") || .contains(".jpeg")
url.lowercase(Locale.US).contains(".jpeg")
) { ) {
Pair(IMAGE_JPG, Bitmap.CompressFormat.JPEG)
} else if (url.lowercase(Locale.US).contains(".png")) {
Pair(IMAGE_PNG, Bitmap.CompressFormat.PNG)
} else if (url.lowercase(Locale.US).contains(".webp")) {
Pair(IMAGE_WEBP, Bitmap.CompressFormat.WEBP)
} else {
return super.shouldInterceptRequest(view, url)
}
try { try {
val image = view.getGlideImageForResource(url, appSettingsService) val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
.get()
return WebResourceResponse( return WebResourceResponse(
mime, IMAGE_JPG,
"UTF-8", "UTF-8",
getBitmapInputStream(image, compression), getBitmapInputStream(image, Bitmap.CompressFormat.JPEG),
) )
} catch (e: ExecutionException) { } 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) return super.shouldInterceptRequest(view, url)
} }
} }
} }
}
@Suppress("detekt:LongMethod", "detekt:ImplicitDefaultLocale")
private fun htmlToWebview() { private fun htmlToWebview() {
val context: Context val context: Context
try { try {
@@ -412,13 +433,13 @@ class ArticleFragment :
val colorSurfaceString = val colorSurfaceString =
String.format( String.format(
"#%06X", "#%06X",
WHITE_COLOR_HEX and (if (colorSurface.data != DATA_NULL_UNDEFINED) colorSurface.data else WHITE_COLOR_HEX), 0xFFFFFF and (if (colorSurface.data != DATA_NULL_UNDEFINED) colorSurface.data else 0xFFFFFF),
) )
val colorOnSurfaceString = val colorOnSurfaceString =
String.format( String.format(
"#%06X", "#%06X",
WHITE_COLOR_HEX and (if (colorOnSurface.data != DATA_NULL_UNDEFINED) colorOnSurface.data else 0), 0xFFFFFF and (if (colorOnSurface.data != DATA_NULL_UNDEFINED) colorOnSurface.data else 0),
) )
try { try {
@@ -432,7 +453,9 @@ class ArticleFragment :
GestureDetector( GestureDetector(
activity, activity,
object : GestureDetector.SimpleOnGestureListener() { object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean = performClick() override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick()
}
}, },
) )
@@ -500,7 +523,7 @@ class ArticleFragment :
| color: ${ | color: ${
String.format( String.format(
"#%06X", "#%06X",
WHITE_COLOR_HEX and context.resources.getColor(R.color.colorAccent), 0xFFFFFF and context.resources.getColor(R.color.colorAccent),
) )
} !important; } !important;
| } | }
@@ -577,8 +600,7 @@ class ArticleFragment :
} }
fun performClick(): Boolean { fun performClick(): Boolean {
if (allImages != null && if (allImages != null && (
(
binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) )

View File

@@ -16,12 +16,11 @@ import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getColorHexCode import bou.amine.apps.readerforselfossv2.utils.getColorHexCode
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.ViewTarget import com.bumptech.glide.request.target.ViewTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@@ -34,15 +33,10 @@ import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.instance import org.kodein.di.instance
private const val DRAWABLE_SIZE = 30 class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
class FilterSheetFragment :
BottomSheetDialogFragment(),
DIAware {
private lateinit var binding: FilterFragmentBinding private lateinit var binding: FilterFragmentBinding
override val di: DI by closestDI() override val di: DI by closestDI()
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
private var selectedChip: Chip? = null private var selectedChip: Chip? = null
@@ -86,8 +80,9 @@ class FilterSheetFragment :
val c = Chip(context) val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END c.ellipsize = TextUtils.TruncateAt.END
context.imageIntoViewTarget( Glide.with(context)
source.getIcon(repository.baseUrl), .load(source.getIcon(repository.baseUrl))
.into(
object : ViewTarget<Chip?, Drawable?>(c) { object : ViewTarget<Chip?, Drawable?>(c) {
override fun onResourceReady( override fun onResourceReady(
resource: Drawable, resource: Drawable,
@@ -100,7 +95,6 @@ class FilterSheetFragment :
} }
} }
}, },
appSettingsService,
) )
c.text = source.title.getHtmlDecoded() c.text = source.title.getHtmlDecoded()
@@ -159,8 +153,8 @@ class FilterSheetFragment :
} }
gd.setColor(gdColor) gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE gd.shape = GradientDrawable.RECTANGLE
gd.setSize(DRAWABLE_SIZE, DRAWABLE_SIZE) gd.setSize(30, 30)
gd.cornerRadius = DRAWABLE_SIZE.toFloat() gd.cornerRadius = 30F
c.chipIcon = gd c.chipIcon = gd
} catch (e: Exception) { } catch (e: Exception) {
e.sendSilentlyWithAcraWithName("tags > GradientDrawable") e.sendSilentlyWithAcraWithName("tags > GradientDrawable")

View File

@@ -6,21 +6,15 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapWithCache import com.bumptech.glide.Glide
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import com.bumptech.glide.load.engine.DiskCacheStrategy
import org.kodein.di.DI import com.bumptech.glide.request.RequestOptions
import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
import org.kodein.di.instance
class ImageFragment : class ImageFragment : Fragment() {
Fragment(),
DIAware {
override val di: DI by closestDI()
private val appSettingsService: AppSettingsService by instance()
private lateinit var imageUrl: String private lateinit var imageUrl: String
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
private var _binding: FragmentImageBinding? = null private var _binding: FragmentImageBinding? = null
val binding get() = _binding private val binding get() = _binding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -37,7 +31,11 @@ class ImageFragment :
val view = binding?.root val view = binding?.root
binding!!.photoView.visibility = View.VISIBLE binding!!.photoView.visibility = View.VISIBLE
requireActivity().bitmapWithCache(imageUrl, binding!!.photoView, appSettingsService) Glide.with(requireActivity())
.asBitmap()
.apply(glideOptions)
.load(imageUrl)
.into(binding!!.photoView)
return view return view
} }

View File

@@ -3,21 +3,23 @@ package bou.amine.apps.readerforselfossv2.android.model
import android.content.Context import android.content.Context
import android.webkit.URLUtil import android.webkit.URLUtil
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.preloadImage
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getImages import bou.amine.apps.readerforselfossv2.utils.getImages
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
fun SelfossModel.Item.preloadImages( fun SelfossModel.Item.preloadImages(context: Context): Boolean {
context: Context,
appSettingsService: AppSettingsService,
): Boolean {
val imageUrls = this.getImages() val imageUrls = this.getImages()
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
try { try {
for (url in imageUrls) { for (url in imageUrls) {
if (URLUtil.isValidUrl(url)) { if (URLUtil.isValidUrl(url)) {
context.preloadImage(url, appSettingsService) Glide.with(context).asBitmap()
.apply(glideOptions)
.load(url).submit()
} }
} }
} catch (e: Error) { } catch (e: Error) {

View File

@@ -26,10 +26,6 @@ import org.kodein.di.android.closestDI
private const val TITLE_TAG = "settingsActivityTitle" private const val TITLE_TAG = "settingsActivityTitle"
const val MAX_ITEMS_NUMBER = 200
private const val MIN_ITEMS_NUMBER = 1
class SettingsActivity : class SettingsActivity :
AppCompatActivity(), AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
@@ -147,7 +143,7 @@ class SettingsActivity :
InputFilter { source, _, _, dest, _, _ -> InputFilter { source, _, _, dest, _, _ ->
try { try {
val input: Int = (dest.toString() + source.toString()).toInt() val input: Int = (dest.toString() + source.toString()).toInt()
if (input in MIN_ITEMS_NUMBER..MAX_ITEMS_NUMBER) return@InputFilter null if (input in 1..200) return@InputFilter null
} catch (nfe: NumberFormatException) { } catch (nfe: NumberFormatException) {
Toast Toast
.makeText( .makeText(
@@ -240,7 +236,7 @@ class SettingsActivity :
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.BUG_URL) openUrl(AppSettingsService.TRACKER_URL)
true true
} }

View File

@@ -16,8 +16,7 @@ fun Context.shareLink(
sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle) sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
sendIntent.type = "text/plain" sendIntent.type = "text/plain"
startActivity( startActivity(
Intent Intent.createChooser(
.createChooser(
sendIntent, sendIntent,
getString(R.string.share), getString(R.string.share),
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),

View File

@@ -59,5 +59,7 @@ class CircleImageView
textView.text = text.toTextDrawableString() textView.text = text.toTextDrawableString()
} }
private fun colorFromIdentifier(key: String): Int = colorScheme[abs(key.hashCode()) % colorScheme.size] private fun colorFromIdentifier(key: String): Int {
return colorScheme[abs(key.hashCode()) % colorScheme.size]
}
} }

View File

@@ -25,8 +25,7 @@ fun Context.openItemUrl(
app: Activity, app: Activity,
) { ) {
if (!linkDecoded.isUrlValid()) { if (!linkDecoded.isUrlValid()) {
Toast Toast.makeText(
.makeText(
this, this,
this.getString(R.string.cant_open_invalid_url), this.getString(R.string.cant_open_invalid_url),
Toast.LENGTH_LONG, Toast.LENGTH_LONG,
@@ -72,7 +71,6 @@ fun Context.openUrlInBrowser(url: String) {
this.mayBeStartActivity(intent) this.mayBeStartActivity(intent)
} }
@Suppress("detekt:SwallowedException")
fun Context.mayBeStartActivity(intent: Intent) { fun Context.mayBeStartActivity(intent: Intent) {
try { try {
this.startActivity(intent) this.startActivity(intent)

View File

@@ -2,130 +2,40 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.webkit.WebView
import android.widget.ImageView import android.widget.ImageView
import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.ViewTarget
import com.google.android.material.chip.Chip
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
private const val PRELOAD_IMAGE_TIMEOUT = 10000
@OptIn(ExperimentalEncodingApi::class)
fun String.toGlideUrl(appSettingsService: AppSettingsService): GlideUrl {
if (appSettingsService.getBasicUserName().isNotEmpty()) {
val authString = "${appSettingsService.getBasicUserName()}:${appSettingsService.getBasicPassword()}"
val authBuf = Base64.encode(authString.toByteArray(Charsets.UTF_8))
return GlideUrl(
this,
LazyHeaders
.Builder()
.addHeader("Authorization", "Basic $authBuf")
.build(),
)
} else {
return GlideUrl(
this,
)
}
}
fun WebView.getGlideImageForResource(
url: String,
appSettingsService: AppSettingsService,
) = Glide
.with(this)
.asBitmap()
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL))
.load(url.toGlideUrl(appSettingsService))
.submit()
.get()
fun Context.preloadImage(
url: String,
appSettingsService: AppSettingsService,
) = Glide
.with(this)
.asBitmap()
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(PRELOAD_IMAGE_TIMEOUT))
.load(url.toGlideUrl(appSettingsService))
.submit()
fun Context.imageIntoViewTarget(
url: String,
target: ViewTarget<Chip?, Drawable?>,
appSettingsService: AppSettingsService,
) = Glide
.with(this)
.load(url.toGlideUrl(appSettingsService))
.into(target)
fun Context.bitmapWithCache(
url: String,
iv: ImageView,
appSettingsService: AppSettingsService,
) = Glide
.with(this)
.asBitmap()
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL))
.load(url.toGlideUrl(appSettingsService))
.into(iv)
fun Context.bitmapCenterCrop( fun Context.bitmapCenterCrop(
url: String, url: String,
iv: ImageView, iv: ImageView,
appSettingsService: AppSettingsService, ) = Glide.with(this)
) = Glide
.with(this)
.asBitmap() .asBitmap()
.load(url.toGlideUrl(appSettingsService)) .load(url)
.apply(RequestOptions.centerCropTransform()) .apply(RequestOptions.centerCropTransform())
.into(iv) .into(iv)
fun Context.bitmapFitCenter(
url: String,
iv: ImageView,
appSettingsService: AppSettingsService,
) = Glide
.with(this)
.asBitmap()
.load(url.toGlideUrl(appSettingsService))
.apply(RequestOptions.fitCenterTransform())
.into(iv)
fun Context.circularDrawable( fun Context.circularDrawable(
url: String, url: String,
view: CircleImageView, view: CircleImageView,
appSettingsService: AppSettingsService,
) { ) {
view.textView.text = "" view.textView.text = ""
Glide Glide.with(this)
.with(this) .load(url)
.load(url.toGlideUrl(appSettingsService))
.into(view.imageView) .into(view.imageView)
} }
private const val BITMAP_INPUT_STREAM_COMPRESSION_QUALITY = 80
fun getBitmapInputStream( fun getBitmapInputStream(
bitmap: Bitmap, bitmap: Bitmap,
compressFormat: Bitmap.CompressFormat, compressFormat: Bitmap.CompressFormat,
): InputStream { ): InputStream {
val byteArrayOutputStream = ByteArrayOutputStream() val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(compressFormat, BITMAP_INPUT_STREAM_COMPRESSION_QUALITY, byteArrayOutputStream) bitmap.compress(compressFormat, 80, byteArrayOutputStream)
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray() val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
return ByteArrayInputStream(bitmapData) return ByteArrayInputStream(bitmapData)
} }

View File

@@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.android.utils.network
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
lateinit var s: Snackbar lateinit var s: Snackbar
@@ -10,7 +11,9 @@ lateinit var s: Snackbar
fun isNetworkAccessible(context: Context): Boolean { fun isNetworkAccessible(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork ?: return false
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return when { return when {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
@@ -19,4 +22,8 @@ fun isNetworkAccessible(context: Context): Boolean {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
else -> false else -> false
} }
} else {
val network = connectivityManager.activeNetworkInfo ?: return false
return network.isConnectedOrConnecting
}
} }

View File

@@ -7,9 +7,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AppViewModel( class AppViewModel(private val repository: Repository) : ViewModel() {
private val repository: Repository,
) : ViewModel() {
private val _networkAvailableProvider = MutableSharedFlow<Boolean>() private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow() val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
private var wasConnected = true private var wasConnected = true

View File

@@ -1,5 +1,3 @@
@file:Suppress("detekt:LargeClass")
package bou.amine.apps.readerforselfossv2.tests.repository package bou.amine.apps.readerforselfossv2.tests.repository
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
@@ -52,6 +50,7 @@ class RepositoryTest {
private val db = mockk<ReaderForSelfossDB>(relaxed = true) private val db = mockk<ReaderForSelfossDB>(relaxed = true)
private val appSettingsService = mockk<AppSettingsService>() private val appSettingsService = mockk<AppSettingsService>()
private val api = mockk<SelfossApi>() private val api = mockk<SelfossApi>()
private lateinit var repository: Repository private lateinit var repository: Repository
private fun initializeRepository( private fun initializeRepository(

View File

@@ -44,7 +44,7 @@ class FakeItemParameters {
var datetime = "2022-09-09T03:32:01-04:00" var datetime = "2022-09-09T03:32:01-04:00"
val title = "Etica della ricerca sotto i riflettori." val title = "Etica della ricerca sotto i riflettori."
val content = 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 unread = true
var starred = true var starred = true
val thumbnail = null val thumbnail = null

View File

@@ -1,786 +0,0 @@
build:
maxIssues: 0
excludeCorrectable: false
weights:
# complexity: 2
# LongParameterList: 1
# style: 1
# comments: 1
config:
validation: true
warningsAsErrors: false
checkExhaustiveness: false
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
excludes: ''
processors:
active: true
exclude:
- 'DetektProgressListener'
# - 'KtFileCountProcessor'
# - 'PackageCountProcessor'
# - 'ClassCountProcessor'
# - 'FunctionCountProcessor'
# - 'PropertyCountProcessor'
# - 'ProjectComplexityProcessor'
# - 'ProjectCognitiveComplexityProcessor'
# - 'ProjectLLOCProcessor'
# - 'ProjectCLOCProcessor'
# - 'ProjectLOCProcessor'
# - 'ProjectSLOCProcessor'
# - 'LicenseHeaderLoaderExtension'
console-reports:
active: true
exclude:
- 'ProjectStatisticsReport'
- 'ComplexityReport'
- 'NotificationReport'
- 'FindingsReport'
- 'FileBasedFindingsReport'
# - 'LiteFindingsReport'
output-reports:
active: true
exclude:
# - 'TxtOutputReport'
# - 'XmlOutputReport'
# - 'HtmlOutputReport'
# - 'MdOutputReport'
# - 'SarifOutputReport'
comments:
active: true
AbsentOrWrongFileLicense:
active: false
licenseTemplateFile: 'license.template'
licenseTemplateIsRegex: false
CommentOverPrivateFunction:
active: false
CommentOverPrivateProperty:
active: false
DeprecatedBlockTag:
active: false
EndOfSentenceFormat:
active: false
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
KDocReferencesNonPublicProperty:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
OutdatedDocumentation:
active: false
matchTypeParameters: true
matchDeclarationsOrder: true
allowParamOnConstructorProperties: false
UndocumentedPublicClass:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
searchInNestedClass: true
searchInInnerClass: true
searchInInnerObject: true
searchInInnerInterface: true
searchInProtectedClass: false
UndocumentedPublicFunction:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
searchProtectedFunction: false
UndocumentedPublicProperty:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
searchProtectedProperty: false
complexity:
active: true
CognitiveComplexMethod:
active: false
threshold: 15
ComplexCondition:
active: true
threshold: 4
ComplexInterface:
active: false
threshold: 10
includeStaticDeclarations: false
includePrivateDeclarations: false
ignoreOverloaded: false
CyclomaticComplexMethod:
active: true
threshold: 15
ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false
ignoreNestingFunctions: false
nestingFunctions:
- 'also'
- 'apply'
- 'forEach'
- 'isNotNull'
- 'ifNull'
- 'let'
- 'run'
- 'use'
- 'with'
LabeledExpression:
active: false
ignoredLabels: [ ]
LargeClass:
active: true
threshold: 600
LongMethod:
active: true
threshold: 60
LongParameterList:
active: true
functionThreshold: 6
constructorThreshold: 7
ignoreDefaultParameters: false
ignoreDataClasses: true
ignoreAnnotatedParameter: [ ]
MethodOverloading:
active: false
threshold: 6
NamedArguments:
active: false
threshold: 3
ignoreArgumentsMatchingNames: false
NestedBlockDepth:
active: true
threshold: 4
NestedScopeFunctions:
active: false
threshold: 1
functions:
- 'kotlin.apply'
- 'kotlin.run'
- 'kotlin.with'
- 'kotlin.let'
- 'kotlin.also'
ReplaceSafeCallChainWithRun:
active: false
StringLiteralDuplication:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
threshold: 3
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'
TooManyFunctions:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/android/*Activity.kt', '**/fragments/*Fragment.kt' ]
thresholdInFiles: 11
thresholdInClasses: 11
thresholdInInterfaces: 11
thresholdInObjects: 11
thresholdInEnums: 11
ignoreDeprecated: false
ignorePrivate: false
ignoreOverridden: false
ignoreAnnotatedFunctions: [ ]
coroutines:
active: true
GlobalCoroutineUsage:
active: false
InjectDispatcher:
active: true
dispatcherNames:
- 'IO'
- 'Default'
- 'Unconfined'
RedundantSuspendModifier:
active: true
SleepInsteadOfDelay:
active: true
SuspendFunSwallowedCancellation:
active: false
SuspendFunWithCoroutineScopeReceiver:
active: false
SuspendFunWithFlowReturnType:
active: true
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: '_|(ignore|expected).*'
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
active: true
EmptyDoWhileBlock:
active: true
EmptyElseBlock:
active: true
EmptyFinallyBlock:
active: true
EmptyForBlock:
active: true
EmptyFunctionBlock:
active: true
ignoreOverridden: false
EmptyIfBlock:
active: true
EmptyInitBlock:
active: true
EmptyKtFile:
active: true
EmptySecondaryConstructor:
active: true
EmptyTryBlock:
active: true
EmptyWhenBlock:
active: true
EmptyWhileBlock:
active: true
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: true
methodNames:
- 'equals'
- 'finalize'
- 'hashCode'
- 'toString'
InstanceOfCheckForException:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
NotImplementedDeclaration:
active: false
ObjectExtendsThrowable:
active: false
PrintStackTrace:
active: true
RethrowCaughtException:
active: true
ReturnFromFinally:
active: true
ignoreLabeled: false
SwallowedException:
active: true
ignoredExceptionTypes:
- 'InterruptedException'
- 'MalformedURLException'
- 'NumberFormatException'
- 'ParseException'
allowedExceptionNameRegex: '_|(ignore|expected).*'
ThrowingExceptionFromFinally:
active: true
ThrowingExceptionInMain:
active: false
ThrowingExceptionsWithoutMessageOrCause:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
exceptions:
- 'ArrayIndexOutOfBoundsException'
- 'Exception'
- 'IllegalArgumentException'
- 'IllegalMonitorStateException'
- 'IllegalStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
ThrowingNewInstanceOfSameException:
active: true
TooGenericExceptionCaught:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
exceptionNames:
- 'ArrayIndexOutOfBoundsException'
- 'Error'
- 'Exception'
- 'IllegalMonitorStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
allowedExceptionNameRegex: '_|(ignore|expected).*'
TooGenericExceptionThrown:
active: true
exceptionNames:
- 'Error'
- 'Exception'
- 'RuntimeException'
- 'Throwable'
naming:
active: true
BooleanPropertyNaming:
active: false
allowedPattern: '^(is|has|are)'
ClassNaming:
active: true
classPattern: '[A-Z][a-zA-Z0-9]*'
ConstructorParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
EnumNaming:
active: true
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: false
forbiddenName: [ ]
FunctionMaxLength:
active: false
maximumFunctionNameLength: 30
FunctionMinLength:
active: false
minimumFunctionNameLength: 3
FunctionNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
functionPattern: '[a-z][a-zA-Z0-9]*'
excludeClassPattern: '$^'
FunctionParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
InvalidPackageDeclaration:
active: true
rootPackage: ''
requireRootInDeclaration: false
LambdaParameterNaming:
active: false
parameterPattern: '[a-z][A-Za-z0-9]*|_'
MatchingDeclarationName:
active: false # done in ktlint
mustBeFirst: true
MemberNameEqualsClassName:
active: true
ignoreOverridden: true
NoNameShadowing:
active: true
NonBooleanPropertyPrefixedWithIs:
active: false
ObjectPropertyNaming:
active: true
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: true
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength:
active: false
maximumVariableNameLength: 64
VariableMinLength:
active: false
minimumVariableNameLength: 1
VariableNaming:
active: true
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
performance:
active: true
ArrayPrimitive:
active: true
CouldBeSequence:
active: false
threshold: 3
ForEachOnRange:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
SpreadOperator:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
UnnecessaryPartOfBinaryExpression:
active: false
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
AvoidReferentialEquality:
active: true
forbiddenTypePatterns:
- 'kotlin.String'
CastNullableToNonNullableType:
active: false
CastToNullableType:
active: false
Deprecation:
active: false
DontDowncastCollectionTypes:
active: false
DoubleMutabilityForCollection:
active: true
mutableTypes:
- 'kotlin.collections.MutableList'
- 'kotlin.collections.MutableMap'
- 'kotlin.collections.MutableSet'
- 'java.util.ArrayList'
- 'java.util.LinkedHashSet'
- 'java.util.HashSet'
- 'java.util.LinkedHashMap'
- 'java.util.HashMap'
ElseCaseInsteadOfExhaustiveWhen:
active: false
ignoredSubjectTypes: [ ]
EqualsAlwaysReturnsTrueOrFalse:
active: true
EqualsWithHashCodeExist:
active: true
ExitOutsideMain:
active: false
ExplicitGarbageCollectionCall:
active: true
HasPlatformType:
active: true
IgnoredReturnValue:
active: true
restrictToConfig: true
returnValueAnnotations:
- 'CheckResult'
- '*.CheckResult'
- 'CheckReturnValue'
- '*.CheckReturnValue'
ignoreReturnValueAnnotations:
- 'CanIgnoreReturnValue'
- '*.CanIgnoreReturnValue'
returnValueTypes:
- 'kotlin.sequences.Sequence'
- 'kotlinx.coroutines.flow.*Flow'
- 'java.util.stream.*Stream'
ignoreFunctionCall: [ ]
ImplicitDefaultLocale:
active: true
ImplicitUnitReturnType:
active: false
allowExplicitReturnType: true
InvalidRange:
active: true
IteratorHasNextCallsNextMethod:
active: true
IteratorNotThrowingNoSuchElementException:
active: true
LateinitUsage:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
ignoreOnClassesPattern: ''
MapGetWithNotNullAssertionOperator:
active: true
MissingPackageDeclaration:
active: false
excludes: [ '**/*.kts' ]
NullCheckOnMutableProperty:
active: false
NullableToStringCall:
active: false
PropertyUsedBeforeDeclaration:
active: false
UnconditionalJumpStatementInLoop:
active: false
UnnecessaryNotNullCheck:
active: false
UnnecessaryNotNullOperator:
active: true
UnnecessarySafeCall:
active: true
UnreachableCatchBlock:
active: true
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
UnsafeCast:
active: true
UnusedUnaryOperator:
active: true
UselessPostfixExpression:
active: true
WrongEqualsTypeParameter:
active: true
style:
active: true
AlsoCouldBeApply:
active: false
BracesOnIfStatements:
active: false
singleLine: 'never'
multiLine: 'always'
BracesOnWhenStatements:
active: false
singleLine: 'necessary'
multiLine: 'consistent'
CanBeNonNullable:
active: false
CascadingCallWrapping:
active: false
includeElvis: true
ClassOrdering:
active: false
CollapsibleIfStatements:
active: false
DataClassContainsFunctions:
active: false
conversionFunctionPrefix:
- 'to'
allowOperators: false
DataClassShouldBeImmutable:
active: false
DestructuringDeclarationWithTooManyEntries:
active: true
maxDestructuringEntries: 3
DoubleNegativeLambda:
active: false
negativeFunctions:
- reason: 'Use `takeIf` instead.'
value: 'takeUnless'
- reason: 'Use `all` instead.'
value: 'none'
negativeFunctionNameParts:
- 'not'
- 'non'
EqualsNullCall:
active: true
EqualsOnSignatureLine:
active: false
ExplicitCollectionElementAccessMethod:
active: false
ExplicitItLambdaParameter:
active: true
ExpressionBodySyntax:
active: false
includeLineWrapping: false
ForbiddenAnnotation:
active: false
annotations:
- reason: 'it is a java annotation. Use `Suppress` instead.'
value: 'java.lang.SuppressWarnings'
- reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.'
value: 'java.lang.Deprecated'
- reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.'
value: 'java.lang.annotation.Documented'
- reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.'
value: 'java.lang.annotation.Target'
- reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.'
value: 'java.lang.annotation.Retention'
- reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.'
value: 'java.lang.annotation.Repeatable'
- reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265'
value: 'java.lang.annotation.Inherited'
ForbiddenComment:
active: true
comments:
- reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
value: 'FIXME:'
- reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
value: 'STOPSHIP:'
- reason: 'Forbidden TODO todo marker in comment, please do the changes.'
value: 'TODO:'
allowedPatterns: ''
ForbiddenImport:
active: false
imports: [ ]
forbiddenPatterns: ''
ForbiddenMethodCall:
active: false
methods:
- reason: 'print does not allow you to configure the output stream. Use a logger instead.'
value: 'kotlin.io.print'
- reason: 'println does not allow you to configure the output stream. Use a logger instead.'
value: 'kotlin.io.println'
ForbiddenSuppress:
active: false
rules: [ ]
ForbiddenVoid:
active: true
ignoreOverridden: false
ignoreUsageInGenerics: false
FunctionOnlyReturningConstant:
active: true
ignoreOverridableFunction: true
ignoreActualFunction: true
excludedFunctions: [ ]
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 1
MagicNumber:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ]
ignoreNumbers:
- '-1'
- '0'
- '1'
- '2'
ignoreHashCodeFunction: true
ignorePropertyDeclaration: false
ignoreLocalVariableDeclaration: false
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: false
ignoreNamedArgument: true
ignoreEnums: false
ignoreRanges: false
ignoreExtensionFunctions: true
MandatoryBracesLoops:
active: false
MaxChainedCallsOnSameLine:
active: false
maxChainedCalls: 5
MaxLineLength:
active: false # done in ktlint
maxLineLength: 140 # default is 120. 140 to match ktlint
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
excludeRawStrings: true
MayBeConst:
active: true
ModifierOrder:
active: true
MultilineLambdaItParameter:
active: false
MultilineRawStringIndentation:
active: false
indentSize: 4
trimmingMethods:
- 'trimIndent'
- 'trimMargin'
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: false # done in ktlint
NoTabs:
active: false
NullableBooleanCheck:
active: false
ObjectLiteralToLambda:
active: true
OptionalAbstractKeyword:
active: true
OptionalUnit:
active: false
PreferToOverPairSyntax:
active: false
ProtectedMemberInFinalClass:
active: true
RedundantExplicitType:
active: false
RedundantHigherOrderMapUsage:
active: true
RedundantVisibilityModifierRule:
active: false
ReturnCount:
active: true
max: 2
excludedFunctions:
- 'equals'
excludeLabeled: false
excludeReturnFromLambda: true
excludeGuardClauses: false
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: true
SpacingBetweenPackageAndImports:
active: false
StringShouldBeRawString:
active: false
maxEscapedCharacterCount: 2
ignoredCharacters: [ ]
ThrowsCount:
active: true
max: 2
excludeGuardClauses: false
TrailingWhitespace:
active: false
TrimMultilineRawString:
active: false
trimmingMethods:
- 'trimIndent'
- 'trimMargin'
UnderscoresInNumericLiterals:
active: false
acceptableLength: 4
allowNonStandardGrouping: false
UnnecessaryAbstractClass:
active: true
UnnecessaryAnnotationUseSiteTarget:
active: false
UnnecessaryApply:
active: true
UnnecessaryBackticks:
active: false
UnnecessaryBracesAroundTrailingLambda:
active: false
UnnecessaryFilter:
active: true
UnnecessaryInheritance:
active: true
UnnecessaryInnerClass:
active: false
UnnecessaryLet:
active: false
UnnecessaryParentheses:
active: false
allowForUnclearPrecedence: false
UntilInsteadOfRangeTo:
active: false
UnusedImports:
active: false
UnusedParameter:
active: true
allowedNames: 'ignored|expected'
UnusedPrivateClass:
active: true
UnusedPrivateMember:
active: true
allowedNames: ''
UnusedPrivateProperty:
active: true
allowedNames: '_|ignored|expected|serialVersionUID'
excludes: [ '**/build.gradle.kts' ]
UseAnyOrNoneInsteadOfFind:
active: true
UseArrayLiteralsInAnnotations:
active: true
UseCheckNotNull:
active: true
UseCheckOrError:
active: true
UseDataClass:
active: false
allowVars: false
UseEmptyCounterpart:
active: false
UseIfEmptyOrIfBlank:
active: false
UseIfInsteadOfWhen:
active: false
ignoreWhenContainingVariableDeclaration: false
UseIsNullOrEmpty:
active: true
UseLet:
active: false
UseOrEmpty:
active: true
UseRequire:
active: true
UseRequireNotNull:
active: true
UseSumOfInsteadOfFlatMapSize:
active: false
UselessCallOnNotNull:
active: true
UtilityClassWithPublicConstructor:
active: true
VarCouldBeVal:
active: true
ignoreLateinitVar: false
WildcardImport:
active: true
excludeImports:
- 'java.util.*'

View File

@@ -1,4 +0,0 @@
**v125010111**
- Debug trying to fix context issues. (#174)
- Changelog for v125010031

View File

@@ -4,13 +4,12 @@ import android.content.Context
import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver
actual class DriverFactory( actual class DriverFactory(private val context: Context) {
private val context: Context, actual fun createDriver(): SqlDriver {
) { return AndroidSqliteDriver(
actual fun createDriver(): SqlDriver =
AndroidSqliteDriver(
ReaderForSelfossDB.Schema, ReaderForSelfossDB.Schema,
context, context,
"ReaderForSelfossV2-android.db", "ReaderForSelfossV2-android.db",
) )
}
} }

View File

@@ -9,14 +9,12 @@ class NaiveTrustManager : X509TrustManager {
chain: Array<out X509Certificate>?, chain: Array<out X509Certificate>?,
authType: String?, authType: String?,
) { ) {
// Nothing
} }
override fun checkServerTrusted( override fun checkServerTrusted(
chain: Array<out X509Certificate>?, chain: Array<out X509Certificate>?,
authType: String?, authType: String?,
) { ) {
// Nothing
} }
override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf() override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()

View File

@@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.utils
import android.text.format.DateUtils import android.text.format.DateUtils
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@Suppress("detekt:UtilityClassWithPublicConstructor")
actual class DateUtils { actual class DateUtils {
actual companion object { actual companion object {
actual fun parseRelativeDate(dateString: String): String { actual fun parseRelativeDate(dateString: String): String {

View File

@@ -12,14 +12,16 @@ actual fun SelfossModel.Item.getIcon(baseUrl: String): String = constructUrl(bas
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String = constructUrl(baseUrl, "thumbnails", thumbnail) actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String = constructUrl(baseUrl, "thumbnails", thumbnail)
val IMAGE_EXTENSION_REGEXP = """\.(jpg|jpeg|png|webp)""".toRegex()
actual fun SelfossModel.Item.getImages(): ArrayList<String> { actual fun SelfossModel.Item.getImages(): ArrayList<String> {
val allImages = ArrayList<String>() val allImages = ArrayList<String>()
for (image in Jsoup.parse(content).getElementsByTag("img")) { for (image in Jsoup.parse(content).getElementsByTag("img")) {
val url = image.attr("src") val url = image.attr("src")
if (IMAGE_EXTENSION_REGEXP.containsMatchIn(url.lowercase(Locale.US))) { 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")
) {
allImages.add(url) allImages.add(url)
} }
} }

View File

@@ -1,11 +1,8 @@
@file:Suppress("detekt:LongParameterList")
package bou.amine.apps.readerforselfossv2.model package bou.amine.apps.readerforselfossv2.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
class MercuryModel { class MercuryModel {
@Suppress("detekt:ConstructorParameterNaming")
@Serializable @Serializable
class ParsedContent( class ParsedContent(
val title: String? = null, val title: String? = null,

View File

@@ -1,5 +1,3 @@
@file:Suppress("detekt:LongParameterList")
package bou.amine.apps.readerforselfossv2.model package bou.amine.apps.readerforselfossv2.model
import bou.amine.apps.readerforselfossv2.utils.DateUtils import bou.amine.apps.readerforselfossv2.utils.DateUtils
@@ -20,10 +18,6 @@ import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
class ModelException(
message: String,
) : Throwable(message)
class SelfossModel { class SelfossModel {
@Serializable @Serializable
data class Tag( data class Tag(
@@ -147,7 +141,7 @@ class SelfossModel {
} }
if (stringUrl.isEmptyOrNullOrNullString()) { if (stringUrl.isEmptyOrNullOrNullString()) {
throw ModelException("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 return stringUrl
@@ -176,7 +170,7 @@ class SelfossModel {
} }
} }
// this seems to be super slow. // TODO: this seems to be super slow.
object TagsListSerializer : KSerializer<List<String>> { object TagsListSerializer : KSerializer<List<String>> {
override fun deserialize(decoder: Decoder): List<String> = override fun deserialize(decoder: Decoder): List<String> =
when (val json = ((decoder as JsonDecoder).decodeJsonElement())) { when (val json = ((decoder as JsonDecoder).decodeJsonElement())) {

View File

@@ -1,5 +1,3 @@
@file:Suppress("detekt:TooManyFunctions")
package bou.amine.apps.readerforselfossv2.repository package bou.amine.apps.readerforselfossv2.repository
import bou.amine.apps.readerforselfossv2.dao.ACTION import bou.amine.apps.readerforselfossv2.dao.ACTION
@@ -13,7 +11,7 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.model.StatusAndData import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.toEntity import bou.amine.apps.readerforselfossv2.utils.toEntity
import bou.amine.apps.readerforselfossv2.utils.toParsedDate import bou.amine.apps.readerforselfossv2.utils.toParsedDate
@@ -25,8 +23,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private const val MAX_ITEMS_NUMBER = 200
class Repository( class Repository(
private val api: SelfossApi, private val api: SelfossApi,
private val appSettingsService: AppSettingsService, private val appSettingsService: AppSettingsService,
@@ -40,26 +36,26 @@ class Repository(
var displayedItems = ItemType.UNREAD var displayedItems = ItemType.UNREAD
private var _tagFilter = MutableStateFlow<SelfossModel.Tag?>(null) private var tagFilterFlow = MutableStateFlow<SelfossModel.Tag?>(null)
var tagFilter = _tagFilter.asStateFlow() var tagFilter = tagFilterFlow.asStateFlow()
private var _sourceFilter = MutableStateFlow<SelfossModel.Source?>(null) private var sourceFilterFlow = MutableStateFlow<SelfossModel.Source?>(null)
var sourceFilter = _sourceFilter.asStateFlow() var sourceFilter = sourceFilterFlow.asStateFlow()
var searchFilter: String? = null var searchFilter: String? = null
var offlineOverride = false var offlineOverride = false
private val _badgeUnread = MutableStateFlow(0) private val badgeUnreadFlow = MutableStateFlow(0)
val badgeUnread = _badgeUnread.asStateFlow() val badgeUnread = badgeUnreadFlow.asStateFlow()
private val _badgeAll = MutableStateFlow(0) private val badgeAllFlow = MutableStateFlow(0)
val badgeAll = _badgeAll.asStateFlow() val badgeAll = badgeAllFlow.asStateFlow()
private val _badgeStarred = MutableStateFlow(0) private val badgeStarredFlow = MutableStateFlow(0)
val badgeStarred = _badgeStarred.asStateFlow() val badgeStarred = badgeStarredFlow.asStateFlow()
private var fetchedTags = false private var fetchedTags = false
private var fetchedSources = false private var fetchedSources = false
private var _readerItems = ArrayList<SelfossModel.Item>() private var readerItems = ArrayList<SelfossModel.Item>()
private var _selectedSource: SelfossModel.SourceDetail? = null private var selectedSource: SelfossModel.SourceDetail? = null
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
@@ -131,7 +127,7 @@ class Repository(
null, null,
null, null,
null, null,
MAX_ITEMS_NUMBER, 200,
) )
return if (items.success && items.data != null) { return if (items.success && items.data != null) {
items.data items.data
@@ -143,23 +139,22 @@ class Repository(
} }
} }
@Suppress("detekt:ForbiddenComment")
suspend fun reloadBadges(): Boolean { suspend fun reloadBadges(): Boolean {
var success = false var success = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
val response = api.stats() val response = api.stats()
if (response.success && response.data != null) { if (response.success && response.data != null) {
_badgeUnread.value = response.data.unread ?: 0 badgeUnreadFlow.value = response.data.unread ?: 0
_badgeAll.value = response.data.total badgeAllFlow.value = response.data.total
_badgeStarred.value = response.data.starred ?: 0 badgeStarredFlow.value = response.data.starred ?: 0
success = true success = true
} }
} else if (appSettingsService.isItemCachingEnabled()) { } else if (appSettingsService.isItemCachingEnabled()) {
// TODO: do this differently, because it's not efficient // TODO: do this differently, because it's not efficient
val dbItems = getDBItems() val dbItems = getDBItems()
_badgeUnread.value = dbItems.filter { item -> item.unread }.size badgeUnreadFlow.value = dbItems.filter { item -> item.unread }.size
_badgeStarred.value = dbItems.filter { item -> item.starred }.size badgeStarredFlow.value = dbItems.filter { item -> item.starred }.size
_badgeAll.value = dbItems.size badgeAllFlow.value = dbItems.size
success = true success = true
} }
return success return success
@@ -321,7 +316,7 @@ class Repository(
private fun markAsReadLocally(item: SelfossModel.Item) { private fun markAsReadLocally(item: SelfossModel.Item) {
if (item.unread) { if (item.unread) {
item.unread = false item.unread = false
_badgeUnread.value -= 1 badgeUnreadFlow.value -= 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@@ -332,7 +327,7 @@ class Repository(
private fun unmarkAsReadLocally(item: SelfossModel.Item) { private fun unmarkAsReadLocally(item: SelfossModel.Item) {
if (!item.unread) { if (!item.unread) {
item.unread = true item.unread = true
_badgeUnread.value += 1 badgeUnreadFlow.value += 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@@ -343,7 +338,7 @@ class Repository(
private fun starrLocally(item: SelfossModel.Item) { private fun starrLocally(item: SelfossModel.Item) {
if (!item.starred) { if (!item.starred) {
item.starred = true item.starred = true
_badgeStarred.value += 1 badgeStarredFlow.value += 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@@ -354,7 +349,7 @@ class Repository(
private fun unstarrLocally(item: SelfossModel.Item) { private fun unstarrLocally(item: SelfossModel.Item) {
if (item.starred) { if (item.starred) {
item.starred = false item.starred = false
_badgeStarred.value -= 1 badgeStarredFlow.value -= 1
} }
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@@ -564,7 +559,6 @@ class Repository(
item.id.toString(), item.id.toString(),
) )
@Suppress("detekt:SwallowedException")
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> { suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> {
try { try {
val newItems = getMaxItemsForBackground(ItemType.UNREAD) val newItems = getMaxItemsForBackground(ItemType.UNREAD)
@@ -620,30 +614,30 @@ class Repository(
} }
fun setTagFilter(tag: SelfossModel.Tag?) { fun setTagFilter(tag: SelfossModel.Tag?) {
_tagFilter.value = tag tagFilterFlow.value = tag
} }
fun setSourceFilter(source: SelfossModel.Source?) { fun setSourceFilter(source: SelfossModel.Source?) {
_sourceFilter.value = source sourceFilterFlow.value = source
} }
fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) { fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) {
_readerItems = readerItems this.readerItems = readerItems
} }
fun getReaderItems(): ArrayList<SelfossModel.Item> = _readerItems fun getReaderItems(): ArrayList<SelfossModel.Item> = readerItems
fun migrate(driverFactory: DriverFactory) { fun migrate(driverFactory: DriverFactory) {
ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1) ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
} }
fun setSelectedSource(source: SelfossModel.SourceDetail) { fun setSelectedSource(source: SelfossModel.SourceDetail) {
_selectedSource = source selectedSource = source
} }
fun unsetSelectedSource() { fun unsetSelectedSource() {
_selectedSource = null selectedSource = null
} }
fun getSelectedSource(): SelfossModel.SourceDetail? = _selectedSource fun getSelectedSource(): SelfossModel.SourceDetail? = selectedSource
} }

View File

@@ -17,8 +17,8 @@ import kotlinx.serialization.json.Json
class MercuryApi { class MercuryApi {
var client = createHttpClient() var client = createHttpClient()
private fun createHttpClient(): HttpClient = private fun createHttpClient(): HttpClient {
HttpClient { return HttpClient {
install(HttpCache) install(HttpCache)
install(ContentNegotiation) { install(ContentNegotiation) {
json( json(
@@ -40,6 +40,7 @@ class MercuryApi {
} }
expectSuccess = false expectSuccess = false
} }
}
suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> = suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> =
bodyOrFailure( bodyOrFailure(

View File

@@ -33,7 +33,6 @@ suspend fun maybeResponse(r: HttpResponse?): SuccessResponse =
SuccessResponse(false) SuccessResponse(false)
} }
@Suppress("detekt:SwallowedException")
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> { suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> {
try { try {
return if (r != null && r.status.isSuccess()) { return if (r != null && r.status.isSuccess()) {

View File

@@ -1,5 +1,3 @@
@file:Suppress("detekt:TooManyFunctions", "detekt:LongParameterList", "detekt:LargeClass")
package bou.amine.apps.readerforselfossv2.rest package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
@@ -37,8 +35,6 @@ import kotlinx.serialization.json.Json
expect fun setupInsecureHttpEngine(config: CIOEngineConfig) expect fun setupInsecureHttpEngine(config: CIOEngineConfig)
private const val VERSION_WHERE_POST_LOGIN_SHOULD_WORK = 5
class SelfossApi( class SelfossApi(
private val appSettingsService: AppSettingsService, private val appSettingsService: AppSettingsService,
) { ) {
@@ -180,7 +176,7 @@ class SelfossApi(
}, },
) )
private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= VERSION_WHERE_POST_LOGIN_SHOULD_WORK // We are missing 4.1.0 private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
suspend fun logout(): SuccessResponse = suspend fun logout(): SuccessResponse =
if (shouldHaveNewLogout()) { if (shouldHaveNewLogout()) {

View File

@@ -1,5 +1,3 @@
@file:Suppress("detekt:TooManyFunctions")
package bou.amine.apps.readerforselfossv2.service package bou.amine.apps.readerforselfossv2.service
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings

View File

@@ -1,19 +1,7 @@
@file:Suppress("detekt:TooManyFunctions")
package bou.amine.apps.readerforselfossv2.service package bou.amine.apps.readerforselfossv2.service
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
private const val DEFAULT_FONT_SIZE = 16
private const val DEFAULT_REFRESH_MINUTES = 360L
private const val MIN_REFRESH_MINUTES = 15L
private const val DEFAULT_API_TIMEOUT = 60L
private const val DEFAULT_ITEMS_NUMBER = 20
class AppSettingsService( class AppSettingsService(
acraSenderServiceProcess: Boolean = false, acraSenderServiceProcess: Boolean = false,
) { ) {
@@ -48,7 +36,7 @@ class AppSettingsService(
private var notifyNewItems: Boolean? = null private var notifyNewItems: Boolean? = null
private var itemsNumber: Int? = null private var itemsNumber: Int? = null
private var apiTimeout: Long? = null private var apiTimeout: Long? = null
private var refreshMinutes: Long = DEFAULT_REFRESH_MINUTES private var refreshMinutes: Long = 360
private var markOnScroll: Boolean? = null private var markOnScroll: Boolean? = null
private var activeAlignment: Int? = null private var activeAlignment: Int? = null
@@ -153,14 +141,13 @@ class AppSettingsService(
return itemsNumber!! return itemsNumber!!
} }
@Suppress("detekt:SwallowedException")
private fun refreshItemsNumber() { private fun refreshItemsNumber() {
itemsNumber = itemsNumber =
try { try {
settings.getString(API_ITEMS_NUMBER, DEFAULT_ITEMS_NUMBER.toString()).toInt() settings.getString(API_ITEMS_NUMBER, "20").toInt()
} catch (e: Exception) { } catch (e: Exception) {
settings.remove(API_ITEMS_NUMBER) settings.remove(API_ITEMS_NUMBER)
DEFAULT_ITEMS_NUMBER 20
} }
} }
@@ -171,24 +158,22 @@ class AppSettingsService(
return apiTimeout!! return apiTimeout!!
} }
@Suppress("detekt:MagicNumber")
private fun secToMs(n: Long) = n * 1000 private fun secToMs(n: Long) = n * 1000
@Suppress("detekt:SwallowedException")
private fun refreshApiTimeout() { private fun refreshApiTimeout() {
apiTimeout = apiTimeout =
secToMs( secToMs(
try { try {
val settingsTimeout = settings.getString(API_TIMEOUT, DEFAULT_API_TIMEOUT.toString()) val settingsTimeout = settings.getString(API_TIMEOUT, "60")
if (settingsTimeout.toLong() > 0) { if (settingsTimeout.toLong() > 0) {
settingsTimeout.toLong() settingsTimeout.toLong()
} else { } else {
settings.remove(API_TIMEOUT) settings.remove(API_TIMEOUT)
DEFAULT_API_TIMEOUT 60
} }
} catch (e: Exception) { } catch (e: Exception) {
settings.remove(API_TIMEOUT) settings.remove(API_TIMEOUT)
DEFAULT_API_TIMEOUT 60
}, },
) )
} }
@@ -302,14 +287,14 @@ class AppSettingsService(
} }
private fun refreshRefreshMinutes() { private fun refreshRefreshMinutes() {
refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, DEFAULT_REFRESH_MINUTES.toString()).toLong() refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
if (refreshMinutes <= MIN_REFRESH_MINUTES) { if (refreshMinutes <= 15) {
refreshMinutes = MIN_REFRESH_MINUTES refreshMinutes = 15
} }
} }
fun getRefreshMinutes(): Long { fun getRefreshMinutes(): Long {
if (refreshMinutes != DEFAULT_REFRESH_MINUTES) { if (refreshMinutes != 360L) {
refreshRefreshMinutes() refreshRefreshMinutes()
} }
return refreshMinutes return refreshMinutes
@@ -383,7 +368,7 @@ class AppSettingsService(
if (fontSize != null) { if (fontSize != null) {
refreshFontSize() refreshFontSize()
} }
return fontSize ?: DEFAULT_FONT_SIZE return fontSize ?: 16
} }
private fun refreshStaticBarEnabled() { private fun refreshStaticBarEnabled() {
@@ -499,11 +484,11 @@ class AppSettingsService(
const val SOURCE_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform" const val SOURCE_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform"
const val BUG_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues" const val TRACKER_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
const val SYNC_CHANNEL_ID = "sync-channel-id" const val SYNC_CHANNEL_ID = "sync-channel-id"
const val NEW_ITEMS_CHANNEL = "new-items-channel-id" const val NEW_ITEMS_CHANNEL_ID = "new-items-channel-id"
const val JUSTIFY = 1 const val JUSTIFY = 1

View File

@@ -4,12 +4,6 @@ import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant import kotlinx.datetime.toInstant
class DateParseException(
message: String,
e: Throwable? = null,
) : Throwable(message, e)
@Suppress("detekt:ThrowsCount")
fun String.toParsedDate(): Long { fun String.toParsedDate(): Long {
// Possible formats are // Possible formats are
// yyyy-mm-dd hh:mm:ss format // yyyy-mm-dd hh:mm:ss format
@@ -23,22 +17,17 @@ fun String.toParsedDate(): Long {
if (this.matches(oldVersionFormat)) { if (this.matches(oldVersionFormat)) {
this.replace(" ", "T") this.replace(" ", "T")
} else if (this.matches(newVersionFormat)) { } else if (this.matches(newVersionFormat)) {
newVersionFormat newVersionFormat.find(this)?.groups?.get(1)?.value ?: throw Exception("Couldn't parse $this")
.find(this)
?.groups
?.get(1)
?.value ?: throw DateParseException("Couldn't parse $this")
} else { } else {
throw DateParseException("Unrecognized format for $this") throw Exception("Unrecognized format for $this")
} }
} catch (e: Exception) { } catch (e: Exception) {
throw DateParseException("parseDate failed for $this", e) throw Exception("parseDate failed for $this", e)
} }
return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
} }
@Suppress("detekt:UtilityClassWithPublicConstructor")
expect class DateUtils() { expect class DateUtils() {
companion object { companion object {
fun parseRelativeDate(dateString: String): String fun parseRelativeDate(dateString: String): String

View File

@@ -17,7 +17,7 @@ fun SOURCE.toView(): SelfossModel.SourceDetail =
this.id.toInt(), this.id.toInt(),
this.title, this.title,
null, null,
this.tags.split(","), this.tags?.split(","),
this.spout, this.spout,
this.error, this.error,
this.icon, this.icon,
@@ -74,7 +74,6 @@ fun SelfossModel.Item.toEntity(): ITEM =
this.author, this.author,
) )
@Suppress("detekt:MagicNumber")
fun SelfossModel.Tag.getColorHexCode(): String = fun SelfossModel.Tag.getColorHexCode(): String =
if (this.color.length == 4) { // #000 if (this.color.length == 4) { // #000
val char1 = this.color.get(1) val char1 = this.color.get(1)

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ 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()
@Suppress("detekt:MagicNumber")
fun String.longHash(): Long { fun String.longHash(): Long {
var h = 98764321261L var h = 98764321261L
val l = this.length val l = this.length

View File

@@ -18,8 +18,7 @@ class DatesTest {
fun new_version_date_should_be_parsed() { fun new_version_date_should_be_parsed() {
val date = newVersionDate.toParsedDate() val date = newVersionDate.toParsedDate()
val expected = val expected =
LocalDateTime(2013, 4, 7, 13, 43, 0, 0) LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(expected, date)
@@ -29,8 +28,7 @@ class DatesTest {
fun new_version_date2_should_be_parsed() { fun new_version_date2_should_be_parsed() {
val date = newVersionDate2.toParsedDate() val date = newVersionDate2.toParsedDate()
val expected = val expected =
LocalDateTime(2013, 4, 7, 13, 43, 0, 0) LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(expected, date)
@@ -40,8 +38,7 @@ class DatesTest {
fun old_version_date_should_be_parsed() { fun old_version_date_should_be_parsed() {
val date = oldVersionDate.toParsedDate() val date = oldVersionDate.toParsedDate()
val expected = val expected =
LocalDateTime(2013, 5, 7, 13, 46, 0, 0) LocalDateTime(2013, 5, 7, 13, 46, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(expected, date)
@@ -51,8 +48,7 @@ class DatesTest {
fun old_version_variant_date_should_be_parsed() { fun old_version_variant_date_should_be_parsed() {
val date = oldVersionDateVariant.toParsedDate() val date = oldVersionDateVariant.toParsedDate()
val expected = val expected =
LocalDateTime(2021, 3, 21, 10, 32, 0, 0) LocalDateTime(2021, 3, 21, 10, 32, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(expected, date)
@@ -62,8 +58,7 @@ class DatesTest {
fun new_version_variant_date_should_be_parsed() { fun new_version_variant_date_should_be_parsed() {
val date = newVersionDateVariant.toParsedDate() val date = newVersionDateVariant.toParsedDate()
val expected = val expected =
LocalDateTime(2022, 12, 24, 17, 0, 8, 0) LocalDateTime(2022, 12, 24, 17, 0, 8, 0).toInstant(TimeZone.currentSystemDefault())
.toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(expected, date)

View File

@@ -4,5 +4,7 @@ import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.native.NativeSqliteDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver
actual class DriverFactory { actual class DriverFactory {
actual fun createDriver(): SqlDriver = NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db") actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
}
} }

View File

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

View File

@@ -1,6 +1,5 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
@Suppress("detekt:UtilityClassWithPublicConstructor")
actual class DateUtils { actual class DateUtils {
actual companion object { actual companion object {
actual fun parseRelativeDate(dateString: String): String { actual fun parseRelativeDate(dateString: String): String {

View File

@@ -1,6 +1,5 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
@Suppress("detekt:UtilityClassWithPublicConstructor")
actual class DateUtils actual constructor() { actual class DateUtils actual constructor() {
actual companion object { actual companion object {
actual fun parseRelativeDate(dateString: String): String { actual fun parseRelativeDate(dateString: String): String {

View File

@@ -4,5 +4,7 @@ import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.native.NativeSqliteDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver
actual class DriverFactory { actual class DriverFactory {
actual fun createDriver(): SqlDriver = NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db") actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
}
} }

View File

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

View File

@@ -1,6 +1,5 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
@Suppress("detekt:UtilityClassWithPublicConstructor")
actual class DateUtils { actual class DateUtils {
actual companion object { actual companion object {
actual fun parseRelativeDate(dateString: String): String { actual fun parseRelativeDate(dateString: String): String {