From 37835a4964223fc476299125aca9125cc02a2a86 Mon Sep 17 00:00:00 2001
From: junkfood <69683722+JunkFood02@users.noreply.github.com>
Date: Wed, 6 Mar 2024 18:54:08 +0800
Subject: [PATCH 1/4] feat(ui): save image as file (#627)
---
app/src/main/AndroidManifest.xml | 3 +-
.../infrastructure/android/AndroidApp.kt | 4 +
.../storage/AndroidImageDownloader.kt | 114 ++++++++++++++++++
.../ui/page/home/reading/ReaderImagePage.kt | 96 ++++++++++++---
.../ui/page/home/reading/ReadingPage.kt | 23 +++-
.../ui/page/home/reading/ReadingViewModel.kt | 13 ++
app/src/main/res/values/strings.xml | 3 +
7 files changed, 238 insertions(+), 18 deletions(-)
create mode 100644 app/src/main/java/me/ash/reader/infrastructure/storage/AndroidImageDownloader.kt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ee54fd859..d44540ecb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,7 +4,8 @@
-
+
diff --git a/app/src/main/java/me/ash/reader/infrastructure/android/AndroidApp.kt b/app/src/main/java/me/ash/reader/infrastructure/android/AndroidApp.kt
index 703262bb3..1e97446a0 100644
--- a/app/src/main/java/me/ash/reader/infrastructure/android/AndroidApp.kt
+++ b/app/src/main/java/me/ash/reader/infrastructure/android/AndroidApp.kt
@@ -17,6 +17,7 @@ import me.ash.reader.infrastructure.di.IODispatcher
import me.ash.reader.infrastructure.net.NetworkDataSource
import me.ash.reader.infrastructure.rss.OPMLDataSource
import me.ash.reader.infrastructure.rss.RssHelper
+import me.ash.reader.infrastructure.storage.AndroidImageDownloader
import me.ash.reader.ui.ext.del
import me.ash.reader.ui.ext.getLatestApk
import me.ash.reader.ui.ext.isGitHub
@@ -92,6 +93,9 @@ class AndroidApp : Application(), Configuration.Provider {
@Inject
lateinit var imageLoader: ImageLoader
+ @Inject
+ lateinit var imageDownloader: AndroidImageDownloader
+
/**
* When the application startup.
*
diff --git a/app/src/main/java/me/ash/reader/infrastructure/storage/AndroidImageDownloader.kt b/app/src/main/java/me/ash/reader/infrastructure/storage/AndroidImageDownloader.kt
new file mode 100644
index 000000000..02342c588
--- /dev/null
+++ b/app/src/main/java/me/ash/reader/infrastructure/storage/AndroidImageDownloader.kt
@@ -0,0 +1,114 @@
+package me.ash.reader.infrastructure.storage
+
+import android.content.Context
+import android.media.MediaScannerConnection
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import android.webkit.URLUtil
+import androidx.annotation.CheckResult
+import androidx.annotation.DeprecatedSinceApi
+import androidx.core.content.contentValuesOf
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import me.ash.reader.R
+import me.ash.reader.infrastructure.di.IODispatcher
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import java.io.FileOutputStream
+import java.io.IOException
+import javax.inject.Inject
+import kotlin.io.path.Path
+import kotlin.io.path.createFile
+import kotlin.io.path.createParentDirectories
+
+private const val TAG = "AndroidImageDownloader"
+
+class AndroidImageDownloader @Inject constructor(
+ @ApplicationContext private val context: Context,
+ @IODispatcher private val ioDispatcher: CoroutineDispatcher,
+ private val okHttpClient: OkHttpClient,
+) {
+ @CheckResult
+ suspend fun downloadImage(imageUrl: String): Result {
+ return withContext(ioDispatcher) {
+ Request.Builder().url(imageUrl).build().runCatching {
+ okHttpClient.newCall(this).execute().run {
+
+ val fileName = URLUtil.guessFileName(
+ imageUrl, header("Content-Disposition"), body.contentType()?.toString()
+ )
+
+ val relativePath =
+ Environment.DIRECTORY_PICTURES + "/" + context.getString(R.string.read_you)
+
+ val resolver = context.contentResolver
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val imageCollection =
+ MediaStore.Images.Media.getContentUri(
+ MediaStore.VOLUME_EXTERNAL_PRIMARY
+ )
+
+
+ val imageDetails = contentValuesOf(
+ MediaStore.Images.Media.DISPLAY_NAME to fileName,
+ MediaStore.Images.Media.RELATIVE_PATH to relativePath,
+ MediaStore.Images.Media.IS_PENDING to 1
+ )
+
+ val imageUri = resolver.insert(imageCollection, imageDetails)
+ ?: return@withContext Result.failure(IOException("Cannot create image"))
+
+ resolver.openFileDescriptor(imageUri, "w", null).use { pfd ->
+ body.byteStream().use {
+ it.copyTo(
+ FileOutputStream(
+ pfd?.fileDescriptor ?: return@withContext Result.failure(
+ IOException("Null fd")
+ )
+ )
+ )
+ }
+ }
+ imageDetails.run {
+ clear()
+ put(MediaStore.Images.Media.IS_PENDING, 0)
+ resolver.update(imageUri, this, null, null)
+ }
+ imageUri
+ } else {
+ saveImageForAndroidP(
+ fileName,
+ Environment.getExternalStoragePublicDirectory(relativePath).path
+ )
+ }
+ }
+ }
+ }
+
+ }
+
+ @DeprecatedSinceApi(29)
+ private fun Response.saveImageForAndroidP(
+ fileName: String,
+ imageDirectory: String,
+ ): Uri {
+ val file = Path(imageDirectory, fileName).createParentDirectories().createFile().toFile()
+
+ body.byteStream().use {
+ it.copyTo(file.outputStream())
+ }
+
+ var contentUri: Uri = Uri.fromFile(file)
+
+ MediaScannerConnection.scanFile(context, arrayOf(file.path), null) { _, uri ->
+ contentUri = uri
+ }
+
+ return contentUri
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReaderImagePage.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReaderImagePage.kt
index 5ce00edb4..49843736c 100644
--- a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReaderImagePage.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReaderImagePage.kt
@@ -1,5 +1,10 @@
package me.ash.reader.ui.page.home.reading
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
@@ -9,24 +14,36 @@ import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
+import androidx.compose.material.icons.outlined.MoreHoriz
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
+import androidx.core.content.ContextCompat
+import androidx.core.view.HapticFeedbackConstantsCompat
import coil.compose.rememberAsyncImagePainter
+
import me.ash.reader.R
-import me.ash.reader.ui.component.base.RYAsyncImage
+import me.ash.reader.ui.ext.showToast
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.ZoomableContentLocation
import me.saket.telephoto.zoomable.rememberZoomableState
@@ -35,7 +52,9 @@ import me.saket.telephoto.zoomable.zoomable
data class ImageData(val imageUrl: String = "", val altText: String = "")
@Composable
-fun ReaderImageViewer(imageData: ImageData, onDismissRequest: () -> Unit = {}) {
+fun ReaderImageViewer(
+ imageData: ImageData, onDownloadImage: (String) -> Unit, onDismissRequest: () -> Unit = {}
+) {
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(usePlatformDefaultWidth = false)
@@ -43,14 +62,15 @@ fun ReaderImageViewer(imageData: ImageData, onDismissRequest: () -> Unit = {}) {
Box(
modifier = Modifier
.fillMaxSize()
-// .background(Color.Black)
.windowInsetsPadding(WindowInsets.systemBars)
) {
- val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
+ val view = LocalView.current
+ val context = LocalContext.current
+
+ val dialogWindowProvider = view.parent as? DialogWindowProvider
dialogWindowProvider?.window?.setDimAmount(1f)
- val zoomableState =
- rememberZoomableState(zoomSpec = ZoomSpec(maxZoomFactor = 4f))
+ val zoomableState = rememberZoomableState(zoomSpec = ZoomSpec(maxZoomFactor = 4f))
val painter = rememberAsyncImagePainter(model = imageData.imageUrl)
@@ -60,8 +80,6 @@ fun ReaderImageViewer(imageData: ImageData, onDismissRequest: () -> Unit = {}) {
)
}
-
-
Image(
painter = painter,
contentDescription = imageData.altText,
@@ -73,18 +91,66 @@ fun ReaderImageViewer(imageData: ImageData, onDismissRequest: () -> Unit = {}) {
)
IconButton(
- onClick = onDismissRequest,
- colors = IconButtonDefaults.iconButtonColors(
- containerColor = Color.Gray.copy(alpha = 0.5f),
- contentColor = Color.White
- ),
- modifier = Modifier.padding(12.dp)
+ onClick = {
+ view.performHapticFeedback(HapticFeedbackConstantsCompat.KEYBOARD_TAP)
+ onDismissRequest()
+ }, colors = IconButtonDefaults.iconButtonColors(
+ containerColor = Color.Black.copy(alpha = 0.5f), contentColor = Color.White
+ ), modifier = Modifier
+ .padding(4.dp)
+ .align(Alignment.TopStart)
) {
Icon(
imageVector = Icons.Outlined.Close,
- contentDescription = stringResource(id = R.string.close)
+ contentDescription = stringResource(id = R.string.close),
+ )
+ }
+ var expanded by remember { mutableStateOf(false) }
+ IconButton(
+ onClick = { expanded = true }, colors = IconButtonDefaults.iconButtonColors(
+ containerColor = Color.Black.copy(alpha = 0.5f), contentColor = Color.White
+ ), modifier = Modifier
+ .padding(4.dp)
+ .align(Alignment.TopEnd)
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.MoreHoriz,
+ contentDescription = stringResource(id = R.string.more),
)
}
+
+ val launcher =
+ rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission(),
+ onResult = { result ->
+ if (result) {
+ onDownloadImage(imageData.imageUrl)
+ } else {
+ context.showToast(context.getString(R.string.permission_denied))
+ }
+ })
+
+ Box(
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(12.dp)
+ ) {
+ DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
+ DropdownMenuItem(text = { Text(text = stringResource(id = R.string.save)) },
+ onClick = {
+ val isStoragePermissionGranted = ContextCompat.checkSelfPermission(
+ context, Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED
+
+ if (Build.VERSION.SDK_INT > 28 || isStoragePermissionGranted) {
+ onDownloadImage(imageData.imageUrl)
+ } else {
+ launcher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ }
+ expanded = false
+ })
+ }
+ }
+
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt
index 32e20cd69..e0f534741 100644
--- a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt
@@ -28,10 +28,12 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
+import me.ash.reader.R
import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle
import me.ash.reader.infrastructure.preference.LocalReadingAutoHideToolbar
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
import me.ash.reader.ui.ext.collectAsStateValue
+import me.ash.reader.ui.ext.showToast
import me.ash.reader.ui.motion.materialSharedAxisY
import me.ash.reader.ui.page.home.HomeViewModel
import kotlin.math.abs
@@ -40,7 +42,9 @@ import kotlin.math.abs
private const val UPWARD = 1
private const val DOWNWARD = -1
-@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
+@OptIn(
+ ExperimentalFoundationApi::class, ExperimentalMaterialApi::class
+)
@Composable
fun ReadingPage(
navController: NavHostController,
@@ -48,6 +52,7 @@ fun ReadingPage(
readingViewModel: ReadingViewModel = hiltViewModel(),
) {
val tonalElevation = LocalReadingPageTonalElevation.current
+ val context = LocalContext.current
val isPullToSwitchArticleEnabled = LocalPullToSwitchArticle.current.value
val readingUiState = readingViewModel.readingUiState.collectAsStateValue()
val readerState = readingViewModel.readerStateStateFlow.collectAsStateValue()
@@ -229,6 +234,20 @@ fun ReadingPage(
}
)
if (showFullScreenImageViewer) {
- ReaderImageViewer(imageData = currentImageData) { showFullScreenImageViewer = false }
+
+ ReaderImageViewer(
+ imageData = currentImageData,
+ onDownloadImage = {
+ readingViewModel.downloadImage(
+ it,
+ onSuccess = { context.showToast(context.getString(R.string.image_saved)) },
+ onFailure = {
+ // FIXME: crash the app for error report
+ th -> throw th
+ }
+ )
+ },
+ onDismissRequest = { showFullScreenImageViewer = false }
+ )
}
}
diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt
index a641e8518..a84362529 100644
--- a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt
@@ -1,5 +1,6 @@
package me.ash.reader.ui.page.home.reading
+import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -20,6 +21,7 @@ import me.ash.reader.domain.service.RssService
import me.ash.reader.infrastructure.di.ApplicationScope
import me.ash.reader.infrastructure.di.IODispatcher
import me.ash.reader.infrastructure.rss.RssHelper
+import me.ash.reader.infrastructure.storage.AndroidImageDownloader
import java.util.Date
import javax.inject.Inject
@@ -31,6 +33,7 @@ class ReadingViewModel @Inject constructor(
private val ioDispatcher: CoroutineDispatcher,
@ApplicationScope
private val applicationScope: CoroutineScope,
+ private val imageDownloader: AndroidImageDownloader
) : ViewModel() {
private val _readingUiState = MutableStateFlow(ReadingUiState())
@@ -195,6 +198,16 @@ class ReadingViewModel @Inject constructor(
} ?: return false
return true
}
+
+ fun downloadImage(
+ url: String,
+ onSuccess: (Uri) -> Unit = {},
+ onFailure: (Throwable) -> Unit = {}
+ ) {
+ viewModelScope.launch {
+ imageDownloader.downloadImage(url).onSuccess(onSuccess).onFailure(onFailure)
+ }
+ }
}
data class ReadingUiState(
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c7534643b..7be9732b6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -421,4 +421,7 @@
Toggle starred
Export
Pull to switch article
+ Save
+ Image saved
+ Permission denied
From f06d8ce05e1028e906374523541b4a6c36d2577c Mon Sep 17 00:00:00 2001
From: junkfood <69683722+JunkFood02@users.noreply.github.com>
Date: Wed, 6 Mar 2024 22:15:43 +0800
Subject: [PATCH 2/4] fix(ui): load new items from paging data
---
.../main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt
index 871922bb1..b4599738e 100644
--- a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleList.kt
@@ -24,7 +24,7 @@ fun LazyListScope.ArticleList(
onSwipeEndToStart: ((ArticleWithFeed) -> Unit)? = null,
) {
for (index in 0 until pagingItems.itemCount) {
- when (val item = pagingItems.peek(index)) {
+ when (val item = pagingItems[index]) {
is ArticleFlowItem.Article -> {
item(key = item.articleWithFeed.article.id) {
// if (item.articleWithFeed.article.isUnread) {
From d40743d5ff25d18b909ecefd25ad3ed30752053c Mon Sep 17 00:00:00 2001
From: Ash
Date: Fri, 8 Mar 2024 12:40:29 +0800
Subject: [PATCH 3/4] fix(sync): replace publish date of an article with the
current time if it is a future date (#638)
---
.../ash/reader/domain/service/AbstractRssRepository.kt | 7 ++++---
.../java/me/ash/reader/domain/service/FeverRssService.kt | 9 +++++++--
.../ash/reader/domain/service/GoogleReaderRssService.kt | 6 +++++-
.../java/me/ash/reader/infrastructure/rss/RssHelper.kt | 9 ++++++---
app/src/main/java/me/ash/reader/ui/ext/DateExt.kt | 2 ++
5 files changed, 24 insertions(+), 9 deletions(-)
diff --git a/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt b/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt
index 0a2a76bf8..566f2511e 100644
--- a/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt
+++ b/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt
@@ -98,11 +98,12 @@ abstract class AbstractRssRepository(
supervisorScope {
coroutineWorker.setProgress(SyncWorker.setIsSyncing(true))
val preTime = System.currentTimeMillis()
+ val preDate = Date(preTime)
val accountId = context.currentAccountId
feedDao.queryAll(accountId)
.chunked(16)
.forEach {
- it.map { feed -> async { syncFeed(feed) } }
+ it.map { feed -> async { syncFeed(feed, preDate) } }
.awaitAll()
.forEach {
if (it.feed.isNotification) {
@@ -165,9 +166,9 @@ abstract class AbstractRssRepository(
articleDao.markAsStarredByArticleId(accountId, articleId, isStarred)
}
- private suspend fun syncFeed(feed: Feed): FeedWithArticle {
+ private suspend fun syncFeed(feed: Feed, preDate: Date = Date()): FeedWithArticle {
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
- val articles = rssHelper.queryRssXml(feed, latest?.link)
+ val articles = rssHelper.queryRssXml(feed, latest?.link, preDate)
if (feed.icon == null) {
val iconLink = rssHelper.queryRssIconLink(feed.url)
if (iconLink != null) {
diff --git a/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt b/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt
index ec7aa99e9..a2972dc5a 100644
--- a/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt
+++ b/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt
@@ -32,6 +32,7 @@ import me.ash.reader.infrastructure.rss.provider.fever.FeverDTO
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.decodeHTML
import me.ash.reader.ui.ext.dollarLast
+import me.ash.reader.ui.ext.isFuture
import me.ash.reader.ui.ext.showToast
import me.ash.reader.ui.ext.spacerDollar
import java.util.Date
@@ -137,6 +138,7 @@ class FeverRssService @Inject constructor(
try {
val preTime = System.currentTimeMillis()
+ val preDate = Date(preTime)
val accountId = context.currentAccountId
val account = accountDao.queryById(accountId)!!
val feverAPI = getFeverAPI()
@@ -192,7 +194,10 @@ class FeverRssService @Inject constructor(
*itemsBody.items?.map {
Article(
id = accountId.spacerDollar(it.id!!),
- date = it.created_on_time?.run { Date(this * 1000) } ?: Date(),
+ date = it.created_on_time
+ ?.run { Date(this * 1000) }
+ ?.takeIf { !it.isFuture(preDate) }
+ ?: preDate,
title = it.title.decodeHTML() ?: context.getString(R.string.empty),
author = it.author,
rawDescription = it.html ?: "",
@@ -204,7 +209,7 @@ class FeverRssService @Inject constructor(
accountId = accountId,
isUnread = (it.is_read ?: 0) <= 0,
isStarred = (it.is_saved ?: 0) > 0,
- updateAt = Date(),
+ updateAt = preDate,
).also {
sinceId = it.id.dollarLast()
}
diff --git a/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt b/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt
index 7510ee2c9..cf3e474bd 100644
--- a/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt
+++ b/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt
@@ -35,6 +35,7 @@ import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderDTO
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.decodeHTML
import me.ash.reader.ui.ext.dollarLast
+import me.ash.reader.ui.ext.isFuture
import me.ash.reader.ui.ext.showToast
import me.ash.reader.ui.ext.spacerDollar
import java.util.Calendar
@@ -405,7 +406,10 @@ class GoogleReaderRssService @Inject constructor(
val articleId = it.id!!.ofItemStreamIdToId()
Article(
id = accountId.spacerDollar(articleId),
- date = it.published?.run { Date(this * 1000) } ?: preDate,
+ date = it.published
+ ?.run { Date(this * 1000) }
+ ?.takeIf { !it.isFuture(preDate) }
+ ?: preDate,
title = it.title.decodeHTML() ?: context.getString(R.string.empty),
author = it.author,
rawDescription = it.summary?.content ?: "",
diff --git a/app/src/main/java/me/ash/reader/infrastructure/rss/RssHelper.kt b/app/src/main/java/me/ash/reader/infrastructure/rss/RssHelper.kt
index a2db73f73..e35dcf646 100644
--- a/app/src/main/java/me/ash/reader/infrastructure/rss/RssHelper.kt
+++ b/app/src/main/java/me/ash/reader/infrastructure/rss/RssHelper.kt
@@ -18,6 +18,7 @@ import me.ash.reader.infrastructure.di.IODispatcher
import me.ash.reader.infrastructure.html.Readability
import me.ash.reader.ui.ext.currentAccountId
import me.ash.reader.ui.ext.decodeHTML
+import me.ash.reader.ui.ext.isFuture
import me.ash.reader.ui.ext.spacerDollar
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -67,6 +68,7 @@ class RssHelper @Inject constructor(
suspend fun queryRssXml(
feed: Feed,
latestLink: String?,
+ preDate: Date = Date(),
): List =
try {
val accountId = context.currentAccountId
@@ -76,7 +78,7 @@ class RssHelper @Inject constructor(
.entries
.asSequence()
.takeWhile { latestLink == null || latestLink != it.link }
- .map { buildArticleFromSyndEntry(feed, accountId, it) }
+ .map { buildArticleFromSyndEntry(feed, accountId, it, preDate) }
.toList()
}
} catch (e: Exception) {
@@ -89,6 +91,7 @@ class RssHelper @Inject constructor(
feed: Feed,
accountId: Int,
syndEntry: SyndEntry,
+ preDate: Date = Date(),
): Article {
val desc = syndEntry.description?.value
val content = syndEntry.contents
@@ -108,7 +111,7 @@ class RssHelper @Inject constructor(
id = accountId.spacerDollar(UUID.randomUUID().toString()),
accountId = accountId,
feedId = feed.id,
- date = syndEntry.publishedDate ?: syndEntry.updatedDate ?: Date(),
+ date = (syndEntry.publishedDate ?: syndEntry.updatedDate).takeIf { !it.isFuture(preDate) } ?: preDate,
title = syndEntry.title.decodeHTML() ?: feed.name,
author = syndEntry.author,
rawDescription = (content ?: desc) ?: "",
@@ -116,7 +119,7 @@ class RssHelper @Inject constructor(
fullContent = content,
img = findImg((content ?: desc) ?: ""),
link = syndEntry.link ?: "",
- updateAt = Date(),
+ updateAt = preDate,
)
}
diff --git a/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt b/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt
index 0717a280e..00126b363 100644
--- a/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt
+++ b/app/src/main/java/me/ash/reader/ui/ext/DateExt.kt
@@ -86,3 +86,5 @@ private fun String.parseToDate(
}
return null
}
+
+fun Date.isFuture(staticDate: Date = Date()): Boolean = this.time > staticDate.time
From 6b29a810ba4bfffc03f7debde97815023b64d76b Mon Sep 17 00:00:00 2001
From: junkfood <69683722+JunkFood02@users.noreply.github.com>
Date: Fri, 8 Mar 2024 16:26:30 +0800
Subject: [PATCH 4/4] style: reformat code
---
.../reader/domain/service/FeverRssService.kt | 231 +++++++++---------
1 file changed, 119 insertions(+), 112 deletions(-)
diff --git a/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt b/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt
index a2972dc5a..fc8fd8b58 100644
--- a/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt
+++ b/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt
@@ -133,142 +133,149 @@ class FeverRssService @Inject constructor(
* 3. Fetch the Fever articles
* 4. Synchronize read/unread and starred/un-starred items
*/
- override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result = supervisorScope {
- coroutineWorker.setProgress(SyncWorker.setIsSyncing(true))
+ override suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result =
+ supervisorScope {
+ coroutineWorker.setProgress(SyncWorker.setIsSyncing(true))
- try {
- val preTime = System.currentTimeMillis()
- val preDate = Date(preTime)
- val accountId = context.currentAccountId
- val account = accountDao.queryById(accountId)!!
- val feverAPI = getFeverAPI()
+ try {
+ val preTime = System.currentTimeMillis()
+ val preDate = Date(preTime)
+ val accountId = context.currentAccountId
+ val account = accountDao.queryById(accountId)!!
+ val feverAPI = getFeverAPI()
- // 1. Fetch the Fever groups
- val groups = feverAPI.getGroups().groups?.map {
- Group(
- id = accountId.spacerDollar(it.id!!),
- name = it.title ?: context.getString(R.string.empty),
- accountId = accountId,
- )
- } ?: emptyList()
- groupDao.insertOrUpdate(groups)
-
- // 2. Fetch the Fever feeds
- val feedsBody = feverAPI.getFeeds()
- val feedsGroupsMap = mutableMapOf()
- feedsBody.feeds_groups?.forEach { feedsGroups ->
- feedsGroups.group_id?.toString()?.let { groupId ->
- feedsGroups.feed_ids?.split(",")?.forEach { feedId ->
- feedsGroupsMap[feedId] = groupId
- }
- }
- }
-
- // Fetch the Fever favicons
- val faviconsById = feverAPI.getFavicons().favicons?.associateBy { it.id } ?: emptyMap()
- feedDao.insertOrUpdate(
- feedsBody.feeds?.map {
- Feed(
+ // 1. Fetch the Fever groups
+ val groups = feverAPI.getGroups().groups?.map {
+ Group(
id = accountId.spacerDollar(it.id!!),
- name = it.title.decodeHTML() ?: context.getString(R.string.empty),
- url = it.url!!,
- groupId = accountId.spacerDollar(feedsGroupsMap[it.id.toString()]!!),
+ name = it.title ?: context.getString(R.string.empty),
accountId = accountId,
- icon = faviconsById[it.favicon_id]?.data
)
} ?: emptyList()
- )
+ groupDao.insertOrUpdate(groups)
- // Handle empty icon for feeds
- val noIconFeeds = feedDao.queryNoIcon(accountId)
- noIconFeeds.forEach {
- it.icon = rssHelper.queryRssIconLink(it.url)
- }
- feedDao.update(*noIconFeeds.toTypedArray())
+ // 2. Fetch the Fever feeds
+ val feedsBody = feverAPI.getFeeds()
+ val feedsGroupsMap = mutableMapOf()
+ feedsBody.feeds_groups?.forEach { feedsGroups ->
+ feedsGroups.group_id?.toString()?.let { groupId ->
+ feedsGroups.feed_ids?.split(",")?.forEach { feedId ->
+ feedsGroupsMap[feedId] = groupId
+ }
+ }
+ }
- // 3. Fetch the Fever articles (up to unlimited counts)
- var sinceId = account.lastArticleId?.dollarLast() ?: ""
- var itemsBody = feverAPI.getItemsSince(sinceId)
- while (itemsBody.items?.isNotEmpty() == true) {
- articleDao.insert(
- *itemsBody.items?.map {
- Article(
+ // Fetch the Fever favicons
+ val faviconsById =
+ feverAPI.getFavicons().favicons?.associateBy { it.id } ?: emptyMap()
+ feedDao.insertOrUpdate(
+ feedsBody.feeds?.map {
+ Feed(
id = accountId.spacerDollar(it.id!!),
- date = it.created_on_time
- ?.run { Date(this * 1000) }
- ?.takeIf { !it.isFuture(preDate) }
- ?: preDate,
- title = it.title.decodeHTML() ?: context.getString(R.string.empty),
- author = it.author,
- rawDescription = it.html ?: "",
- shortDescription = Readability.parseToText(it.html, it.url).take(110),
- fullContent = it.html,
- img = rssHelper.findImg(it.html ?: ""),
- link = it.url ?: "",
- feedId = accountId.spacerDollar(it.feed_id!!),
+ name = it.title.decodeHTML() ?: context.getString(R.string.empty),
+ url = it.url!!,
+ groupId = accountId.spacerDollar(feedsGroupsMap[it.id.toString()]!!),
accountId = accountId,
- isUnread = (it.is_read ?: 0) <= 0,
- isStarred = (it.is_saved ?: 0) > 0,
- updateAt = preDate,
- ).also {
- sinceId = it.id.dollarLast()
- }
- }?.toTypedArray() ?: emptyArray()
+ icon = faviconsById[it.favicon_id]?.data
+ )
+ } ?: emptyList()
)
- if (itemsBody.items?.size!! >= 50) {
- itemsBody = feverAPI.getItemsSince(sinceId)
- } else {
- break
+
+ // Handle empty icon for feeds
+ val noIconFeeds = feedDao.queryNoIcon(accountId)
+ noIconFeeds.forEach {
+ it.icon = rssHelper.queryRssIconLink(it.url)
}
- }
+ feedDao.update(*noIconFeeds.toTypedArray())
- // 4. Synchronize read/unread and starred/un-starred
- val unreadArticleIds = feverAPI.getUnreadItems().unread_item_ids?.split(",")
- val starredArticleIds = feverAPI.getSavedItems().saved_item_ids?.split(",")
- val articleMeta = articleDao.queryMetadataAll(accountId)
- for (meta: ArticleMeta in articleMeta) {
- val articleId = meta.id.dollarLast()
- val shouldBeUnread = unreadArticleIds?.contains(articleId)
- val shouldBeStarred = starredArticleIds?.contains(articleId)
- if (meta.isUnread != shouldBeUnread) {
- articleDao.markAsReadByArticleId(accountId, meta.id, shouldBeUnread ?: true)
+ // 3. Fetch the Fever articles (up to unlimited counts)
+ var sinceId = account.lastArticleId?.dollarLast() ?: ""
+ var itemsBody = feverAPI.getItemsSince(sinceId)
+ while (itemsBody.items?.isNotEmpty() == true) {
+ articleDao.insert(
+ *itemsBody.items?.map {
+ Article(
+ id = accountId.spacerDollar(it.id!!),
+ date = it.created_on_time
+ ?.run { Date(this * 1000) }
+ ?.takeIf { !it.isFuture(preDate) }
+ ?: preDate,
+ title = it.title.decodeHTML() ?: context.getString(R.string.empty),
+ author = it.author,
+ rawDescription = it.html ?: "",
+ shortDescription = Readability.parseToText(it.html, it.url)
+ .take(110),
+ fullContent = it.html,
+ img = rssHelper.findImg(it.html ?: ""),
+ link = it.url ?: "",
+ feedId = accountId.spacerDollar(it.feed_id!!),
+ accountId = accountId,
+ isUnread = (it.is_read ?: 0) <= 0,
+ isStarred = (it.is_saved ?: 0) > 0,
+ updateAt = preDate,
+ ).also {
+ sinceId = it.id.dollarLast()
+ }
+ }?.toTypedArray() ?: emptyArray()
+ )
+ if (itemsBody.items?.size!! >= 50) {
+ itemsBody = feverAPI.getItemsSince(sinceId)
+ } else {
+ break
+ }
}
- if (meta.isStarred != shouldBeStarred) {
- articleDao.markAsStarredByArticleId(accountId, meta.id, shouldBeStarred ?: false)
+
+ // 4. Synchronize read/unread and starred/un-starred
+ val unreadArticleIds = feverAPI.getUnreadItems().unread_item_ids?.split(",")
+ val starredArticleIds = feverAPI.getSavedItems().saved_item_ids?.split(",")
+ val articleMeta = articleDao.queryMetadataAll(accountId)
+ for (meta: ArticleMeta in articleMeta) {
+ val articleId = meta.id.dollarLast()
+ val shouldBeUnread = unreadArticleIds?.contains(articleId)
+ val shouldBeStarred = starredArticleIds?.contains(articleId)
+ if (meta.isUnread != shouldBeUnread) {
+ articleDao.markAsReadByArticleId(accountId, meta.id, shouldBeUnread ?: true)
+ }
+ if (meta.isStarred != shouldBeStarred) {
+ articleDao.markAsStarredByArticleId(
+ accountId,
+ meta.id,
+ shouldBeStarred ?: false
+ )
+ }
}
- }
- // Remove orphaned groups and feeds, after synchronizing the starred/un-starred
- val groupIds = groups.map { it.id }
- groupDao.queryAll(accountId).forEach {
- if (!groupIds.contains(it.id)) {
- super.deleteGroup(it, true)
+ // Remove orphaned groups and feeds, after synchronizing the starred/un-starred
+ val groupIds = groups.map { it.id }
+ groupDao.queryAll(accountId).forEach {
+ if (!groupIds.contains(it.id)) {
+ super.deleteGroup(it, true)
+ }
}
- }
- feedDao.queryAll(accountId).forEach {
- if (!feedsGroupsMap.contains(it.id.dollarLast())) {
- super.deleteFeed(it, true)
+ feedDao.queryAll(accountId).forEach {
+ if (!feedsGroupsMap.contains(it.id.dollarLast())) {
+ super.deleteFeed(it, true)
+ }
}
- }
- Log.i("RLog", "onCompletion: ${System.currentTimeMillis() - preTime}")
- accountDao.update(account.apply {
- updateAt = Date()
- if (sinceId.isNotEmpty()) {
- lastArticleId = accountId.spacerDollar(sinceId)
+ Log.i("RLog", "onCompletion: ${System.currentTimeMillis() - preTime}")
+ accountDao.update(account.apply {
+ updateAt = Date()
+ if (sinceId.isNotEmpty()) {
+ lastArticleId = accountId.spacerDollar(sinceId)
+ }
+ })
+ ListenableWorker.Result.success(SyncWorker.setIsSyncing(false))
+ } catch (e: Exception) {
+ Log.e("RLog", "On sync exception: ${e.message}", e)
+ withContext(mainDispatcher) {
+ context.showToast(e.message)
}
- })
- ListenableWorker.Result.success(SyncWorker.setIsSyncing(false))
- } catch (e: Exception) {
- Log.e("RLog", "On sync exception: ${e.message}", e)
- withContext(mainDispatcher) {
- context.showToast(e.message)
+ ListenableWorker.Result.failure(SyncWorker.setIsSyncing(false))
}
- ListenableWorker.Result.failure(SyncWorker.setIsSyncing(false))
}
- }
override suspend fun markAsRead(
groupId: String?,