Add feed's icon in articles list

This commit is contained in:
Ash 2022-03-03 20:25:04 +08:00
parent 8d296b4b01
commit bfe1307b27
12 changed files with 238 additions and 143 deletions

View File

@ -19,7 +19,7 @@ data class Feed(
@ColumnInfo
val name: String,
@ColumnInfo
var icon: String? = null,
var icon: ByteArray? = null,
@ColumnInfo
val url: String,
@ColumnInfo(index = true)
@ -31,4 +31,37 @@ data class Feed(
) {
@Ignore
var important: Int? = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Feed
if (id != other.id) return false
if (name != other.name) return false
if (icon != null) {
if (other.icon == null) return false
if (!icon.contentEquals(other.icon)) return false
} else if (other.icon != null) return false
if (url != other.url) return false
if (groupId != other.groupId) return false
if (accountId != other.accountId) return false
if (isFullContent != other.isFullContent) return false
if (important != other.important) return false
return true
}
override fun hashCode(): Int {
var result = id ?: 0
result = 31 * result + name.hashCode()
result = 31 * result + (icon?.contentHashCode() ?: 0)
result = 31 * result + url.hashCode()
result = 31 * result + groupId
result = 31 * result + accountId
result = 31 * result + isFullContent.hashCode()
result = 31 * result + (important ?: 0)
return result
}
}

View File

