Compare commits

..

18 Commits

Author SHA1 Message Date
Amine Louveau 417a33eb25 Merge pull request 'Running migrations.' (#118) from fix-migration into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/118
2022-12-28 14:49:11 +00:00
aminecmi 2e7f7f23b3 No duplicate builds for PRs.
continuous-integration/drone/push Build is passing
2022-12-28 15:34:22 +01:00
aminecmi e5e182761e Running migrations.
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build was killed
2022-12-28 15:27:17 +01:00
Amine Louveau a094d88799 Merge pull request 'Make the author field nullable' (#117) from davidoskky/ReaderForSelfoss-multiplatform:author into master
continuous-integration/drone/push Build was killed
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/117
2022-12-28 14:25:36 +00:00
davidoskky e51915d1cd Include author field when updating the database
continuous-integration/drone/pr Build is passing
2022-12-28 14:25:56 +01:00
davidoskky 3a654f6ede Migrate the database table 2022-12-28 14:25:34 +01:00
davidoskky 5227751dca Make the author field nullable
continuous-integration/drone/pr Build is passing
2022-12-28 11:02:43 +01:00
aminecmi 27eafe4ff4 Delete sources from DB and reload items on source deletion.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-26 22:27:28 +01:00
aminecmi 8c83a9408b Drone should work better.
continuous-integration/drone/push Build was killed
2022-12-26 22:26:28 +01:00
aminecmi fe2410f719 Handling author field.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2022-12-26 21:49:55 +01:00
aminecmi a5e86bfb77 Date format issues.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing
2022-12-26 15:02:19 +01:00
aminecmi 23be633798 Add api version to the reports.
continuous-integration/drone/push Build is running
2022-12-25 22:45:12 +01:00
aminecmi 813e0707d8 Date format issue.
continuous-integration/drone/push Build is running
2022-12-25 22:41:34 +01:00
aminecmi 9ed9bf07fc Items in repository.
continuous-integration/drone/push Build is passing
2022-12-23 22:53:16 +01:00
aminecmi 47265c10d0 Trying nexus build.
continuous-integration/drone/push Build is passing
2022-12-23 14:59:58 +01:00
aminecmi 5cc633246a Debugging images issues.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-22 20:28:49 +01:00
aminecmi 1f40385786 Context should not be null, but handle the case for now.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-19 22:08:28 +01:00
aminecmi eb2876324a This seems to be needed.
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-19 20:47:04 +01:00
23 changed files with 232 additions and 161 deletions
+4 -5
View File
@@ -17,6 +17,7 @@ steps:
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- ./gradlew koverMergedXmlReport - ./gradlew koverMergedXmlReport
environment: environment:
TZ: Europe/Paris
SONAR_HOST_URL: SONAR_HOST_URL:
from_secret: sonarScannerHostUrl from_secret: sonarScannerHostUrl
SONAR_LOGIN: SONAR_LOGIN:
@@ -34,7 +35,6 @@ steps:
trigger: trigger:
event: event:
- push - push
- pull_request
--- ---
kind: pipeline kind: pipeline
@@ -50,6 +50,7 @@ steps:
- git remote add pushing https://$GITEA_USR:$GITEA_PASS@gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform.git - git remote add pushing https://$GITEA_USR:$GITEA_PASS@gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform.git
- git push pushing --tags - git push pushing --tags
environment: environment:
TZ: Europe/Paris
GITEA_USR: GITEA_USR:
from_secret: giteaUsr from_secret: giteaUsr
GITEA_PASS: GITEA_PASS:
@@ -75,10 +76,7 @@ steps:
from_secret: privateKey from_secret: privateKey
command_timeout: 2m command_timeout: 2m
script: script:
- cd /home/ubuntu - cd /home/ubuntu && sudo rm -rf /var/www/amine/version.txt && sudo chown www-data:www-data ./version.txt && sudo mv version.txt /var/www/amine/
- sudo rm -rf /var/www/amine/version.txt
- sudo chown www-data:www-data ./version.txt
- sudo mv version.txt /var/www/amine/
trigger: trigger:
event: event:
@@ -117,6 +115,7 @@ steps:
- echo "Verify" - echo "Verify"
- $ANDROID_HOME/build-tools/31.0.0/apksigner verify signed.apk - $ANDROID_HOME/build-tools/31.0.0/apksigner verify signed.apk
environment: environment:
TZ: Europe/Paris
YOUR_KEYSTORE_PASSWORD: YOUR_KEYSTORE_PASSWORD:
from_secret: keyPass from_secret: keyPass
YOUR_KEY_ALIAS: YOUR_KEY_ALIAS:
@@ -23,6 +23,7 @@ import com.mikepenz.aboutlibraries.LibsBuilder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.ACRA
import org.kodein.di.DIAware 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
@@ -129,6 +130,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
private fun goToMain() { private fun goToMain() {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
repository.updateApiVersion() repository.updateApiVersion()
ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString())
} }
val intent = Intent(this, HomeActivity::class.java) val intent = Intent(this, HomeActivity::class.java)
startActivity(intent) startActivity(intent)
@@ -3,10 +3,7 @@ package bou.amine.apps.readerforselfossv2.android
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build import android.os.Build
import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@@ -18,8 +15,6 @@ import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.github.ln_12.library.ConnectivityStatus import com.github.ln_12.library.ConnectivityStatus
import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
@@ -83,6 +78,8 @@ class MyApp : MultiDexApplication(), DIAware {
} }
} }
} }
repository.migrate(driverFactory)
} }
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
@@ -30,6 +30,8 @@ class ReaderActivity : AppCompatActivity(), DIAware {
private lateinit var binding: ActivityReaderBinding private lateinit var binding: ActivityReaderBinding
private var allItems: ArrayList<SelfossModel.Item> = ArrayList()
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()
@@ -61,12 +63,14 @@ class ReaderActivity : AppCompatActivity(), DIAware {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
if (allItems.isEmpty()) { currentItem = intent.getIntExtra("currentItem", 0)
allItems = repository.getReaderItems()
if (allItems.isEmpty() || currentItem > allItems.size) {
finish() finish()
} }
currentItem = intent.getIntExtra("currentItem", 0)
readItem(allItems[currentItem]) readItem(allItems[currentItem])
binding.pager.adapter = ScreenSlidePagerAdapter(this) binding.pager.adapter = ScreenSlidePagerAdapter(this)
@@ -214,8 +218,4 @@ class ReaderActivity : AppCompatActivity(), DIAware {
startActivity(intent) startActivity(intent)
overridePendingTransition(0, 0) overridePendingTransition(0, 0)
} }
companion object {
var allItems: ArrayList<SelfossModel.Item> = ArrayList()
}
} }
@@ -62,7 +62,7 @@ class ItemCardAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = itm.sourceAndDateText() binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate()
if (!appSettingsService.isFullHeightCardsEnabled()) { if (!appSettingsService.isFullHeightCardsEnabled()) {
binding.itemImage.maxHeight = imageMaxHeight binding.itemImage.maxHeight = imageMaxHeight
@@ -132,8 +132,8 @@ class ItemCardAdapter(
private fun handleLinkOpening() { private fun handleLinkOpening() {
binding.root.setOnClickListener { binding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl( c.openItemUrl(
items,
bindingAdapterPosition, bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(), items[bindingAdapterPosition].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(), appSettingsService.isArticleViewerEnabled(),
@@ -51,7 +51,7 @@ class ItemListAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = itm.sourceAndDateText() binding.sourceTitleAndDate.text = itm.sourceAuthorAndDate()
if (itm.getThumbnail(repository.baseUrl).isEmpty()) { if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
@@ -84,8 +84,8 @@ class ItemListAdapter(
private fun handleLinkOpening() { private fun handleLinkOpening() {
binding.root.setOnClickListener { binding.root.setOnClickListener {
repository.setReaderItems(items)
c.openItemUrl( c.openItemUrl(
items,
bindingAdapterPosition, bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(), items[bindingAdapterPosition].getLinkDecoded(),
appSettingsService.isArticleViewerEnabled(), appSettingsService.isArticleViewerEnabled(),
@@ -78,9 +78,9 @@ class SourcesListAdapter(
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn) val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener { deleteBtn.setOnClickListener {
val (id) = items[bindingAdapterPosition] val (id, title) = items[bindingAdapterPosition]
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(id) val successfullyDeletedSource = repository.deleteSource(id, title)
if (successfullyDeletedSource) { if (successfullyDeletedSource) {
items.removeAt(bindingAdapterPosition) items.removeAt(bindingAdapterPosition)
notifyItemRemoved(bindingAdapterPosition) notifyItemRemoved(bindingAdapterPosition)
@@ -101,7 +101,7 @@ class ArticleFragment : Fragment(), DIAware {
contentText = item.content contentText = item.content
contentTitle = item.title.getHtmlDecoded() contentTitle = item.title.getHtmlDecoded()
contentImage = item.getThumbnail(repository.baseUrl) contentImage = item.getThumbnail(repository.baseUrl)
contentSource = item.sourceAndDateText() contentSource = item.sourceAuthorAndDate()
allImages = item.getImages() allImages = item.getImages()
fontSize = appSettingsService.getFontSize() fontSize = appSettingsService.getFontSize()
@@ -347,7 +347,7 @@ class ArticleFragment : Fragment(), DIAware {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)) return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG))
} catch ( e : ExecutionException) { } catch ( e : ExecutionException) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg") e.sendSilentlyWithAcraWithName("shouldInterceptRequest > jpeg > $url")
} }
} }
else if (url.lowercase(Locale.US).contains(".png")) { else if (url.lowercase(Locale.US).contains(".png")) {
@@ -355,7 +355,7 @@ class ArticleFragment : Fragment(), DIAware {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG)) return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG))
} catch ( e : ExecutionException) { } catch ( e : ExecutionException) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > png") e.sendSilentlyWithAcraWithName("shouldInterceptRequest > png > $url")
} }
} }
else if (url.lowercase(Locale.US).contains(".webp")) { else if (url.lowercase(Locale.US).contains(".webp")) {
@@ -363,7 +363,7 @@ class ArticleFragment : Fragment(), DIAware {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)) return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP))
} catch ( e : ExecutionException) { } catch ( e : ExecutionException) {
e.sendSilentlyWithAcraWithName("shouldInterceptRequest > webp") e.sendSilentlyWithAcraWithName("shouldInterceptRequest > webp > $url")
} }
} }
@@ -1,6 +1,6 @@
package bou.amine.apps.readerforselfossv2.android.fragments package bou.amine.apps.readerforselfossv2.android.fragments
import android.annotation.SuppressLint import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
@@ -13,9 +13,7 @@ import android.view.ViewGroup
import bou.amine.apps.readerforselfossv2.android.HomeActivity import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@@ -38,16 +36,14 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
override val di: DI by closestDI() override val di: DI by closestDI()
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
private var selectedChip: Chip? = null private var selectedChip: Chip? = null
@SuppressLint("ResourceAsColor")
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
val binding = val binding =
bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding.inflate( bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding.inflate(
inflater, inflater,
@@ -55,21 +51,64 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
false false
) )
val context: Context? = context
val tagGroup = binding.tagsGroup val tagGroup = binding.tagsGroup
val sourceGroup = binding.sourcesGroup val sourceGroup = binding.sourcesGroup
if (context == null) {
dismiss()
Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} else {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val tags = repository.getTags() val tags = repository.getTags()
tags.forEach { tag -> tags.forEach { tag ->
val c = chipForTag(tag) val c = Chip(context)
c.text = tag.tag
val gd = GradientDrawable()
val gdColor = try {
Color.parseColor(tag.color)
} catch (e: IllegalArgumentException) {
e.sendSilentlyWithAcraWithName("color issue " + tag.color)
resources.getColor(R.color.colorPrimary)
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
c.chipIcon = gd
c.setOnCloseIconClickListener {
(it as Chip).isCloseIconVisible = false
selectedChip = null
repository.setTagFilter(null)
}
c.setOnClickListener {
if (selectedChip != null) {
selectedChip!!.isCloseIconVisible = false
}
(it as Chip).isCloseIconVisible = true
selectedChip = it
repository.setTagFilter(tag)
repository.setSourceFilter(null)
}
if (repository.tagFilter.value?.equals(tag) == true) {
c.isCloseIconVisible = true
selectedChip = c
}
tagGroup.addView(c) tagGroup.addView(c)
} }
repository.getSources().forEach { source -> repository.getSources().forEach { source ->
val c = Chip(requireContext()) val c = Chip(context)
Glide.with(requireContext()) Glide.with(context)
.load(source.getIcon(repository.baseUrl)) .load(source.getIcon(repository.baseUrl))
.listener(object : RequestListener<Drawable?> { .listener(object : RequestListener<Drawable?> {
override fun onLoadFailed( override fun onLoadFailed(
@@ -124,6 +163,7 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
binding.progressBar2.visibility = GONE binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE binding.filterView.visibility = VISIBLE
} }
}
binding.floatingActionButton2.setOnClickListener { binding.floatingActionButton2.setOnClickListener {
(activity as HomeActivity).getElementsAccordingToTab() (activity as HomeActivity).getElementsAccordingToTab()
@@ -133,49 +173,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
return binding.root return binding.root
} }
private fun chipForTag(tag: SelfossModel.Tag): Chip {
val c = Chip(requireContext())
c.text = tag.tag
val gd = GradientDrawable()
val gdColor = try {
Color.parseColor(tag.color)
} catch (e: IllegalArgumentException) {
e.sendSilentlyWithAcraWithName("color issue " + tag.color)
resources.getColor(R.color.colorPrimary)
}
gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30)
gd.cornerRadius = 30F
c.chipIcon = gd
c.setOnCloseIconClickListener {
(it as Chip).isCloseIconVisible = false
selectedChip = null
repository.setTagFilter(null)
}
c.setOnClickListener {
if (selectedChip != null) {
selectedChip!!.isCloseIconVisible = false
}
(it as Chip).isCloseIconVisible = true
selectedChip = it
repository.setTagFilter(tag)
repository.setSourceFilter(null)
}
if (repository.tagFilter.value?.equals(tag) == true) {
c.isCloseIconVisible = true
selectedChip = c
}
return c
}
companion object { companion object {
const val TAG = "ModalBottomSheet" const val TAG = "FilterModalBottomSheet"
} }
@@ -16,7 +16,8 @@ fun SelfossModel.Item.toParcelable() : ParecelableItem =
this.icon, this.icon,
this.link, this.link,
this.sourcetitle, this.sourcetitle,
this.tags.joinToString(",") this.tags.joinToString(","),
this.author
) )
fun ParecelableItem.toModel() : SelfossModel.Item = fun ParecelableItem.toModel() : SelfossModel.Item =
SelfossModel.Item( SelfossModel.Item(
@@ -30,7 +31,8 @@ fun ParecelableItem.toModel() : SelfossModel.Item =
this.icon, this.icon,
this.link, this.link,
this.sourcetitle, this.sourcetitle,
this.tags.split(",") this.tags.split(","),
this.author
) )
data class ParecelableItem( data class ParecelableItem(
val id: Int, val id: Int,
@@ -43,7 +45,8 @@ data class ParecelableItem(
val icon: String?, val icon: String?,
val link: String, val link: String,
val sourcetitle: String, val sourcetitle: String,
val tags: String val tags: String,
val author: String?
) : Parcelable { ) : Parcelable {
companion object { companion object {
@@ -65,7 +68,8 @@ data class ParecelableItem(
icon = source.readString(), icon = source.readString(),
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()
) )
override fun describeContents() = 0 override fun describeContents() = 0
@@ -82,5 +86,6 @@ data class ParecelableItem(
dest.writeString(link) dest.writeString(link)
dest.writeString(sourcetitle) dest.writeString(sourcetitle)
dest.writeString(tags) dest.writeString(tags)
dest.writeString(author)
} }
} }
@@ -18,7 +18,6 @@ import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.openItemUrl( fun Context.openItemUrl(
allItems: ArrayList<SelfossModel.Item>,
currentItem: Int, currentItem: Int,
linkDecoded: String, linkDecoded: String,
articleViewer: Boolean, articleViewer: Boolean,
@@ -33,7 +32,6 @@ fun Context.openItemUrl(
).show() ).show()
} else { } else {
if (articleViewer) { if (articleViewer) {
ReaderActivity.allItems = allItems
val intent = Intent(this, ReaderActivity::class.java) val intent = Intent(this, ReaderActivity::class.java)
intent.putExtra("currentItem", currentItem) intent.putExtra("currentItem", currentItem)
app.startActivity(intent) app.startActivity(intent)
+14 -1
View File
@@ -11,11 +11,14 @@ class DatesTest {
private val v3Date = "2013-04-07T13:43:00+01:00" private val v3Date = "2013-04-07T13:43:00+01:00"
private val v4Date = "2013-04-07 13:43:00" private val v4Date = "2013-04-07 13:43:00"
private val bug1Date = "2022-12-24T17:00:08+00"
@Test @Test
fun v3_date_should_be_parsed() { fun v3_date_should_be_parsed() {
val date = DateUtils.parseDate(v3Date) val date = DateUtils.parseDate(v3Date)
val expected = LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.of("UTC+1")) .toEpochMilliseconds() val expected =
LocalDateTime(2013, 4, 7, 14, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(date, expected) assertEquals(date, expected)
} }
@@ -30,4 +33,14 @@ class DatesTest {
assertEquals(date, expected) assertEquals(date, expected)
} }
@Test
fun bug1_date_should_be_parsed() {
val date = DateUtils.parseDate(bug1Date)
val expected =
LocalDateTime(2022, 12, 24, 18, 0, 8, 0).toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds()
assertEquals(date, expected)
}
} }
+7 -3
View File
@@ -58,6 +58,7 @@ class RepositoryTest {
data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED) data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED)
) )
every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit
every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems() every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems()
every { db.tagsQueries.deleteAllTags() } returns Unit every { db.tagsQueries.deleteAllTags() } returns Unit
every { db.tagsQueries.transaction(any(), any()) } returns Unit every { db.tagsQueries.transaction(any(), any()) } returns Unit
@@ -798,10 +799,11 @@ class RepositoryTest {
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.deleteSource(5) response = repository.deleteSource(5, "src")
} }
coVerify(exactly = 1) { api.deleteSource(5) } coVerify(exactly = 1) { api.deleteSource(5) }
coVerify(exactly = 1) { db.itemsQueries.deleteItemsWhereSource("src") }
assertSame(true, response) assertSame(true, response)
} }
@@ -812,10 +814,11 @@ class RepositoryTest {
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.deleteSource(5) response = repository.deleteSource(5, "src")
} }
coVerify(exactly = 1) { api.deleteSource(5) } coVerify(exactly = 1) { api.deleteSource(5) }
coVerify(exactly = 0) { db.itemsQueries.deleteItemsWhereSource("src") }
assertSame(false, response) assertSame(false, response)
} }
@@ -826,10 +829,11 @@ class RepositoryTest {
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.deleteSource(5) response = repository.deleteSource(5, "src")
} }
coVerify(exactly = 0) { api.deleteSource(5) } coVerify(exactly = 0) { api.deleteSource(5) }
coVerify(exactly = 1) { db.itemsQueries.deleteItemsWhereSource("src") }
assertSame(false, response) assertSame(false, response)
} }
+5 -2
View File
@@ -17,7 +17,8 @@ fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<I
icon = item.icon, icon = item.icon,
link = item.link, link = item.link,
sourcetitle = item.sourcetitle, sourcetitle = item.sourcetitle,
tags = item.tags tags = item.tags,
author = item.author
) )
) )
} }
@@ -35,7 +36,8 @@ fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<S
icon = item.icon, icon = item.icon,
link = item.link, link = item.link,
sourcetitle = item.sourcetitle, sourcetitle = item.sourcetitle,
tags = item.tags.split(',') tags = item.tags.split(','),
author = item.author
) )
) )
} }
@@ -54,4 +56,5 @@ class FakeItemParameters {
"https://ilblogdellasci.wordpress.com/2022/09/09/etica-della-ricerca-sotto-i-riflettori/" "https://ilblogdellasci.wordpress.com/2022/09/09/etica-della-ricerca-sotto-i-riflettori/"
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"
} }
+6 -4
View File
@@ -17,10 +17,12 @@ plugins {
allprojects { allprojects {
repositories { repositories {
google() maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
mavenCentral() // IMPORTANT : Add back when new library added
jcenter() // google()
maven { url = uri("https://www.jitpack.io") } // mavenCentral()
// jcenter()
// maven { url = uri("https://www.jitpack.io") }
} }
} }
+9 -5
View File
@@ -2,16 +2,20 @@ val pushCache: String by settings
pluginManagement { pluginManagement {
repositories { repositories {
google() maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
gradlePluginPortal() // IMPORTANT : Add back when new plugin added
mavenCentral() // google()
// gradlePluginPortal()
// mavenCentral()
} }
} }
dependencyResolutionManagement { dependencyResolutionManagement {
repositories { repositories {
google() maven { url = uri("https://nexus.amine-louveau.fr/repository/maven-public/")}
mavenCentral() // IMPORTANT : Add back when new library added
// google()
// mavenCentral()
} }
} }
@@ -10,7 +10,11 @@ actual class DateUtils {
return try { return try {
Instant.parse(dateString).toEpochMilliseconds() Instant.parse(dateString).toEpochMilliseconds()
} catch (e: Exception) { } catch (e: Exception) {
LocalDateTime.parse(dateString.replace(" ", "T")).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() var str = dateString.replace(" ", "T")
if (str.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{2}".toRegex())) {
str = str.split("+")[0]
}
LocalDateTime.parse(str).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
} }
} }
@@ -57,7 +57,6 @@ class SelfossModel {
val error: String, val error: String,
val icon: String? val icon: String?
) )
@Serializable @Serializable
data class Item( data class Item(
val id: Int, val id: Int,
@@ -73,7 +72,8 @@ class SelfossModel {
val link: String, val link: String,
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?
) { ) {
// TODO: maybe find a better way to handle these kind of urls // TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String { fun getLinkDecoded(): String {
@@ -102,8 +102,14 @@ class SelfossModel {
return stringUrl return stringUrl
} }
fun sourceAndDateText(): String = fun sourceAuthorAndDate(): String {
this.sourcetitle.getHtmlDecoded() + DateUtils.parseRelativeDate(this.datetime) var txt = this.sourcetitle.getHtmlDecoded()
if (!this.author.isNullOrBlank()) {
txt += " (by ${this.author}) "
}
txt += DateUtils.parseRelativeDate(this.datetime)
return txt
}
fun toggleStar(): Item { fun toggleStar(): Item {
this.starred = !this.starred this.starred = !this.starred
@@ -111,6 +117,7 @@ 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> {
@@ -47,6 +47,8 @@ class Repository(
private var fetchedSources = false private var fetchedSources = false
private var fetchedTags = false private var fetchedTags = false
private var _readerItems = ArrayList<SelfossModel.Item>()
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// TODO: Use the updatedSince parameter // TODO: Use the updatedSince parameter
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
@@ -357,13 +359,20 @@ class Repository(
return response return response
} }
suspend fun deleteSource(id: Int): 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)
success = response.isSuccess success = response.isSuccess
} }
// We filter on success or if the network isn't available
if (success || !isNetworkAvailable()) {
items = ArrayList(items.filter { it.sourcetitle != title })
setReaderItems(items)
db.itemsQueries.deleteItemsWhereSource(title)
}
return success return success
} }
@@ -502,6 +511,7 @@ class Repository(
item.link, item.link,
item.sourcetitle, item.sourcetitle,
item.tags.joinToString(","), item.tags.joinToString(","),
item.author,
item.id.toString() item.id.toString()
) )
@@ -558,4 +568,16 @@ class Repository(
fun setSourceFilter(source: SelfossModel.Source?) { fun setSourceFilter(source: SelfossModel.Source?) {
_sourceFilter.value = source _sourceFilter.value = source
} }
fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) {
_readerItems = readerItems
}
fun getReaderItems(): ArrayList<SelfossModel.Item> {
return _readerItems
}
fun migrate(driverFactory: DriverFactory) {
ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
}
} }
@@ -47,7 +47,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
install(HttpCookies) install(HttpCookies)
install(HttpRequestRetry) { install(HttpRequestRetry) {
maxRetries = 1 maxRetries = 2
retryIf { _, response -> retryIf { _, response ->
response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo() response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo()
} }
@@ -51,7 +51,8 @@ fun ITEM.toView(): SelfossModel.Item =
this.icon, this.icon,
this.link, this.link,
this.sourcetitle, this.sourcetitle,
this.tags.split(",") this.tags.split(","),
this.author
) )
fun SelfossModel.Item.toEntity(): ITEM = fun SelfossModel.Item.toEntity(): ITEM =
@@ -66,5 +67,6 @@ fun SelfossModel.Item.toEntity(): ITEM =
this.icon, this.icon,
this.link, this.link,
this.sourcetitle.getHtmlDecoded(), this.sourcetitle.getHtmlDecoded(),
this.tags.joinToString(",") this.tags.joinToString(","),
this.author
) )
@@ -0,0 +1,6 @@
CREATE TABLE ITEM_BACKUP AS SELECT `id`, `datetime`, `title`, `content`,
`unread`, `starred`, `thumbnail`, `icon`, `link`, `sourcetitle`,
`tags` FROM ITEM;
ALTER TABLE ITEM_BACKUP ADD COLUMN `author` TEXT;
DROP TABLE ITEM;
ALTER TABLE ITEM_BACKUP RENAME TO ITEM;
@@ -10,6 +10,7 @@ CREATE TABLE ITEM (
`link` TEXT NOT NULL, `link` TEXT NOT NULL,
`sourcetitle` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL,
`tags` TEXT NOT NULL, `tags` TEXT NOT NULL,
`author` TEXT,
PRIMARY KEY(`id`) PRIMARY KEY(`id`)
); );
@@ -26,5 +27,8 @@ INSERT OR REPLACE INTO ITEM VALUES ?;
deleteItem: deleteItem:
DELETE FROM ITEM WHERE `id` = ?; DELETE FROM ITEM WHERE `id` = ?;
deleteItemsWhereSource:
DELETE FROM ITEM WHERE `sourcetitle` = ?;
updateItem: updateItem:
UPDATE ITEM SET `datetime` = ?, `title` = ?, `content` = ?, `unread` = ?, `starred` = ?, `thumbnail` = ?, `icon` = ?, `link` = ?, `sourcetitle` = ?, `tags` = ? WHERE `id` = ?; UPDATE ITEM SET `datetime` = ?, `title` = ?, `content` = ?, `unread` = ?, `starred` = ?, `thumbnail` = ?, `icon` = ?, `link` = ?, `sourcetitle` = ?, `tags` = ?, `author` = ? WHERE `id` = ?;