Add articles image preview (#67)

* Add articles image preview

* Lowers the pagingItem state
This commit is contained in:
Ashinch 2022-05-13 23:21:21 +08:00 committed by GitHub
parent 6583f3326c
commit c79649bb77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 464 additions and 65 deletions

View File

@ -28,6 +28,15 @@ android {
vectorDrawables { vectorDrawables {
useSupportLibrary true useSupportLibrary true
} }
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation": "$projectDir/schemas".toString(),
"room.incremental" : "true"
]
}
}
} }
flavorDimensions "channel" flavorDimensions "channel"
@ -103,6 +112,8 @@ dependencies {
implementation("io.coil-kt:coil-svg:$coil") implementation("io.coil-kt:coil-svg:$coil")
implementation("io.coil-kt:coil-gif:$coil") implementation("io.coil-kt:coil-gif:$coil")
implementation "org.conscrypt:conscrypt-android:2.5.2"
// https://square.github.io/okhttp/changelogs/changelog/ // https://square.github.io/okhttp/changelogs/changelog/
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.6" implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.6"
implementation "com.squareup.retrofit2:retrofit:$retrofit2" implementation "com.squareup.retrofit2:retrofit:$retrofit2"

View 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')"
]
}
}

View File

@ -5,18 +5,18 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- Disable automatic updates in F-Droid --> <!-- Disable automatic updates in F-Droid -->
<!-- <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />--> <!-- <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />-->
<application <application
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/read_you" android:label="@string/read_you"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Reader"> android:theme="@style/Theme.Reader"
android:usesCleartextTraffic="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@ -29,10 +29,18 @@ import me.ash.reader.data.source.AppNetworkDataSource
import me.ash.reader.data.source.OpmlLocalDataSource import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.data.source.ReaderDatabase import me.ash.reader.data.source.ReaderDatabase
import me.ash.reader.ui.ext.* import me.ash.reader.ui.ext.*
import org.conscrypt.Conscrypt
import java.security.Security
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
class App : Application(), Configuration.Provider, ImageLoader { 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 @Inject
lateinit var readerDatabase: ReaderDatabase lateinit var readerDatabase: ReaderDatabase

View File

@ -374,7 +374,7 @@ interface ArticleDao {
@Query( @Query(
""" """
SELECT a.id, a.date, a.title, a.author, a.rawDescription, 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 a.accountId, a.isUnread, a.isStarred, a.isReadLater
FROM article AS a FROM article AS a
LEFT JOIN feed AS b ON b.id = a.feedId LEFT JOIN feed AS b ON b.id = a.feedId
@ -394,7 +394,7 @@ interface ArticleDao {
@Query( @Query(
""" """
SELECT a.id, a.date, a.title, a.author, a.rawDescription, 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 a.accountId, a.isUnread, a.isStarred, a.isReadLater
FROM article AS a FROM article AS a
LEFT JOIN feed AS b ON b.id = a.feedId LEFT JOIN feed AS b ON b.id = a.feedId
@ -416,7 +416,7 @@ interface ArticleDao {
@Query( @Query(
""" """
SELECT a.id, a.date, a.title, a.author, a.rawDescription, 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 a.accountId, a.isUnread, a.isStarred, a.isReadLater
FROM article AS a FROM article AS a
LEFT JOIN feed AS b ON b.id = a.feedId LEFT JOIN feed AS b ON b.id = a.feedId
@ -483,7 +483,7 @@ interface ArticleDao {
@Query( @Query(
""" """
SELECT a.id, a.date, a.title, a.author, a.rawDescription, 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 a.accountId, a.isUnread, a.isStarred, a.isReadLater
FROM article AS a LEFT JOIN feed AS b FROM article AS a LEFT JOIN feed AS b
ON a.feedId = b.id ON a.feedId = b.id
@ -503,25 +503,19 @@ interface ArticleDao {
) )
suspend fun queryById(id: String): ArticleWithFeed? suspend fun queryById(id: String): ArticleWithFeed?
@Insert
suspend fun insert(article: Article): Long
@Insert @Insert
suspend fun insertList(articles: List<Article>): List<Long> suspend fun insertList(articles: List<Article>): List<Long>
@Update @Update
suspend fun update(vararg article: Article) suspend fun update(vararg article: Article)
@Delete
suspend fun delete(vararg article: Article)
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
@Transaction @Transaction
@Query( @Query(
""" """
INSERT INTO article INSERT INTO article
SELECT :id, :date, :title, :author, :rawDescription, SELECT :id, :date, :title, :author, :rawDescription,
:shortDescription, :fullContent, :link, :feedId, :shortDescription, :fullContent, :img, :link, :feedId,
:accountId, :isUnread, :isStarred, :isReadLater :accountId, :isUnread, :isStarred, :isReadLater
WHERE NOT EXISTS(SELECT 1 FROM article WHERE link = :link AND accountId = :accountId) WHERE NOT EXISTS(SELECT 1 FROM article WHERE link = :link AND accountId = :accountId)
""" """
@ -534,6 +528,7 @@ interface ArticleDao {
rawDescription: String, rawDescription: String,
shortDescription: String, shortDescription: String,
fullContent: String? = null, fullContent: String? = null,
img: String? = null,
link: String, link: String,
feedId: String, feedId: String,
accountId: Int, accountId: Int,
@ -552,6 +547,7 @@ interface ArticleDao {
article.rawDescription, article.rawDescription,
article.shortDescription, article.shortDescription,
article.fullContent, article.fullContent,
article.img,
article.link, article.link,
article.feedId, article.feedId,
article.accountId, article.accountId,

View File

@ -32,6 +32,8 @@ data class Article(
@ColumnInfo @ColumnInfo
var fullContent: String? = null, var fullContent: String? = null,
@ColumnInfo @ColumnInfo
var img: String? = null,
@ColumnInfo
val link: String, val link: String,
@ColumnInfo(index = true) @ColumnInfo(index = true)
val feedId: String, val feedId: String,

View File

@ -116,13 +116,23 @@ class RssHelper @Inject constructor(
.trim(), .trim(),
fullContent = content, fullContent = content,
link = it.link ?: "", link = it.link ?: "",
) ).apply {
img = findImg(rawDescription)
}
) )
} }
a 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) @Throws(Exception::class)
suspend fun queryRssIcon( suspend fun queryRssIcon(
feedDao: FeedDao, feedDao: FeedDao,

View File

@ -2,6 +2,8 @@ package me.ash.reader.data.source
import android.content.Context import android.content.Context
import androidx.room.* 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.AccountDao
import me.ash.reader.data.dao.ArticleDao import me.ash.reader.data.dao.ArticleDao
import me.ash.reader.data.dao.FeedDao import me.ash.reader.data.dao.FeedDao
@ -14,8 +16,7 @@ import java.util.*
@Database( @Database(
entities = [Account::class, Feed::class, Article::class, Group::class], entities = [Account::class, Feed::class, Article::class, Group::class],
version = 1, version = 2,
exportSchema = false,
) )
@TypeConverters(ReaderDatabase.Converters::class) @TypeConverters(ReaderDatabase.Converters::class)
abstract class ReaderDatabase : RoomDatabase() { abstract class ReaderDatabase : RoomDatabase() {
@ -33,7 +34,7 @@ abstract class ReaderDatabase : RoomDatabase() {
context.applicationContext, context.applicationContext,
ReaderDatabase::class.java, ReaderDatabase::class.java,
"Reader" "Reader"
).build().also { ).addMigrations(*allMigrations).build().also {
instance = it instance = it
} }
} }
@ -52,4 +53,19 @@ abstract class ReaderDatabase : RoomDatabase() {
return date?.time return date?.time
} }
} }
}
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()
)
}
} }

View File

@ -3,6 +3,9 @@ package me.ash.reader.ui.component
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.DefaultAlpha
@ -31,6 +34,31 @@ fun AsyncImage(
@DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp, @DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp,
) { ) {
val context = LocalContext.current 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( coil.compose.AsyncImage(
modifier = modifier, modifier = modifier,
@ -45,20 +73,8 @@ fun AsyncImage(
contentDescription = contentDescription, contentDescription = contentDescription,
contentScale = contentScale, contentScale = contentScale,
imageLoader = context.imageLoader, imageLoader = context.imageLoader,
placeholder = placeholder?.let { placeholder = placeholderPainter,
forwardingPainter( error = errorPainter,
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
)
},
) )
} }

View File

@ -1,6 +1,9 @@
package me.ash.reader.ui.ext package me.ash.reader.ui.ext
import androidx.compose.foundation.lazy.LazyListState 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 import kotlin.math.abs
fun LazyListState.calculateTopBarAnimateValue(start: Float, end: Float): Float = 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) if (start < end) (start + increase).coerceIn(start, end)
else (start - increase).coerceIn(end, start) 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()
}
}

View File

@ -9,7 +9,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
@ -38,11 +37,7 @@ fun HomeEntry(
homeViewModel: HomeViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = homeViewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue() val filterState = homeViewModel.filterState.collectAsStateValue()
val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
val navController = rememberAnimatedNavController() val navController = rememberAnimatedNavController()
val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) } val intent by rememberSaveable { mutableStateOf(context.findActivity()?.intent) }
@ -116,7 +111,6 @@ fun HomeEntry(
FlowPage( FlowPage(
navController = navController, navController = navController,
homeViewModel = homeViewModel, homeViewModel = homeViewModel,
pagingItems = pagingItems,
) )
} }
animatedComposable(route = "${RouteName.READING}/{articleId}") { animatedComposable(route = "${RouteName.READING}/{articleId}") {

View File

@ -3,7 +3,6 @@ package me.ash.reader.ui.page.home
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.paging.* import androidx.paging.*
import androidx.work.WorkManager import androidx.work.WorkManager
import com.google.accompanist.pager.ExperimentalPagerApi
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* 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 me.ash.reader.ui.page.home.flow.FlowItemView
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalPagerApi::class)
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject constructor( class HomeViewModel @Inject constructor(
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
@ -112,7 +110,6 @@ data class FilterState(
val filter: Filter = Filter.All, val filter: Filter = Filter.All,
) )
@OptIn(ExperimentalPagerApi::class)
data class HomeViewState( data class HomeViewState(
val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(), val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
val searchContent: String = "", val searchContent: String = "",

View File

@ -15,13 +15,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.size.Scale
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.preference.* import me.ash.reader.data.preference.*
import me.ash.reader.ui.component.AsyncImage
import me.ash.reader.ui.ext.formatAsString import me.ash.reader.ui.ext.formatAsString
@Composable @Composable
@ -40,11 +43,12 @@ fun ArticleItem(
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp)
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(20.dp))
.clickable { onClick(articleWithFeed) } .clickable { onClick(articleWithFeed) }
.padding(horizontal = 12.dp, vertical = 12.dp) .padding(horizontal = 12.dp, vertical = 12.dp)
.alpha(if (articleWithFeed.article.isStarred || articleWithFeed.article.isUnread) 1f else 0.5f), .alpha(if (articleWithFeed.article.isStarred || articleWithFeed.article.isUnread) 1f else 0.5f),
) { ) {
// Upper
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@ -64,6 +68,7 @@ fun ArticleItem(
) )
} }
// Right
if (articleListDate.value) { if (articleListDate.value) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -95,6 +100,8 @@ fun ArticleItem(
} }
} }
} }
// Lower
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
@ -108,10 +115,12 @@ fun ArticleItem(
) {} ) {}
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
} }
// Article // Article
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.weight(1f),
) { ) {
// Title // Title
Text( Text(
text = articleWithFeed.article.title, text = articleWithFeed.article.title,
@ -120,6 +129,7 @@ fun ArticleItem(
maxLines = if (articleListDesc.value) 2 else 4, maxLines = if (articleListDesc.value) 2 else 4,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
// Description // Description
if (articleListDesc.value && articleWithFeed.article.shortDescription.isNotBlank()) { if (articleListDesc.value && articleWithFeed.article.shortDescription.isNotBlank()) {
Text( 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,
)
}
} }
} }
} }