@ -93,7 +93,6 @@ class RssRepository @Inject constructor(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.setRequiresDeviceIdle(true)
.build()
).addTag("sync").build()
workManager.enqueue(syncWorkerRequest)
@ -161,18 +160,18 @@ class RssRepository @Inject constructor(
val articles = mutableListOf<Article>()
chunked[it].forEach { feed ->
val latest = articleDao.queryLatestByFeedId(accountId, feed.id ?: 0)
// if (feed.icon == null) {
// queryRssIcon(feedDao, feed, latest?.link)
// }
articles.addAll(
queryRssXml(
rssNetworkDataSource,
accountId,
feed,
latest?.title,
).also {
if (feed.icon == null && it.isNotEmpty()) {
queryRssIcon(feedDao, feed, it.first().link)
}
}
)
)
syncState.update {
it.copy(
feedCount = feeds.size,
@ -280,34 +279,44 @@ class RssRepository @Inject constructor(
articleLink: String?,
) {
if (articleLink == null) return
val exe = OkHttpClient()
.newCall(Request.Builder().url(articleLink).build()).execute()
val content = exe.body?.string()
Log.i("rlog", "queryRssIcon: $content")
val execute = OkHttpClient()
.newCall(Request.Builder().url(articleLink).build())
.execute()
val content = execute.body?.string()
val regex =
Regex("""<link(.+?)rel="shortcut icon"(.+?)type="image/x-icon"(.+?)href="(.+?)"""")
Regex("""<link(.+?)rel="shortcut icon"(.+?)href="(.+?)"""")
if (content != null) {
var iconLink = regex
.find(content)
?.groups?.get(4)
?.groups?.get(3)
?.value
Log.i("rlog", "queryRssIcon: $iconLink")
if (iconLink != null) {
if (iconLink.startsWith("//")) {
iconLink = "http:$iconLink"
}
if (iconLink.startsWith("/")) {
val domainRegex =
Regex("""http(s)?://(([\w-]+\.)+\w+(:\d{1,5})?)""")
iconLink =
"http://${domainRegex.find(articleLink)?.groups?.get(2)?.value}$iconLink"
}
saveRssIcon(feedDao, feed, iconLink)
} else {
saveRssIcon(feedDao, feed, "")
// saveRssIcon(feedDao, feed, "")
}
} else {
saveRssIcon(feedDao, feed, "")
// saveRssIcon(feedDao, feed, "")
}
}
private suspend fun saveRssIcon(feedDao: FeedDao, feed: Feed, iconLink: String) {
val execute = OkHttpClient()
.newCall(Request.Builder().url(iconLink).build())
.execute()
feedDao.update(
feed.apply {
icon = iconLink
icon = execute.body?.bytes()
}
)
}

View File

@ -1,11 +1,15 @@
package me.ash.reader.ui.page.home.article
import android.graphics.BitmapFactory
import android.util.Log
import android.widget.Toast
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBackIosNew
import androidx.compose.material.icons.rounded.DoneAll
@ -17,7 +21,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -31,6 +38,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.collect
import me.ash.reader.DateTimeExt
import me.ash.reader.DateTimeExt.toString
import me.ash.reader.R
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.ui.data.Filter
@ -97,7 +105,7 @@ fun ArticlePage(
"${viewState.filterImportant}${filterState.filter.description}"
},
listState = viewState.listState,
startOffset = Offset(20f, 72f),
startOffset = Offset(if (true) 52f else 20f, 72f),
startHeight = 50f,
startTitleFontSize = 24f,
startDescriptionFontSize = 14f,
@ -161,7 +169,7 @@ fun ArticlePage(
item { Spacer(modifier = Modifier.height(40.dp)) }
}
stickyHeader {
ArticleDateHeader(currentItemDay)
ArticleDateHeader(currentItemDay, true)
}
}
item {
@ -214,6 +222,7 @@ private fun ArticleItem(
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
modifier = Modifier.padding(start = 32.dp),
text = articleWithFeed.feed.name,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
@ -232,6 +241,45 @@ private fun ArticleItem(
)
}
Spacer(modifier = modifier.height(1.dp))
if (true) {
Box(
modifier = Modifier
.padding(top = 3.dp)
.size(24.dp)
.border(
2.dp,
MaterialTheme.colorScheme.inverseOnSurface,
RoundedCornerShape(4.dp)
),
) {
if (articleWithFeed.feed.icon == null) {
Icon(
painter = painterResource(id = R.drawable.default_folder),
contentDescription = "icon",
modifier = modifier
.fillMaxSize()
.padding(2.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer,
)
} else {
Image(
painter = BitmapPainter(
BitmapFactory.decodeByteArray(
articleWithFeed.feed.icon,
0,
articleWithFeed.feed.icon!!.size
).asImageBitmap()
),
contentDescription = "icon",
modifier = modifier
.fillMaxSize()
.padding(2.dp),
)
}
}
Spacer(modifier = Modifier.width(8.dp))
}
Column {
Text(
text = articleWithFeed.article.title,
fontSize = 18.sp,
@ -255,9 +303,10 @@ private fun ArticleItem(
}
}
}
}
@Composable
private fun ArticleDateHeader(date: String) {
private fun ArticleDateHeader(date: String, isDisplayIcon: Boolean) {
Row(
modifier = Modifier
.height(28.dp)
@ -269,7 +318,7 @@ private fun ArticleDateHeader(date: String) {
text = date,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(horizontal = 20.dp),
modifier = Modifier.padding(start = (if (isDisplayIcon) 52 else 20).dp),
fontWeight = FontWeight.SemiBold,
)
}

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.page.home.feed
import android.graphics.BitmapFactory
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@ -20,15 +21,15 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.flow.collect
import me.ash.reader.DateTimeExt
import me.ash.reader.DateTimeExt.toString
import me.ash.reader.R
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupWithFeed
@ -239,10 +240,21 @@ private fun ColumnScope.FeedList(
) {
Column(modifier = Modifier.animateContentSize()) {
feeds.forEach { feed ->
Log.i("RLog", "FeedList: ${feed.icon}")
BarButton(
barButtonType = ItemType(
// icon = feed.icon ?: "",
icon = painterResource(id = R.drawable.default_folder),
icon = if (feed.icon == null) {
null
} else {
BitmapPainter(
BitmapFactory.decodeByteArray(
feed.icon,
0,
feed.icon!!.size
).asImageBitmap()
)
},
content = feed.name,
important = feed.important ?: 0
)

View File

@ -168,6 +168,7 @@ private fun Header(
) {
Column(
modifier = Modifier
.fillMaxWidth()
.roundClick {
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(article.link))

View File

@ -0,0 +1,5 @@
package me.ash.reader.ui.util
object Symbol {
const val nothing: String = "null"
}

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.widget
import android.view.HapticFeedbackConstants
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutLinearInEasing
@ -27,6 +28,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -83,7 +85,9 @@ fun AppNavigationBar(
}
}
Divider(modifier = Modifier.alpha(0.3f))
Divider(
modifier = Modifier.alpha(0.3f),
)
Box(
modifier = modifier
.background(MaterialTheme.colorScheme.surface)
@ -141,6 +145,7 @@ private fun FilterBar(
filter: Filter,
onSelected: (Filter) -> Unit = {},
) {
val view = LocalView.current
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center,
@ -168,6 +173,7 @@ private fun FilterBar(
)
.clip(CircleShape)
.clickable(onClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
onSelected(
when (index) {
0 -> Filter.Starred
@ -237,6 +243,7 @@ private fun ReaderBar(
starredOnClick: (afterIsStarred: Boolean) -> Unit = {},
fullContentOnClick: (afterIsFullContent: Boolean) -> Unit = {},
) {
val view = LocalView.current
var fullContent by remember { mutableStateOf(isFullContent) }
Row(
horizontalArrangement = Arrangement.SpaceBetween,
@ -246,6 +253,7 @@ private fun ReaderBar(
.padding(horizontal = 8.dp)
) {
CanBeDisabledIconButton(
modifier = Modifier.size(18.dp),
disabled = disabled,
imageVector = if (isUnread) {
Icons.Rounded.Circle
@ -255,6 +263,7 @@ private fun ReaderBar(
contentDescription = "Mark Unread",
tint = MaterialTheme.colorScheme.primary,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
unreadOnClick(!isUnread)
}
CanBeDisabledIconButton(
@ -268,6 +277,7 @@ private fun ReaderBar(
contentDescription = "Starred",
tint = MaterialTheme.colorScheme.primary,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
starredOnClick(!isStarred)
}
CanBeDisabledIconButton(
@ -277,7 +287,7 @@ private fun ReaderBar(
contentDescription = "Next Article",
tint = MaterialTheme.colorScheme.primary,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
disabled = disabled,
@ -285,7 +295,7 @@ private fun ReaderBar(
contentDescription = "Add Tag",
tint = MaterialTheme.colorScheme.primary,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
disabled = disabled,
@ -298,6 +308,7 @@ private fun ReaderBar(
contentDescription = "Full Content Parsing",
tint = MaterialTheme.colorScheme.primary,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
val afterIsFullContent = !fullContent
fullContent = afterIsFullContent
fullContentOnClick(afterIsFullContent)

View File

@ -1,6 +1,8 @@
package me.ash.reader.ui.widget
import android.graphics.BitmapFactory
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -12,11 +14,17 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.ash.reader.R
import me.ash.reader.ui.util.paddingFixedHorizontal
import me.ash.reader.ui.util.roundClick
@ -43,11 +51,10 @@ fun BarButton(
end = if (barButtonType is FirstExpandType) 2.dp else 10.dp
)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
when (barButtonType) {
is SecondExpandType -> {
Icon(
imageVector = barButtonType.img as ImageVector,
imageVector = barButtonType.img,
contentDescription = "icon",
modifier = Modifier
.padding(end = 4.dp)
@ -62,24 +69,35 @@ fun BarButton(
modifier = modifier
.padding(start = 28.dp, end = 4.dp)
.size(24.dp)
// .background(if (barButtonType.img.isBlank()) MaterialTheme.colorScheme.inversePrimary else Color.Unspecified),
.background(MaterialTheme.colorScheme.inversePrimary),
.background(
if (barButtonType.icon != null) Color.Unspecified
else MaterialTheme.colorScheme.inversePrimary
),
// .background(MaterialTheme.colorScheme.inversePrimary),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
if (barButtonType.icon == null) {
Icon(
// painter = rememberImagePainter(barButtonType.img),
painter = barButtonType.img,
painter = painterResource(id = R.drawable.default_folder),
contentDescription = "icon",
modifier = modifier.fillMaxSize(),
tint = MaterialTheme.colorScheme.onPrimaryContainer,
)
} else {
Image(
painter = barButtonType.icon,
contentDescription = "icon",
modifier = modifier.fillMaxSize(),
)
}
}
}
}
when (barButtonType) {
is ButtonType -> {
AnimatedText(
modifier = Modifier.weight(1f),
text = barButtonType.text,
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
@ -88,6 +106,11 @@ fun BarButton(
}
else -> {
Text(
modifier = Modifier
.weight(1f)
.padding(end = 20.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
text = barButtonType.text,
fontSize = if (barButtonType is FirstExpandType) 22.sp else 18.sp,
fontWeight = if (barButtonType is FirstExpandType) FontWeight.Bold else FontWeight.SemiBold,
@ -96,8 +119,6 @@ fun BarButton(
)
}
}
}
when (barButtonType) {
is ButtonType, is ItemType, is SecondExpandType -> {
AnimatedText(
@ -163,13 +184,15 @@ class SecondExpandType(
class ItemType(
// private val icon: String,
private val icon: Painter,
val icon: BitmapPainter?,
private val content: String,
private val important: Int,
) : BarButtonType {
// override val img: String
override val img: Painter
get() = icon
get() = icon ?: BitmapPainter(
BitmapFactory.decodeByteArray(byteArrayOf(), 0, 0).asImageBitmap()
)
override val text: String
get() = content
override val additional: Any

View File

@ -73,7 +73,7 @@ fun BoxScope.TopTitleBox(
interactionSource = MutableInteractionSource(),
indication = null,
onClickLabel = "回到顶部",
onClick = clickable ?: {}
onClick = clickable
),
contentAlignment = Alignment.Center
) {
@ -86,6 +86,7 @@ fun BoxScope.TopTitleBox(
)
Spacer(modifier = Modifier.height(SpacerHeight.dp))
AnimatedText(
modifier = Modifier.width(200.dp),
text = description,
fontWeight = FontWeight.SemiBold,
fontSize = descriptionFontSize.sp,

View File

@ -1,8 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M0,0H28V28H0V0Z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="44dp"
android:viewportWidth="44"
android:viewportHeight="44" />

View File

@ -1,36 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="647.6362dp"
android:height="632.1738dp"
android:viewportWidth="647.6362"
android:viewportHeight="632.1738">
<path
android:pathData="M411.146,142.174L236.636,142.174a15.018,15.018 0,0 0,-15 15v387.85l-2,0.61 -42.81,13.11a8.007,8.007 0,0 1,-9.99 -5.31L39.496,137.484a8.003,8.003 0,0 1,5.31 -9.99l65.97,-20.2 191.25,-58.54 65.97,-20.2a7.989,7.989 0,0 1,9.99 5.3l32.55,106.32Z"
android:fillColor="#f2f2f2"/>
<path
android:pathData="M449.226,140.174l-39.23,-128.14a16.994,16.994 0,0 0,-21.23 -11.28l-92.75,28.39L104.776,87.694l-92.75,28.4a17.015,17.015 0,0 0,-11.28 21.23l134.08,437.93a17.027,17.027 0,0 0,16.26 12.03,16.789 16.789,0 0,0 4.97,-0.75l63.58,-19.46 2,-0.62v-2.09l-2,0.61 -64.17,19.65a15.015,15.015 0,0 1,-18.73 -9.95l-134.07,-437.94a14.979,14.979 0,0 1,9.95 -18.73l92.75,-28.4 191.24,-58.54 92.75,-28.4a15.156,15.156 0,0 1,4.41 -0.66,15.015 15.015,0 0,1 14.32,10.61l39.05,127.56 0.62,2h2.08Z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M122.681,127.821a9.016,9.016 0,0 1,-8.611 -6.367l-12.88,-42.072a8.999,8.999 0,0 1,5.971 -11.24l175.939,-53.864a9.009,9.009 0,0 1,11.241 5.971l12.88,42.072a9.01,9.01 0,0 1,-5.971 11.241L125.31,127.426A8.976,8.976 0,0 1,122.681 127.821Z"
android:fillColor="#6c63ff"/>
<path
android:pathData="M190.154,24.955m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
android:fillColor="#6c63ff"/>
<path
android:pathData="M190.154,24.955m-12.665,0a12.665,12.665 0,1 1,25.329 0a12.665,12.665 0,1 1,-25.329 0"
android:fillColor="#fff"/>
<path
android:pathData="M602.636,582.174h-338a8.51,8.51 0,0 1,-8.5 -8.5v-405a8.51,8.51 0,0 1,8.5 -8.5h338a8.51,8.51 0,0 1,8.5 8.5v405A8.51,8.51 0,0 1,602.636 582.174Z"
android:fillColor="#e6e6e6"/>
<path
android:pathData="M447.136,140.174h-210.5a17.024,17.024 0,0 0,-17 17v407.8l2,-0.61v-407.19a15.018,15.018 0,0 1,15 -15L447.756,142.174ZM630.636,140.174h-394a17.024,17.024 0,0 0,-17 17v458a17.024,17.024 0,0 0,17 17h394a17.024,17.024 0,0 0,17 -17v-458A17.024,17.024 0,0 0,630.636 140.174ZM645.636,615.174a15.018,15.018 0,0 1,-15 15h-394a15.018,15.018 0,0 1,-15 -15v-458a15.018,15.018 0,0 1,15 -15h394a15.018,15.018 0,0 1,15 15Z"
android:fillColor="#3f3d56"/>
<path
android:pathData="M525.636,184.174h-184a9.01,9.01 0,0 1,-9 -9v-44a9.01,9.01 0,0 1,9 -9h184a9.01,9.01 0,0 1,9 9v44A9.01,9.01 0,0 1,525.636 184.174Z"
android:fillColor="#6c63ff"/>
<path
android:pathData="M433.636,105.174m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
android:fillColor="#6c63ff"/>
<path
android:pathData="M433.636,105.174m-12.182,0a12.182,12.182 0,1 1,24.364 0a12.182,12.182 0,1 1,-24.364 0"
android:fillColor="#fff"/>
</vector>