chore: lint cleaning.

This commit is contained in:
aminecmi 2023-10-12 20:25:24 +02:00
parent f101d22f54
commit 137580ccf9
59 changed files with 1820 additions and 1376 deletions

View File

@ -13,10 +13,10 @@ steps:
- 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 - curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip && unzip detekt-cli-1.23.1.zip
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Linting..." - echo "Linting..."
- ktlint || true - ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' || true
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Detecting..." - echo "Detecting..."
- ./detekt-cli-1.23.1/bin/detekt-cli --all-rules || true - ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
command_timeout: 1m command_timeout: 1m
- name: BuildAndTest - name: BuildAndTest

View File

@ -6,4 +6,4 @@ import org.acra.ktx.sendSilentlyWithAcra
fun Throwable.sendSilentlyWithAcraWithName(name: String) { fun Throwable.sendSilentlyWithAcraWithName(name: String) {
ACRA.errorReporter.putCustomData("error_source", name) ACRA.errorReporter.putCustomData("error_source", name)
this.sendSilentlyWithAcra() this.sendSilentlyWithAcra()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,4 +26,4 @@ fun isNetworkAccessible(context: Context): Boolean {
val network = connectivityManager.activeNetworkInfo ?: return false val network = connectivityManager.activeNetworkInfo ?: return false
return network.isConnectedOrConnecting return network.isConnectedOrConnecting
} }
} }

View File

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

View File

