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 { 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 "androidx.datastore:datastore-preferences:1.0.0"
implementation "com.airbnb.android:lottie-compose:4.2.2" implementation "com.airbnb.android:lottie-compose:4.2.2"
implementation "androidx.work:work-runtime-ktx:2.8.0-alpha01" implementation "androidx.work:work-runtime-ktx:2.8.0-alpha01"
@ -82,7 +83,7 @@ dependencies {
implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha01" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha01"
implementation "androidx.compose.material:material:1.2.0-alpha03" 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.material:material-icons-extended:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'

View File

@ -1,10 +1,6 @@
package me.ash.reader package me.ash.reader
import android.app.Application 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 dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -18,10 +14,6 @@ import me.ash.reader.data.source.ReaderDatabase
import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.data.source.RssNetworkDataSource
import javax.inject.Inject import javax.inject.Inject
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@DelicateCoroutinesApi @DelicateCoroutinesApi
@HiltAndroidApp @HiltAndroidApp
class App : Application() { class App : Application() {

View File

@ -29,9 +29,9 @@ fun <T> DataStore<Preferences>.get(dataStoreKeys: DataStoreKeys<T>): T? {
if (exception is IOException) { if (exception is IOException) {
Log.e("RLog", "Get data store error $exception") Log.e("RLog", "Get data store error $exception")
exception.printStackTrace() exception.printStackTrace()
emit(emptyPreferences()) emit(emptyPreferences())
} else { } else {
throw exception throw exception
} }
}.map { }.map {
it[dataStoreKeys.key] it[dataStoreKeys.key]

View File

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

View File

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

View File

@ -8,14 +8,10 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.util.Log 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.app.NotificationCompat
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
import androidx.work.* import androidx.work.*
import com.github.muhrifqii.parserss.ParseRSS import com.github.muhrifqii.parserss.ParseRSS
import com.google.accompanist.pager.ExperimentalPagerApi
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -40,7 +36,7 @@ import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@DelicateCoroutinesApi
class RssRepository @Inject constructor( class RssRepository @Inject constructor(
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
@ -87,10 +83,6 @@ class RssRepository @Inject constructor(
return workManager.getWorkInfosByTag("sync").get().size.toString() return workManager.getWorkInfosByTag("sync").get().size.toString()
} }
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
suspend fun sync(isWork: Boolean? = false) { suspend fun sync(isWork: Boolean? = false) {
if (isWork == true) { if (isWork == true) {
workManager.cancelAllWork() workManager.cancelAllWork()
@ -108,10 +100,6 @@ class RssRepository @Inject constructor(
} }
} }
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@DelicateCoroutinesApi @DelicateCoroutinesApi
companion object { companion object {
data class SyncState( data class SyncState(
@ -215,7 +203,10 @@ class RssRepository @Inject constructor(
val ids = articleDao.insertList(articleList) val ids = articleDao.insertList(articleList)
articleList.forEachIndexed { index, article -> articleList.forEachIndexed { index, article ->
Log.i("RlOG", "combine ${article.feedId}: ${article.title}") 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) .setSmallIcon(R.drawable.ic_launcher_foreground)
.setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE) .setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
.setContentTitle(article.title) .setContentTitle(article.title)
@ -346,10 +337,6 @@ class RssRepository @Inject constructor(
} }
} }
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@DelicateCoroutinesApi @DelicateCoroutinesApi
class SyncWorker( class SyncWorker(
context: Context, 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.clickable
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.RoundedCornerShape 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.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer 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.ExperimentalPagerApi
import com.google.accompanist.pager.PagerScope import com.google.accompanist.pager.PagerScope
import com.google.accompanist.pager.calculateCurrentOffsetForPage 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 import kotlin.math.absoluteValue
@Composable @OptIn(ExperimentalPagerApi::class)
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
fun Modifier.pagerAnimate(pagerScope: PagerScope, page: Int): Modifier { fun Modifier.pagerAnimate(pagerScope: PagerScope, page: Int): Modifier {
return graphicsLayer { return graphicsLayer {
// Calculate the absolute offset for the current page from the // 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 fun Modifier.paddingFixedHorizontal(top: Dp = 0.dp, bottom: Dp = 0.dp) = this
.padding(horizontal = 10.dp) .padding(horizontal = 10.dp)
.padding(top = top, bottom = bottom) .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 package me.ash.reader.ui.page.common
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.*
import androidx.compose.foundation.ExperimentalFoundationApi 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.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsHeight import com.google.accompanist.insets.navigationBarsHeight
import com.google.accompanist.insets.statusBarsPadding 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 com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.ui.page.home.HomePage import me.ash.reader.ui.page.home.HomePage
import me.ash.reader.ui.page.settings.SettingsPage import me.ash.reader.ui.page.settings.SettingsPage
import me.ash.reader.ui.theme.AppTheme import me.ash.reader.ui.theme.AppTheme
@ExperimentalAnimationApi @OptIn(ExperimentalAnimationApi::class)
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Composable @Composable
fun HomeEntry() { fun HomeEntry() {
val navController = rememberNavController() val navController = rememberAnimatedNavController()
AppTheme { AppTheme {
ProvideWindowInsets { ProvideWindowInsets {
@ -45,15 +42,91 @@ fun HomeEntry() {
.weight(1f) .weight(1f)
.statusBarsPadding() .statusBarsPadding()
) { ) {
NavHost( AnimatedNavHost(
modifier = Modifier.background(MaterialTheme.colorScheme.surface), modifier = Modifier.background(MaterialTheme.colorScheme.surface),
navController = navController, navController = navController,
startDestination = RouteName.HOME, 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) 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) 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 android.view.HapticFeedbackConstants
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
@ -36,11 +36,12 @@ import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import me.ash.reader.data.constant.Filter import me.ash.reader.data.constant.Filter
import me.ash.reader.data.constant.NavigationBarItem import me.ash.reader.data.constant.NavigationBarItem
import me.ash.reader.ui.widget.CanBeDisabledIconButton
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ExperimentalPagerApi @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun AppNavigationBar( fun HomeBottomNavBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
pagerState: PagerState, pagerState: PagerState,
filter: Filter, filter: Filter,

View File

@ -2,14 +2,10 @@ package me.ash.reader.ui.page.home
import android.util.Log import android.util.Log
import androidx.activity.compose.BackHandler 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.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -18,24 +14,19 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.constant.Symbol 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.article.ArticlePage
import me.ash.reader.ui.page.home.feed.FeedPage 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.ReadPage
import me.ash.reader.ui.page.home.read.ReadViewAction import me.ash.reader.ui.page.home.read.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel import me.ash.reader.ui.page.home.read.ReadViewModel
import me.ash.reader.ui.util.collectAsStateValue import me.ash.reader.ui.widget.ViewPager
import me.ash.reader.ui.util.findActivity
import me.ash.reader.ui.util.pagerAnimate
import me.ash.reader.ui.widget.AppNavigationBar
@ExperimentalAnimationApi @OptIn(ExperimentalPagerApi::class)
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Composable @Composable
fun HomePage( fun HomePage(
navController: NavHostController, navController: NavHostController,
@ -48,7 +39,7 @@ fun HomePage(
val readState = readViewModel.viewState.collectAsStateValue() val readState = readViewModel.viewState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
DisposableEffect(Unit) { LaunchedEffect(Unit) {
context.findActivity()?.let { activity -> context.findActivity()?.let { activity ->
activity.intent?.let { intent -> activity.intent?.let { intent ->
intent.extras?.get(Symbol.EXTRA_ARTICLE_ID)?.let { 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) { BackHandler(true) {
@ -103,117 +92,78 @@ fun HomePage(
} }
} }
// val items = listOf(
// Color.Red,
// Color.Blue,
// Color.Green,
// )
Column { Column {
// CustomPager( ViewPager(
// items = items, modifier = Modifier.weight(1f),
// 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,
state = viewState.pagerState, state = viewState.pagerState,
modifier = Modifier.weight(1f) composableList = listOf(
) { page -> {
when (page) { FeedPage(
0 -> FeedPage( navController = navController,
navController = navController, filter = filterState.filter,
modifier = Modifier.pagerAnimate(this, page), groupAndFeedOnClick = { currentGroup, currentFeed ->
filter = filterState.filter, viewModel.dispatch(
groupAndFeedOnClick = { currentGroup, currentFeed -> HomeViewAction.ChangeFilter(
viewModel.dispatch( filterState.copy(
HomeViewAction.ChangeFilter( group = currentGroup,
filterState.copy( feed = currentFeed,
group = currentGroup, )
feed = currentFeed,
) )
) )
) viewModel.dispatch(
viewModel.dispatch( HomeViewAction.ScrollToPage(
HomeViewAction.ScrollToPage( scope = scope,
scope = scope, targetPage = 1,
targetPage = 1, )
) )
) },
}, )
) },
1 -> ArticlePage( {
navController = navController, ArticlePage(
modifier = Modifier.pagerAnimate(this, page), navController = navController,
BackOnClick = { BackOnClick = {
viewModel.dispatch( viewModel.dispatch(
HomeViewAction.ScrollToPage( HomeViewAction.ScrollToPage(
scope = scope, scope = scope,
targetPage = 0, targetPage = 0,
)
) )
) },
}, articleOnClick = {
articleOnClick = { readViewModel.dispatch(ReadViewAction.ScrollToItem(0))
readViewModel.dispatch(ReadViewAction.ScrollToItem(0)) readViewModel.dispatch(ReadViewAction.InitData(it))
readViewModel.dispatch(ReadViewAction.InitData(it)) if (it.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
if (it.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent) else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) viewModel.dispatch(
viewModel.dispatch( HomeViewAction.ScrollToPage(
HomeViewAction.ScrollToPage( scope = scope,
scope = scope, targetPage = 2,
targetPage = 2, )
) )
) },
}, )
) },
2 -> ReadPage( {
navController = navController, ReadPage(
modifier = Modifier.pagerAnimate(this, page), navController = navController,
btnBackOnClickListener = { btnBackOnClickListener = {
viewModel.dispatch( viewModel.dispatch(
HomeViewAction.ScrollToPage( HomeViewAction.ScrollToPage(
scope = scope, scope = scope,
targetPage = 1, targetPage = 1,
callback = { callback = {
readViewModel.dispatch(ReadViewAction.ClearArticle) readViewModel.dispatch(ReadViewAction.ClearArticle)
} }
)
) )
) },
}, )
) },
} ),
} )
AppNavigationBar( HomeBottomNavBar(
modifier = Modifier modifier = Modifier
.height(60.dp) .height(60.dp)
.fillMaxWidth(), .fillMaxWidth(),

View File

@ -1,8 +1,5 @@
package me.ash.reader.ui.page.home 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
@ -15,16 +12,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.constant.Filter
import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group import me.ash.reader.data.group.Group
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.constant.Filter
import javax.inject.Inject import javax.inject.Inject
@ExperimentalAnimationApi @OptIn(ExperimentalPagerApi::class)
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject constructor( class HomeViewModel @Inject constructor(
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
@ -36,6 +30,8 @@ class HomeViewModel @Inject constructor(
private val _filterState = MutableStateFlow(FilterState()) private val _filterState = MutableStateFlow(FilterState())
val filterState = _filterState.asStateFlow() val filterState = _filterState.asStateFlow()
val syncState = RssRepository.syncState
fun dispatch(action: HomeViewAction) { fun dispatch(action: HomeViewAction) {
when (action) { when (action) {
is HomeViewAction.Sync -> sync(action.callback) is HomeViewAction.Sync -> sync(action.callback)
@ -79,8 +75,8 @@ data class FilterState(
val filter: Filter = Filter.All, val filter: Filter = Filter.All,
) )
@ExperimentalPagerApi @OptIn(ExperimentalPagerApi::class)
data class HomeViewState( data class HomeViewState constructor(
val pagerState: PagerState = PagerState(1), 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 package me.ash.reader.ui.page.home.article
import android.graphics.BitmapFactory
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.border import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.height
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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset 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.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.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import me.ash.reader.DateTimeExt import me.ash.reader.DateTimeExt
import me.ash.reader.DateTimeExt.toString import me.ash.reader.DateTimeExt.toString
import me.ash.reader.R
import me.ash.reader.data.article.ArticleWithFeed import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.data.constant.Filter 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.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel 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.AnimateLazyColumn
import me.ash.reader.ui.widget.TopTitleBox import me.ash.reader.ui.widget.TopTitleBox
@OptIn(ExperimentalFoundationApi::class)
@DelicateCoroutinesApi @DelicateCoroutinesApi
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Composable @Composable
fun ArticlePage( fun ArticlePage(
navController: NavHostController, navController: NavHostController,
modifier: Modifier, modifier: Modifier = Modifier,
homeViewModel: HomeViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel(),
viewModel: ArticleViewModel = hiltViewModel(), viewModel: ArticleViewModel = hiltViewModel(),
BackOnClick: () -> Unit, BackOnClick: () -> Unit,
@ -70,7 +46,7 @@ fun ArticlePage(
val filterState = homeViewModel.filterState.collectAsStateValue() val filterState = homeViewModel.filterState.collectAsStateValue()
val pagingItems = viewState.pagingData?.collectAsLazyPagingItems() val pagingItems = viewState.pagingData?.collectAsLazyPagingItems()
val refreshState = rememberSwipeRefreshState(isRefreshing = viewState.isRefreshing) val refreshState = rememberSwipeRefreshState(isRefreshing = viewState.isRefreshing)
val syncState = RssRepository.syncState.collectAsStateValue() val syncState = homeViewModel.syncState.collectAsStateValue()
LaunchedEffect(homeViewModel.filterState) { LaunchedEffect(homeViewModel.filterState) {
homeViewModel.filterState.collect { state -> homeViewModel.filterState.collect { state ->
@ -115,39 +91,15 @@ fun ArticlePage(
viewModel.dispatch(ArticleViewAction.ScrollToItem(0)) viewModel.dispatch(ArticleViewAction.ScrollToItem(0))
} }
Column { Column {
SmallTopAppBar( ArticlePageTopBar(
title = {}, backOnClick = BackOnClick,
navigationIcon = { readAllOnClick = {
IconButton(BackOnClick) { viewModel.dispatch(ArticleViewAction.PeekSyncWork)
Icon( Toast.makeText(context, viewState.syncWorkInfo, Toast.LENGTH_LONG)
imageVector = Icons.Rounded.ArrowBackIosNew, .show()
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary
)
}
}, },
actions = { searchOnClick = {
IconButton(onClick = {
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
)
}
}, },
) )
@ -190,140 +142,4 @@ 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 android.graphics.BitmapFactory
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.ui.util.paddingFixedHorizontal import me.ash.reader.ui.extension.paddingFixedHorizontal
import me.ash.reader.ui.util.roundClick import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.widget.AnimatedText
@ExperimentalAnimationApi
@Composable @Composable
fun BarButton( fun FeedBar(
barButtonType: BarButtonType, barButtonType: BarButtonType,
iconOnClickListener: () -> Unit = {}, iconOnClickListener: () -> Unit = {},
onClickListener: () -> 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 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.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed 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.Icons
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material.icons.rounded.* import androidx.compose.runtime.Composable
import androidx.compose.material3.* import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.* import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier 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.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.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import me.ash.reader.DateTimeExt import me.ash.reader.DateTimeExt
import me.ash.reader.DateTimeExt.toString import me.ash.reader.DateTimeExt.toString
import me.ash.reader.data.constant.Filter import me.ash.reader.data.constant.Filter
import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupWithFeed import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.util.collectAsStateValue import me.ash.reader.ui.page.home.feed.subscribe.SubscribeDialog
import me.ash.reader.ui.widget.* import me.ash.reader.ui.widget.TopTitleBox
import java.io.InputStream
@ExperimentalComposeUiApi
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Composable @Composable
fun FeedPage( fun FeedPage(
navController: NavHostController, navController: NavHostController,
modifier: Modifier, modifier: Modifier = Modifier,
viewModel: FeedViewModel = hiltViewModel(), viewModel: FeedViewModel = hiltViewModel(),
homeViewModel: HomeViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel(),
filter: Filter, filter: Filter,
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> }, groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
) { ) {
val viewState = viewModel.viewState.collectAsStateValue() val viewState = viewModel.viewState.collectAsStateValue()
val syncState = RssRepository.syncState.collectAsStateValue() val syncState = homeViewModel.syncState.collectAsStateValue()
var addFeedDialogVisible by remember { mutableStateOf(false) }
LaunchedEffect(homeViewModel.filterState) { LaunchedEffect(homeViewModel.filterState) {
homeViewModel.filterState.collect { state -> homeViewModel.filterState.collect { state ->
@ -87,11 +56,24 @@ fun FeedPage(
} }
Box( Box(
modifier.fillMaxSize() modifier = modifier.fillMaxSize()
) { ) {
AddFeedDialog( SubscribeDialog(
visible = addFeedDialogVisible, visible = viewState.subscribeDialogVisible,
hiddenFunction = { addFeedDialogVisible = false }, hiddenFunction = {
viewModel.dispatch(FeedViewAction.ChangeSubscribeDialogVisible(false))
},
inputContent = viewState.subscribeDialogFeedLink,
onValueChange = {
viewModel.dispatch(
FeedViewAction.InputSubscribeFeedLink(it)
)
},
onKeyboardAction = {
viewModel.dispatch(
FeedViewAction.ChangeSubscribeDialogVisible(false)
)
},
openInputStreamCallback = { openInputStreamCallback = {
viewModel.dispatch(FeedViewAction.AddFromFile(it)) viewModel.dispatch(FeedViewAction.AddFromFile(it))
}, },
@ -113,42 +95,14 @@ fun FeedPage(
viewModel.dispatch(FeedViewAction.ScrollToItem(0)) viewModel.dispatch(FeedViewAction.ScrollToItem(0))
} }
Column { Column {
SmallTopAppBar( FeedPageTopBar(
title = {}, navController = navController,
navigationIcon = { isSyncing = syncState.isSyncing,
IconButton(onClick = { syncOnClick = {
navController.navigate(route = RouteName.SETTINGS) homeViewModel.dispatch(HomeViewAction.Sync())
}) {
Icon(
modifier = Modifier.size(22.dp),
imageVector = Icons.Outlined.Settings,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.primary
)
}
}, },
actions = { subscribeOnClick = {
IconButton(onClick = { viewModel.dispatch(FeedViewAction.ChangeSubscribeDialogVisible(true))
if (syncState.isSyncing) return@IconButton
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,
)
}
}, },
) )
LazyColumn( LazyColumn(
@ -157,7 +111,7 @@ fun FeedPage(
) { ) {
item { item {
Spacer(modifier = Modifier.height(114.dp)) Spacer(modifier = Modifier.height(114.dp))
BarButton( FeedBar(
barButtonType = ButtonType( barButtonType = ButtonType(
content = filter.title, content = filter.title,
important = viewState.filterImportant important = viewState.filterImportant
@ -168,13 +122,13 @@ fun FeedPage(
} }
item { item {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
BarButton( FeedBar(
barButtonType = FirstExpandType( barButtonType = FirstExpandType(
content = "Feeds", content = "Feeds",
icon = Icons.Rounded.ExpandMore icon = Icons.Rounded.ExpandMore
) )
) { ) {
viewModel.dispatch(FeedViewAction.ChangeGroupVisible) viewModel.dispatch(FeedViewAction.ChangeGroupVisible(!viewState.groupsVisible))
} }
} }
itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed -> 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.FetchData -> fetchData(action.isStarred, action.isUnread)
is FeedViewAction.AddFromFile -> addFromFile(action.inputStream) is FeedViewAction.AddFromFile -> addFromFile(action.inputStream)
is FeedViewAction.ChangeFeedVisible -> changeFeedVisible(action.index) is FeedViewAction.ChangeFeedVisible -> changeFeedVisible(action.index)
is FeedViewAction.ChangeGroupVisible -> changeGroupVisible() is FeedViewAction.ChangeGroupVisible -> changeGroupVisible(action.visible)
is FeedViewAction.ScrollToItem -> scrollToItem(action.index) 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 { _viewState.update {
it.copy( it.copy(
groupsVisible = !_viewState.value.groupsVisible groupsVisible = visible
) )
} }
} }
@ -143,6 +165,8 @@ data class FeedViewState(
val feedsVisible: List<Boolean> = emptyList(), val feedsVisible: List<Boolean> = emptyList(),
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),
val groupsVisible: Boolean = true, val groupsVisible: Boolean = true,
var subscribeDialogVisible: Boolean = false,
var subscribeDialogFeedLink: String = "",
) )
sealed class FeedViewAction { sealed class FeedViewAction {
@ -163,8 +187,19 @@ sealed class FeedViewAction {
val index: Int val index: Int
) : FeedViewAction() ) : FeedViewAction()
object ChangeGroupVisible : FeedViewAction() data class ChangeGroupVisible(
val visible: Boolean
) : FeedViewAction()
data class ScrollToItem( data class ScrollToItem(
val index: Int val index: Int
) : FeedViewAction() ) : 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 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.animation.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.selection.SelectionContainer 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition import com.airbnb.lottie.compose.rememberLottieComposition
import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import me.ash.reader.DateTimeExt import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.DateTimeExt.toString import me.ash.reader.ui.extension.paddingFixedHorizontal
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.widget.WebView import me.ash.reader.ui.widget.WebView
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Composable @Composable
fun ReadPage( fun ReadPage(
navController: NavHostController, navController: NavHostController,
@ -69,38 +48,7 @@ fun ReadPage(
Column( Column(
modifier.fillMaxSize() modifier.fillMaxSize()
) { ) {
ReadPageTopBar(btnBackOnClickListener)
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,
)
}
},
)
val composition by rememberLottieComposition( val composition by rememberLottieComposition(
LottieCompositionSpec.Url( 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 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.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi import me.ash.reader.ui.extension.paddingFixedHorizontal
import me.ash.reader.ui.util.paddingFixedHorizontal import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.util.roundClick
import me.ash.reader.ui.widget.TopTitleBox import me.ash.reader.ui.widget.TopTitleBox
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Composable @Composable
fun SettingsPage( fun SettingsPage(
navController: NavHostController, navController: NavHostController,
@ -156,10 +149,6 @@ fun Item(
} }
} }
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalPagerApi
@ExperimentalFoundationApi
@Preview @Preview
@Composable @Composable
fun SettingsPreview() { fun SettingsPreview() {

View File

@ -1,18 +1,21 @@
package me.ash.reader.ui.theme 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(0xFF4D4D4D)
val md_theme_light_primary = Color(0xFF6750A4) val md_theme_light_primary = Color(0xFF6750A4)
val md_theme_light_onPrimary = Color(0xFFFFFFFF) val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFEADDFF) val md_theme_light_primaryContainer = Color(0xFFEADDFF)
val md_theme_light_onPrimaryContainer = Color(0xFF21005D) val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
//val md_theme_light_secondary = Color(0xFF868686) //val md_theme_light_secondary = Color(0xFF868686)
val md_theme_light_secondary = Color(0xFF625B71) val md_theme_light_secondary = Color(0xFF625B71)
val md_theme_light_onSecondary = Color(0xFFFFFFFF) val md_theme_light_onSecondary = Color(0xFFFFFFFF)
//val md_theme_light_secondaryContainer = Color(0xFFEAEAEA) //val md_theme_light_secondaryContainer = Color(0xFFEAEAEA)
val md_theme_light_secondaryContainer = Color(0xFFE8DEF8) val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
val md_theme_light_onSecondaryContainer = Color(0xFF1D192B) val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
//val md_theme_light_tertiary = Color(0xFFC1C1C1) //val md_theme_light_tertiary = Color(0xFFC1C1C1)
val md_theme_light_tertiary = Color(0xFF7D5260) val md_theme_light_tertiary = Color(0xFF7D5260)
val md_theme_light_onTertiary = Color(0xFFFFFFFF) 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_errorContainer = Color(0xFFF9DEDC)
val md_theme_light_onError = Color(0xFFFFFFFF) val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410E0B) val md_theme_light_onErrorContainer = Color(0xFF410E0B)
//val md_theme_light_background = Color(0xFFF7F5F4) //val md_theme_light_background = Color(0xFFF7F5F4)
val md_theme_light_background = Color(0xFFFFFBFE) val md_theme_light_background = Color(0xFFFFFBFE)
val md_theme_light_onBackground = Color(0xFF1C1B1F) val md_theme_light_onBackground = Color(0xFF1C1B1F)
//val md_theme_light_surface = Color(0xFFF7F5F4) //val md_theme_light_surface = Color(0xFFF7F5F4)
val md_theme_light_surface = Color(0xFFFFFBFE) val md_theme_light_surface = Color(0xFFFFFBFE)
val md_theme_light_onSurface = Color(0xFF1C1B1F) val md_theme_light_onSurface = Color(0xFF1C1B1F)
val md_theme_light_surfaceVariant = Color(0xFFE7E0EC) val md_theme_light_surfaceVariant = Color(0xFFE7E0EC)
//val md_theme_light_onSurfaceVariant = md_theme_light_secondary //val md_theme_light_onSurfaceVariant = md_theme_light_secondary
val md_theme_light_onSurfaceVariant = Color(0xFF49454F) val md_theme_light_onSurfaceVariant = Color(0xFF49454F)
val md_theme_light_outline = Color(0xFF79747E) val md_theme_light_outline = Color(0xFF79747E)

View File

@ -1,116 +1,118 @@
package me.ash.reader.ui.theme package me.ash.reader.ui.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
//Replace with your font locations //Replace with your font locations
val Roboto = FontFamily.Default val Roboto = FontFamily.Default
val AppTypography = Typography( val AppTypography = Typography(
displayLarge = TextStyle( displayLarge = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 57.sp, fontSize = 57.sp,
lineHeight = 64.sp, lineHeight = 64.sp,
letterSpacing = -0.25.sp, letterSpacing = -0.25.sp,
), ),
displayMedium = TextStyle( displayMedium = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 45.sp, fontSize = 45.sp,
lineHeight = 52.sp, lineHeight = 52.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
displaySmall = TextStyle( displaySmall = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 36.sp, fontSize = 36.sp,
lineHeight = 44.sp, lineHeight = 44.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
headlineLarge = TextStyle( headlineLarge = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 32.sp, fontSize = 32.sp,
lineHeight = 40.sp, lineHeight = 40.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
headlineMedium = TextStyle( headlineMedium = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 28.sp, fontSize = 28.sp,
lineHeight = 36.sp, lineHeight = 36.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
headlineSmall = TextStyle( headlineSmall = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 24.sp, fontSize = 24.sp,
lineHeight = 32.sp, lineHeight = 32.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
titleLarge = TextStyle( titleLarge = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 22.sp, fontSize = 22.sp,
lineHeight = 28.sp, lineHeight = 28.sp,
letterSpacing = 0.sp, letterSpacing = 0.sp,
), ),
titleMedium = TextStyle( titleMedium = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.1.sp, letterSpacing = 0.1.sp,
), ),
titleSmall = TextStyle( titleSmall = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.1.sp, letterSpacing = 0.1.sp,
), ),
labelLarge = TextStyle( labelLarge = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.1.sp, letterSpacing = 0.1.sp,
), ),
bodyLarge = TextStyle( bodyLarge = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.5.sp, letterSpacing = 0.5.sp,
), ),
bodyMedium = TextStyle( bodyMedium = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.25.sp, letterSpacing = 0.25.sp,
), ),
bodySmall = TextStyle( bodySmall = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.4.sp, letterSpacing = 0.4.sp,
), ),
labelMedium = TextStyle( labelMedium = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.5.sp, letterSpacing = 0.5.sp,
), ),
labelSmall = TextStyle( labelSmall = TextStyle(
fontFamily = Roboto, fontFamily = Roboto,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 11.sp, fontSize = 11.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.5.sp, letterSpacing = 0.5.sp,
), ),
) )

View File

@ -10,7 +10,6 @@ import androidx.recyclerview.widget.ListUpdateCallback
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ExperimentalAnimationApi
@Suppress("UpdateTransitionLabel", "TransitionPropertiesLabel") @Suppress("UpdateTransitionLabel", "TransitionPropertiesLabel")
@SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter") @SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter")
/** /**
@ -63,7 +62,6 @@ fun <T> updateAnimatedItemsState(
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].item == newList[newItemPosition] oldList[oldItemPosition].item == newList[newItemPosition]
} }
val diffResult = calculateDiff(false, diffCb) val diffResult = calculateDiff(false, diffCb)
val compositeList = oldList.toMutableList() 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.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
@ExperimentalAnimationApi @OptIn(ExperimentalAnimationApi::class)
@Composable @Composable
fun AnimatedText( fun AnimatedText(
text: String, text: String,

View File

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

View File

@ -16,7 +16,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ExperimentalPagerApi @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun BoxScope.MaskBox( fun BoxScope.MaskBox(
modifier: Modifier = Modifier, 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 package me.ash.reader.ui.widget
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import me.ash.reader.ui.util.calculateTopBarAnimateValue import me.ash.reader.ui.extension.calculateTopBarAnimateValue
@ExperimentalAnimationApi
@Composable @Composable
fun BoxScope.TopTitleBox( fun BoxScope.TopTitleBox(
title: String, 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.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.navigation.compose.hiltViewModel 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.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel import me.ash.reader.ui.page.home.read.ReadViewModel
import me.ash.reader.ui.util.collectAsStateValue
@Composable @Composable
fun WebView( fun WebView(