Add articles image preview (#67)
* Add articles image preview * Lowers the pagingItem state
This commit is contained in:
parent
6583f3326c
commit
c79649bb77
|
@ -28,6 +28,15 @@ android {
|
|||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments += [
|
||||
"room.schemaLocation": "$projectDir/schemas".toString(),
|
||||
"room.incremental" : "true"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "channel"
|
||||
|
@ -103,6 +112,8 @@ dependencies {
|
|||
implementation("io.coil-kt:coil-svg:$coil")
|
||||
implementation("io.coil-kt:coil-gif:$coil")
|
||||
|
||||
implementation "org.conscrypt:conscrypt-android:2.5.2"
|
||||
|
||||
// https://square.github.io/okhttp/changelogs/changelog/
|
||||
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.6"
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit2"
|
||||
|
|
321
app/schemas/me.ash.reader.data.source.ReaderDatabase/2.json
Normal file
321
app/schemas/me.ash.reader.data.source.ReaderDatabase/2.json
Normal file
|
@ -0,0 +1,321 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "98462c2e9c32394054102313366e7262",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "account",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `type` INTEGER NOT NULL, `updateAt` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updateAt",
|
||||
"columnName": "updateAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "feed",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT, `url` TEXT NOT NULL, `groupId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `isNotification` INTEGER NOT NULL DEFAULT false, `isFullContent` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`), FOREIGN KEY(`groupId`) REFERENCES `group`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "groupId",
|
||||
"columnName": "groupId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isNotification",
|
||||
"columnName": "isNotification",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isFullContent",
|
||||
"columnName": "isFullContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_feed_groupId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"groupId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_groupId` ON `${TABLE_NAME}` (`groupId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_feed_accountId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"accountId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_accountId` ON `${TABLE_NAME}` (`accountId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "group",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"groupId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "article",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `date` INTEGER NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `rawDescription` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `fullContent` TEXT, `img` TEXT, `link` TEXT NOT NULL, `feedId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `isUnread` INTEGER NOT NULL DEFAULT true, `isStarred` INTEGER NOT NULL DEFAULT false, `isReadLater` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`), FOREIGN KEY(`feedId`) REFERENCES `feed`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "date",
|
||||
"columnName": "date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "author",
|
||||
"columnName": "author",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "rawDescription",
|
||||
"columnName": "rawDescription",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shortDescription",
|
||||
"columnName": "shortDescription",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "fullContent",
|
||||
"columnName": "fullContent",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "img",
|
||||
"columnName": "img",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "link",
|
||||
"columnName": "link",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "feedId",
|
||||
"columnName": "feedId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isUnread",
|
||||
"columnName": "isUnread",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "true"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isStarred",
|
||||
"columnName": "isStarred",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isReadLater",
|
||||
"columnName": "isReadLater",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "false"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_article_feedId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"feedId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_article_feedId` ON `${TABLE_NAME}` (`feedId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_article_accountId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"accountId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_article_accountId` ON `${TABLE_NAME}` (`accountId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "feed",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"feedId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "group",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `accountId` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_group_accountId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"accountId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_group_accountId` ON `${TABLE_NAME}` (`accountId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '98462c2e9c32394054102313366e7262')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -13,10 +13,10 @@
|
|||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/read_you"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Reader">
|
||||
android:theme="@style/Theme.Reader"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
|
|
@ -29,10 +29,18 @@ import me.ash.reader.data.source.AppNetworkDataSource
|
|||
import me.ash.reader.data.source.OpmlLocalDataSource
|
||||
import me.ash.reader.data.source.ReaderDatabase
|
||||
import me.ash.reader.ui.ext.*
|
||||
import org.conscrypt.Conscrypt
|
||||
import java.security.Security
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class App : Application(), Configuration.Provider, ImageLoader {
|
||||
init {
|
||||
// From: https://gitlab.com/spacecowboy/Feeder
|
||||
// Install Conscrypt to handle TLSv1.3 pre Android10
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var readerDatabase: ReaderDatabase
|
||||
|
||||
|
|
|
@ -374,7 +374,7 @@ interface ArticleDao {
|
|||
@Query(
|
||||
"""
|
||||
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
||||
a.shortDescription, a.fullContent, a.link, a.feedId,
|
||||
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
||||
a.accountId, a.isUnread, a.isStarred, a.isReadLater
|
||||
FROM article AS a
|
||||
LEFT JOIN feed AS b ON b.id = a.feedId
|
||||
|
@ -394,7 +394,7 @@ interface ArticleDao {
|
|||
@Query(
|
||||
"""
|
||||
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
||||
a.shortDescription, a.fullContent, a.link, a.feedId,
|
||||
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
||||
a.accountId, a.isUnread, a.isStarred, a.isReadLater
|
||||
FROM article AS a
|
||||
LEFT JOIN feed AS b ON b.id = a.feedId
|
||||
|
@ -416,7 +416,7 @@ interface ArticleDao {
|
|||
@Query(
|
||||
"""
|
||||
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
||||
a.shortDescription, a.fullContent, a.link, a.feedId,
|
||||
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
||||
a.accountId, a.isUnread, a.isStarred, a.isReadLater
|
||||
FROM article AS a
|
||||
LEFT JOIN feed AS b ON b.id = a.feedId
|
||||
|
@ -483,7 +483,7 @@ interface ArticleDao {
|
|||
@Query(
|
||||
"""
|
||||
SELECT a.id, a.date, a.title, a.author, a.rawDescription,
|
||||
a.shortDescription, a.fullContent, a.link, a.feedId,
|
||||
a.shortDescription, a.fullContent, a.img, a.link, a.feedId,
|
||||
a.accountId, a.isUnread, a.isStarred, a.isReadLater
|
||||
FROM article AS a LEFT JOIN feed AS b
|
||||
ON a.feedId = b.id
|
||||
|
@ -503,25 +503,19 @@ interface ArticleDao {
|
|||
)
|
||||
suspend fun queryById(id: String): ArticleWithFeed?
|
||||
|
||||
@Insert
|
||||
suspend fun insert(article: Article): Long
|
||||
|
||||
@Insert
|
||||
suspend fun insertList(articles: List<Article>): List<Long>
|
||||
|
||||
@Update
|
||||
suspend fun update(vararg article: Article)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg article: Article)
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
INSERT INTO article
|
||||
SELECT :id, :date, :title, :author, :rawDescription,
|
||||
:shortDescription, :fullContent, :link, :feedId,
|
||||
:shortDescription, :fullContent, :img, :link, :feedId,
|
||||
:accountId, :isUnread, :isStarred, :isReadLater
|
||||
WHERE NOT EXISTS(SELECT 1 FROM article WHERE link = :link AND accountId = :accountId)
|
||||
"""
|
||||
|
@ -534,6 +528,7 @@ interface ArticleDao {
|
|||
rawDescription: String,
|
||||
shortDescription: String,
|
||||
fullContent: String? = null,
|
||||
img: String? = null,
|
||||
link: String,
|
||||
feedId: String,
|
||||
accountId: Int,
|
||||
|
@ -552,6 +547,7 @@ interface ArticleDao {
|
|||
article.rawDescription,
|
||||
article.shortDescription,
|
||||
article.fullContent,
|
||||
article.img,
|
||||
article.link,
|
||||
article.feedId,
|
||||
article.accountId,
|
||||
|
|
|
@ -32,6 +32,8 @@ data class Article(
|
|||
@ColumnInfo
|
||||
var fullContent: String? = null,
|
||||
@ColumnInfo
|
||||
var img: String? = null,
|
||||
@ColumnInfo
|
||||
val link: String,
|
||||
@ColumnInfo(index = true)
|
||||
val feedId: String,
|
||||
|
|
|
@ -116,13 +116,23 @@ class RssHelper @Inject constructor(
|
|||
.trim(),
|
||||
fullContent = content,
|
||||
link = it.link ?: "",
|
||||
)
|
||||
).apply {
|
||||
img = findImg(rawDescription)
|
||||
}
|
||||
)
|
||||
}
|
||||
a
|
||||
}
|
||||
}
|
||||
|
||||
private fun findImg(rawDescription: String): String? {
|
||||
// From: https://gitlab.com/spacecowboy/Feeder
|
||||
// Using negative lookahead to skip data: urls, being inline base64
|
||||
// And capturing original quote to use as ending quote
|
||||
val regex = """img.*?src=(["'])((?!data).*?)\1""".toRegex(RegexOption.DOT_MATCHES_ALL)
|
||||
return regex.find(rawDescription)?.groupValues?.get(2)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
suspend fun queryRssIcon(
|
||||
feedDao: FeedDao,
|
||||
|
|
|
@ -2,6 +2,8 @@ package me.ash.reader.data.source
|
|||
|
||||
import android.content.Context
|
||||
import androidx.room.*
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import me.ash.reader.data.dao.AccountDao
|
||||
import me.ash.reader.data.dao.ArticleDao
|
||||
import me.ash.reader.data.dao.FeedDao
|
||||
|
@ -14,8 +16,7 @@ import java.util.*
|
|||
|
||||
@Database(
|
||||
entities = [Account::class, Feed::class, Article::class, Group::class],
|
||||
version = 1,
|
||||
exportSchema = false,
|
||||
version = 2,
|
||||
)
|
||||
@TypeConverters(ReaderDatabase.Converters::class)
|
||||
abstract class ReaderDatabase : RoomDatabase() {
|
||||
|
@ -33,7 +34,7 @@ abstract class ReaderDatabase : RoomDatabase() {
|
|||
context.applicationContext,
|
||||
ReaderDatabase::class.java,
|
||||
"Reader"
|
||||
).build().also {
|
||||
).addMigrations(*allMigrations).build().also {
|
||||
instance = it
|
||||
}
|
||||
}
|
||||
|
@ -53,3 +54,18 @@ abstract class ReaderDatabase : RoomDatabase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
val allMigrations = arrayOf(
|
||||
MIGRATION_1_2,
|
||||
)
|
||||
|
||||
@Suppress("ClassName")
|
||||
object MIGRATION_1_2 : Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""
|
||||
ALTER TABLE article ADD COLUMN img TEXT DEFAULT NULL
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -3,6 +3,9 @@ package me.ash.reader.ui.component
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.DefaultAlpha
|
||||
|
@ -31,6 +34,31 @@ fun AsyncImage(
|
|||
@DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val placeholderPainterResource = placeholder?.run { painterResource(this) }
|
||||
val errorPainterResource = error?.run { painterResource(this) }
|
||||
val placeholderPainter by remember {
|
||||
mutableStateOf(
|
||||
placeholderPainterResource?.run {
|
||||
forwardingPainter(
|
||||
painter = this,
|
||||
colorFilter = ColorFilter.tint(color),
|
||||
alpha = 0.1f,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
val errorPainter by remember {
|
||||
mutableStateOf(
|
||||
errorPainterResource?.run {
|
||||
forwardingPainter(
|
||||
painter = this,
|
||||
colorFilter = ColorFilter.tint(color),
|
||||
alpha = 0.1f,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
coil.compose.AsyncImage(
|
||||
modifier = modifier,
|
||||
|
@ -45,20 +73,8 @@ fun AsyncImage(
|
|||
contentDescription = contentDescription,
|
||||
contentScale = contentScale,
|
||||
imageLoader = context.imageLoader,
|
||||
placeholder = placeholder?.let {
|
||||
forwardingPainter(
|
||||
painter = painterResource(it),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||
alpha = 0.5f,
|
||||
)
|
||||
},
|
||||
error = error?.let {
|
||||
forwardingPainter(
|
||||
painter = painterResource(it),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onError),
|
||||
alpha = 0.5f
|
||||
)
|
||||
},
|
||||
placeholder = placeholderPainter,
|
||||
error = errorPainter,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package me.ash.reader.ui.ext
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import kotlin.math.abs
|
||||
|
||||
fun LazyListState.calculateTopBarAnimateValue(start: Float, end: Float): Float =
|
||||
|
@ -12,3 +15,16 @@ fun LazyListState.calculateTopBarAnimateValue(start: Float, end: Float): Float =
|
|||
if (start < end) (start + increase).coerceIn(start, end)
|
||||
else (start - increase).coerceIn(end, start)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T : Any> LazyPagingItems<T>.rememberLazyListState(): LazyListState {
|
||||
// After recreation, LazyPagingItems first return 0 items, then the cached items.
|
||||
// This behavior/issue is resetting the LazyListState scroll position.
|
||||
// Below is a workaround. More info: https://issuetracker.google.com/issues/177245496.
|
||||
return when (itemCount) {
|
||||
// Return a different LazyListState instance.
|
||||
0 -> remember(this) { LazyListState(0, 0) }
|
||||
// Return rememberLazyListState (normal case).
|
||||
else -> androidx.compose.foundation.lazy.rememberLazyListState()
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
|
@ -38,11 +37,7 @@ fun HomeEntry(
|
|||
homeViewModel: HomeViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val viewState = homeViewModel.viewState.collectAsStateValue()
|
||||
val filterState = homeViewModel.filterState.collectAsStateValue()
|
||||
val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
|
||||
|
||||
val navController = rememberAnimatedNavController()
|
||||
|
||||
val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) }
|
||||
|
@ -116,7 +111,6 @@ fun HomeEntry(
|
|||
FlowPage(
|
||||
navController = navController,
|
||||
homeViewModel = homeViewModel,
|
||||
pagingItems = pagingItems,
|
||||
)
|
||||
}
|
||||
animatedComposable(route = "${RouteName.READING}/{articleId}") {
|
||||
|
|
|
@ -3,7 +3,6 @@ package me.ash.reader.ui.page.home
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.paging.*
|
||||
import androidx.work.WorkManager
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
@ -17,7 +16,6 @@ import me.ash.reader.data.repository.SyncWorker
|
|||
import me.ash.reader.ui.page.home.flow.FlowItemView
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val rssRepository: RssRepository,
|
||||
|
@ -112,7 +110,6 @@ data class FilterState(
|
|||
val filter: Filter = Filter.All,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
data class HomeViewState(
|
||||
val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
|
||||
val searchContent: String = "",
|
||||
|
|
|
@ -15,13 +15,16 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.size.Scale
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.data.entity.ArticleWithFeed
|
||||
import me.ash.reader.data.preference.*
|
||||
import me.ash.reader.ui.component.AsyncImage
|
||||
import me.ash.reader.ui.ext.formatAsString
|
||||
|
||||
@Composable
|
||||
|
@ -40,11 +43,12 @@ fun ArticleItem(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.clickable { onClick(articleWithFeed) }
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||
.alpha(if (articleWithFeed.article.isStarred || articleWithFeed.article.isUnread) 1f else 0.5f),
|
||||
) {
|
||||
// Upper
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
|
@ -64,6 +68,7 @@ fun ArticleItem(
|
|||
)
|
||||
}
|
||||
|
||||
// Right
|
||||
if (articleListDate.value) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
@ -95,6 +100,8 @@ fun ArticleItem(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lower
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
|
@ -108,10 +115,12 @@ fun ArticleItem(
|
|||
) {}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
}
|
||||
|
||||
// Article
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
|
||||
// Title
|
||||
Text(
|
||||
text = articleWithFeed.article.title,
|
||||
|
@ -120,6 +129,7 @@ fun ArticleItem(
|
|||
maxLines = if (articleListDesc.value) 2 else 4,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
// Description
|
||||
if (articleListDesc.value && articleWithFeed.article.shortDescription.isNotBlank()) {
|
||||
Text(
|
||||
|
@ -131,6 +141,19 @@ fun ArticleItem(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Image
|
||||
if (articleWithFeed.article.img != null && articleListImage.value) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.size(80.dp)
|
||||
.clip(RoundedCornerShape(20.dp)),
|
||||
data = articleWithFeed.article.img,
|
||||
scale = Scale.FILL,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.flow
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
@ -22,7 +21,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.R
|
||||
|
@ -43,7 +42,6 @@ import me.ash.reader.ui.theme.palette.onDark
|
|||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalFoundationApi::class,
|
||||
com.google.accompanist.pager.ExperimentalPagerApi::class,
|
||||
androidx.compose.ui.ExperimentalComposeUiApi::class,
|
||||
)
|
||||
|
@ -52,8 +50,9 @@ fun FlowPage(
|
|||
navController: NavHostController,
|
||||
flowViewModel: FlowViewModel = hiltViewModel(),
|
||||
homeViewModel: HomeViewModel,
|
||||
pagingItems: LazyPagingItems<FlowItemView>,
|
||||
) {
|
||||
val homeViewView = homeViewModel.viewState.collectAsStateValue()
|
||||
val pagingItems = homeViewView.pagingData.collectAsLazyPagingItems()
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
|
||||
val articleListTonalElevation = LocalFlowArticleListTonalElevation.current
|
||||
|
@ -177,14 +176,6 @@ fun FlowPage(
|
|||
)
|
||||
},
|
||||
content = {
|
||||
// if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount == 0) {
|
||||
// LottieAnimation(
|
||||
// modifier = Modifier
|
||||
// .alpha(0.7f)
|
||||
// .padding(80.dp),
|
||||
// url = "https://assets7.lottiefiles.com/packages/lf20_l4ny0jjm.json",
|
||||
// )
|
||||
// }
|
||||
SwipeRefresh(
|
||||
onRefresh = {
|
||||
if (!isSyncing) {
|
||||
|
|
|
@ -174,10 +174,13 @@ fun FlowPageStyle(
|
|||
}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.article_images),
|
||||
enable = false,
|
||||
onClick = {},
|
||||
onClick = {
|
||||
(!articleListImage).put(context, scope)
|
||||
},
|
||||
) {
|
||||
Switch(activated = false, enable = false)
|
||||
Switch(activated = articleListImage.value) {
|
||||
(!articleListImage).put(context, scope)
|
||||
}
|
||||
}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.article_desc),
|
||||
|
@ -403,6 +406,7 @@ fun FlowPagePreview(
|
|||
accountId = 0,
|
||||
date = Date(),
|
||||
isStarred = true,
|
||||
img = "https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1yZWxhdGVkfDJ8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=800&q=60"
|
||||
),
|
||||
feed = Feed(
|
||||
id = "",
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||
<base-config
|
||||
cleartextTrafficPermitted="true"
|
||||
tools:ignore="InsecureBaseConfiguration" />
|
||||
</network-security-config>
|
Loading…
Reference in New Issue
Block a user