@ -8,11 +8,10 @@ import kotlinx.datetime.toInstant
import org.junit.Test import org.junit.Test
class DatesTest { class DatesTest {
private val newVersionDateVariant = "2022-12-24T17:00:08+00"
private val newVersionDateVariant = "2022-12-24T17:00:08+00" private val newVersionDate = "2013-04-07T13:43:00+01:00"
private val newVersionDate = "2013-04-07T13:43:00+01:00" private val oldVersionDate = "2013-05-07 13:46:00"
private val oldVersionDate = "2013-05-07 13:46:00" private val oldVersionDateVariant = "2021-03-21 10:32:00.000000"
private val oldVersionDateVariant = "2021-03-21 10:32:00.000000"
@Test @Test
fun new_version_date_should_be_parsed() { fun new_version_date_should_be_parsed() {
@ -53,5 +52,4 @@ class DatesTest {
assertEquals(expected, date) assertEquals(expected, date)
} }
} }

View File

@ -42,11 +42,11 @@ class RepositoryTest {
private val NUMBER_STARRED = 20 private val NUMBER_STARRED = 20
private lateinit var repository: Repository private lateinit var repository: Repository
private fun initializeRepository( private fun initializeRepository(
isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow( isConnectionAvailable: MutableStateFlow<Boolean> =
true MutableStateFlow(
) true,
),
) { ) {
repository = Repository(api, appSettingsService, isConnectionAvailable, db) repository = Repository(api, appSettingsService, isConnectionAvailable, db)
@ -64,14 +64,16 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.apiInformation() } returns StatusAndData( coEvery { api.apiInformation() } returns
success = true, StatusAndData(
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)) success = true,
) data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)),
coEvery { api.stats() } returns StatusAndData( )
success = true, coEvery { api.stats() } returns
data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED) StatusAndData(
) success = true,
data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED),
)
every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit
every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems() every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems()
@ -101,7 +103,7 @@ class RepositoryTest {
fun get_api_4_date_with_api_1_version_stored() { fun get_api_4_date_with_api_1_version_stored() {
every { appSettingsService.getApiVersion() } returns 1 every { appSettingsService.getApiVersion() } returns 1
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
every { appSettingsService.updateApiVersion(any()) } returns Unit every { appSettingsService.updateApiVersion(any()) } returns Unit
initializeRepository() initializeRepository()
@ -116,10 +118,11 @@ class RepositoryTest {
@Test @Test
fun get_public_access() { fun get_public_access() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns StatusAndData( coEvery { api.apiInformation() } returns
success = true, StatusAndData(
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)) success = true,
) data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)),
)
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
initializeRepository() initializeRepository()
@ -131,10 +134,11 @@ class RepositoryTest {
@Test @Test
fun get_public_access_username_not_empty() { fun get_public_access_username_not_empty() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns StatusAndData( coEvery { api.apiInformation() } returns
success = true, StatusAndData(
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)) success = true,
) data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, true)),
)
every { appSettingsService.getUserName() } returns "username" every { appSettingsService.getUserName() } returns "username"
initializeRepository() initializeRepository()
@ -146,10 +150,11 @@ class RepositoryTest {
@Test @Test
fun get_public_access_no_auth() { fun get_public_access_no_auth() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns StatusAndData( coEvery { api.apiInformation() } returns
success = true, StatusAndData(
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, false)) success = true,
) data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(true, false)),
)
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
initializeRepository() initializeRepository()
@ -161,10 +166,11 @@ class RepositoryTest {
@Test @Test
fun get_public_access_disabled() { fun get_public_access_disabled() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns StatusAndData( coEvery { api.apiInformation() } returns
success = true, StatusAndData(
data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)) success = true,
) data = SelfossModel.ApiInformation("2.19-ba1e8e3", "4.0.0", SelfossModel.ApiConfiguration(false, true)),
)
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
initializeRepository() initializeRepository()
@ -180,10 +186,10 @@ class RepositoryTest {
val itemParameters = FakeItemParameters() val itemParameters = FakeItemParameters()
itemParameters.datetime = "2021-04-23 11:45:32" itemParameters.datetime = "2021-04-23 11:45:32"
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = generateTestApiItem(itemParameters) data = generateTestApiItem(itemParameters),
) )
initializeRepository() initializeRepository()
runBlocking { runBlocking {
@ -196,7 +202,7 @@ class RepositoryTest {
@Test @Test
fun get_newer_items() { fun get_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
runBlocking { runBlocking {
@ -211,7 +217,7 @@ class RepositoryTest {
@Test @Test
fun get_all_newer_items() { fun get_all_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.ALL repository.displayedItems = ItemType.ALL
@ -227,7 +233,7 @@ class RepositoryTest {
@Test @Test
fun get_newer_starred_items() { fun get_newer_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.STARRED repository.displayedItems = ItemType.STARRED
@ -264,10 +270,10 @@ class RepositoryTest {
itemParameter3.tags = "Other, Tag" itemParameter3.tags = "Other, Tag"
itemParameter3.id = "3" itemParameter3.id = "3"
coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems( coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
itemParameter1 itemParameter1,
) + ) +
generateTestDBItems(itemParameter2) + generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3) generateTestDBItems(itemParameter3)
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -292,24 +298,26 @@ class RepositoryTest {
itemParameter3.sourcetitle = "Other" itemParameter3.sourcetitle = "Other"
itemParameter3.id = "3" itemParameter3.id = "3"
coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems( coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
itemParameter1 itemParameter1,
) + ) +
generateTestDBItems(itemParameter2) + generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3) generateTestDBItems(itemParameter3)
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
repository.setSourceFilter(SelfossModel.SourceDetail( repository.setSourceFilter(
1, SelfossModel.SourceDetail(
"Test", 1,
null, "Test",
listOf("tags"), null,
SPOUT, listOf("tags"),
"", SPOUT,
IMAGE_URL, "",
SelfossModel.SourceParams("url") IMAGE_URL,
)) SelfossModel.SourceParams("url"),
),
)
runBlocking { runBlocking {
repository.getNewerItems() repository.getNewerItems()
} }
@ -322,7 +330,7 @@ class RepositoryTest {
@Test @Test
fun get_older_items() { fun get_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.items = ArrayList(generateTestApiItem()) repository.items = ArrayList(generateTestApiItem())
@ -338,7 +346,7 @@ class RepositoryTest {
@Test @Test
fun get_all_older_items() { fun get_all_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.items = ArrayList(generateTestApiItem()) repository.items = ArrayList(generateTestApiItem())
@ -355,7 +363,7 @@ class RepositoryTest {
@Test @Test
fun get_older_starred_items() { fun get_older_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.STARRED repository.displayedItems = ItemType.STARRED
@ -592,14 +600,16 @@ class RepositoryTest {
} }
private fun prepareTags(): Pair<List<SelfossModel.Tag>, List<TAG>> { private fun prepareTags(): Pair<List<SelfossModel.Tag>, List<TAG>> {
val tags = listOf( val tags =
SelfossModel.Tag("test", "red", 6), listOf(
SelfossModel.Tag("second", "yellow", 0) SelfossModel.Tag("test", "red", 6),
) SelfossModel.Tag("second", "yellow", 0),
val tagsDB = listOf( )
TAG("test_DB", "red", 6), val tagsDB =
TAG("second_DB", "yellow", 0) listOf(
) TAG("test_DB", "red", 6),
TAG("second_DB", "yellow", 0),
)
coEvery { api.tags() } returns StatusAndData(success = true, data = tags) coEvery { api.tags() } returns StatusAndData(success = true, data = tags)
coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB coEvery { db.tagsQueries.tags().executeAsList() } returns tagsDB
@ -621,48 +631,50 @@ class RepositoryTest {
} }
private fun prepareSources(): Pair<ArrayList<SelfossModel.SourceDetail>, List<SOURCE>> { private fun prepareSources(): Pair<ArrayList<SelfossModel.SourceDetail>, List<SOURCE>> {
val sources = arrayListOf( val sources =
SelfossModel.SourceDetail( arrayListOf(
1, SelfossModel.SourceDetail(
"First source", 1,
null, "First source",
listOf("Test", "second"), null,
SPOUT, listOf("Test", "second"),
"", SPOUT,
IMAGE_URL_2, "",
SelfossModel.SourceParams("url") IMAGE_URL_2,
), SelfossModel.SourceParams("url"),
SelfossModel.SourceDetail( ),
2, SelfossModel.SourceDetail(
"Second source", 2,
null, "Second source",
listOf("second"), null,
SPOUT, listOf("second"),
"", SPOUT,
IMAGE_URL, "",
SelfossModel.SourceParams("url") IMAGE_URL,
SelfossModel.SourceParams("url"),
),
) )
) val sourcesDB =
val sourcesDB = listOf( listOf(
SOURCE( SOURCE(
"1", "1",
"First DB source", "First DB source",
"Test,second", "Test,second",
SPOUT, SPOUT,
"", "",
IMAGE_URL_2, IMAGE_URL_2,
"url" "url",
), ),
SOURCE( SOURCE(
"2", "2",
"Second source", "Second source",
"second", "second",
SPOUT, SPOUT,
"", "",
IMAGE_URL, IMAGE_URL,
"url" "url",
),
) )
)
coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources) coEvery { api.sourcesDetailed() } returns StatusAndData(success = true, data = sources)
every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB every { db.sourcesQueries.sources().executeAsList() } returns sourcesDB
@ -791,17 +803,18 @@ class RepositoryTest {
@Test @Test
fun create_source() { fun create_source() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.createSource( response =
"test", repository.createSource(
FEED_URL, "test",
SPOUT, FEED_URL,
TAGS, SPOUT,
) TAGS,
)
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
@ -818,17 +831,18 @@ class RepositoryTest {
@Test @Test
fun create_source_but_response_fails() { fun create_source_but_response_fails() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(false) SuccessResponse(false)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.createSource( response =
"test", repository.createSource(
FEED_URL, "test",
SPOUT, FEED_URL,
TAGS SPOUT,
) TAGS,
)
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
@ -836,7 +850,7 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any() any(),
) )
} }
assertSame(false, response) assertSame(false, response)
@ -845,17 +859,18 @@ class RepositoryTest {
@Test @Test
fun create_source_without_connection() { fun create_source_without_connection() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.createSource( response =
"test", repository.createSource(
FEED_URL, "test",
SPOUT, FEED_URL,
TAGS SPOUT,
) TAGS,
)
} }
coVerify(exactly = 0) { coVerify(exactly = 0) {
@ -916,10 +931,11 @@ class RepositoryTest {
@Test @Test
fun update_remote() { fun update_remote() {
coEvery { api.update() } returns StatusAndData( coEvery { api.update() } returns
success = true, StatusAndData(
data = "finished" success = true,
) data = "finished",
)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -933,10 +949,11 @@ class RepositoryTest {
@Test @Test
fun update_remote_but_response_fails() { fun update_remote_but_response_fails() {
coEvery { api.update() } returns StatusAndData( coEvery { api.update() } returns
success = false, StatusAndData(
data = "unallowed access" success = false,
) data = "unallowed access",
)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -950,10 +967,11 @@ class RepositoryTest {
@Test @Test
fun update_remote_with_unallowed_access() { fun update_remote_with_unallowed_access() {
coEvery { api.update() } returns StatusAndData( coEvery { api.update() } returns
success = true, StatusAndData(
data = "unallowed access" success = true,
) data = "unallowed access",
)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -967,10 +985,11 @@ class RepositoryTest {
@Test @Test
fun update_remote_without_connection() { fun update_remote_without_connection() {
coEvery { api.update() } returns StatusAndData( coEvery { api.update() } returns
success = true, StatusAndData(
data = "undocumented..." success = true,
) data = "undocumented...",
)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
@ -1037,7 +1056,7 @@ class RepositoryTest {
appSettingsService.refreshLoginInformation( appSettingsService.refreshLoginInformation(
BASE_URL, BASE_URL,
"login", "login",
"password" "password",
) )
} }
} }
@ -1057,13 +1076,14 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any() any(),
)
} returnsMany
listOf(
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
) )
} returnsMany listOf(
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
)
initializeRepository() initializeRepository()
prepareSearch() prepareSearch()
@ -1077,7 +1097,7 @@ class RepositoryTest {
@Test @Test
fun cache_items_but_response_fails() { fun cache_items_but_response_fails() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = false, data = generateTestApiItem()) StatusAndData(success = false, data = generateTestApiItem())
initializeRepository() initializeRepository()
prepareSearch() prepareSearch()
@ -1091,7 +1111,7 @@ class RepositoryTest {
@Test @Test
fun cache_items_without_connection() { fun cache_items_without_connection() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = false, data = generateTestApiItem()) StatusAndData(success = false, data = generateTestApiItem())
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
prepareSearch() prepareSearch()
@ -1113,9 +1133,9 @@ class RepositoryTest {
SPOUT, SPOUT,
"", "",
IMAGE_URL_2, IMAGE_URL_2,
SelfossModel.SourceParams("url") SelfossModel.SourceParams("url"),
) ),
) )
repository.searchFilter = "search" repository.searchFilter = "search"
} }
} }

View File

@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.repository
import bou.amine.apps.readerforselfossv2.dao.ITEM import bou.amine.apps.readerforselfossv2.dao.ITEM
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> { fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> {
return listOf( return listOf(
ITEM( ITEM(
@ -18,8 +17,8 @@ fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<I
link = item.link, link = item.link,
sourcetitle = item.sourcetitle, sourcetitle = item.sourcetitle,
tags = item.tags, tags = item.tags,
author = item.author author = item.author,
) ),
) )
} }
@ -37,8 +36,8 @@ fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<S
link = item.link, link = item.link,
sourcetitle = item.sourcetitle, sourcetitle = item.sourcetitle,
tags = item.tags.split(','), tags = item.tags.split(','),
author = item.author author = item.author,
) ),
) )
} }
@ -57,4 +56,4 @@ class FakeItemParameters {
var sourcetitle = "La Chimica e la Società" var sourcetitle = "La Chimica e la Società"
var tags = "Chimica, Testing" var tags = "Chimica, Testing"
var author = "Someone important" var author = "Someone important"
} }

View File

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

View File

@ -5,13 +5,19 @@ import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
class NaiveTrustManager : X509TrustManager { class NaiveTrustManager : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {} override fun checkClientTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
) {}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {} override fun checkServerTrusted(
chain: Array<out X509Certificate>?,
authType: String?,
) {}
override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf() override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
} }
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) { actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
config.https.trustManager = NaiveTrustManager() config.https.trustManager = NaiveTrustManager()
} }

View File

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

View File

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

View File

@ -2,7 +2,6 @@ package bou.amine.apps.readerforselfossv2.DI
import bou.amine.apps.readerforselfossv2.rest.MercuryApi import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.bind import org.kodein.di.bind
import org.kodein.di.instance import org.kodein.di.instance
@ -11,4 +10,4 @@ import org.kodein.di.singleton
val networkModule by DI.Module { val networkModule by DI.Module {
bind<SelfossApi>() with singleton { SelfossApi(instance()) } bind<SelfossApi>() with singleton { SelfossApi(instance()) }
bind<MercuryApi>() with singleton { MercuryApi() } bind<MercuryApi>() with singleton { MercuryApi() }
} }

View File

@ -4,4 +4,4 @@ import com.squareup.sqldelight.db.SqlDriver
expect class DriverFactory { expect class DriverFactory {
fun createDriver(): SqlDriver fun createDriver(): SqlDriver
} }

View File

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

View File

@ -1,3 +1,3 @@
package bou.amine.apps.readerforselfossv2.model package bou.amine.apps.readerforselfossv2.model
class NetworkUnavailableException : Exception() class NetworkUnavailableException : Exception()

View File

@ -18,4 +18,4 @@ class StatusAndData<T>(val success: Boolean, val data: T? = null) {
return StatusAndData(false) return StatusAndData(false)
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -36,8 +36,8 @@ import kotlinx.serialization.json.Json
expect fun setupInsecureHTTPEngine(config: CIOEngineConfig) expect fun setupInsecureHTTPEngine(config: CIOEngineConfig)
class SelfossApi(private val appSettingsService: AppSettingsService) { class SelfossApi(private val appSettingsService: AppSettingsService) {
var client = createHttpClient() var client = createHttpClient()
fun createHttpClient() = fun createHttpClient() =
HttpClient(CIO) { HttpClient(CIO) {
if (appSettingsService.getSelfSigned()) { if (appSettingsService.getSelfSigned()) {
@ -47,19 +47,22 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
install(ContentNegotiation) { install(ContentNegotiation) {
install(HttpCache) install(HttpCache)
json(Json { json(
prettyPrint = true Json {
isLenient = true prettyPrint = true
ignoreUnknownKeys = true isLenient = true
explicitNulls = false ignoreUnknownKeys = true
}) explicitNulls = false
},
)
} }
install(Logging) { install(Logging) {
logger = object : Logger { logger =
override fun log(message: String) { object : Logger {
Napier.d(message, tag = "LogApiCalls") override fun log(message: String) {
Napier.d(message, tag = "LogApiCalls")
}
} }
}
level = LogLevel.INFO level = LogLevel.INFO
} }
install(HttpTimeout) { install(HttpTimeout) {
@ -83,8 +86,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
expectSuccess = false expectSuccess = false
} }
fun url(path: String) = fun url(path: String) = "${appSettingsService.getBaseUrl()}$path"
"${appSettingsService.getBaseUrl()}$path"
fun refreshLoginInformation() { fun refreshLoginInformation() {
appSettingsService.refreshApiSettings() appSettingsService.refreshApiSettings()
@ -100,12 +102,15 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
// Api version was introduces after the POST login, so when there is a version, it should be available // Api version was introduces after the POST login, so when there is a version, it should be available
private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1 private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
private fun hasLoginInfo() = private fun hasLoginInfo() =
appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword() appSettingsService.getUserName().isNotEmpty() &&
.isNotEmpty() appSettingsService.getPassword()
.isNotEmpty()
suspend fun login(): SuccessResponse = suspend fun login(): SuccessResponse =
if (appSettingsService.getUserName().isNotEmpty() && appSettingsService.getPassword() if (appSettingsService.getUserName().isNotEmpty() &&
appSettingsService.getPassword()
.isNotEmpty() .isNotEmpty()
) { ) {
if (shouldHavePostLogin()) { if (shouldHavePostLogin()) {
@ -117,30 +122,49 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
SuccessResponse(true) SuccessResponse(true)
} }
private suspend fun getLogin() = maybeResponse(client.tryToGet(url("/login")) { private suspend fun getLogin() =
parameter("username", appSettingsService.getUserName()) maybeResponse(
parameter("password", appSettingsService.getPassword()) client.tryToGet(url("/login")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { parameter("username", appSettingsService.getUserName())
headers { parameter("password", appSettingsService.getPassword())
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
) headers {
} append(
} HttpHeaders.Authorization,
}) constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
private suspend fun postLogin() = maybeResponse(client.tryToPost(url("/login")) { private suspend fun postLogin() =
parameter("username", appSettingsService.getUserName()) maybeResponse(
parameter("password", appSettingsService.getPassword()) client.tryToPost(url("/login")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { parameter("username", appSettingsService.getUserName())
headers { parameter("password", appSettingsService.getPassword())
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
) headers {
} append(
} HttpHeaders.Authorization,
}) constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
private fun shouldHaveNewLogout() = private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
suspend fun logout(): SuccessResponse = suspend fun logout(): SuccessResponse =
if (shouldHaveNewLogout()) { if (shouldHaveNewLogout()) {
@ -150,23 +174,42 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
private suspend fun maybeLogoutIfAvailable() = private suspend fun maybeLogoutIfAvailable() =
responseOrSuccessIf404(client.tryToGet(url("/logout")) { responseOrSuccessIf404(
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { client.tryToGet(url("/logout")) {
headers { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) headers {
) append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
} }
} },
}) )
private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current")) { private suspend fun doLogout() =
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { maybeResponse(
headers { client.tryToDelete(url("/api/session/current")) {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
) headers {
} append(
} HttpHeaders.Authorization,
}) constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun getItems( suspend fun getItems(
type: String, type: String,
@ -175,213 +218,340 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
source: Long?, source: Long?,
search: String?, search: String?,
updatedSince: String?, updatedSince: String?,
items: Int? = null items: Int? = null,
): StatusAndData<List<SelfossModel.Item>> = ): StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure(client.tryToGet(url("/items")) { bodyOrFailure(
if (!shouldHavePostLogin()) { client.tryToGet(url("/items")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
parameter("type", type)
parameter("tag", tag)
parameter("source", source)
parameter("search", search)
parameter("updatedsince", updatedSince)
parameter("items", items ?: appSettingsService.getItemsNumber())
parameter("offset", offset)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun getItemsWithoutCatch(): StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure(client.get(url("/items")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
parameter("type", "all")
parameter("items", 1)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun stats(): StatusAndData<SelfossModel.Stats> =
bodyOrFailure(client.tryToGet(url("/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
bodyOrFailure(client.tryToGet(url("/tags")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun update(): StatusAndData<String> =
bodyOrFailure(client.tryToGet(url("/update")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
bodyOrFailure(client.tryToGet(url("/sources/spouts")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> =
bodyOrFailure(client.tryToGet(url("/sources/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> =
bodyOrFailure(client.tryToGet(url("/sources/list")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
bodyOrFailure(client.tryToGet(url("/api/about")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun markAsRead(id: String): SuccessResponse =
maybeResponse(client.tryToPost(url("/mark/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun unmarkAsRead(id: String): SuccessResponse =
maybeResponse(client.tryToPost(url("/unmark/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun starr(id: String): SuccessResponse =
maybeResponse(client.tryToPost(url("/starr/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun unstarr(id: String): SuccessResponse =
maybeResponse(client.tryToPost(url("/unstarr/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword()))
)
}
}
})
suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
maybeResponse(client.tryToSubmitForm(
url = url("/mark"),
formParameters = Parameters.build {
if (!shouldHavePostLogin()) { if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
ids.map { append("ids[]", it) } parameter("type", type)
}, parameter("tag", tag)
block = { parameter("source", source)
parameter("search", search)
parameter("updatedsince", updatedSince)
parameter("items", items ?: appSettingsService.getItemsNumber())
parameter("offset", offset)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
} },
)) )
suspend fun getItemsWithoutCatch(): StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure(
client.get(url("/items")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
parameter("type", "all")
parameter("items", 1)
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun stats(): StatusAndData<SelfossModel.Stats> =
bodyOrFailure(
client.tryToGet(url("/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
bodyOrFailure(
client.tryToGet(url("/tags")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun update(): StatusAndData<String> =
bodyOrFailure(
client.tryToGet(url("/update")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
bodyOrFailure(
client.tryToGet(url("/sources/spouts")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> =
bodyOrFailure(
client.tryToGet(url("/sources/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> =
bodyOrFailure(
client.tryToGet(url("/sources/list")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
bodyOrFailure(
client.tryToGet(url("/api/about")) {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun markAsRead(id: String): SuccessResponse =
maybeResponse(
client.tryToPost(url("/mark/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun unmarkAsRead(id: String): SuccessResponse =
maybeResponse(
client.tryToPost(url("/unmark/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun starr(id: String): SuccessResponse =
maybeResponse(
client.tryToPost(url("/starr/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun unstarr(id: String): SuccessResponse =
maybeResponse(
client.tryToPost(url("/unstarr/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
maybeResponse(
client.tryToSubmitForm(
url = url("/mark"),
formParameters =
Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
ids.map { append("ids[]", it) }
},
block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
),
)
suspend fun createSourceForVersion( suspend fun createSourceForVersion(
title: String, title: String,
@ -394,7 +564,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
createSource("tags[]", title, url, spout, tags) createSource("tags[]", title, url, spout, tags)
} else { } else {
createSource("tags", title, url, spout, tags) createSource("tags", title, url, spout, tags)
} },
) )
private suspend fun createSource( private suspend fun createSource(
@ -402,28 +572,36 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
title: String, title: String,
url: String, url: String,
spout: String, spout: String,
tags: String tags: String,
): HttpResponse? = ): HttpResponse? =
client.tryToSubmitForm( client.tryToSubmitForm(
url = url("/source"), url = url("/source"),
formParameters = Parameters.build { formParameters =
if (!shouldHavePostLogin()) { Parameters.build {
append("username", appSettingsService.getUserName()) if (!shouldHavePostLogin()) {
append("password", appSettingsService.getPassword()) append("username", appSettingsService.getUserName())
} append("password", appSettingsService.getPassword())
append("title", title) }
append("url", url) append("title", title)
append("spout", spout) append("url", url)
append(tagsParamName, tags) append("spout", spout)
}, append(tagsParamName, tags)
},
block = { block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
} },
) )
suspend fun updateSourceForVersion( suspend fun updateSourceForVersion(
@ -431,14 +609,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
title: String, title: String,
url: String, url: String,
spout: String, spout: String,
tags: String tags: String,
): SuccessResponse = ): SuccessResponse =
maybeResponse( maybeResponse(
if (appSettingsService.getApiVersion() > 1) { if (appSettingsService.getApiVersion() > 1) {
updateSource(id, "tags[]", title, url, spout, tags) updateSource(id, "tags[]", title, url, spout, tags)
} else { } else {
updateSource(id, "tags", title, url, spout, tags) updateSource(id, "tags", title, url, spout, tags)
} },
) )
private suspend fun updateSource( private suspend fun updateSource(
@ -451,44 +629,54 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
): HttpResponse? = ): HttpResponse? =
client.tryToSubmitForm( client.tryToSubmitForm(
url = url("/source/$id"), url = url("/source/$id"),
formParameters = Parameters.build { formParameters =
if (!shouldHavePostLogin()) { Parameters.build {
append("username", appSettingsService.getUserName()) if (!shouldHavePostLogin()) {
append("password", appSettingsService.getPassword()) append("username", appSettingsService.getUserName())
} append("password", appSettingsService.getPassword())
append("title", title) }
append("url", url) append("title", title)
append("spout", spout) append("url", url)
append(tagsParamName, tags) append("spout", spout)
}, append(tagsParamName, tags)
},
block = { block = {
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers { headers {
append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
) )
} }
} }
} },
) )
suspend fun deleteSource(id: Int): SuccessResponse = suspend fun deleteSource(id: Int): SuccessResponse =
maybeResponse(client.tryToDelete(url("/source/$id")) { maybeResponse(
if (!shouldHavePostLogin()) { client.tryToDelete(url("/source/$id")) {
parameter("username", appSettingsService.getUserName()) if (!shouldHavePostLogin()) {
parameter("password", appSettingsService.getPassword()) parameter("username", appSettingsService.getUserName())
} parameter("password", appSettingsService.getPassword())
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword()
)
)
)
} }
} if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
}) headers {
} append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,4 +7,4 @@ actual class DriverFactory {
actual fun createDriver(): SqlDriver { actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db") return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
} }
} }

View File

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

View File

@ -10,4 +10,4 @@ actual class DateUtils {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
} }

View File

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

View File

@ -7,4 +7,4 @@ actual class DriverFactory {
actual fun createDriver(): SqlDriver { actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db") return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
} }
} }

View File

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

View File

@ -1,7 +1,5 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
actual class DateUtils { actual class DateUtils {
actual companion object { actual companion object {
actual fun parseDate(dateString: String): Long { actual fun parseDate(dateString: String): Long {
@ -12,5 +10,4 @@ actual class DateUtils {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
}
}

View File

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