Add subscribe feed UI

This commit is contained in:
Ash 2022-03-07 17:05:38 +08:00
parent 5fff554bba
commit 11ca1f1ae8
42 changed files with 1396 additions and 926 deletions

View File

@ -50,7 +50,8 @@ android {
}
dependencies {
implementation("io.coil-kt:coil-compose:1.4.0")
implementation("com.google.accompanist:accompanist-flowlayout:0.24.3-alpha")
implementation("com.google.accompanist:accompanist-navigation-animation:0.24.3-alpha")
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "com.airbnb.android:lottie-compose:4.2.2"
implementation "androidx.work:work-runtime-ktx:2.8.0-alpha01"
@ -82,7 +83,7 @@ dependencies {
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha01"
implementation "androidx.compose.material:material:1.2.0-alpha03"
implementation "androidx.compose.material3:material3:1.0.0-alpha05"
implementation "androidx.compose.material3:material3:1.0.0-alpha06"
implementation "androidx.compose.material:material-icons-extended:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'

View File

@ -1,10 +1,6 @@
package me.ash.reader
import android.app.Application
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.material3.ExperimentalMaterial3Api
import com.google.accompanist.pager.ExperimentalPagerApi
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
@ -18,10 +14,6 @@ import me.ash.reader.data.source.ReaderDatabase
import me.ash.reader.data.source.RssNetworkDataSource
import javax.inject.Inject
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@DelicateCoroutinesApi
@HiltAndroidApp
class App : Application() {

View File

@ -3,18 +3,10 @@ package me.ash.reader
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.core.view.WindowCompat
import com.google.accompanist.pager.ExperimentalPagerApi
import dagger.hilt.android.AndroidEntryPoint
import me.ash.reader.ui.page.common.HomeEntry
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@AndroidEntryPoint
class MainActivity : ComponentActivity() {

View File

@ -11,7 +11,6 @@ interface AccountDao {
)
suspend fun queryAll(): List<Account>
@Query(
"""
SELECT * FROM account

View File

@ -8,14 +8,10 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.work.*
import com.github.muhrifqii.parserss.ParseRSS
import com.google.accompanist.pager.ExperimentalPagerApi
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.*
@ -40,7 +36,7 @@ import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@DelicateCoroutinesApi
class RssRepository @Inject constructor(
@ApplicationContext
private val context: Context,
@ -87,10 +83,6 @@ class RssRepository @Inject constructor(
return workManager.getWorkInfosByTag("sync").get().size.toString()
}
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
suspend fun sync(isWork: Boolean? = false) {
if (isWork == true) {
workManager.cancelAllWork()
@ -108,10 +100,6 @@ class RssRepository @Inject constructor(
}
}
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@DelicateCoroutinesApi
companion object {
data class SyncState(
@ -215,7 +203,10 @@ class RssRepository @Inject constructor(
val ids = articleDao.insertList(articleList)
articleList.forEachIndexed { index, article ->
Log.i("RlOG", "combine ${article.feedId}: ${article.title}")
val builder = NotificationCompat.Builder(context, Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
val builder = NotificationCompat.Builder(
context,
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE
)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
.setContentTitle(article.title)
@ -346,10 +337,6 @@ class RssRepository @Inject constructor(
}
}
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@DelicateCoroutinesApi
class SyncWorker(
context: Context,

View File

@ -0,0 +1,11 @@
package me.ash.reader.ui.extension
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}

View File

@ -0,0 +1,14 @@
package me.ash.reader.ui.extension
import androidx.compose.foundation.lazy.LazyListState
import kotlin.math.abs
fun LazyListState.calculateTopBarAnimateValue(start: Float, end: Float): Float =
if (firstVisibleItemIndex != 0) end
else {
val variable = firstVisibleItemScrollOffset.coerceAtLeast(0).toFloat()
val duration = 256f
val increase = abs(start - end) * (variable / duration)
if (start < end) (start + increase).coerceIn(start, end)
else (start - increase).coerceIn(end, start)
}

View File

@ -1,14 +1,8 @@
package me.ash.reader.ui.util
package me.ash.reader.ui.extension
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
@ -18,28 +12,9 @@ import androidx.compose.ui.unit.lerp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerScope
import com.google.accompanist.pager.calculateCurrentOffsetForPage
import kotlinx.coroutines.flow.StateFlow
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.math.abs
import kotlin.math.absoluteValue
@Composable
fun <T> StateFlow<T>.collectAsStateValue(
context: CoroutineContext = EmptyCoroutineContext
): T = collectAsState(context).value
fun LazyListState.calculateTopBarAnimateValue(start: Float, end: Float): Float =
if (firstVisibleItemIndex != 0) end
else {
val variable = firstVisibleItemScrollOffset.coerceAtLeast(0).toFloat()
val duration = 256f
val increase = abs(start - end) * (variable / duration)
if (start < end) (start + increase).coerceIn(start, end)
else (start - increase).coerceIn(end, start)
}
@ExperimentalPagerApi
@OptIn(ExperimentalPagerApi::class)
fun Modifier.pagerAnimate(pagerScope: PagerScope, page: Int): Modifier {
return graphicsLayer {
// Calculate the absolute offset for the current page from the
@ -73,9 +48,3 @@ fun Modifier.roundClick(onClick: () -> Unit = {}) = this
fun Modifier.paddingFixedHorizontal(top: Dp = 0.dp, bottom: Dp = 0.dp) = this
.padding(horizontal = 10.dp)
.padding(top = top, bottom = bottom)
fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}

View File

@ -0,0 +1,12 @@
package me.ash.reader.ui.extension
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.flow.StateFlow
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@Composable
fun <T> StateFlow<T>.collectAsStateValue(
context: CoroutineContext = EmptyCoroutineContext
): T = collectAsState(context).value

View File

@ -1,36 +1,33 @@
package me.ash.reader.ui.page.common
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.animation.*
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsHeight
import com.google.accompanist.insets.statusBarsPadding
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.ui.page.home.HomePage
import me.ash.reader.ui.page.settings.SettingsPage
import me.ash.reader.ui.theme.AppTheme
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun HomeEntry() {
val navController = rememberNavController()
val navController = rememberAnimatedNavController()
AppTheme {
ProvideWindowInsets {
@ -45,15 +42,91 @@ fun HomeEntry() {
.weight(1f)
.statusBarsPadding()
) {
NavHost(
AnimatedNavHost(
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
navController = navController,
startDestination = RouteName.HOME,
) {
composable(route = RouteName.HOME) {
composable(
route = RouteName.HOME,
enterTransition = {
slideInHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
initialOffsetX = { -it }
) + fadeIn(animationSpec = tween(220, delayMillis = 90))
},
exitTransition = {
slideOutHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
targetOffsetX = { it }
) + fadeOut(animationSpec = tween(220, delayMillis = 90))
},
popEnterTransition = {
slideInHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
initialOffsetX = { -it }
) + fadeIn(animationSpec = tween(220, delayMillis = 90))
},
popExitTransition = {
slideOutHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
targetOffsetX = { it }
) + fadeOut(animationSpec = tween(220, delayMillis = 90))
},
) {
HomePage(navController)
}
composable(route = RouteName.SETTINGS) {
composable(
route = RouteName.SETTINGS,
enterTransition = {
slideInHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
initialOffsetX = { -it }
) + fadeIn(animationSpec = tween(220, delayMillis = 90))
},
exitTransition = {
slideOutHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
targetOffsetX = { -it }
) + fadeOut(animationSpec = tween(220, delayMillis = 90))
},
popEnterTransition = {
slideInHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
initialOffsetX = { -it }
) + fadeIn(animationSpec = tween(220, delayMillis = 90))
},
popExitTransition = {
slideOutHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
targetOffsetX = { -it }
) + fadeOut(animationSpec = tween(220, delayMillis = 90))
},
) {
SettingsPage(navController)
}
}

View File

@ -1,4 +1,4 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.page.home
import android.view.HapticFeedbackConstants
import androidx.compose.animation.AnimatedVisibility
@ -36,11 +36,12 @@ import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import me.ash.reader.data.constant.Filter
import me.ash.reader.data.constant.NavigationBarItem
import me.ash.reader.ui.widget.CanBeDisabledIconButton
import kotlin.math.absoluteValue
@ExperimentalPagerApi
@OptIn(ExperimentalPagerApi::class)
@Composable
fun AppNavigationBar(
fun HomeBottomNavBar(
modifier: Modifier = Modifier,
pagerState: PagerState,
filter: Filter,

View File

@ -2,14 +2,10 @@ package me.ash.reader.ui.page.home
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
@ -18,24 +14,19 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import me.ash.reader.data.constant.Symbol
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.findActivity
import me.ash.reader.ui.page.home.article.ArticlePage
import me.ash.reader.ui.page.home.feed.FeedPage
import me.ash.reader.ui.page.home.read.ReadPage
import me.ash.reader.ui.page.home.read.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel
import me.ash.reader.ui.util.collectAsStateValue
import me.ash.reader.ui.util.findActivity
import me.ash.reader.ui.util.pagerAnimate
import me.ash.reader.ui.widget.AppNavigationBar
import me.ash.reader.ui.widget.ViewPager
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@OptIn(ExperimentalPagerApi::class)
@Composable
fun HomePage(
navController: NavHostController,
@ -48,7 +39,7 @@ fun HomePage(
val readState = readViewModel.viewState.collectAsStateValue()
val scope = rememberCoroutineScope()
DisposableEffect(Unit) {
LaunchedEffect(Unit) {
context.findActivity()?.let { activity ->
activity.intent?.let { intent ->
intent.extras?.get(Symbol.EXTRA_ARTICLE_ID)?.let {
@ -69,11 +60,9 @@ fun HomePage(
)
}
}
intent.extras?.clear()
intent.extras?.remove(Symbol.EXTRA_ARTICLE_ID)
}
}
onDispose { }
}
BackHandler(true) {
@ -103,56 +92,14 @@ fun HomePage(
}
}
// val items = listOf(
// Color.Red,
// Color.Blue,
// Color.Green,
// )
Column {
// CustomPager(
// items = items,
// modifier = Modifier
// .fillMaxWidth()
// .height(256.dp),
// itemFraction = .75f,
// overshootFraction = .75f,
// initialIndex = 3,
// itemSpacing = 16.dp,
// ) {
// items.forEachIndexed { index, item ->
// if (index % 2 == 0) {
// Box(
// modifier = Modifier
// .fillMaxSize()
// .background(item),
// contentAlignment = Alignment.Center
// ) {
// Text(
// text = item.toString(),
// modifier = Modifier.padding(all = 16.dp),
//// style = MaterialTheme.typography.h6,
// )
// }
// } else {
// Image(
// modifier = Modifier.fillMaxSize(),
// painter = painterResource(id = R.drawable.ic_launcher_foreground),
// contentDescription = null,
// )
// }
// }
// }
HorizontalPager(
count = 3,
ViewPager(
modifier = Modifier.weight(1f),
state = viewState.pagerState,
modifier = Modifier.weight(1f)
) { page ->
when (page) {
0 -> FeedPage(
composableList = listOf(
{
FeedPage(
navController = navController,
modifier = Modifier.pagerAnimate(this, page),
filter = filterState.filter,
groupAndFeedOnClick = { currentGroup, currentFeed ->
viewModel.dispatch(
@ -171,9 +118,10 @@ fun HomePage(
)
},
)
1 -> ArticlePage(
},
{
ArticlePage(
navController = navController,
modifier = Modifier.pagerAnimate(this, page),
BackOnClick = {
viewModel.dispatch(
HomeViewAction.ScrollToPage(
@ -196,9 +144,10 @@ fun HomePage(
)
},
)
2 -> ReadPage(
},
{
ReadPage(
navController = navController,
modifier = Modifier.pagerAnimate(this, page),
btnBackOnClickListener = {
viewModel.dispatch(
HomeViewAction.ScrollToPage(
@ -211,9 +160,10 @@ fun HomePage(
)
},
)
}
}
AppNavigationBar(
},
),
)
HomeBottomNavBar(
modifier = Modifier
.height(60.dp)
.fillMaxWidth(),

View File

@ -1,8 +1,5 @@
package me.ash.reader.ui.page.home
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.accompanist.pager.ExperimentalPagerApi
@ -15,16 +12,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import me.ash.reader.data.constant.Filter
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.constant.Filter
import javax.inject.Inject
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@OptIn(ExperimentalPagerApi::class)
@HiltViewModel
class HomeViewModel @Inject constructor(
private val rssRepository: RssRepository,
@ -36,6 +30,8 @@ class HomeViewModel @Inject constructor(
private val _filterState = MutableStateFlow(FilterState())
val filterState = _filterState.asStateFlow()
val syncState = RssRepository.syncState
fun dispatch(action: HomeViewAction) {
when (action) {
is HomeViewAction.Sync -> sync(action.callback)
@ -79,8 +75,8 @@ data class FilterState(
val filter: Filter = Filter.All,
)
@ExperimentalPagerApi
data class HomeViewState(
@OptIn(ExperimentalPagerApi::class)
data class HomeViewState constructor(
val pagerState: PagerState = PagerState(1),
)

View File

@ -0,0 +1,37 @@
package me.ash.reader.ui.page.home.article
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun ArticleDateHeader(
date: String,
isDisplayIcon: Boolean
) {
Row(
modifier = Modifier
.height(28.dp)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = date,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(start = (if (isDisplayIcon) 52 else 20).dp),
fontWeight = FontWeight.SemiBold,
)
}
}

View File

@ -0,0 +1,143 @@
package me.ash.reader.ui.page.home.article
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
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.DateTimeExt
import me.ash.reader.DateTimeExt.toString
import me.ash.reader.R
import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.ui.extension.paddingFixedHorizontal
import me.ash.reader.ui.extension.roundClick
@Composable
fun ArticleItem(
modifier: Modifier = Modifier,
articleWithFeed: ArticleWithFeed? = null,
isStarredFilter: Boolean,
index: Int,
articleOnClick: (ArticleWithFeed) -> Unit,
) {
if (articleWithFeed == null) return
Column(
modifier = modifier
.paddingFixedHorizontal(
top = if (index == 0) 8.dp else 0.dp,
bottom = 8.dp
)
.roundClick {
articleOnClick(articleWithFeed)
}
.alpha(
if (isStarredFilter || articleWithFeed.article.isUnread) {
1f
} else {
0.7f
}
)
) {
Column(modifier = modifier.padding(10.dp)) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
modifier = Modifier.padding(start = 32.dp),
text = articleWithFeed.feed.name,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = if (isStarredFilter || articleWithFeed.article.isUnread) {
MaterialTheme.colorScheme.tertiary
} else {
MaterialTheme.colorScheme.outline
},
)
Text(
text = articleWithFeed.article.date.toString(
DateTimeExt.HH_MM
),
fontSize = 13.sp,
color = MaterialTheme.colorScheme.outline
)
}
Spacer(modifier = modifier.height(1.dp))
Row {
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,
fontWeight = FontWeight.Bold,
color = if (isStarredFilter || articleWithFeed.article.isUnread) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.outline
},
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = modifier.height(1.dp))
Text(
text = articleWithFeed.article.shortDescription,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.outline,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
}

View File

@ -1,65 +1,41 @@
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
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
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.constant.Filter
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.util.collectAsStateValue
import me.ash.reader.ui.util.paddingFixedHorizontal
import me.ash.reader.ui.util.roundClick
import me.ash.reader.ui.widget.AnimateLazyColumn
import me.ash.reader.ui.widget.TopTitleBox
@OptIn(ExperimentalFoundationApi::class)
@DelicateCoroutinesApi
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Composable
fun ArticlePage(
navController: NavHostController,
modifier: Modifier,
modifier: Modifier = Modifier,
homeViewModel: HomeViewModel = hiltViewModel(),
viewModel: ArticleViewModel = hiltViewModel(),
BackOnClick: () -> Unit,
@ -70,7 +46,7 @@ fun ArticlePage(
val filterState = homeViewModel.filterState.collectAsStateValue()
val pagingItems = viewState.pagingData?.collectAsLazyPagingItems()
val refreshState = rememberSwipeRefreshState(isRefreshing = viewState.isRefreshing)
val syncState = RssRepository.syncState.collectAsStateValue()
val syncState = homeViewModel.syncState.collectAsStateValue()
LaunchedEffect(homeViewModel.filterState) {
homeViewModel.filterState.collect { state ->
@ -115,39 +91,15 @@ fun ArticlePage(
viewModel.dispatch(ArticleViewAction.ScrollToItem(0))
}
Column {
SmallTopAppBar(
title = {},
navigationIcon = {
IconButton(BackOnClick) {
Icon(
imageVector = Icons.Rounded.ArrowBackIosNew,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary
)
}
},
actions = {
IconButton(onClick = {
ArticlePageTopBar(
backOnClick = BackOnClick,
readAllOnClick = {
viewModel.dispatch(ArticleViewAction.PeekSyncWork)
Toast.makeText(context, viewState.syncWorkInfo, Toast.LENGTH_LONG)
.show()
}) {
Icon(
imageVector = Icons.Rounded.DoneAll,
contentDescription = "Done All",
tint = MaterialTheme.colorScheme.primary
)
}
IconButton(onClick = {
if (syncState.isSyncing) return@IconButton
homeViewModel.dispatch(HomeViewAction.Sync())
}) {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = "Search",
tint = MaterialTheme.colorScheme.primary
)
}
},
searchOnClick = {
},
)
@ -191,139 +143,3 @@ fun ArticlePage(
}
}
}
@Composable
private fun ArticleItem(
modifier: Modifier = Modifier,
articleWithFeed: ArticleWithFeed?,
isStarredFilter: Boolean,
index: Int,
articleOnClick: (ArticleWithFeed) -> Unit,
) {
if (articleWithFeed == null) return
Column(
modifier = modifier
.paddingFixedHorizontal(
top = if (index == 0) 8.dp else 0.dp,
bottom = 8.dp
)
.roundClick {
articleOnClick(articleWithFeed)
}
.alpha(
if (isStarredFilter || articleWithFeed.article.isUnread) {
1f
} else {
0.7f
}
)
) {
Column(modifier = modifier.padding(10.dp)) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
modifier = Modifier.padding(start = 32.dp),
text = articleWithFeed.feed.name,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = if (isStarredFilter || articleWithFeed.article.isUnread) {
MaterialTheme.colorScheme.tertiary
} else {
MaterialTheme.colorScheme.outline
},
)
Text(
text = articleWithFeed.article.date.toString(
DateTimeExt.HH_MM
),
fontSize = 13.sp,
color = MaterialTheme.colorScheme.outline
)
}
Spacer(modifier = modifier.height(1.dp))
Row {
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,
fontWeight = FontWeight.Bold,
color = if (isStarredFilter || articleWithFeed.article.isUnread) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.outline
},
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = modifier.height(1.dp))
Text(
text = articleWithFeed.article.shortDescription,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.outline,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
}
@Composable
private fun ArticleDateHeader(date: String, isDisplayIcon: Boolean) {
Row(
modifier = Modifier
.height(28.dp)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = date,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(start = (if (isDisplayIcon) 52 else 20).dp),
fontWeight = FontWeight.SemiBold,
)
}
}

View File

@ -0,0 +1,47 @@
package me.ash.reader.ui.page.home.article
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBackIosNew
import androidx.compose.material.icons.rounded.DoneAll
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.runtime.Composable
@Composable
fun ArticlePageTopBar(
backOnClick: () -> Unit = {},
readAllOnClick: () -> Unit = {},
searchOnClick: () -> Unit = {},
) {
SmallTopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = backOnClick) {
Icon(
imageVector = Icons.Rounded.ArrowBackIosNew,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary
)
}
},
actions = {
IconButton(onClick = readAllOnClick) {
Icon(
imageVector = Icons.Rounded.DoneAll,
contentDescription = "Done All",
tint = MaterialTheme.colorScheme.primary
)
}
IconButton(onClick = searchOnClick) {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = "Search",
tint = MaterialTheme.colorScheme.primary
)
}
},
)
}

View File

@ -1,7 +1,6 @@
package me.ash.reader.ui.widget
package me.ash.reader.ui.page.home.feed
import android.graphics.BitmapFactory
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -25,12 +24,12 @@ 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
import me.ash.reader.ui.extension.paddingFixedHorizontal
import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.widget.AnimatedText
@ExperimentalAnimationApi
@Composable
fun BarButton(
fun FeedBar(
barButtonType: BarButtonType,
iconOnClickListener: () -> Unit = {},
onClickListener: () -> Unit = {},

View File

@ -0,0 +1,51 @@
package me.ash.reader.ui.page.home.feed
import android.graphics.BitmapFactory
import android.util.Log
import androidx.compose.animation.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import me.ash.reader.data.feed.Feed
@Composable
fun ColumnScope.FeedList(
visible: Boolean,
feeds: List<Feed>,
onClick: (currentFeed: Feed?) -> Unit = {},
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Column(modifier = Modifier.animateContentSize()) {
feeds.forEach { feed ->
Log.i("RLog", "FeedList: ${feed.icon}")
FeedBar(
barButtonType = ItemType(
// icon = feed.icon ?: "",
icon = if (feed.icon == null) {
null
} else {
BitmapPainter(
BitmapFactory.decodeByteArray(
feed.icon,
0,
feed.icon!!.size
).asImageBitmap()
)
},
content = feed.name,
important = feed.important ?: 0
)
) {
onClick(feed)
}
}
}
}
}

View File

@ -1,72 +1,41 @@
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
import androidx.compose.animation.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
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.data.constant.Filter
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.util.collectAsStateValue
import me.ash.reader.ui.widget.*
import java.io.InputStream
import me.ash.reader.ui.page.home.feed.subscribe.SubscribeDialog
import me.ash.reader.ui.widget.TopTitleBox
@ExperimentalComposeUiApi
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Composable
fun FeedPage(
navController: NavHostController,
modifier: Modifier,
modifier: Modifier = Modifier,
viewModel: FeedViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(),
filter: Filter,
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
) {
val viewState = viewModel.viewState.collectAsStateValue()
val syncState = RssRepository.syncState.collectAsStateValue()
var addFeedDialogVisible by remember { mutableStateOf(false) }
val syncState = homeViewModel.syncState.collectAsStateValue()
LaunchedEffect(homeViewModel.filterState) {
homeViewModel.filterState.collect { state ->
@ -87,11 +56,24 @@ fun FeedPage(
}
Box(
modifier.fillMaxSize()
modifier = modifier.fillMaxSize()
) {
AddFeedDialog(
visible = addFeedDialogVisible,
hiddenFunction = { addFeedDialogVisible = false },
SubscribeDialog(
visible = viewState.subscribeDialogVisible,
hiddenFunction = {
viewModel.dispatch(FeedViewAction.ChangeSubscribeDialogVisible(false))
},
inputContent = viewState.subscribeDialogFeedLink,
onValueChange = {
viewModel.dispatch(
FeedViewAction.InputSubscribeFeedLink(it)
)
},
onKeyboardAction = {
viewModel.dispatch(
FeedViewAction.ChangeSubscribeDialogVisible(false)
)
},
openInputStreamCallback = {
viewModel.dispatch(FeedViewAction.AddFromFile(it))
},
@ -113,42 +95,14 @@ fun FeedPage(
viewModel.dispatch(FeedViewAction.ScrollToItem(0))
}
Column {
SmallTopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = {
navController.navigate(route = RouteName.SETTINGS)
}) {
Icon(
modifier = Modifier.size(22.dp),
imageVector = Icons.Outlined.Settings,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary
)
}
},
actions = {
IconButton(onClick = {
if (syncState.isSyncing) return@IconButton
FeedPageTopBar(
navController = navController,
isSyncing = syncState.isSyncing,
syncOnClick = {
homeViewModel.dispatch(HomeViewAction.Sync())
}) {
Icon(
modifier = Modifier.size(26.dp),
imageVector = Icons.Rounded.Refresh,
contentDescription = "Sync",
tint = MaterialTheme.colorScheme.primary,
)
}
IconButton(onClick = {
addFeedDialogVisible = true
}) {
Icon(
modifier = Modifier.size(26.dp),
imageVector = Icons.Rounded.Add,
contentDescription = "Subscribe",
tint = MaterialTheme.colorScheme.primary,
)
}
},
subscribeOnClick = {
viewModel.dispatch(FeedViewAction.ChangeSubscribeDialogVisible(true))
},
)
LazyColumn(
@ -157,7 +111,7 @@ fun FeedPage(
) {
item {
Spacer(modifier = Modifier.height(114.dp))
BarButton(
FeedBar(
barButtonType = ButtonType(
content = filter.title,
important = viewState.filterImportant
@ -168,13 +122,13 @@ fun FeedPage(
}
item {
Spacer(modifier = Modifier.height(10.dp))
BarButton(
FeedBar(
barButtonType = FirstExpandType(
content = "Feeds",
icon = Icons.Rounded.ExpandMore
)
) {
viewModel.dispatch(FeedViewAction.ChangeGroupVisible)
viewModel.dispatch(FeedViewAction.ChangeGroupVisible(!viewState.groupsVisible))
}
}
itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed ->
@ -193,181 +147,3 @@ fun FeedPage(
}
}
}
@ExperimentalComposeUiApi
@Composable
private fun AddFeedDialog(
visible: Boolean,
hiddenFunction: () -> Unit,
openInputStreamCallback: (InputStream) -> Unit,
) {
val context = LocalContext.current
var inputString by remember { mutableStateOf("") }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { uri ->
context.contentResolver.openInputStream(uri)?.let { inputStream ->
openInputStreamCallback(inputStream)
}
}
}
val focusRequester = remember { FocusRequester() }
val localFocusManager = LocalFocusManager.current
val localSoftwareKeyboardController = LocalSoftwareKeyboardController.current
Dialog(
visible = visible,
onDismissRequest = hiddenFunction,
icon = {
Icon(
imageVector = Icons.Rounded.RssFeed,
contentDescription = "Subscribe",
)
},
title = { Text("订阅") },
text = {
Spacer(modifier = Modifier.height(10.dp))
TextField(
modifier = Modifier
.focusRequester(focusRequester)
.onFocusChanged { if(it.isFocused) localSoftwareKeyboardController?.hide() }
.focusable(),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.onSurface,
textColor = MaterialTheme.colorScheme.onSurface,
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
),
value = inputString,
onValueChange = {
inputString = it
},
placeholder = {
Text(
text = "订阅源或站点链接",
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
)
},
singleLine = true,
trailingIcon = {
IconButton(onClick = {}) {
Icon(
imageVector = Icons.Rounded.ContentPaste,
contentDescription = "Paste",
tint = MaterialTheme.colorScheme.primary
)
}
},
keyboardActions = KeyboardActions(
onDone = {
hiddenFunction()
}
)
)
Spacer(modifier = Modifier.height(10.dp))
},
confirmButton = {
TextButton(
enabled = inputString.isNotEmpty(),
onClick = {
hiddenFunction()
}
) {
Text(
text = "搜索",
color = if (inputString.isNotEmpty()) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
}
)
}
},
dismissButton = {
TextButton(
onClick = {
launcher.launch("*/*")
hiddenFunction()
}
) {
Text("导入OPML文件")
}
},
)
}
@ExperimentalAnimationApi
@Composable
private fun ColumnScope.GroupList(
modifier: Modifier = Modifier,
groupVisible: Boolean,
feedVisible: Boolean,
groupWithFeed: GroupWithFeed,
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
expandOnClick: () -> Unit
) {
AnimatedVisibility(
visible = groupVisible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Column(modifier = modifier) {
BarButton(
barButtonType = SecondExpandType(
content = groupWithFeed.group.name,
icon = Icons.Rounded.ExpandMore,
important = groupWithFeed.group.important ?: 0,
),
iconOnClickListener = expandOnClick
) {
groupAndFeedOnClick(groupWithFeed.group, null)
}
FeedList(
visible = feedVisible,
feeds = groupWithFeed.feeds,
onClick = { currentFeed ->
groupAndFeedOnClick(null, currentFeed)
}
)
}
}
}
@ExperimentalAnimationApi
@Composable
private fun ColumnScope.FeedList(
visible: Boolean,
feeds: List<Feed>,
onClick: (currentFeed: Feed?) -> Unit = {},
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Column(modifier = Modifier.animateContentSize()) {
feeds.forEach { feed ->
Log.i("RLog", "FeedList: ${feed.icon}")
BarButton(
barButtonType = ItemType(
// icon = feed.icon ?: "",
icon = if (feed.icon == null) {
null
} else {
BitmapPainter(
BitmapFactory.decodeByteArray(
feed.icon,
0,
feed.icon!!.size
).asImageBitmap()
)
},
content = feed.name,
important = feed.important ?: 0
)
) {
onClick(feed)
}
}
}
}
}

View File

@ -0,0 +1,61 @@
package me.ash.reader.ui.page.home.feed
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import me.ash.reader.ui.page.common.RouteName
@Composable
fun FeedPageTopBar(
navController: NavHostController,
isSyncing: Boolean = false,
syncOnClick: () -> Unit = {},
subscribeOnClick: () -> Unit = {},
) {
SmallTopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = {
navController.navigate(route = RouteName.SETTINGS)
}) {
Icon(
modifier = Modifier.size(22.dp),
imageVector = Icons.Outlined.Settings,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary
)
}
},
actions = {
IconButton(onClick = {
if (isSyncing) return@IconButton
syncOnClick()
}) {
Icon(
modifier = Modifier.size(26.dp),
imageVector = Icons.Rounded.Refresh,
contentDescription = "Sync",
tint = MaterialTheme.colorScheme.primary,
)
}
IconButton(onClick = subscribeOnClick) {
Icon(
modifier = Modifier.size(26.dp),
imageVector = Icons.Rounded.Add,
contentDescription = "Subscribe",
tint = MaterialTheme.colorScheme.primary,
)
}
},
)
}

View File

@ -31,8 +31,30 @@ class FeedViewModel @Inject constructor(
is FeedViewAction.FetchData -> fetchData(action.isStarred, action.isUnread)
is FeedViewAction.AddFromFile -> addFromFile(action.inputStream)
is FeedViewAction.ChangeFeedVisible -> changeFeedVisible(action.index)
is FeedViewAction.ChangeGroupVisible -> changeGroupVisible()
is FeedViewAction.ChangeGroupVisible -> changeGroupVisible(action.visible)
is FeedViewAction.ScrollToItem -> scrollToItem(action.index)
is FeedViewAction.ChangeSubscribeDialogVisible -> changeAddFeedDialogVisible(action.visible)
is FeedViewAction.InputSubscribeFeedLink -> inputSubscribeFeedLink(action.subscribeFeedLink)
}
}
private fun inputSubscribeFeedLink(subscribeFeedLink: String) {
viewModelScope.launch {
_viewState.update {
it.copy(
subscribeDialogFeedLink = subscribeFeedLink
)
}
}
}
private fun changeAddFeedDialogVisible(visible: Boolean) {
viewModelScope.launch {
_viewState.update {
it.copy(
subscribeDialogVisible = visible
)
}
}
}
@ -121,10 +143,10 @@ class FeedViewModel @Inject constructor(
}
}
private fun changeGroupVisible() {
private fun changeGroupVisible(visible: Boolean) {
_viewState.update {
it.copy(
groupsVisible = !_viewState.value.groupsVisible
groupsVisible = visible
)
}
}
@ -143,6 +165,8 @@ data class FeedViewState(
val feedsVisible: List<Boolean> = emptyList(),
val listState: LazyListState = LazyListState(),
val groupsVisible: Boolean = true,
var subscribeDialogVisible: Boolean = false,
var subscribeDialogFeedLink: String = "",
)
sealed class FeedViewAction {
@ -163,8 +187,19 @@ sealed class FeedViewAction {
val index: Int
) : FeedViewAction()
object ChangeGroupVisible : FeedViewAction()
data class ChangeGroupVisible(
val visible: Boolean
) : FeedViewAction()
data class ScrollToItem(
val index: Int
) : FeedViewAction()
data class ChangeSubscribeDialogVisible(
val visible: Boolean
) : FeedViewAction()
data class InputSubscribeFeedLink(
val subscribeFeedLink: String
) : FeedViewAction()
}

View File

@ -0,0 +1,48 @@
package me.ash.reader.ui.page.home.feed
import androidx.compose.animation.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupWithFeed
@Composable
fun ColumnScope.GroupList(
modifier: Modifier = Modifier,
groupVisible: Boolean,
feedVisible: Boolean,
groupWithFeed: GroupWithFeed,
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
expandOnClick: () -> Unit
) {
AnimatedVisibility(
visible = groupVisible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Column(modifier = modifier) {
FeedBar(
barButtonType = SecondExpandType(
content = groupWithFeed.group.name,
icon = Icons.Rounded.ExpandMore,
important = groupWithFeed.group.important ?: 0,
),
iconOnClickListener = expandOnClick
) {
groupAndFeedOnClick(groupWithFeed.group, null)
}
FeedList(
visible = feedVisible,
feeds = groupWithFeed.feeds,
onClick = { currentFeed ->
groupAndFeedOnClick(null, currentFeed)
}
)
}
}
}

View File

@ -0,0 +1,171 @@
package me.ash.reader.ui.page.home.feed.subscribe
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.flowlayout.MainAxisAlignment
import me.ash.reader.ui.widget.SelectionChip
@Composable
fun ResultViewPage() {
Column {
Link()
Spacer(modifier = Modifier.height(26.dp))
Preset()
Spacer(modifier = Modifier.height(26.dp))
AddToGroup()
Spacer(modifier = Modifier.height(6.dp))
}
}
@Composable
private fun Link() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
SelectionContainer {
Text(
text = "https://material.io/feed.xml",
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
)
}
}
}
@Composable
private fun Preset() {
Text(
text = "预设",
color = MaterialTheme.colorScheme.primary,
fontSize = 14.sp,
)
Spacer(modifier = Modifier.height(10.dp))
FlowRow(
mainAxisAlignment = MainAxisAlignment.Start,
crossAxisSpacing = 10.dp,
mainAxisSpacing = 10.dp,
) {
SelectionChip(
selected = true,
selectedIcon = {
Icon(
imageVector = Icons.Outlined.Notifications,
contentDescription = "Check",
modifier = Modifier.size(20.dp)
)
},
onClick = { /*TODO*/ },
) {
Text(
text = "接收通知",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
)
}
SelectionChip(
selected = false,
selectedIcon = {
Icon(
imageVector = Icons.Outlined.Article,
contentDescription = "Check",
modifier = Modifier.size(20.dp)
)
},
onClick = { /*TODO*/ }
) {
Text(
text = "全文输出",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
)
}
}
}
@Composable
private fun AddToGroup() {
Text(
text = "添加到组",
color = MaterialTheme.colorScheme.primary,
fontSize = 14.sp,
)
Spacer(modifier = Modifier.height(10.dp))
FlowRow(
mainAxisAlignment = MainAxisAlignment.Start,
crossAxisSpacing = 10.dp,
mainAxisSpacing = 10.dp,
) {
SelectionChip(
selected = false,
onClick = { /*TODO*/ },
) {
Text(
text = "未分组",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
)
}
SelectionChip(
selected = true,
onClick = { /*TODO*/ }
) {
Text(
text = "技术",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
)
}
SelectionChip(
selected = true,
onClick = { /*TODO*/ }
) {
Text(
text = "新鲜事",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
)
}
SelectionChip(
selected = false,
onClick = { /*TODO*/ }
) {
Text(
text = "游戏",
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
)
}
SelectionChip(
selected = true,
onClick = { /*TODO*/ },
) {
BasicTextField(
modifier = Modifier.width(56.dp),
value = "新建分组",
onValueChange = {},
textStyle = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
),
singleLine = true,
)
}
}
}

View File

@ -0,0 +1,75 @@
package me.ash.reader.ui.page.home.feed.subscribe
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ContentPaste
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
@Composable
fun SearchViewPage(
inputContent: String = "",
onValueChange: (String) -> Unit = {},
onKeyboardAction: () -> Unit = {},
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
delay(100) // ???
focusRequester.requestFocus()
}
Spacer(modifier = Modifier.height(10.dp))
TextField(
modifier = Modifier.focusRequester(focusRequester),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.onSurface,
textColor = MaterialTheme.colorScheme.onSurface,
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
),
value = inputContent,
onValueChange = {
onValueChange(it)
},
placeholder = {
Text(
text = "订阅源或站点链接",
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
)
},
singleLine = true,
trailingIcon = {
IconButton(onClick = {
// focusRequester.requestFocus()
}) {
Icon(
imageVector = Icons.Rounded.ContentPaste,
contentDescription = "Paste",
tint = MaterialTheme.colorScheme.primary
)
}
},
keyboardActions = KeyboardActions(
onDone = {
onKeyboardAction()
}
)
)
Spacer(modifier = Modifier.height(10.dp))
}

View File

@ -0,0 +1,80 @@
package me.ash.reader.ui.page.home.feed.subscribe
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.RssFeed
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import me.ash.reader.ui.widget.Dialog
import java.io.InputStream
@Composable
fun SubscribeDialog(
visible: Boolean,
hiddenFunction: () -> Unit,
inputContent: String = "",
onValueChange: (String) -> Unit = {},
onKeyboardAction: () -> Unit = {},
openInputStreamCallback: (InputStream) -> Unit,
) {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { uri ->
context.contentResolver.openInputStream(uri)?.let { inputStream ->
openInputStreamCallback(inputStream)
}
}
}
Dialog(
visible = visible,
onDismissRequest = hiddenFunction,
icon = {
Icon(
imageVector = Icons.Rounded.RssFeed,
contentDescription = "Subscribe",
)
},
title = { Text("订阅") },
text = {
SubscribeViewPager(
inputContent = inputContent,
onValueChange = onValueChange,
onKeyboardAction = onKeyboardAction,
)
},
confirmButton = {
TextButton(
enabled = inputContent.isNotEmpty(),
onClick = {
hiddenFunction()
}
) {
Text(
text = "搜索",
color = if (inputContent.isNotEmpty()) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
}
)
}
},
dismissButton = {
TextButton(
onClick = {
launcher.launch("*/*")
hiddenFunction()
}
) {
Text("导入OPML文件")
}
},
)
}

View File

@ -0,0 +1,28 @@
package me.ash.reader.ui.page.home.feed.subscribe
import androidx.compose.runtime.Composable
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.ui.widget.ViewPager
@OptIn(ExperimentalPagerApi::class)
@Composable
fun SubscribeViewPager(
inputContent: String = "",
onValueChange: (String) -> Unit = {},
onKeyboardAction: () -> Unit = {}
) {
ViewPager(
composableList = listOf(
{
SearchViewPage(
inputContent = inputContent,
onValueChange = onValueChange,
onKeyboardAction = onKeyboardAction,
)
},
{
ResultViewPage()
}
)
)
}

View File

@ -0,0 +1,67 @@
package me.ash.reader.ui.page.home.read
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.ash.reader.DateTimeExt
import me.ash.reader.DateTimeExt.toString
import me.ash.reader.data.article.Article
import me.ash.reader.data.feed.Feed
import me.ash.reader.ui.extension.roundClick
@Composable
fun Header(
context: Context,
article: Article,
feed: Feed
) {
Column(
modifier = Modifier
.fillMaxWidth()
.roundClick {
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(article.link))
)
}
) {
Column(modifier = Modifier.padding(10.dp)) {
Text(
text = article.date.toString(DateTimeExt.YYYY_MM_DD_HH_MM, true),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.outline,
fontWeight = FontWeight.Medium,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = article.title,
fontSize = 27.sp,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
lineHeight = 34.sp,
)
Spacer(modifier = Modifier.height(4.dp))
article.author?.let {
Text(
text = article.author,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.outline,
fontWeight = FontWeight.Medium,
)
}
Text(
text = feed.name,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.outline,
fontWeight = FontWeight.Medium,
)
}
}
}

View File

@ -1,47 +1,26 @@
package me.ash.reader.ui.page.home.read
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.MoreHoriz
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
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.data.article.Article
import me.ash.reader.data.feed.Feed
import me.ash.reader.ui.util.collectAsStateValue
import me.ash.reader.ui.util.paddingFixedHorizontal
import me.ash.reader.ui.util.roundClick
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.paddingFixedHorizontal
import me.ash.reader.ui.widget.WebView
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Composable
fun ReadPage(
navController: NavHostController,
@ -69,38 +48,7 @@ fun ReadPage(
Column(
modifier.fillMaxSize()
) {
SmallTopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = { btnBackOnClickListener() }) {
Icon(
modifier = Modifier.size(28.dp),
imageVector = Icons.Rounded.Close,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary
)
}
},
actions = {
IconButton(onClick = { /*TODO*/ }) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = Icons.Rounded.Share,
contentDescription = "Add",
tint = MaterialTheme.colorScheme.primary,
)
}
IconButton(onClick = { /*TODO*/ }) {
Icon(
modifier = Modifier.size(28.dp),
imageVector = Icons.Rounded.MoreHoriz,
contentDescription = "Add",
tint = MaterialTheme.colorScheme.primary,
)
}
},
)
ReadPageTopBar(btnBackOnClickListener)
val composition by rememberLottieComposition(
LottieCompositionSpec.Url(
@ -158,52 +106,3 @@ fun ReadPage(
}
}
}
@Composable
private fun Header(
context: Context,
article: Article,
feed: Feed
) {
Column(
modifier = Modifier
.fillMaxWidth()
.roundClick {
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(article.link))
)
}
) {
Column(modifier = Modifier.padding(10.dp)) {
Text(
text = article.date.toString(DateTimeExt.YYYY_MM_DD_HH_MM, true),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.outline,
fontWeight = FontWeight.Medium,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = article.title,
fontSize = 27.sp,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
lineHeight = 34.sp,
)
Spacer(modifier = Modifier.height(4.dp))
article.author?.let {
Text(
text = article.author,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.outline,
fontWeight = FontWeight.Medium,
)
}
Text(
text = feed.name,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.outline,
fontWeight = FontWeight.Medium,
)
}
}
}

View File

@ -0,0 +1,51 @@
package me.ash.reader.ui.page.home.read
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.MoreHoriz
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ReadPageTopBar(
btnBackOnClickListener: () -> Unit = {},
) {
SmallTopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = { btnBackOnClickListener() }) {
Icon(
modifier = Modifier.size(28.dp),
imageVector = Icons.Rounded.Close,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary
)
}
},
actions = {
IconButton(onClick = { /*TODO*/ }) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = Icons.Rounded.Share,
contentDescription = "Add",
tint = MaterialTheme.colorScheme.primary,
)
}
IconButton(onClick = { /*TODO*/ }) {
Icon(
modifier = Modifier.size(28.dp),
imageVector = Icons.Rounded.MoreHoriz,
contentDescription = "Add",
tint = MaterialTheme.colorScheme.primary,
)
}
},
)
}

View File

@ -1,7 +1,5 @@
package me.ash.reader.ui.page.settings
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@ -20,15 +18,10 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.ui.util.paddingFixedHorizontal
import me.ash.reader.ui.util.roundClick
import me.ash.reader.ui.extension.paddingFixedHorizontal
import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.widget.TopTitleBox
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Composable
fun SettingsPage(
navController: NavHostController,
@ -156,10 +149,6 @@ fun Item(
}
}
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Preview
@Composable
fun SettingsPreview() {

View File

@ -1,18 +1,21 @@
package me.ash.reader.ui.theme
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color
//val md_theme_light_primary = Color(0xFF4D4D4D)
val md_theme_light_primary = Color(0xFF6750A4)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFEADDFF)
val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
//val md_theme_light_secondary = Color(0xFF868686)
val md_theme_light_secondary = Color(0xFF625B71)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
//val md_theme_light_secondaryContainer = Color(0xFFEAEAEA)
val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
//val md_theme_light_tertiary = Color(0xFFC1C1C1)
val md_theme_light_tertiary = Color(0xFF7D5260)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
@ -22,13 +25,16 @@ val md_theme_light_error = Color(0xFFB3261E)
val md_theme_light_errorContainer = Color(0xFFF9DEDC)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410E0B)
//val md_theme_light_background = Color(0xFFF7F5F4)
val md_theme_light_background = Color(0xFFFFFBFE)
val md_theme_light_onBackground = Color(0xFF1C1B1F)
//val md_theme_light_surface = Color(0xFFF7F5F4)
val md_theme_light_surface = Color(0xFFFFFBFE)
val md_theme_light_onSurface = Color(0xFF1C1B1F)
val md_theme_light_surfaceVariant = Color(0xFFE7E0EC)
//val md_theme_light_onSurfaceVariant = md_theme_light_secondary
val md_theme_light_onSurfaceVariant = Color(0xFF49454F)
val md_theme_light_outline = Color(0xFF79747E)

View File

@ -1,9 +1,11 @@
package me.ash.reader.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
//Replace with your font locations
val Roboto = FontFamily.Default

View File

@ -10,7 +10,6 @@ import androidx.recyclerview.widget.ListUpdateCallback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ExperimentalAnimationApi
@Suppress("UpdateTransitionLabel", "TransitionPropertiesLabel")
@SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter")
/**
@ -63,7 +62,6 @@ fun <T> updateAnimatedItemsState(
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].item == newList[newItemPosition]
}
val diffResult = calculateDiff(false, diffCb)
val compositeList = oldList.toMutableList()

View File

@ -18,7 +18,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
@ExperimentalAnimationApi
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedText(
text: String,

View File

@ -64,7 +64,7 @@ fun <T : Any> CustomPager(
overshootFraction,
onItemSelect = { index -> onItemSelect(items[index]) },
) {
items.forEach{ item ->
items.forEach { item ->
Box(
modifier = when (orientation) {
Orientation.Horizontal -> Modifier.fillMaxWidth()
@ -126,7 +126,7 @@ fun <T : Any> Pager(
val spacing = state.itemSpacing.roundToInt()
val itemDimensionWithSpace = itemDimension + state.itemSpacing
val first = ceil(
(dragOffset -itemDimension - centerOffset) / itemDimensionWithSpace
(dragOffset - itemDimension - centerOffset) / itemDimensionWithSpace
).toInt().coerceAtLeast(0)
val last = ((dimension + dragOffset - centerOffset) / itemDimensionWithSpace).toInt()
.coerceAtMost(items.lastIndex)
@ -269,8 +269,12 @@ private class PagerState {
val extra = if (remainder > itemDimension / 2f) 1 else 0
val lastVisibleIndex =
(targetOffset.absoluteValue / itemDimension.toFloat()).toInt() + extra
targetOffset = (lastVisibleIndex * (itemDimension + itemSpacing) * targetOffset.sign)
.coerceIn(0f, (numberOfItems - 1).toFloat() * (itemDimension + itemSpacing))
targetOffset =
(lastVisibleIndex * (itemDimension + itemSpacing) * targetOffset.sign)
.coerceIn(
0f,
(numberOfItems - 1).toFloat() * (itemDimension + itemSpacing)
)
dragOffset.animateTo(
animationSpec = animationSpec,
targetValue = targetOffset,

View File

@ -16,7 +16,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import kotlin.math.absoluteValue
@ExperimentalPagerApi
@OptIn(ExperimentalPagerApi::class)
@Composable
fun BoxScope.MaskBox(
modifier: Modifier = Modifier,

View File

@ -0,0 +1,59 @@
package me.ash.reader.ui.widget
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ChipDefaults
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FilterChip
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SelectionChip(
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = CircleShape,
selectedIcon: @Composable () -> Unit = {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = "Check",
modifier = Modifier.size(20.dp)
)
},
content: @Composable RowScope.() -> Unit
) {
FilterChip(
modifier = modifier,
colors = ChipDefaults.filterChipColors(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurface,
leadingIconColor = MaterialTheme.colorScheme.onSurface,
disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer,
selectedContentColor = MaterialTheme.colorScheme.onSurface,
selectedLeadingIconColor = MaterialTheme.colorScheme.onSurface
),
interactionSource = interactionSource,
enabled = enabled,
selected = selected,
selectedIcon = selectedIcon,
shape = shape,
onClick = onClick,
content = content,
)
}

View File

@ -1,6 +1,5 @@
package me.ash.reader.ui.widget
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -17,9 +16,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import me.ash.reader.ui.util.calculateTopBarAnimateValue
import me.ash.reader.ui.extension.calculateTopBarAnimateValue
@ExperimentalAnimationApi
@Composable
fun BoxScope.TopTitleBox(
title: String,

View File

@ -0,0 +1,33 @@
package me.ash.reader.ui.widget
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState
@OptIn(ExperimentalPagerApi::class)
@Composable
fun ViewPager(
modifier: Modifier = Modifier,
state: PagerState = com.google.accompanist.pager.rememberPagerState(),
composableList: List<@Composable () -> Unit>,
userScrollEnabled: Boolean = true
) {
HorizontalPager(
count = composableList.size,
state = state,
verticalAlignment = Alignment.Top,
modifier = modifier
.animateContentSize()
.wrapContentHeight()
.fillMaxWidth(),
userScrollEnabled = userScrollEnabled,
) { page ->
composableList[page]()
}
}

View File

@ -13,9 +13,9 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.page.home.read.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel
import me.ash.reader.ui.util.collectAsStateValue
@Composable
fun WebView(