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
87 changed files with 693 additions and 1621 deletions

View File

@@ -3,34 +3,25 @@ root = true
[*]
insert_final_newline = true
[.editorconfig]
insert_final_newline = false
ij_kotlin_line_break_after_multiline_when_entry = false
[{*.kt,*.kts}]
[*.{kt,kts}]
# Disable wildcard imports entirely
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
end_of_line = lf
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
ij_kotlin_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
indent_size = 4
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 = 4
ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = unset
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
ktlint_code_style = ktlint_official
ktlint_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_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2
ktlint_ignore_back_ticked_identifier = false
ktlint_property_naming_constant_naming = screaming_snake_case
max_line_length = 140
[**/build]
ktlint = disabled

View File

@@ -16,13 +16,13 @@ jobs:
java-version: '17'
cache: gradle
- 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
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...
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
- 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:
needs: Lint
uses: ./.gitea/workflows/common_build.yml

View File

@@ -1,38 +1,3 @@
**v125010241
- Merge pull request 'fix: Link not opening.' (#178) from fix-open-link into master
- refactor: context fragments issues.
- logs: Context issues.
- fix: Handle empty url issue, again.
- fix: Link not opening.
- Changelog for v125010201
--------------------------------------------------------------------
**v125010201
- fix: Handle empty url issue.
- Merge pull request 'Removed the floating bar.' (#177) from floating-bar into master
- chore: changing actions in reader fragment.
- Changelog for v125010131
--------------------------------------------------------------------
**v125010131
- fix: reload the adapter when it's needed. Fixes #128. (#176)
- feat: basic auth and images loading. Fixes #172. (#175)
- Changelog for v125010111
--------------------------------------------------------------------
**v125010111
- Debug trying to fix context issues. (#174)
- Changelog for v125010031
--------------------------------------------------------------------
**v125010031
- Merge pull request 'Bump dependencies' (#173) from upgarde into master

View File

@@ -156,7 +156,7 @@ dependencies {
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
// Themes
implementation("com.leinardi.android:speed-dial:3.3.0")
implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
// Pager
implementation("me.relex:circleindicator:2.1.6")

View File

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

View File

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

View File

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

View File

@@ -42,7 +42,6 @@ class SettingsActivityGeneralTest {
onView(withText(R.string.pref_header_general)).perform(click())
}
@Suppress("detekt:LongMethod")
@Test
fun testGeneral() {
onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
@@ -65,6 +64,19 @@ class SettingsActivityGeneralTest {
),
),
)
onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
matches(
allOf(
isDisplayed(),
not(isChecked()),
),
),
)
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
isEnabled(),
),
)
onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
matches(
@@ -106,7 +118,6 @@ class SettingsActivityGeneralTest {
)
}
@Suppress("detekt:ForbiddenComment")
@Test
fun testGeneralActionsNumberItems() {
onView(withText(R.string.pref_api_items_number_title)).perform(click())
@@ -148,6 +159,19 @@ class SettingsActivityGeneralTest {
@Test
fun testGeneralActionsCheckboxes() {
// article viewer settings
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
isEnabled(),
),
)
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
not(isEnabled()),
),
)
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled())))
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled()))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,9 +22,7 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
class ReaderActivity :
AppCompatActivity(),
DIAware {
class ReaderActivity : AppCompatActivity(), DIAware {
private var currentItem: Int = 0
private lateinit var toolbarMenu: Menu
@@ -53,7 +51,6 @@ class ReaderActivity :
showMenuItem(false)
}
@Suppress("detekt:SwallowedException")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityReaderBinding.inflate(layoutInflater)
@@ -102,9 +99,8 @@ class ReaderActivity :
oldInstanceState.clear()
}
private inner class ScreenSlidePagerAdapter(
fa: FragmentActivity,
) : FragmentStateAdapter(fa) {
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allItems.size
override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
@@ -113,8 +109,8 @@ class ReaderActivity :
override fun onKeyDown(
keyCode: Int,
event: KeyEvent?,
): Boolean =
when (keyCode) {
): Boolean {
return when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
val currentFragment =
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
@@ -133,6 +129,7 @@ class ReaderActivity :
super.onKeyDown(keyCode, event)
}
}
}
private fun alignmentMenu() {
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.instance
class SourcesActivity :
AppCompatActivity(),
DIAware {
class SourcesActivity : AppCompatActivity(), DIAware {
private lateinit var binding: ActivitySourcesBinding
override val di by closestDI()
@@ -70,12 +68,11 @@ class SourcesActivity :
binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
} else {
Toast
.makeText(
this@SourcesActivity,
R.string.cant_get_sources,
Toast.LENGTH_SHORT,
).show()
Toast.makeText(
this@SourcesActivity,
R.string.cant_get_sources,
Toast.LENGTH_SHORT,
).show()
}
CountingIdlingResourceSingleton.decrement()
}

View File

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

View File

@@ -118,18 +118,16 @@ class ItemCardAdapter(
binding.itemImage.setImageDrawable(null)
} else {
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()) {
binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else {
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage, appSettingsService)
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
}
}
}
inner class ViewHolder(
val binding: CardItemBinding,
) : RecyclerView.ViewHolder(binding.root)
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
}