View File

@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.flow
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -22,7 +21,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
@ -43,7 +42,6 @@ import me.ash.reader.ui.theme.palette.onDark
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalFoundationApi::class,
com.google.accompanist.pager.ExperimentalPagerApi::class, com.google.accompanist.pager.ExperimentalPagerApi::class,
androidx.compose.ui.ExperimentalComposeUiApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class,
) )
@ -52,8 +50,9 @@ fun FlowPage(
navController: NavHostController, navController: NavHostController,
flowViewModel: FlowViewModel = hiltViewModel(), flowViewModel: FlowViewModel = hiltViewModel(),
homeViewModel: HomeViewModel, homeViewModel: HomeViewModel,
pagingItems: LazyPagingItems<FlowItemView>,
) { ) {
val homeViewView = homeViewModel.viewState.collectAsStateValue()
val pagingItems = homeViewView.pagingData.collectAsLazyPagingItems()
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val topBarTonalElevation = LocalFlowTopBarTonalElevation.current val topBarTonalElevation = LocalFlowTopBarTonalElevation.current
val articleListTonalElevation = LocalFlowArticleListTonalElevation.current val articleListTonalElevation = LocalFlowArticleListTonalElevation.current
@ -177,14 +176,6 @@ fun FlowPage(
) )
}, },
content = { 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( SwipeRefresh(
onRefresh = { onRefresh = {
if (!isSyncing) { if (!isSyncing) {

View File

@ -174,10 +174,13 @@ fun FlowPageStyle(
} }
SettingItem( SettingItem(
title = stringResource(R.string.article_images), 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( SettingItem(
title = stringResource(R.string.article_desc), title = stringResource(R.string.article_desc),
@ -403,6 +406,7 @@ fun FlowPagePreview(
accountId = 0, accountId = 0,
date = Date(), date = Date(),
isStarred = true, 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( feed = Feed(
id = "", id = "",

View File

@ -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>