Add feed's favicon from UI Avatars (#70)
* Add feed's favicon from UI Avatars * Fix backwards writes
This commit is contained in:
parent
c79649bb77
commit
f76abd98be
|
@ -108,6 +108,7 @@ dependencies {
|
||||||
implementation "com.rometools:rome:$rome"
|
implementation "com.rometools:rome:$rome"
|
||||||
|
|
||||||
// https://coil-kt.github.io/coil/changelog/
|
// https://coil-kt.github.io/coil/changelog/
|
||||||
|
implementation("io.coil-kt:coil-base:$coil")
|
||||||
implementation("io.coil-kt:coil-compose:$coil")
|
implementation("io.coil-kt:coil-compose:$coil")
|
||||||
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")
|
||||||
|
|
|
@ -1,24 +1,11 @@
|
||||||
package me.ash.reader
|
package me.ash.reader
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.hilt.work.HiltWorkerFactory
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import coil.ComponentRegistry
|
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.decode.DataSource
|
|
||||||
import coil.decode.GifDecoder
|
|
||||||
import coil.decode.ImageDecoderDecoder
|
|
||||||
import coil.decode.SvgDecoder
|
|
||||||
import coil.disk.DiskCache
|
|
||||||
import coil.memory.MemoryCache
|
|
||||||
import coil.request.*
|
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -29,12 +16,16 @@ 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 okhttp3.Cache
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
|
import java.io.File
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class App : Application(), Configuration.Provider, ImageLoader {
|
class App : Application(), Configuration.Provider {
|
||||||
init {
|
init {
|
||||||
// From: https://gitlab.com/spacecowboy/Feeder
|
// From: https://gitlab.com/spacecowboy/Feeder
|
||||||
// Install Conscrypt to handle TLSv1.3 pre Android10
|
// Install Conscrypt to handle TLSv1.3 pre Android10
|
||||||
|
@ -88,6 +79,9 @@ class App : Application(), Configuration.Provider, ImageLoader {
|
||||||
@DispatcherDefault
|
@DispatcherDefault
|
||||||
lateinit var dispatcherDefault: CoroutineDispatcher
|
lateinit var dispatcherDefault: CoroutineDispatcher
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var imageLoader: ImageLoader
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
CrashHandler(this)
|
CrashHandler(this)
|
||||||
|
@ -130,58 +124,29 @@ class App : Application(), Configuration.Provider, ImageLoader {
|
||||||
.setWorkerFactory(workerFactory)
|
.setWorkerFactory(workerFactory)
|
||||||
.setMinimumLoggingLevel(android.util.Log.DEBUG)
|
.setMinimumLoggingLevel(android.util.Log.DEBUG)
|
||||||
.build()
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
override val components: ComponentRegistry
|
fun cachingHttpClient(
|
||||||
get() = ComponentRegistry.Builder()
|
cacheDirectory: File? = null,
|
||||||
.add(SvgDecoder.Factory())
|
cacheSize: Long = 10L * 1024L * 1024L,
|
||||||
.add(
|
trustAllCerts: Boolean = true,
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
connectTimeoutSecs: Long = 30L,
|
||||||
ImageDecoderDecoder.Factory()
|
readTimeoutSecs: Long = 30L
|
||||||
} else {
|
): OkHttpClient {
|
||||||
GifDecoder.Factory()
|
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
|
||||||
}
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
override val defaults: DefaultRequestOptions
|
|
||||||
get() = DefaultRequestOptions()
|
|
||||||
override val diskCache: DiskCache
|
|
||||||
get() = DiskCache.Builder()
|
|
||||||
.directory(cacheDir.resolve("images"))
|
|
||||||
.maxSizePercent(0.02)
|
|
||||||
.build()
|
|
||||||
override val memoryCache: MemoryCache
|
|
||||||
get() = MemoryCache.Builder(this)
|
|
||||||
.maxSizePercent(0.25)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun enqueue(request: ImageRequest): Disposable {
|
if (cacheDirectory != null) {
|
||||||
// Always call onStart before onSuccess.
|
builder.cache(Cache(cacheDirectory, cacheSize))
|
||||||
request.target?.onStart(request.placeholder)
|
|
||||||
val result = ColorDrawable(Color.BLACK)
|
|
||||||
request.target?.onSuccess(result)
|
|
||||||
return object : Disposable {
|
|
||||||
override val job = CompletableDeferred(newResult(request, result))
|
|
||||||
override val isDisposed get() = true
|
|
||||||
override fun dispose() {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute(request: ImageRequest): ImageResult {
|
builder
|
||||||
return newResult(request, ColorDrawable(Color.BLACK))
|
.connectTimeout(connectTimeoutSecs, TimeUnit.SECONDS)
|
||||||
}
|
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
|
||||||
|
.followRedirects(true)
|
||||||
|
|
||||||
override fun newBuilder(): ImageLoader.Builder {
|
// if (trustAllCerts) {
|
||||||
throw UnsupportedOperationException()
|
// builder.trustAllCerts()
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun shutdown() {
|
return builder.build()
|
||||||
}
|
|
||||||
|
|
||||||
private fun newResult(request: ImageRequest, drawable: Drawable): SuccessResult {
|
|
||||||
return SuccessResult(
|
|
||||||
drawable = drawable,
|
|
||||||
request = request,
|
|
||||||
dataSource = DataSource.MEMORY_CACHE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -4,16 +4,22 @@ import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.profileinstaller.ProfileInstallerInitializer
|
import androidx.profileinstaller.ProfileInstallerInitializer
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.compose.LocalImageLoader
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import me.ash.reader.data.preference.LanguagesPreference
|
import me.ash.reader.data.preference.LanguagesPreference
|
||||||
import me.ash.reader.data.preference.SettingsProvider
|
import me.ash.reader.data.preference.SettingsProvider
|
||||||
import me.ash.reader.ui.ext.languages
|
import me.ash.reader.ui.ext.languages
|
||||||
import me.ash.reader.ui.page.common.HomeEntry
|
import me.ash.reader.ui.page.common.HomeEntry
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
@Inject
|
||||||
|
lateinit var imageLoader: ImageLoader
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -27,8 +33,12 @@ class MainActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
SettingsProvider {
|
CompositionLocalProvider(
|
||||||
HomeEntry()
|
LocalImageLoader provides imageLoader,
|
||||||
|
) {
|
||||||
|
SettingsProvider {
|
||||||
|
HomeEntry()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ data class Article(
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
var fullContent: String? = null,
|
var fullContent: String? = null,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
var img: String? = null,
|
val img: String? = null,
|
||||||
@ColumnInfo
|
@ColumnInfo
|
||||||
val link: String,
|
val link: String,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
package me.ash.reader.data.module
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.decode.GifDecoder
|
||||||
|
import coil.decode.ImageDecoderDecoder
|
||||||
|
import coil.decode.SvgDecoder
|
||||||
|
import coil.disk.DiskCache
|
||||||
|
import coil.memory.MemoryCache
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import me.ash.reader.cachingHttpClient
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object ImageLoaderModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideImageLoader(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
): ImageLoader {
|
||||||
|
return ImageLoader.Builder(context)
|
||||||
|
.okHttpClient(
|
||||||
|
okHttpClient = cachingHttpClient(
|
||||||
|
cacheDirectory = context.cacheDir.resolve("http")
|
||||||
|
).newBuilder()
|
||||||
|
//.addNetworkInterceptor(UserAgentInterceptor)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.dispatcher(Dispatchers.Default) // This slightly improves scrolling performance
|
||||||
|
.components{
|
||||||
|
add(SvgDecoder.Factory())
|
||||||
|
add(
|
||||||
|
if (SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
ImageDecoderDecoder.Factory()
|
||||||
|
} else {
|
||||||
|
GifDecoder.Factory()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.diskCache(
|
||||||
|
DiskCache.Builder()
|
||||||
|
.directory(context.cacheDir.resolve("images"))
|
||||||
|
.maxSizePercent(0.02)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.memoryCache(
|
||||||
|
MemoryCache.Builder(context)
|
||||||
|
.maxSizePercent(0.25)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -115,10 +115,9 @@ class RssHelper @Inject constructor(
|
||||||
.take(100)
|
.take(100)
|
||||||
.trim(),
|
.trim(),
|
||||||
fullContent = content,
|
fullContent = content,
|
||||||
|
img = findImg((desc ?: content) ?: ""),
|
||||||
link = it.link ?: "",
|
link = it.link ?: "",
|
||||||
).apply {
|
)
|
||||||
img = findImg(rawDescription)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
a
|
a
|
||||||
|
@ -130,7 +129,8 @@ class RssHelper @Inject constructor(
|
||||||
// Using negative lookahead to skip data: urls, being inline base64
|
// Using negative lookahead to skip data: urls, being inline base64
|
||||||
// And capturing original quote to use as ending quote
|
// And capturing original quote to use as ending quote
|
||||||
val regex = """img.*?src=(["'])((?!data).*?)\1""".toRegex(RegexOption.DOT_MATCHES_ALL)
|
val regex = """img.*?src=(["'])((?!data).*?)\1""".toRegex(RegexOption.DOT_MATCHES_ALL)
|
||||||
return regex.find(rawDescription)?.groupValues?.get(2)
|
// Base64 encoded images can be quite large - and crash database cursors
|
||||||
|
return regex.find(rawDescription)?.groupValues?.get(2)?.takeIf { !it.startsWith("data:") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
|
|
@ -3,9 +3,7 @@ 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.Immutable
|
||||||
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
|
||||||
|
@ -14,7 +12,7 @@ import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import coil.imageLoader
|
import coil.compose.LocalImageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.size.Precision
|
import coil.size.Precision
|
||||||
import coil.size.Scale
|
import coil.size.Scale
|
||||||
|
@ -33,37 +31,10 @@ fun AsyncImage(
|
||||||
@DrawableRes placeholder: Int? = R.drawable.ic_hourglass_empty_black_24dp,
|
@DrawableRes placeholder: Int? = R.drawable.ic_hourglass_empty_black_24dp,
|
||||||
@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 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,
|
||||||
model = ImageRequest
|
model = ImageRequest
|
||||||
.Builder(context)
|
.Builder(LocalContext.current)
|
||||||
.data(data)
|
.data(data)
|
||||||
.crossfade(true)
|
.crossfade(true)
|
||||||
.scale(scale)
|
.scale(scale)
|
||||||
|
@ -72,9 +43,21 @@ fun AsyncImage(
|
||||||
.build(),
|
.build(),
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
imageLoader = context.imageLoader,
|
imageLoader = LocalImageLoader.current,
|
||||||
placeholder = placeholderPainter,
|
placeholder = placeholder?.run {
|
||||||
error = errorPainter,
|
forwardingPainter(
|
||||||
|
painter = painterResource(this),
|
||||||
|
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||||
|
alpha = 0.1f,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
error = error?.run {
|
||||||
|
forwardingPainter(
|
||||||
|
painter = painterResource(this),
|
||||||
|
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||||
|
alpha = 0.1f,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,12 +73,14 @@ fun forwardingPainter(
|
||||||
onDraw: DrawScope.(ForwardingDrawInfo) -> Unit = DefaultOnDraw,
|
onDraw: DrawScope.(ForwardingDrawInfo) -> Unit = DefaultOnDraw,
|
||||||
): Painter = ForwardingPainter(painter, alpha, colorFilter, onDraw)
|
): Painter = ForwardingPainter(painter, alpha, colorFilter, onDraw)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
data class ForwardingDrawInfo(
|
data class ForwardingDrawInfo(
|
||||||
val painter: Painter,
|
val painter: Painter,
|
||||||
val alpha: Float,
|
val alpha: Float,
|
||||||
val colorFilter: ColorFilter?,
|
val colorFilter: ColorFilter?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
private class ForwardingPainter(
|
private class ForwardingPainter(
|
||||||
private val painter: Painter,
|
private val painter: Painter,
|
||||||
private var alpha: Float,
|
private var alpha: Float,
|
||||||
|
|
45
app/src/main/java/me/ash/reader/ui/page/home/FeedIcon.kt
Normal file
45
app/src/main/java/me/ash/reader/ui/page/home/FeedIcon.kt
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package me.ash.reader.ui.page.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FeedIcon(
|
||||||
|
feedName: String,
|
||||||
|
size: Dp = 20.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// val url by remember {
|
||||||
|
// mutableStateOf(
|
||||||
|
// "https://ui-avatars.com/api/?length=1&background=random&name=${
|
||||||
|
// URLEncoder.encode(
|
||||||
|
// feedName,
|
||||||
|
// Charsets.UTF_8.toString()
|
||||||
|
// )
|
||||||
|
// }"
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// AsyncImage(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .size(size)
|
||||||
|
// .clip(CircleShape),
|
||||||
|
// contentDescription = feedName,
|
||||||
|
// data = url,
|
||||||
|
// placeholder = null,
|
||||||
|
// )
|
||||||
|
}
|
|
@ -60,7 +60,7 @@ class HomeViewModel @Inject constructor(
|
||||||
private fun fetchArticles() {
|
private fun fetchArticles() {
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
pagingData = Pager(PagingConfig(pageSize = 15)) {
|
pagingData = Pager(PagingConfig(pageSize = 50)) {
|
||||||
if (_viewState.value.searchContent.isNotBlank()) {
|
if (_viewState.value.searchContent.isNotBlank()) {
|
||||||
rssRepository.get().searchArticles(
|
rssRepository.get().searchArticles(
|
||||||
content = _viewState.value.searchContent.trim(),
|
content = _viewState.value.searchContent.trim(),
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
package me.ash.reader.ui.page.home.feeds
|
package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
@ -20,6 +20,7 @@ import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import me.ash.reader.data.entity.Feed
|
import me.ash.reader.data.entity.Feed
|
||||||
|
import me.ash.reader.ui.page.home.FeedIcon
|
||||||
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewAction
|
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewAction
|
||||||
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewModel
|
import me.ash.reader.ui.page.home.feeds.option.feed.FeedOptionViewModel
|
||||||
import kotlin.math.ln
|
import kotlin.math.ln
|
||||||
|
@ -30,7 +31,6 @@ import kotlin.math.ln
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun FeedItem(
|
fun FeedItem(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
feed: Feed,
|
feed: Feed,
|
||||||
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
|
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
|
||||||
tonalElevation: Dp,
|
tonalElevation: Dp,
|
||||||
|
@ -38,6 +38,11 @@ fun FeedItem(
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val tonalElevationAlpha by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
(ln(tonalElevation.value + 1.4f) + 2f) / 100f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -63,12 +68,7 @@ fun FeedItem(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.weight(1f)) {
|
Row(modifier = Modifier.weight(1f)) {
|
||||||
Row(
|
FeedIcon(feed.name)
|
||||||
modifier = Modifier
|
|
||||||
.size(20.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
|
|
||||||
) {}
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
|
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
|
||||||
text = feed.name,
|
text = feed.name,
|
||||||
|
@ -78,10 +78,10 @@ fun FeedItem(
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (feed.important ?: 0 != 0) {
|
if ((feed.important ?: 0) != 0) {
|
||||||
Badge(
|
Badge(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
|
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(
|
||||||
alpha = (ln(tonalElevation.value + 1.4f) + 2f) / 100f
|
alpha = tonalElevationAlpha
|
||||||
),
|
),
|
||||||
contentColor = MaterialTheme.colorScheme.outline,
|
contentColor = MaterialTheme.colorScheme.outline,
|
||||||
content = {
|
content = {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.ExpandLess
|
import androidx.compose.material.icons.rounded.ExpandLess
|
||||||
import androidx.compose.material.icons.rounded.ExpandMore
|
import androidx.compose.material.icons.rounded.ExpandMore
|
||||||
|
@ -32,7 +31,7 @@ import me.ash.reader.ui.ext.alphaLN
|
||||||
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewAction
|
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewAction
|
||||||
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewModel
|
import me.ash.reader.ui.page.home.feeds.option.group.GroupOptionViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
|
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupItem(
|
fun GroupItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -114,7 +113,6 @@ fun GroupItem(
|
||||||
Column {
|
Column {
|
||||||
feeds.forEach { feed ->
|
feeds.forEach { feed ->
|
||||||
FeedItem(
|
FeedItem(
|
||||||
modifier = Modifier.padding(horizontal = 20.dp),
|
|
||||||
feed = feed,
|
feed = feed,
|
||||||
tonalElevation = tonalElevation,
|
tonalElevation = tonalElevation,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -6,8 +6,6 @@ import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.CreateNewFolder
|
import androidx.compose.material.icons.outlined.CreateNewFolder
|
||||||
import androidx.compose.material.icons.outlined.Edit
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
import androidx.compose.material.icons.rounded.RssFeed
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -26,6 +24,7 @@ import me.ash.reader.ui.component.TextFieldDialog
|
||||||
import me.ash.reader.ui.ext.collectAsStateValue
|
import me.ash.reader.ui.ext.collectAsStateValue
|
||||||
import me.ash.reader.ui.ext.roundClick
|
import me.ash.reader.ui.ext.roundClick
|
||||||
import me.ash.reader.ui.ext.showToast
|
import me.ash.reader.ui.ext.showToast
|
||||||
|
import me.ash.reader.ui.page.home.FeedIcon
|
||||||
import me.ash.reader.ui.page.home.feeds.subscribe.ResultView
|
import me.ash.reader.ui.page.home.feeds.subscribe.ResultView
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@ -56,12 +55,13 @@ fun FeedOptionDrawer(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
FeedIcon(feedName = feed?.name ?: "", size = 24.dp)
|
||||||
modifier = Modifier.roundClick { },
|
// Icon(
|
||||||
imageVector = Icons.Rounded.RssFeed,
|
// modifier = Modifier.roundClick { },
|
||||||
contentDescription = feed?.name ?: stringResource(R.string.unknown),
|
// imageVector = Icons.Rounded.RssFeed,
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
// contentDescription = feed?.name ?: stringResource(R.string.unknown),
|
||||||
)
|
// tint = MaterialTheme.colorScheme.secondary,
|
||||||
|
// )
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.roundClick {
|
modifier = Modifier.roundClick {
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package me.ash.reader.ui.page.home.flow
|
package me.ash.reader.ui.page.home.flow
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Star
|
import androidx.compose.material.icons.rounded.Star
|
||||||
|
@ -26,10 +24,10 @@ 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.component.AsyncImage
|
||||||
import me.ash.reader.ui.ext.formatAsString
|
import me.ash.reader.ui.ext.formatAsString
|
||||||
|
import me.ash.reader.ui.page.home.FeedIcon
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArticleItem(
|
fun ArticleItem(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
articleWithFeed: ArticleWithFeed,
|
articleWithFeed: ArticleWithFeed,
|
||||||
onClick: (ArticleWithFeed) -> Unit = {},
|
onClick: (ArticleWithFeed) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
@ -107,12 +105,7 @@ fun ArticleItem(
|
||||||
) {
|
) {
|
||||||
// Feed icon
|
// Feed icon
|
||||||
if (articleListFeedIcon.value) {
|
if (articleListFeedIcon.value) {
|
||||||
Row(
|
FeedIcon(articleWithFeed.feed.name)
|
||||||
modifier = Modifier
|
|
||||||
.size(20.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
|
|
||||||
) {}
|
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,15 +30,14 @@ fun LazyListScope.ArticleList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is FlowItemView.Date -> {
|
is FlowItemView.Date -> {
|
||||||
val separator = pagingItems[index] as FlowItemView.Date
|
if (item.showSpacer) item { Spacer(modifier = Modifier.height(40.dp)) }
|
||||||
if (separator.showSpacer) item { Spacer(modifier = Modifier.height(40.dp)) }
|
|
||||||
if (articleListDateStickyHeader) {
|
if (articleListDateStickyHeader) {
|
||||||
stickyHeader(key = separator.date) {
|
stickyHeader(key = item.date) {
|
||||||
StickyHeader(separator.date, articleListFeedIcon, articleListTonalElevation)
|
StickyHeader(item.date, articleListFeedIcon, articleListTonalElevation)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item(key = separator.date) {
|
item(key = item.date) {
|
||||||
StickyHeader(separator.date, articleListFeedIcon, articleListTonalElevation)
|
StickyHeader(item.date, articleListFeedIcon, articleListTonalElevation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.entity.ArticleWithFeed
|
import me.ash.reader.data.entity.ArticleWithFeed
|
||||||
import me.ash.reader.data.repository.RssRepository
|
import me.ash.reader.data.repository.RssRepository
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import javax.annotation.concurrent.Immutable
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
@ -116,7 +117,10 @@ enum class MarkAsReadBefore {
|
||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
sealed class FlowItemView {
|
sealed class FlowItemView {
|
||||||
|
@Immutable
|
||||||
class Article(val articleWithFeed: ArticleWithFeed) : FlowItemView()
|
class Article(val articleWithFeed: ArticleWithFeed) : FlowItemView()
|
||||||
|
@Immutable
|
||||||
class Date(val date: String, val showSpacer: Boolean) : FlowItemView()
|
class Date(val date: String, val showSpacer: Boolean) : FlowItemView()
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package me.ash.reader.ui.page.home.read
|
package me.ash.reader.ui.page.home.read
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
@ -35,7 +36,6 @@ fun ReadPage(
|
||||||
) {
|
) {
|
||||||
val viewState = readViewModel.viewState.collectAsStateValue()
|
val viewState = readViewModel.viewState.collectAsStateValue()
|
||||||
val isScrollDown = viewState.listState.isScrollDown()
|
val isScrollDown = viewState.listState.isScrollDown()
|
||||||
// val isScrollDown by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
navController.currentBackStackEntryFlow.collect {
|
navController.currentBackStackEntryFlow.collect {
|
||||||
|
@ -46,6 +46,7 @@ fun ReadPage(
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(viewState.articleWithFeed?.article?.id) {
|
LaunchedEffect(viewState.articleWithFeed?.article?.id) {
|
||||||
|
Log.i("RLog", "ReadPage: ${viewState.articleWithFeed}")
|
||||||
viewState.articleWithFeed?.let {
|
viewState.articleWithFeed?.let {
|
||||||
if (it.article.isUnread) {
|
if (it.article.isUnread) {
|
||||||
readViewModel.dispatch(ReadViewAction.MarkUnread(false))
|
readViewModel.dispatch(ReadViewAction.MarkUnread(false))
|
||||||
|
|
Loading…
Reference in New Issue
Block a user