View File

@@ -65,15 +65,13 @@ class ItemListAdapter(
if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else {
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService)
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
}
} else {
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService)
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage)
}
}
}
inner class ViewHolder(
val binding: ListItemBinding,
) : RecyclerView.ViewHolder(binding.root)
inner class ViewHolder(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.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers

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

View File

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

View File

@@ -2,14 +2,18 @@ package bou.amine.apps.readerforselfossv2.android.fragments
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.graphics.Bitmap
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.util.TypedValue
import android.util.TypedValue.DATA_NULL_UNDEFINED
import android.view.GestureDetector
import android.view.InflateException
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
@@ -19,6 +23,7 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import bou.amine.apps.readerforselfossv2.android.ImageActivity
import bou.amine.apps.readerforselfossv2.android.R
@@ -27,15 +32,10 @@ import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
import bou.amine.apps.readerforselfossv2.android.model.toModel
import bou.amine.apps.readerforselfossv2.android.model.toParcelable
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.addHomeMadeActionItem
import bou.amine.apps.readerforselfossv2.android.utils.getColorFromAttr
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.getGlideImageForResource
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.MercuryModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
@@ -46,7 +46,11 @@ import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getImages
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
import com.leinardi.android.speeddial.SpeedDialView
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.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -61,19 +65,9 @@ import java.util.Locale
import java.util.concurrent.ExecutionException
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
private const val DEFAULT_FONT_SIZE = 16
class ArticleFragment :
Fragment(),
DIAware {
private var colorOnSurface: Int = 0
private var colorSurface: Int = 0
private var fontSize: Int = DEFAULT_FONT_SIZE
class ArticleFragment : Fragment(), DIAware {
private var fontSize: Int = 16
private lateinit var item: SelfossModel.Item
private lateinit var url: String
private lateinit var contentText: String
@@ -81,7 +75,7 @@ class ArticleFragment :
private lateinit var contentImage: String
private lateinit var contentTitle: String
private lateinit var allImages: ArrayList<String>
private lateinit var fab: SpeedDialView
private lateinit var fab: FloatingActionButton
private lateinit var textAlignment: String
private lateinit var binding: FragmentArticleBinding
@@ -92,6 +86,7 @@ class ArticleFragment :
private var typeface: Typeface? = null
private var resId: Int = 0
private var font = ""
private var staticBar = false
private val mercuryApi: MercuryApi by instance()
@@ -103,7 +98,6 @@ class ArticleFragment :
item = pi.toModel()
}
@Suppress("detekt:LongMethod")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -118,9 +112,6 @@ class ArticleFragment :
e.sendSilentlyWithAcra()
}
colorOnSurface = getColorFromAttr(R.attr.colorOnSurface)
colorSurface = getColorFromAttr(R.attr.colorSurface)
contentText = item.content
contentTitle = item.title.getHtmlDecoded()
contentImage = item.getThumbnail(repository.baseUrl)
@@ -134,11 +125,23 @@ class ArticleFragment :
allImages = item.getImages()
fontSize = appSettingsService.getFontSize()
staticBar = appSettingsService.isStaticBarEnabled()
font = appSettingsService.getFont()
refreshAlignment()
handleFloatingToolbar()
fab = binding.fab
fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
fab.rippleColor = resources.getColor(R.color.colorAccentDark)
val floatingToolbar: FloatingToolbar = handleFloatingToolbar()
if (staticBar) {
fab.hide()
floatingToolbar.show()
}
binding.source.text = contentSource
if (typeface != null) {
@@ -146,20 +149,37 @@ class ArticleFragment :
}
handleContent()
binding.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
floatingToolbar.hide()
fab.hide()
} else {
if (staticBar) {
floatingToolbar.show()
} else {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
}
}
},
)
} catch (e: InflateException) {
e.sendSilentlyWithAcraWithName("webview not available")
maybeIfContext {
AlertDialog
.Builder(it)
.setMessage(it.getString(R.string.webview_dialog_issue_message))
.setTitle(it.getString(R.string.webview_dialog_issue_title))
try {
AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton(
android.R.string.ok,
) { _, _ ->
appSettingsService.disableArticleViewer()
requireActivity().finish()
}.create()
}
.create()
.show()
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
}
}
@@ -181,84 +201,70 @@ class ArticleFragment :
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding.imageView.visibility = View.VISIBLE
maybeIfContext { it.bitmapFitCenter(contentImage, binding.imageView, appSettingsService) }
Glide
.with(requireContext())
.asBitmap()
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else {
binding.imageView.visibility = View.GONE
}
}
}
private fun handleFloatingToolbar() {
fab = binding.speedDial
fab.mainFabClosedIconColor = colorOnSurface
fab.mainFabOpenedIconColor = colorOnSurface
maybeIfContext { handleFloatingToolbarActionItems(it) }
fab.setOnActionSelectedListener { actionItem ->
when (actionItem.id) {
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
R.id.unread_action ->
if (this@ArticleFragment.item.unread) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = false
maybeIfContext {
Toast
.makeText(
it,
R.string.marked_as_read,
Toast.LENGTH_LONG,
).show()
}
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = true
maybeIfContext {
Toast
.makeText(
it,
R.string.marked_as_unread,
Toast.LENGTH_LONG,
).show()
}
}
else -> Unit
}
false
private fun handleFloatingToolbar(): FloatingToolbar {
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
if (appSettingsService.getPublicAccess()) {
floatingToolbar.setMenu(R.menu.reader_toolbar_no_read)
}
}
floatingToolbar.attachFab(fab)
private fun handleFloatingToolbarActionItems(c: Context) {
fab.addHomeMadeActionItem(
R.id.share_action,
resources.getDrawable(R.drawable.ic_share_white_24dp),
R.string.reader_action_share,
colorOnSurface,
colorSurface,
c,
)
fab.addHomeMadeActionItem(
R.id.open_action,
resources.getDrawable(R.drawable.ic_open_in_browser_white_24dp),
R.string.reader_action_open,
colorOnSurface,
colorSurface,
c,
)
fab.addHomeMadeActionItem(
R.id.unread_action,
resources.getDrawable(R.drawable.ic_baseline_white_eye_24dp),
R.string.unmark,
colorOnSurface,
colorSurface,
c,
floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent))
floatingToolbar.setClickListener(
object : FloatingToolbar.ItemClickListener {
override fun onItemClick(item: MenuItem) {
when (item.itemId) {
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
R.id.unread_action ->
try {
if (this@ArticleFragment.item.unread) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = false
Toast.makeText(
requireContext(),
R.string.marked_as_read,
Toast.LENGTH_LONG,
).show()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.unmarkAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = true
Toast.makeText(
context,
R.string.marked_as_unread,
Toast.LENGTH_LONG,
).show()
}
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
}
else -> Unit
}
}
override fun onItemLongClick(item: MenuItem?) {
// We do nothing
}
},
)
return floatingToolbar
}
private fun refreshAlignment() {
@@ -270,7 +276,6 @@ class ArticleFragment :
}
}
@Suppress("detekt:SwallowedException")
private fun getContentFromMercury() {
binding.progressBar.visibility = View.VISIBLE
@@ -309,12 +314,17 @@ class ArticleFragment :
}
}
private fun handleLeadImage(leadImageUrl: String?) {
if (!leadImageUrl.isNullOrEmpty()) {
maybeIfContext {
binding.imageView.visibility = View.VISIBLE
it.bitmapFitCenter(leadImageUrl, binding.imageView, appSettingsService)
}
private fun handleLeadImage(lead_image_url: String?) {
if (!lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE
Glide
.with(requireContext())
.asBitmap()
.load(
lead_image_url,
)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else {
binding.imageView.visibility = View.GONE
}
@@ -327,85 +337,125 @@ class ArticleFragment :
override fun shouldOverrideUrlLoading(
view: WebView?,
url: String,
): Boolean =
if (url.isUrlValid() &&
): Boolean {
return if (context != null &&
url.isUrlValid() &&
binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) {
maybeIfContext { it.openUrlInBrowserAsNewTask(url) }
requireContext().openUrlInBrowser(url)
true
} else {
false
}
}
@Suppress("detekt:SwallowedException", "detekt:ReturnCount")
@Deprecated("Deprecated in Java")
override fun shouldInterceptRequest(
view: WebView,
url: String,
): WebResourceResponse? {
val (mime: String?, compression: Bitmap.CompressFormat) =
if (url
.lowercase(Locale.US)
.contains(".jpg") ||
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)
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US)
.contains(".jpeg")
) {
try {
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
.get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG),
)
} catch (e: ExecutionException) {
// Do nothing
}
} else if (url.lowercase(Locale.US).contains(".png")) {
try {
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
.get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.PNG),
)
} catch (e: ExecutionException) {
// Do nothing
}
} else if (url.lowercase(Locale.US).contains(".webp")) {
try {
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
.get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
)
} catch (e: ExecutionException) {
// Do nothing
}
try {
val image = view.getGlideImageForResource(url, appSettingsService)
return WebResourceResponse(
mime,
"UTF-8",
getBitmapInputStream(image, compression),
)
} catch (e: ExecutionException) {
return super.shouldInterceptRequest(view, url)
}
return super.shouldInterceptRequest(view, url)
}
}
}
@Suppress("detekt:LongMethod", "detekt:ImplicitDefaultLocale")
private fun htmlToWebview() {
maybeIfContext {
val context: Context
try {
context = requireContext()
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
return
}
val colorOnSurface = TypedValue()
val colorSurface = TypedValue()
try {
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = it.obtainStyledAttributes(resId, attrs)
val a: TypedArray = context.obtainStyledAttributes(resId, attrs)
binding.webcontent.settings.standardFontFamily = a.getString(0)
""
binding.webcontent.visibility = View.VISIBLE
context.theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
context.theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context issue when setting attributes, but context wasn't null before")
}
binding.webcontent.visibility = View.VISIBLE
val colorSurfaceString =
String.format(
"#%06X",
WHITE_COLOR_HEX and (if (colorSurface != DATA_NULL_UNDEFINED) colorSurface else WHITE_COLOR_HEX),
0xFFFFFF and (if (colorSurface.data != DATA_NULL_UNDEFINED) colorSurface.data else 0xFFFFFF),
)
val colorOnSurfaceString =
String.format(
"#%06X",
WHITE_COLOR_HEX and (if (colorOnSurface != DATA_NULL_UNDEFINED) colorOnSurface else 0),
0xFFFFFF and (if (colorOnSurface.data != DATA_NULL_UNDEFINED) colorOnSurface.data else 0),
)
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false
handleImageLoading()
try {
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false
handleImageLoading()
val gestureDetector =
GestureDetector(
activity,
object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean = performClick()
override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick()
}
},
)
@@ -414,50 +464,49 @@ class ArticleFragment :
event,
)
}
binding.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Gesture detector issue ?")
e.sendSilentlyWithAcraWithName("Context is null but wasn't, and that's causing issues with webview config")
return
}
binding.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
var baseUrl: String? = null
try {
val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) {
e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
}
var baseUrl: String? = null
try {
val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host
} catch (e: MalformedURLException) {
e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
}
val fontName: String =
maybeIfContext {
val fontName =
when (font) {
it.getString(R.string.open_sans_font_id) -> "Open Sans"
it.getString(R.string.roboto_font_id) -> "Roboto"
it.getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
getString(R.string.open_sans_font_id) -> "Open Sans"
getString(R.string.roboto_font_id) -> "Roboto"
getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
else -> ""
}
}?.toString().orEmpty()
val fontLinkAndStyle =
if (fontName.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${
fontName.replace(
" ",
"+",
)
}" rel="stylesheet">
val fontLinkAndStyle =
if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${
fontName.replace(
" ",
"+",
)
}" rel="stylesheet">
|<style>
| * {
| font-family: '$fontName';
| }
|</style>
""".trimMargin()
} else {
""
}
try {
""".trimMargin()
} else {
""
}
binding.webcontent.loadDataWithBaseURL(
baseUrl,
"""<html>
@@ -474,7 +523,7 @@ class ArticleFragment :
| color: ${
String.format(
"#%06X",
WHITE_COLOR_HEX and (maybeIfContext { it.resources.getColor(R.color.colorAccent) } as Int),
0xFFFFFF and context.resources.getColor(R.color.colorAccent),
)
} !important;
| }
@@ -531,8 +580,10 @@ class ArticleFragment :
private fun openInBrowserAfterFailing() {
binding.progressBar.visibility = View.GONE
maybeIfContext {
it.openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
try {
requireContext().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
}
}
@@ -549,8 +600,7 @@ class ArticleFragment :
}
fun performClick(): Boolean {
if (allImages != null &&
(
if (allImages != null && (
binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
)

View File

@@ -1,5 +1,6 @@
package bou.amine.apps.readerforselfossv2.android.fragments
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
@@ -15,13 +16,11 @@ import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget
import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext
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.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.ViewTarget
import com.bumptech.glide.request.transition.Transition
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.instance
private const val DRAWABLE_SIZE = 30
class FilterSheetFragment :
BottomSheetDialogFragment(),
DIAware {
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
private lateinit var binding: FilterFragmentBinding
override val di: DI by closestDI()
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
private var selectedChip: Chip? = null
@@ -60,8 +54,8 @@ class FilterSheetFragment :
try {
CoroutineScope(Dispatchers.Main).launch {
handleTagChips()
handleSourceChips()
handleTagChips(requireContext())
handleSourceChips(requireContext())
binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE
@@ -79,16 +73,16 @@ class FilterSheetFragment :
return binding.root
}
private suspend fun handleSourceChips() {
private suspend fun handleSourceChips(context: Context) {
val sourceGroup = binding.sourcesGroup
repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END
maybeIfContext {
it.imageIntoViewTarget(
source.getIcon(repository.baseUrl),
Glide.with(context)
.load(source.getIcon(repository.baseUrl))
.into(
object : ViewTarget<Chip?, Drawable?>(c) {
override fun onResourceReady(
resource: Drawable,
@@ -101,9 +95,7 @@ class FilterSheetFragment :
}
}
},
appSettingsService,
)
}
c.text = source.title.getHtmlDecoded()
@@ -139,7 +131,7 @@ class FilterSheetFragment :
}
}
private suspend fun handleTagChips() {
private suspend fun handleTagChips(context: Context) {
val tagGroup = binding.tagsGroup
val tags = repository.getTags()
@@ -161,8 +153,8 @@ class FilterSheetFragment :
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(DRAWABLE_SIZE, DRAWABLE_SIZE)
gd.cornerRadius = DRAWABLE_SIZE.toFloat()
gd.setSize(30, 30)
gd.cornerRadius = 30F
c.chipIcon = gd
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("tags > GradientDrawable")

View File

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

View File

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

View File

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

View File

@@ -2,12 +2,7 @@ package bou.amine.apps.readerforselfossv2.android.utils
import android.content.Context
import android.content.Intent
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.fragment.app.Fragment
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
fun Context.shareLink(
@@ -21,39 +16,9 @@ fun Context.shareLink(
sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
sendIntent.type = "text/plain"
startActivity(
Intent
.createChooser(
sendIntent,
getString(R.string.share),
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
Intent.createChooser(
sendIntent,
getString(R.string.share),
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
)
}
@ColorInt
fun Fragment.getColorFromAttr(
@AttrRes attrColor: Int,
resolveRefs: Boolean = true,
): Int {
val typedValue = TypedValue()
maybeIfContextWithLog { this.requireContext().theme.resolveAttribute(attrColor, typedValue, resolveRefs) }
return typedValue.data
}
@Suppress("detekt:SwallowedException")
fun Fragment.maybeIfContext(fn: (Context) -> Any): Any? {
try {
return fn(this.requireContext())
} catch (e: Exception) {
// Do nothing
return null
}
}
fun Fragment.maybeIfContextWithLog(fn: (Context) -> Any): Any? {
try {
return fn(this.requireContext())
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("Fragment context issue...")
return null
}
}

View File

@@ -59,5 +59,7 @@ class CircleImageView
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,12 +25,11 @@ fun Context.openItemUrl(
app: Activity,
) {
if (!linkDecoded.isUrlValid()) {
Toast
.makeText(
this,
this.getString(R.string.cant_open_invalid_url),
Toast.LENGTH_LONG,
).show()
Toast.makeText(
this,
this.getString(R.string.cant_open_invalid_url),
Toast.LENGTH_LONG,
).show()
} else {
if (articleViewer) {
val intent = Intent(this, ReaderActivity::class.java)
@@ -72,7 +71,6 @@ fun Context.openUrlInBrowser(url: String) {
this.mayBeStartActivity(intent)
}
@Suppress("detekt:SwallowedException")
fun Context.mayBeStartActivity(intent: Intent) {
try {
this.startActivity(intent)

View File

@@ -1,13 +1,6 @@
package bou.amine.apps.readerforselfossv2.android.utils.bottombar
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import bou.amine.apps.readerforselfossv2.android.R
import com.ashokvarma.bottomnavigation.TextBadgeItem
import com.leinardi.android.speeddial.SpeedDialActionItem
import com.leinardi.android.speeddial.SpeedDialView
fun TextBadgeItem.removeBadge(): TextBadgeItem {
this.setText("")
@@ -16,25 +9,3 @@ fun TextBadgeItem.removeBadge(): TextBadgeItem {
}
fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this
@Suppress("detekt:LongParameterList")
fun SpeedDialView.addHomeMadeActionItem(
@IdRes actionId: Int,
actionIcon: Drawable,
@StringRes labelId: Int,
colorOnSurface: Int,
colorSurface: Int,
context: Context,
) {
this.addActionItem(
SpeedDialActionItem
.Builder(actionId, actionIcon)
.setFabBackgroundColor(context.resources.getColor(R.color.colorAccent))
.setFabImageTintColor(colorOnSurface)
.setLabel(context.getString(labelId))
.setLabelClickable(false)
.setLabelBackgroundColor(colorOnSurface)
.setLabelColor(colorSurface)
.create(),
)
}

View File

@@ -2,135 +2,40 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.webkit.WebView
import android.widget.ImageView
import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
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.target.ViewTarget
import com.google.android.material.chip.Chip
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
private const val PRELOAD_IMAGE_TIMEOUT = 10000
@Suppress("detekt:ReturnCount")
@OptIn(ExperimentalEncodingApi::class)
fun String.toGlideUrl(appSettingsService: AppSettingsService): Any { // GlideUrl Or String
if (this.isEmptyOrNullOrNullString()) {
return ""
}
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(
url: String,
iv: ImageView,
appSettingsService: AppSettingsService,
) = Glide
.with(this)
) = Glide.with(this)
.asBitmap()
.load(url.toGlideUrl(appSettingsService))
.load(url)
.apply(RequestOptions.centerCropTransform())
.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(
url: String,
view: CircleImageView,
appSettingsService: AppSettingsService,
) {
view.textView.text = ""
Glide
.with(this)
.load(url.toGlideUrl(appSettingsService))
Glide.with(this)
.load(url)
.into(view.imageView)
}
private const val BITMAP_INPUT_STREAM_COMPRESSION_QUALITY = 80
fun getBitmapInputStream(
bitmap: Bitmap,
compressFormat: Bitmap.CompressFormat,
): InputStream {
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(compressFormat, BITMAP_INPUT_STREAM_COMPRESSION_QUALITY, byteArrayOutputStream)
bitmap.compress(compressFormat, 80, byteArrayOutputStream)
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
return ByteArrayInputStream(bitmapData)
}

View File

@@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.android.utils.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import com.google.android.material.snackbar.Snackbar
lateinit var s: Snackbar
@@ -10,13 +11,19 @@ lateinit var s: Snackbar
fun isNetworkAccessible(context: Context): Boolean {
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 {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
else -> false
return when {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
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.launch
class AppViewModel(
private val repository: Repository,
) : ViewModel() {
class AppViewModel(private val repository: Repository) : ViewModel() {
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
private var wasConnected = true

View File

@@ -71,13 +71,35 @@
</androidx.core.widget.NestedScrollView>
<com.leinardi.android.speeddial.SpeedDialView
android:id="@+id/speedDial"
android:layout_width="wrap_content"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
app:layout_behavior="@string/speeddial_scrolling_view_snackbar_behavior"
app:sdMainFabClosedSrc="@drawable/ic_add_white_24dp" />
android:layout_gravity="start|bottom|end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.github.rubensousa.floatingtoolbar.FloatingToolbar
android:id="@+id/floatingToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
app:floatingMenu="@menu/reader_toolbar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:src="@drawable/ic_add_white_24dp"
app:backgroundTint="?attr/colorAccent"
app:fabSize="mini"
app:rippleColor="?attr/colorAccentDark" />
</FrameLayout>
<FrameLayout
android:id="@+id/progressBar"
@@ -97,5 +119,4 @@
android:progressTint="?attr/colorAccent" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/unread_action"
android:icon="@drawable/ic_baseline_white_eye_24dp"
android:title="@string/unmark"
app:showAsAction="ifRoom" />
<item
android:id="@+id/open_action"
android:icon="@drawable/ic_open_in_browser_white_24dp"
android:title="@string/reader_action_open"
app:showAsAction="ifRoom" />
<item
android:id="@+id/share_action"
android:icon="@drawable/ic_share_white_24dp"
android:title="@string/reader_action_share"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Linksbündig</string>
<string name="reader_text_align_justify">Blocksatz</string>
<string name="settings_reader_font">Schriftgröße im Lesemodus</string>
<string name="reader_static_bar_title">Statische untere Leiste im Lesemodus</string>
<string name="reader_static_bar_on">Die untere Leiste wird dauerhaft angezeigt</string>
<string name="reader_static_bar_off">Die untere Leiste kann über einen schwebenden Button angezeigt werden</string>
<string name="remove_source">Quelle entfernen</string>
<string name="pref_theme_title">Heller/Dunkler Modus</string>
<string name="mode_dark">Dunkler Modus</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Alinear a la izquierda</string>
<string name="reader_text_align_justify">Justificado</string>
<string name="settings_reader_font">Modo lectura</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Aligner à gauche</string>
<string name="reader_text_align_justify">Justifier le texte</string>
<string name="settings_reader_font">Police du lecteur d\'articles</string>
<string name="reader_static_bar_title">Barre statique pour le visionneur d\'articles</string>
<string name="reader_static_bar_on">La barre sera affichée</string>
<string name="reader_static_bar_off">La barre sera affichée grâce au bouton</string>
<string name="remove_source">Supprimer la source</string>
<string name="pref_theme_title">Thème Clair/Sombre</string>
<string name="mode_dark">Thème sombre</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Aliñar á esquerda</string>
<string name="reader_text_align_justify">Xustificado</string>
<string name="settings_reader_font">Modo lector</string>
<string name="reader_static_bar_title">Barra inferior estática na vista de artigos</string>
<string name="reader_static_bar_on">A barra inferior mostrarase sempre</string>
<string name="reader_static_bar_off">A barra inferior pode mostrarse a través do botón flotante</string>
<string name="remove_source">Eliminar fonte</string>
<string name="pref_theme_title">Modo Claro/Escuro</string>
<string name="mode_dark">Modo escuro</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>

View File

@@ -23,7 +23,6 @@
<string name="wrong_infos">"Controleer de gegevens nogmaals."</string>
<string name="all_posts_not_read">"Fout bij markeren als gelezen"</string>
<string name="all_posts_read">"Alle artikelen gemarkeerd als gelezen"</string>
<string name="undo_string">"Ongedaan maken"</string>
<string name="addStringNoUrl">"Login om bronnen toe te voegen"</string>
<string name="cant_get_sources">"Kan de lijst met bronnen niet ophalen"</string>
<string name="cant_create_source">"Kan bron niet creëeren"</string>
@@ -106,6 +105,9 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>
@@ -129,4 +131,5 @@
<string name="action_about">"Over"</string>
<string name="marked_as_read">"Artikel gelezen"</string>
<string name="marked_as_unread">"Item unread"</string>
<string name="undo_string">"Ongedaan maken"</string>
</resources>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">左对齐</string>
<string name="reader_text_align_justify">左右对齐</string>
<string name="settings_reader_font">阅读器字体</string>
<string name="reader_static_bar_title">文章查看器中的静态底部栏</string>
<string name="reader_static_bar_on">底部栏将始终显示</string>
<string name="reader_static_bar_off">底部栏可以通过浮动按钮显示</string>
<string name="remove_source">删除源</string>
<string name="pref_theme_title">浅色/深色模式</string>
<string name="mode_dark">深色模式</string>

View File

@@ -106,6 +106,9 @@
<string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="unread_action" type="id" />
<item name="open_action" type="id" />
<item name="share_action" type="id" />
</resources>

View File

@@ -108,6 +108,9 @@
<string name="source_code_pro_font_id" translatable="false">source_code_pro_medium</string>
<string name="open_sans_font_id" translatable="false">open_sans</string>
<string name="roboto_font_id" translatable="false">roboto</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string>

View File

@@ -30,6 +30,14 @@
android:summaryOn="@string/pref_article_viewer_on"
android:title="@string/pref_article_viewer_title"
app:iconSpaceReserved="false"/>
<SwitchPreference
android:defaultValue="false"
android:dependency="prefer_article_viewer"
android:key="reader_static_bar"
android:summaryOff="@string/reader_static_bar_off"
android:summaryOn="@string/reader_static_bar_on"
android:title="@string/reader_static_bar_title"
app:iconSpaceReserved="false"/>
<PreferenceCategory
android:title="@string/pref_general_category_displaying">

View File

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

View File

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

View File

@@ -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

@@ -1,5 +0,0 @@
**v125010131**
- fix: reload the adapter when it's needed. Fixes #128. (#176)
- feat: basic auth and images loading. Fixes #172. (#175)
- Changelog for v125010111

View File

@@ -1,6 +0,0 @@
**v125010201**
- fix: Handle empty url issue.
- Merge pull request 'Removed the floating bar.' (#177) from floating-bar into master
- chore: changing actions in reader fragment.
- Changelog for v125010131

View File

@@ -1,8 +0,0 @@
**v125010241**
- Merge pull request 'fix: Link not opening.' (#178) from fix-open-link into master
- refactor: context fragments issues.
- logs: Context issues.
- fix: Handle empty url issue, again.
- fix: Link not opening.
- Changelog for v125010201

View File

@@ -18,7 +18,7 @@ kotlin.code.style=official
#Android
android.useAndroidX=true
#android.nonTransitiveRClass=true
android.enableJetifier=false
android.enableJetifier=true
android.nonTransitiveRClass=false
#MPP
kotlin.mpp.enableCInteropCommonization=true

View File

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

View File

@@ -9,14 +9,12 @@ class NaiveTrustManager : X509TrustManager {
chain: Array<out X509Certificate>?,
authType: String?,
) {
// Nothing
}
override fun checkServerTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
) {
// Nothing
}
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 kotlinx.datetime.Clock
@Suppress("detekt:UtilityClassWithPublicConstructor")
actual class DateUtils {
actual companion object {
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)
val IMAGE_EXTENSION_REGEXP = """\.(jpg|jpeg|png|webp)""".toRegex()
actual fun SelfossModel.Item.getImages(): ArrayList<String> {
val allImages = ArrayList<String>()
for (image in Jsoup.parse(content).getElementsByTag("img")) {
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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,6 @@ suspend fun maybeResponse(r: HttpResponse?): SuccessResponse =
SuccessResponse(false)
}
@Suppress("detekt:SwallowedException")
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> {
try {
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
import bou.amine.apps.readerforselfossv2.model.SelfossModel
@@ -37,8 +35,6 @@ import kotlinx.serialization.json.Json
expect fun setupInsecureHttpEngine(config: CIOEngineConfig)
private const val VERSION_WHERE_POST_LOGIN_SHOULD_WORK = 5
class SelfossApi(
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 =
if (shouldHaveNewLogout()) {

View File

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

View File

@@ -1,19 +1,7 @@
@file:Suppress("detekt:TooManyFunctions")
package bou.amine.apps.readerforselfossv2.service
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(
acraSenderServiceProcess: Boolean = false,
) {
@@ -48,11 +36,12 @@ class AppSettingsService(
private var notifyNewItems: Boolean? = null
private var itemsNumber: Int? = 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 activeAlignment: Int? = null
private var fontSize: Int? = null
private var staticBar: Boolean? = null
private var font: String = ""
private var theme: Int? = null
@@ -152,14 +141,13 @@ class AppSettingsService(
return itemsNumber!!
}
@Suppress("detekt:SwallowedException")
private fun refreshItemsNumber() {
itemsNumber =
try {
settings.getString(API_ITEMS_NUMBER, DEFAULT_ITEMS_NUMBER.toString()).toInt()
settings.getString(API_ITEMS_NUMBER, "20").toInt()
} catch (e: Exception) {
settings.remove(API_ITEMS_NUMBER)
DEFAULT_ITEMS_NUMBER
20
}
}
@@ -170,24 +158,22 @@ class AppSettingsService(
return apiTimeout!!
}
@Suppress("detekt:MagicNumber")
private fun secToMs(n: Long) = n * 1000
@Suppress("detekt:SwallowedException")
private fun refreshApiTimeout() {
apiTimeout =
secToMs(
try {
val settingsTimeout = settings.getString(API_TIMEOUT, DEFAULT_API_TIMEOUT.toString())
val settingsTimeout = settings.getString(API_TIMEOUT, "60")
if (settingsTimeout.toLong() > 0) {
settingsTimeout.toLong()
} else {
settings.remove(API_TIMEOUT)
DEFAULT_API_TIMEOUT
60
}
} catch (e: Exception) {
settings.remove(API_TIMEOUT)
DEFAULT_API_TIMEOUT
60
},
)
}
@@ -301,14 +287,14 @@ class AppSettingsService(
}
private fun refreshRefreshMinutes() {
refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, DEFAULT_REFRESH_MINUTES.toString()).toLong()
if (refreshMinutes <= MIN_REFRESH_MINUTES) {
refreshMinutes = MIN_REFRESH_MINUTES
refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
if (refreshMinutes <= 15) {
refreshMinutes = 15
}
}
fun getRefreshMinutes(): Long {
if (refreshMinutes != DEFAULT_REFRESH_MINUTES) {
if (refreshMinutes != 360L) {
refreshRefreshMinutes()
}
return refreshMinutes
@@ -382,7 +368,18 @@ class AppSettingsService(
if (fontSize != null) {
refreshFontSize()
}
return fontSize ?: DEFAULT_FONT_SIZE
return fontSize ?: 16
}
private fun refreshStaticBarEnabled() {
staticBar = settings.getBoolean(READER_STATIC_BAR, false)
}
fun isStaticBarEnabled(): Boolean {
if (staticBar != null) {
refreshStaticBarEnabled()
}
return staticBar == true
}
private fun refreshFont() {
@@ -437,6 +434,7 @@ class AppSettingsService(
refreshActiveAllignment()
refreshFontSize()
refreshFont()
refreshStaticBarEnabled()
refreshCurrentTheme()
}
@@ -486,11 +484,11 @@ class AppSettingsService(
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 NEW_ITEMS_CHANNEL = "new-items-channel-id"
const val NEW_ITEMS_CHANNEL_ID = "new-items-channel-id"
const val JUSTIFY = 1
@@ -534,6 +532,8 @@ class AppSettingsService(
const val READER_FONT = "reader_font"
const val READER_STATIC_BAR = "reader_static_bar"
const val READER_FONT_SIZE = "reader_font_size"
const val TEXT_ALIGN = "text_align"

View File

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

View File

@@ -17,7 +17,7 @@ fun SOURCE.toView(): SelfossModel.SourceDetail =
this.id.toInt(),
this.title,
null,
this.tags.split(","),
this.tags?.split(","),
this.spout,
this.error,
this.icon,
@@ -74,7 +74,6 @@ fun SelfossModel.Item.toEntity(): ITEM =
this.author,
)
@Suppress("detekt:MagicNumber")
fun SelfossModel.Tag.getColorHexCode(): String =
if (this.color.length == 4) { // #000
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()
@Suppress("detekt:MagicNumber")
fun String.longHash(): Long {
var h = 98764321261L
val l = this.length

View File

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

View File

@@ -4,5 +4,7 @@ import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.native.NativeSqliteDriver
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
@Suppress("detekt:EmptyFunctionBlock")
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
}

View File

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

View File

@@ -1,6 +1,5 @@
package bou.amine.apps.readerforselfossv2.utils
@Suppress("detekt:UtilityClassWithPublicConstructor")
actual class DateUtils actual constructor() {
actual companion object {
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
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
@Suppress("detekt:EmptyFunctionBlock")
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
}

View File

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