mirror of https://github.com/readrops/Readrops.git
Add support for Fever favicons
This commit is contained in:
parent
3c99bafe43
commit
f67f817b82
|
@ -56,7 +56,7 @@ class FeverDataSource(private val service: FeverService) {
|
|||
sinceId = unreadIds.first().toLong()
|
||||
},
|
||||
async { starredIds = service.getStarredItemsIds(body) },
|
||||
async { favicons = listOf() }
|
||||
async { favicons = service.getFavicons(body) }
|
||||
)
|
||||
.awaitAll()
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class FeverDataSource(private val service: FeverService) {
|
|||
async { feverFeeds = service.getFeeds(body) },
|
||||
async { unreadIds = service.getUnreadItemsIds(body) },
|
||||
async { starredIds = service.getStarredItemsIds(body) },
|
||||
async { favicons = listOf() },
|
||||
async { favicons = service.getFavicons(body) },
|
||||
async {
|
||||
items = buildList {
|
||||
var localSinceId = lastSinceId
|
||||
|
|
|
@ -6,9 +6,11 @@ import com.readrops.api.utils.extensions.skipField
|
|||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
data class Favicon(
|
||||
val id: Int,
|
||||
val id: String,
|
||||
val data: ByteArray
|
||||
)
|
||||
|
||||
|
@ -17,6 +19,7 @@ class FeverFaviconsAdapter {
|
|||
@ToJson
|
||||
fun toJson(favicons: List<Favicon>) = ""
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
@SuppressLint("CheckResult")
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): List<Favicon> = with(reader) {
|
||||
|
@ -41,14 +44,14 @@ class FeverFaviconsAdapter {
|
|||
while (hasNext()) {
|
||||
when (selectName(NAMES)) {
|
||||
0 -> id = nextInt()
|
||||
1 -> data = nextString().toByteArray()
|
||||
1 -> data = Base64.decode(nextString().substringAfter("base64,"))
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
if (id > 0 && data != null) {
|
||||
favicons += Favicon(
|
||||
id = id,
|
||||
id = id.toString(),
|
||||
data = data,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,11 +11,12 @@ import coil.ImageLoaderFactory
|
|||
import coil.disk.DiskCache
|
||||
import com.readrops.api.apiModule
|
||||
import com.readrops.app.util.CrashActivity
|
||||
import com.readrops.app.util.FeverFaviconFetcher
|
||||
import com.readrops.db.dbModule
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.logger.Level
|
||||
import kotlin.system.exitProcess
|
||||
|
@ -49,10 +50,11 @@ open class ReadropsApp : Application(), KoinComponent, ImageLoaderFactory {
|
|||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this)
|
||||
.okHttpClient { get() }
|
||||
.components { add(FeverFaviconFetcher.Factory(get())) }
|
||||
.diskCache {
|
||||
DiskCache.Builder()
|
||||
.directory(this.cacheDir.resolve("image_cache"))
|
||||
.maxSizePercent(0.05)
|
||||
.maximumMaxSizeBytes(1024 * 1024 * 100)
|
||||
.build()
|
||||
}
|
||||
.crossfade(true)
|
||||
|
|
|
@ -55,7 +55,10 @@ class FeverRepository(
|
|||
|
||||
SyncResult(
|
||||
items = newItems,
|
||||
feeds = newFeeds
|
||||
feeds = newFeeds,
|
||||
favicons = favicons.associateBy { favicon ->
|
||||
feverFeeds.feeds.find { it.remoteId == favicon.id }!!
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.readrops.app.repositories
|
||||
|
||||
import com.readrops.api.services.fever.adapters.Favicon
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
|
@ -11,7 +12,8 @@ typealias ErrorResult = HashMap<Feed, Exception>
|
|||
|
||||
data class SyncResult(
|
||||
val items: List<Item> = listOf(),
|
||||
val feeds: List<Feed> = listOf()
|
||||
val feeds: List<Feed> = listOf(),
|
||||
val favicons: Map<Feed, Favicon> = emptyMap() // only for Fever
|
||||
)
|
||||
|
||||
interface Repository {
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.app.PendingIntent
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.Action
|
||||
|
@ -22,7 +23,10 @@ import androidx.work.WorkInfo
|
|||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import coil.annotation.ExperimentalCoilApi
|
||||
import coil.imageLoader
|
||||
import com.readrops.api.services.Credentials
|
||||
import com.readrops.api.services.fever.adapters.Favicon
|
||||
import com.readrops.api.utils.AuthInterceptor
|
||||
import com.readrops.app.MainActivity
|
||||
import com.readrops.app.R
|
||||
|
@ -33,6 +37,7 @@ import com.readrops.app.repositories.SyncResult
|
|||
import com.readrops.app.util.FeedColors
|
||||
import com.readrops.app.util.putSerializable
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.account.Account
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
@ -147,9 +152,13 @@ class SyncWorker(
|
|||
syncResults[account] = result.first
|
||||
} else {
|
||||
get<AuthInterceptor>().credentials = Credentials.toCredentials(account)
|
||||
|
||||
val syncResult = repository.synchronize()
|
||||
|
||||
if (syncResult.favicons.isNotEmpty()) {
|
||||
loadFeverFavicons(syncResult.favicons, notificationBuilder)
|
||||
} else {
|
||||
fetchFeedColors(syncResult, notificationBuilder)
|
||||
}
|
||||
|
||||
syncResults[account] = syncResult
|
||||
}
|
||||
|
@ -236,6 +245,50 @@ class SyncWorker(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
private suspend fun loadFeverFavicons(
|
||||
favicons: Map<Feed, Favicon>,
|
||||
notificationBuilder: Builder
|
||||
) {
|
||||
if (notificationManager.areNotificationsEnabled()) {
|
||||
// can't make detailed progress as the favicon might already exist in cache
|
||||
notificationBuilder.setContentTitle("Loading icons and colors")
|
||||
.setProgress(0, 0, true)
|
||||
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
|
||||
}
|
||||
|
||||
val diskCache = applicationContext.imageLoader.diskCache!!
|
||||
|
||||
for ((feed, favicon) in favicons) {
|
||||
val key = "account_${feed.accountId}_feed_${feed.id}"
|
||||
val snapshot = diskCache.openSnapshot(key)
|
||||
|
||||
if (snapshot == null) {
|
||||
try {
|
||||
diskCache.openEditor(key)!!.apply {
|
||||
diskCache.fileSystem.write(data) {
|
||||
write(favicon.data)
|
||||
}
|
||||
commit()
|
||||
}
|
||||
|
||||
database.feedDao().updateFeedIconUrl(feed.id, key)
|
||||
val bitmap =
|
||||
BitmapFactory.decodeByteArray(favicon.data, 0, favicon.data.size)
|
||||
|
||||
if (bitmap != null) {
|
||||
val color = FeedColors.getFeedColor(bitmap)
|
||||
database.feedDao().updateFeedColor(feed.id, color)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "${feed.name}: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
snapshot?.close()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun displaySyncResults(syncResults: Map<Account, SyncResult>) {
|
||||
val notificationContent = SyncAnalyzer(applicationContext, database)
|
||||
.getNotificationContent(syncResults)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.readrops.app.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.palette.graphics.Palette
|
||||
|
@ -19,6 +20,10 @@ object FeedColors : KoinComponent {
|
|||
).execute()
|
||||
|
||||
val bitmap = BitmapFactory.decodeStream(response.body?.byteStream()) ?: return 0
|
||||
return getFeedColor(bitmap)
|
||||
}
|
||||
|
||||
fun getFeedColor(bitmap: Bitmap): Int {
|
||||
val palette = Palette.from(bitmap).generate()
|
||||
|
||||
val dominantSwatch = palette.dominantSwatch
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
package com.readrops.app.util
|
||||
|
||||
import android.util.Patterns
|
||||
import coil.ImageLoader
|
||||
import coil.annotation.ExperimentalCoilApi
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.ImageSource
|
||||
import coil.disk.DiskCache
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
data class FeedKey(val faviconUrl: String?)
|
||||
|
||||
/**
|
||||
* Custom Coil Fetcher to load Feed favicons from either an http source or a file source
|
||||
*/
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
class FeverFaviconFetcher(
|
||||
private val data: FeedKey,
|
||||
private val diskCache: DiskCache,
|
||||
private val okHttpClient: OkHttpClient
|
||||
) : Fetcher {
|
||||
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
return when {
|
||||
data.faviconUrl == null -> null
|
||||
Patterns.WEB_URL.matcher(data.faviconUrl).matches() -> httpLoader()
|
||||
else -> fileLoader()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fileLoader(): FetchResult? {
|
||||
val diskCacheKey = data.faviconUrl!!
|
||||
val snapshot = diskCache.openSnapshot(diskCacheKey)
|
||||
|
||||
return if (snapshot != null) {
|
||||
SourceResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = MIME_TYPE,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun httpLoader(): FetchResult? {
|
||||
val diskCacheKey = data.faviconUrl!!
|
||||
val snapshot = diskCache.openSnapshot(diskCacheKey)
|
||||
|
||||
return if (snapshot != null) {
|
||||
SourceResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = MIME_TYPE,
|
||||
dataSource = DataSource.NETWORK
|
||||
)
|
||||
} else {
|
||||
val request = Request.Builder()
|
||||
.url(diskCacheKey)
|
||||
.build()
|
||||
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
if (!response.isSuccessful || response.code == 304 || response.body == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val httpSnapshot = diskCache.openEditor(diskCacheKey)!!.run {
|
||||
diskCache.fileSystem.write(data) {
|
||||
write(response.body!!.bytes())
|
||||
}
|
||||
|
||||
commitAndOpenSnapshot()
|
||||
}
|
||||
|
||||
return if (httpSnapshot != null) {
|
||||
SourceResult(
|
||||
source = httpSnapshot.toImageSource(),
|
||||
mimeType = MIME_TYPE,
|
||||
dataSource = DataSource.NETWORK
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
|
||||
return ImageSource(
|
||||
file = data,
|
||||
diskCacheKey = this@FeverFaviconFetcher.data.faviconUrl,
|
||||
closeable = this,
|
||||
)
|
||||
}
|
||||
|
||||
class Factory(private val okHttpClient: OkHttpClient) : Fetcher.Factory<FeedKey> {
|
||||
|
||||
override fun create(data: FeedKey, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return FeverFaviconFetcher(data, imageLoader.diskCache!!, okHttpClient)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIME_TYPE = "image/*"
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.util.FeedKey
|
||||
|
||||
@Composable
|
||||
fun FeedIcon(
|
||||
|
@ -16,7 +17,7 @@ fun FeedIcon(
|
|||
size: Dp = 24.dp
|
||||
) {
|
||||
AsyncImage(
|
||||
model = iconUrl,
|
||||
model = FeedKey(iconUrl),
|
||||
error = painterResource(id = R.drawable.ic_rss_feed_grey),
|
||||
placeholder = painterResource(R.drawable.ic_rss_feed_grey),
|
||||
fallback = painterResource(id = R.drawable.ic_rss_feed_grey),
|
||||
|
|
|
@ -70,6 +70,9 @@ abstract class FeedDao : BaseDao<Feed> {
|
|||
@Query("Select * From Feed Where id in (:ids)")
|
||||
abstract suspend fun selectFromIds(ids: List<Int>): List<Feed>
|
||||
|
||||
@Query("Update Feed set icon_url = :iconUrl Where id = :feedId")
|
||||
abstract suspend fun updateFeedIconUrl(feedId: Int, iconUrl: String)
|
||||
|
||||
/**
|
||||
* Insert, update and delete feeds by account
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue