Add subscribe feed UI
This commit is contained in:
parent
5fff554bba
commit
11ca1f1ae8
|
@ -50,7 +50,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation("io.coil-kt:coil-compose:1.4.0")
|
||||
implementation("com.google.accompanist:accompanist-flowlayout:0.24.3-alpha")
|
||||
implementation("com.google.accompanist:accompanist-navigation-animation:0.24.3-alpha")
|
||||
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
||||
implementation "com.airbnb.android:lottie-compose:4.2.2"
|
||||
implementation "androidx.work:work-runtime-ktx:2.8.0-alpha01"
|
||||
|
@ -82,7 +83,7 @@ dependencies {
|
|||
implementation "androidx.compose.ui:ui:$compose_version"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha01"
|
||||
implementation "androidx.compose.material:material:1.2.0-alpha03"
|
||||
implementation "androidx.compose.material3:material3:1.0.0-alpha05"
|
||||
implementation "androidx.compose.material3:material3:1.0.0-alpha06"
|
||||
implementation "androidx.compose.material:material-icons-extended:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
package me.ash.reader
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
|
@ -18,10 +14,6 @@ import me.ash.reader.data.source.ReaderDatabase
|
|||
import me.ash.reader.data.source.RssNetworkDataSource
|
||||
import javax.inject.Inject
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@DelicateCoroutinesApi
|
||||
@HiltAndroidApp
|
||||
class App : Application() {
|
||||
|
|
|
@ -3,18 +3,10 @@ package me.ash.reader
|
|||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import me.ash.reader.ui.page.common.HomeEntry
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ interface AccountDao {
|
|||
)
|
||||
suspend fun queryAll(): List<Account>
|
||||
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM account
|
||||
|
|
|
@ -8,14 +8,10 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.work.*
|
||||
import com.github.muhrifqii.parserss.ParseRSS
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
@ -40,7 +36,7 @@ import java.util.*
|
|||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
class RssRepository @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
|
@ -87,10 +83,6 @@ class RssRepository @Inject constructor(
|
|||
return workManager.getWorkInfosByTag("sync").get().size.toString()
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
suspend fun sync(isWork: Boolean? = false) {
|
||||
if (isWork == true) {
|
||||
workManager.cancelAllWork()
|
||||
|
@ -108,10 +100,6 @@ class RssRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@DelicateCoroutinesApi
|
||||
companion object {
|
||||
data class SyncState(
|
||||
|
@ -215,7 +203,10 @@ class RssRepository @Inject constructor(
|
|||
val ids = articleDao.insertList(articleList)
|
||||
articleList.forEachIndexed { index, article ->
|
||||
Log.i("RlOG", "combine ${article.feedId}: ${article.title}")
|
||||
val builder = NotificationCompat.Builder(context, Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
|
||||
.setContentTitle(article.title)
|
||||
|
@ -346,10 +337,6 @@ class RssRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@DelicateCoroutinesApi
|
||||
class SyncWorker(
|
||||
context: Context,
|
||||
|
|
11
app/src/main/java/me/ash/reader/ui/extension/ContextExt.kt
Normal file
11
app/src/main/java/me/ash/reader/ui/extension/ContextExt.kt
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -1,14 +1,8 @@
|
|||
package me.ash.reader.ui.util
|
||||
package me.ash.reader.ui.extension
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
|
@ -18,28 +12,9 @@ import androidx.compose.ui.unit.lerp
|
|||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.PagerScope
|
||||
import com.google.accompanist.pager.calculateCurrentOffsetForPage
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun <T> StateFlow<T>.collectAsStateValue(
|
||||
context: CoroutineContext = EmptyCoroutineContext
|
||||
): T = collectAsState(context).value
|
||||
|
||||
fun LazyListState.calculateTopBarAnimateValue(start: Float, end: Float): Float =
|
||||
if (firstVisibleItemIndex != 0) end
|
||||
else {
|
||||
val variable = firstVisibleItemScrollOffset.coerceAtLeast(0).toFloat()
|
||||
val duration = 256f
|
||||
val increase = abs(start - end) * (variable / duration)
|
||||
if (start < end) (start + increase).coerceIn(start, end)
|
||||
else (start - increase).coerceIn(end, start)
|
||||
}
|
||||
|
||||
@ExperimentalPagerApi
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
fun Modifier.pagerAnimate(pagerScope: PagerScope, page: Int): Modifier {
|
||||
return graphicsLayer {
|
||||
// Calculate the absolute offset for the current page from the
|
||||
|
@ -73,9 +48,3 @@ fun Modifier.roundClick(onClick: () -> Unit = {}) = this
|
|||
fun Modifier.paddingFixedHorizontal(top: Dp = 0.dp, bottom: Dp = 0.dp) = this
|
||||
.padding(horizontal = 10.dp)
|
||||
.padding(top = top, bottom = bottom)
|
||||
|
||||
fun Context.findActivity(): Activity? = when (this) {
|
||||
is Activity -> this
|
||||
is ContextWrapper -> baseContext.findActivity()
|
||||
else -> null
|
||||
}
|
12
app/src/main/java/me/ash/reader/ui/extension/StateFlowExt.kt
Normal file
12
app/src/main/java/me/ash/reader/ui/extension/StateFlowExt.kt
Normal 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
|
|
@ -1,36 +1,33 @@
|
|||
package me.ash.reader.ui.page.common
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsHeight
|
||||
import com.google.accompanist.insets.statusBarsPadding
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||
import com.google.accompanist.navigation.animation.composable
|
||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import me.ash.reader.ui.page.home.HomePage
|
||||
import me.ash.reader.ui.page.settings.SettingsPage
|
||||
import me.ash.reader.ui.theme.AppTheme
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun HomeEntry() {
|
||||
val navController = rememberNavController()
|
||||
val navController = rememberAnimatedNavController()
|
||||
|
||||
AppTheme {
|
||||
ProvideWindowInsets {
|
||||
|
@ -45,15 +42,91 @@ fun HomeEntry() {
|
|||
.weight(1f)
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
NavHost(
|
||||
AnimatedNavHost(
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||
navController = navController,
|
||||
startDestination = RouteName.HOME,
|
||||
) {
|
||||
composable(route = RouteName.HOME) {
|
||||
composable(
|
||||
route = RouteName.HOME,
|
||||
enterTransition = {
|
||||
slideInHorizontally(
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
dampingRatio = Spring.DampingRatioNoBouncy
|
||||
),
|
||||
initialOffsetX = { -it }
|
||||
) + fadeIn(animationSpec = tween(220, delayMillis = 90))
|
||||
},
|
||||
exitTransition = {
|
||||
slideOutHorizontally(
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
dampingRatio = Spring.DampingRatioNoBouncy
|
||||
),
|
||||
targetOffsetX = { it }
|
||||
) + fadeOut(animationSpec = tween(220, delayMillis = 90))
|
||||
},
|
||||
popEnterTransition = {
|
||||
slideInHorizontally(
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
dampingRatio = Spring.DampingRatioNoBouncy
|
||||
),
|
||||
initialOffsetX = { -it }
|
||||
) + fadeIn(animationSpec = tween(220, delayMillis = 90))
|
||||
},
|
||||
popExitTransition = {
|
||||
slideOutHorizontally(
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
dampingRatio = Spring.DampingRatioNoBouncy
|
||||
),
|
||||
targetOffsetX = { it }
|
||||
) + fadeOut(animationSpec = tween(220, delayMillis = 90))
|
||||
},
|
||||
) {
|
||||
HomePage(navController)
|
||||
}
|
||||
composable(route = RouteName.SETTINGS) {
|
||||
composable(
|
||||
route = RouteName.SETTINGS,
|
||||
enterTransition = {
|
||||
slideInHorizontally(
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
dampingRatio = Spring.DampingRatioNoBouncy
|
||||
),
|
||||
initialOffsetX = { -it }
|
||||
) + fadeIn(animationSpec = tween(220, delayMillis = 90))
|
||||
},
|
||||
exitTransition = {
|
||||
slideOutHorizontally(
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
dampingRatio = Spring.DampingRatioNoBouncy
|
||||
),
|
||||
targetOffsetX = { -it }
|
||||
) + fadeOut(animationSpec = tween(220, delayMillis = 90))
|
||||
},
|
||||
popEnterTransition = {
|
||||
slideInHorizontally(
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
dampingRatio = Spring.DampingRatioNoBouncy
|
||||
),
|
||||
initialOffsetX = { -it }
|
||||
) + fadeIn(animationSpec = tween(220, delayMillis = 90))
|
||||
},
|
||||
popExitTransition = {
|
||||
slideOutHorizontally(
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
dampingRatio = Spring.DampingRatioNoBouncy
|
||||
),
|
||||
targetOffsetX = { -it }
|
||||
) + fadeOut(animationSpec = tween(220, delayMillis = 90))
|
||||
},
|
||||
) {
|
||||
SettingsPage(navController)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package me.ash.reader.ui.widget
|
||||
package me.ash.reader.ui.page.home
|
||||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
|
@ -36,11 +36,12 @@ import com.google.accompanist.pager.ExperimentalPagerApi
|
|||
import com.google.accompanist.pager.PagerState
|
||||
import me.ash.reader.data.constant.Filter
|
||||
import me.ash.reader.data.constant.NavigationBarItem
|
||||
import me.ash.reader.ui.widget.CanBeDisabledIconButton
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@ExperimentalPagerApi
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun AppNavigationBar(
|
||||
fun HomeBottomNavBar(
|
||||
modifier: Modifier = Modifier,
|
||||
pagerState: PagerState,
|
||||
filter: Filter,
|
|
@ -2,14 +2,10 @@ package me.ash.reader.ui.page.home
|
|||
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -18,24 +14,19 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.constant.Symbol
|
||||
import me.ash.reader.ui.extension.collectAsStateValue
|
||||
import me.ash.reader.ui.extension.findActivity
|
||||
import me.ash.reader.ui.page.home.article.ArticlePage
|
||||
import me.ash.reader.ui.page.home.feed.FeedPage
|
||||
import me.ash.reader.ui.page.home.read.ReadPage
|
||||
import me.ash.reader.ui.page.home.read.ReadViewAction
|
||||
import me.ash.reader.ui.page.home.read.ReadViewModel
|
||||
import me.ash.reader.ui.util.collectAsStateValue
|
||||
import me.ash.reader.ui.util.findActivity
|
||||
import me.ash.reader.ui.util.pagerAnimate
|
||||
import me.ash.reader.ui.widget.AppNavigationBar
|
||||
import me.ash.reader.ui.widget.ViewPager
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun HomePage(
|
||||
navController: NavHostController,
|
||||
|
@ -48,7 +39,7 @@ fun HomePage(
|
|||
val readState = readViewModel.viewState.collectAsStateValue()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
LaunchedEffect(Unit) {
|
||||
context.findActivity()?.let { activity ->
|
||||
activity.intent?.let { intent ->
|
||||
intent.extras?.get(Symbol.EXTRA_ARTICLE_ID)?.let {
|
||||
|
@ -69,11 +60,9 @@ fun HomePage(
|
|||
)
|
||||
}
|
||||
}
|
||||
intent.extras?.clear()
|
||||
intent.extras?.remove(Symbol.EXTRA_ARTICLE_ID)
|
||||
}
|
||||
}
|
||||
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
BackHandler(true) {
|
||||
|
@ -103,56 +92,14 @@ fun HomePage(
|
|||
}
|
||||
}
|
||||
|
||||
// val items = listOf(
|
||||
// Color.Red,
|
||||
// Color.Blue,
|
||||
// Color.Green,
|
||||
// )
|
||||
|
||||
Column {
|
||||
// CustomPager(
|
||||
// items = items,
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .height(256.dp),
|
||||
// itemFraction = .75f,
|
||||
// overshootFraction = .75f,
|
||||
// initialIndex = 3,
|
||||
// itemSpacing = 16.dp,
|
||||
// ) {
|
||||
// items.forEachIndexed { index, item ->
|
||||
// if (index % 2 == 0) {
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .fillMaxSize()
|
||||
// .background(item),
|
||||
// contentAlignment = Alignment.Center
|
||||
// ) {
|
||||
// Text(
|
||||
// text = item.toString(),
|
||||
// modifier = Modifier.padding(all = 16.dp),
|
||||
//// style = MaterialTheme.typography.h6,
|
||||
// )
|
||||
// }
|
||||
// } else {
|
||||
// Image(
|
||||
// modifier = Modifier.fillMaxSize(),
|
||||
// painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
// contentDescription = null,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
HorizontalPager(
|
||||
count = 3,
|
||||
ViewPager(
|
||||
modifier = Modifier.weight(1f),
|
||||
state = viewState.pagerState,
|
||||
modifier = Modifier.weight(1f)
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> FeedPage(
|
||||
composableList = listOf(
|
||||
{
|
||||
FeedPage(
|
||||
navController = navController,
|
||||
modifier = Modifier.pagerAnimate(this, page),
|
||||
filter = filterState.filter,
|
||||
groupAndFeedOnClick = { currentGroup, currentFeed ->
|
||||
viewModel.dispatch(
|
||||
|
@ -171,9 +118,10 @@ fun HomePage(
|
|||
)
|
||||
},
|
||||
)
|
||||
1 -> ArticlePage(
|
||||
},
|
||||
{
|
||||
ArticlePage(
|
||||
navController = navController,
|
||||
modifier = Modifier.pagerAnimate(this, page),
|
||||
BackOnClick = {
|
||||
viewModel.dispatch(
|
||||
HomeViewAction.ScrollToPage(
|
||||
|
@ -196,9 +144,10 @@ fun HomePage(
|
|||
)
|
||||
},
|
||||
)
|
||||
2 -> ReadPage(
|
||||
},
|
||||
{
|
||||
ReadPage(
|
||||
navController = navController,
|
||||
modifier = Modifier.pagerAnimate(this, page),
|
||||
btnBackOnClickListener = {
|
||||
viewModel.dispatch(
|
||||
HomeViewAction.ScrollToPage(
|
||||
|
@ -211,9 +160,10 @@ fun HomePage(
|
|||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
AppNavigationBar(
|
||||
},
|
||||
),
|
||||
)
|
||||
HomeBottomNavBar(
|
||||
modifier = Modifier
|
||||
.height(60.dp)
|
||||
.fillMaxWidth(),
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
package me.ash.reader.ui.page.home
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
|
@ -15,16 +12,13 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.data.constant.Filter
|
||||
import me.ash.reader.data.feed.Feed
|
||||
import me.ash.reader.data.group.Group
|
||||
import me.ash.reader.data.repository.RssRepository
|
||||
import me.ash.reader.data.constant.Filter
|
||||
import javax.inject.Inject
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val rssRepository: RssRepository,
|
||||
|
@ -36,6 +30,8 @@ class HomeViewModel @Inject constructor(
|
|||
private val _filterState = MutableStateFlow(FilterState())
|
||||
val filterState = _filterState.asStateFlow()
|
||||
|
||||
val syncState = RssRepository.syncState
|
||||
|
||||
fun dispatch(action: HomeViewAction) {
|
||||
when (action) {
|
||||
is HomeViewAction.Sync -> sync(action.callback)
|
||||
|
@ -79,8 +75,8 @@ data class FilterState(
|
|||
val filter: Filter = Filter.All,
|
||||
)
|
||||
|
||||
@ExperimentalPagerApi
|
||||
data class HomeViewState(
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
data class HomeViewState constructor(
|
||||
val pagerState: PagerState = PagerState(1),
|
||||
)
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +1,41 @@
|
|||
package me.ash.reader.ui.page.home.article
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
||||
import androidx.compose.material.icons.rounded.DoneAll
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import me.ash.reader.DateTimeExt
|
||||
import me.ash.reader.DateTimeExt.toString
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.data.article.ArticleWithFeed
|
||||
import me.ash.reader.data.constant.Filter
|
||||
import me.ash.reader.data.repository.RssRepository
|
||||
import me.ash.reader.ui.extension.collectAsStateValue
|
||||
import me.ash.reader.ui.page.home.HomeViewAction
|
||||
import me.ash.reader.ui.page.home.HomeViewModel
|
||||
import me.ash.reader.ui.util.collectAsStateValue
|
||||
import me.ash.reader.ui.util.paddingFixedHorizontal
|
||||
import me.ash.reader.ui.util.roundClick
|
||||
import me.ash.reader.ui.widget.AnimateLazyColumn
|
||||
import me.ash.reader.ui.widget.TopTitleBox
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@DelicateCoroutinesApi
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun ArticlePage(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier,
|
||||
modifier: Modifier = Modifier,
|
||||
homeViewModel: HomeViewModel = hiltViewModel(),
|
||||
viewModel: ArticleViewModel = hiltViewModel(),
|
||||
BackOnClick: () -> Unit,
|
||||
|
@ -70,7 +46,7 @@ fun ArticlePage(
|
|||
val filterState = homeViewModel.filterState.collectAsStateValue()
|
||||
val pagingItems = viewState.pagingData?.collectAsLazyPagingItems()
|
||||
val refreshState = rememberSwipeRefreshState(isRefreshing = viewState.isRefreshing)
|
||||
val syncState = RssRepository.syncState.collectAsStateValue()
|
||||
val syncState = homeViewModel.syncState.collectAsStateValue()
|
||||
|
||||
LaunchedEffect(homeViewModel.filterState) {
|
||||
homeViewModel.filterState.collect { state ->
|
||||
|
@ -115,39 +91,15 @@ fun ArticlePage(
|
|||
viewModel.dispatch(ArticleViewAction.ScrollToItem(0))
|
||||
}
|
||||
Column {
|
||||
SmallTopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(BackOnClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ArrowBackIosNew,
|
||||
contentDescription = "Back",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
ArticlePageTopBar(
|
||||
backOnClick = BackOnClick,
|
||||
readAllOnClick = {
|
||||
viewModel.dispatch(ArticleViewAction.PeekSyncWork)
|
||||
Toast.makeText(context, viewState.syncWorkInfo, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.DoneAll,
|
||||
contentDescription = "Done All",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
if (syncState.isSyncing) return@IconButton
|
||||
homeViewModel.dispatch(HomeViewAction.Sync())
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = "Search",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
searchOnClick = {
|
||||
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -191,139 +143,3 @@ fun ArticlePage(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArticleItem(
|
||||
modifier: Modifier = Modifier,
|
||||
articleWithFeed: ArticleWithFeed?,
|
||||
isStarredFilter: Boolean,
|
||||
index: Int,
|
||||
articleOnClick: (ArticleWithFeed) -> Unit,
|
||||
) {
|
||||
if (articleWithFeed == null) return
|
||||
Column(
|
||||
modifier = modifier
|
||||
.paddingFixedHorizontal(
|
||||
top = if (index == 0) 8.dp else 0.dp,
|
||||
bottom = 8.dp
|
||||
)
|
||||
.roundClick {
|
||||
articleOnClick(articleWithFeed)
|
||||
}
|
||||
.alpha(
|
||||
if (isStarredFilter || articleWithFeed.article.isUnread) {
|
||||
1f
|
||||
} else {
|
||||
0.7f
|
||||
}
|
||||
)
|
||||
) {
|
||||
Column(modifier = modifier.padding(10.dp)) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 32.dp),
|
||||
text = articleWithFeed.feed.name,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (isStarredFilter || articleWithFeed.article.isUnread) {
|
||||
MaterialTheme.colorScheme.tertiary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outline
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = articleWithFeed.article.date.toString(
|
||||
DateTimeExt.HH_MM
|
||||
),
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
Spacer(modifier = modifier.height(1.dp))
|
||||
Row {
|
||||
if (true) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 3.dp)
|
||||
.size(24.dp)
|
||||
.border(
|
||||
2.dp,
|
||||
MaterialTheme.colorScheme.inverseOnSurface,
|
||||
RoundedCornerShape(4.dp)
|
||||
),
|
||||
) {
|
||||
if (articleWithFeed.feed.icon == null) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.default_folder),
|
||||
contentDescription = "icon",
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(2.dp),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = BitmapPainter(
|
||||
BitmapFactory.decodeByteArray(
|
||||
articleWithFeed.feed.icon,
|
||||
0,
|
||||
articleWithFeed.feed.icon!!.size
|
||||
).asImageBitmap()
|
||||
),
|
||||
contentDescription = "icon",
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = articleWithFeed.article.title,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (isStarredFilter || articleWithFeed.article.isUnread) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outline
|
||||
},
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = modifier.height(1.dp))
|
||||
Text(
|
||||
text = articleWithFeed.article.shortDescription,
|
||||
fontSize = 18.sp,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArticleDateHeader(date: String, isDisplayIcon: Boolean) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(28.dp)
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = date,
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.padding(start = (if (isDisplayIcon) 52 else 20).dp),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package me.ash.reader.ui.widget
|
||||
package me.ash.reader.ui.page.home.feed
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
@ -25,12 +24,12 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.ui.util.paddingFixedHorizontal
|
||||
import me.ash.reader.ui.util.roundClick
|
||||
import me.ash.reader.ui.extension.paddingFixedHorizontal
|
||||
import me.ash.reader.ui.extension.roundClick
|
||||
import me.ash.reader.ui.widget.AnimatedText
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun BarButton(
|
||||
fun FeedBar(
|
||||
barButtonType: BarButtonType,
|
||||
iconOnClickListener: () -> Unit = {},
|
||||
onClickListener: () -> Unit = {},
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,72 +1,41 @@
|
|||
package me.ash.reader.ui.page.home.feed
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.material.icons.rounded.ExpandMore
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import me.ash.reader.DateTimeExt
|
||||
import me.ash.reader.DateTimeExt.toString
|
||||
import me.ash.reader.data.constant.Filter
|
||||
import me.ash.reader.data.feed.Feed
|
||||
import me.ash.reader.data.group.Group
|
||||
import me.ash.reader.data.group.GroupWithFeed
|
||||
import me.ash.reader.data.repository.RssRepository
|
||||
import me.ash.reader.ui.page.common.RouteName
|
||||
import me.ash.reader.ui.extension.collectAsStateValue
|
||||
import me.ash.reader.ui.page.home.HomeViewAction
|
||||
import me.ash.reader.ui.page.home.HomeViewModel
|
||||
import me.ash.reader.ui.util.collectAsStateValue
|
||||
import me.ash.reader.ui.widget.*
|
||||
import java.io.InputStream
|
||||
import me.ash.reader.ui.page.home.feed.subscribe.SubscribeDialog
|
||||
import me.ash.reader.ui.widget.TopTitleBox
|
||||
|
||||
|
||||
@ExperimentalComposeUiApi
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun FeedPage(
|
||||
navController: NavHostController,
|
||||
modifier: Modifier,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: FeedViewModel = hiltViewModel(),
|
||||
homeViewModel: HomeViewModel = hiltViewModel(),
|
||||
filter: Filter,
|
||||
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
|
||||
) {
|
||||
val viewState = viewModel.viewState.collectAsStateValue()
|
||||
val syncState = RssRepository.syncState.collectAsStateValue()
|
||||
var addFeedDialogVisible by remember { mutableStateOf(false) }
|
||||
val syncState = homeViewModel.syncState.collectAsStateValue()
|
||||
|
||||
LaunchedEffect(homeViewModel.filterState) {
|
||||
homeViewModel.filterState.collect { state ->
|
||||
|
@ -87,11 +56,24 @@ fun FeedPage(
|
|||
}
|
||||
|
||||
Box(
|
||||
modifier.fillMaxSize()
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
AddFeedDialog(
|
||||
visible = addFeedDialogVisible,
|
||||
hiddenFunction = { addFeedDialogVisible = false },
|
||||
SubscribeDialog(
|
||||
visible = viewState.subscribeDialogVisible,
|
||||
hiddenFunction = {
|
||||
viewModel.dispatch(FeedViewAction.ChangeSubscribeDialogVisible(false))
|
||||
},
|
||||
inputContent = viewState.subscribeDialogFeedLink,
|
||||
onValueChange = {
|
||||
viewModel.dispatch(
|
||||
FeedViewAction.InputSubscribeFeedLink(it)
|
||||
)
|
||||
},
|
||||
onKeyboardAction = {
|
||||
viewModel.dispatch(
|
||||
FeedViewAction.ChangeSubscribeDialogVisible(false)
|
||||
)
|
||||
},
|
||||
openInputStreamCallback = {
|
||||
viewModel.dispatch(FeedViewAction.AddFromFile(it))
|
||||
},
|
||||
|
@ -113,42 +95,14 @@ fun FeedPage(
|
|||
viewModel.dispatch(FeedViewAction.ScrollToItem(0))
|
||||
}
|
||||
Column {
|
||||
SmallTopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
navController.navigate(route = RouteName.SETTINGS)
|
||||
}) {
|
||||
Icon(
|
||||
modifier = Modifier.size(22.dp),
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = "Back",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
if (syncState.isSyncing) return@IconButton
|
||||
FeedPageTopBar(
|
||||
navController = navController,
|
||||
isSyncing = syncState.isSyncing,
|
||||
syncOnClick = {
|
||||
homeViewModel.dispatch(HomeViewAction.Sync())
|
||||
}) {
|
||||
Icon(
|
||||
modifier = Modifier.size(26.dp),
|
||||
imageVector = Icons.Rounded.Refresh,
|
||||
contentDescription = "Sync",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
addFeedDialogVisible = true
|
||||
}) {
|
||||
Icon(
|
||||
modifier = Modifier.size(26.dp),
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = "Subscribe",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
},
|
||||
subscribeOnClick = {
|
||||
viewModel.dispatch(FeedViewAction.ChangeSubscribeDialogVisible(true))
|
||||
},
|
||||
)
|
||||
LazyColumn(
|
||||
|
@ -157,7 +111,7 @@ fun FeedPage(
|
|||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(114.dp))
|
||||
BarButton(
|
||||
FeedBar(
|
||||
barButtonType = ButtonType(
|
||||
content = filter.title,
|
||||
important = viewState.filterImportant
|
||||
|
@ -168,13 +122,13 @@ fun FeedPage(
|
|||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
BarButton(
|
||||
FeedBar(
|
||||
barButtonType = FirstExpandType(
|
||||
content = "Feeds",
|
||||
icon = Icons.Rounded.ExpandMore
|
||||
)
|
||||
) {
|
||||
viewModel.dispatch(FeedViewAction.ChangeGroupVisible)
|
||||
viewModel.dispatch(FeedViewAction.ChangeGroupVisible(!viewState.groupsVisible))
|
||||
}
|
||||
}
|
||||
itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed ->
|
||||
|
@ -193,181 +147,3 @@ fun FeedPage(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalComposeUiApi
|
||||
@Composable
|
||||
private fun AddFeedDialog(
|
||||
visible: Boolean,
|
||||
hiddenFunction: () -> Unit,
|
||||
openInputStreamCallback: (InputStream) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var inputString by remember { mutableStateOf("") }
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
||||
it?.let { uri ->
|
||||
context.contentResolver.openInputStream(uri)?.let { inputStream ->
|
||||
openInputStreamCallback(inputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val localFocusManager = LocalFocusManager.current
|
||||
val localSoftwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
Dialog(
|
||||
visible = visible,
|
||||
onDismissRequest = hiddenFunction,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.RssFeed,
|
||||
contentDescription = "Subscribe",
|
||||
)
|
||||
},
|
||||
title = { Text("订阅") },
|
||||
text = {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { if(it.isFocused) localSoftwareKeyboardController?.hide() }
|
||||
.focusable(),
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Transparent,
|
||||
cursorColor = MaterialTheme.colorScheme.onSurface,
|
||||
textColor = MaterialTheme.colorScheme.onSurface,
|
||||
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
value = inputString,
|
||||
onValueChange = {
|
||||
inputString = it
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "订阅源或站点链接",
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
IconButton(onClick = {}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ContentPaste,
|
||||
contentDescription = "Paste",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
hiddenFunction()
|
||||
}
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = inputString.isNotEmpty(),
|
||||
onClick = {
|
||||
hiddenFunction()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "搜索",
|
||||
color = if (inputString.isNotEmpty()) {
|
||||
Color.Unspecified
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
launcher.launch("*/*")
|
||||
hiddenFunction()
|
||||
}
|
||||
) {
|
||||
Text("导入OPML文件")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
private fun ColumnScope.GroupList(
|
||||
modifier: Modifier = Modifier,
|
||||
groupVisible: Boolean,
|
||||
feedVisible: Boolean,
|
||||
groupWithFeed: GroupWithFeed,
|
||||
groupAndFeedOnClick: (currentGroup: Group?, currentFeed: Feed?) -> Unit = { _, _ -> },
|
||||
expandOnClick: () -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = groupVisible,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
BarButton(
|
||||
barButtonType = SecondExpandType(
|
||||
content = groupWithFeed.group.name,
|
||||
icon = Icons.Rounded.ExpandMore,
|
||||
important = groupWithFeed.group.important ?: 0,
|
||||
),
|
||||
iconOnClickListener = expandOnClick
|
||||
) {
|
||||
groupAndFeedOnClick(groupWithFeed.group, null)
|
||||
}
|
||||
FeedList(
|
||||
visible = feedVisible,
|
||||
feeds = groupWithFeed.feeds,
|
||||
onClick = { currentFeed ->
|
||||
groupAndFeedOnClick(null, currentFeed)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
private fun ColumnScope.FeedList(
|
||||
visible: Boolean,
|
||||
feeds: List<Feed>,
|
||||
onClick: (currentFeed: Feed?) -> Unit = {},
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
Column(modifier = Modifier.animateContentSize()) {
|
||||
feeds.forEach { feed ->
|
||||
Log.i("RLog", "FeedList: ${feed.icon}")
|
||||
BarButton(
|
||||
barButtonType = ItemType(
|
||||
// icon = feed.icon ?: "",
|
||||
icon = if (feed.icon == null) {
|
||||
null
|
||||
} else {
|
||||
BitmapPainter(
|
||||
BitmapFactory.decodeByteArray(
|
||||
feed.icon,
|
||||
0,
|
||||
feed.icon!!.size
|
||||
).asImageBitmap()
|
||||
)
|
||||
},
|
||||
content = feed.name,
|
||||
important = feed.important ?: 0
|
||||
)
|
||||
) {
|
||||
onClick(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
|
@ -31,8 +31,30 @@ class FeedViewModel @Inject constructor(
|
|||
is FeedViewAction.FetchData -> fetchData(action.isStarred, action.isUnread)
|
||||
is FeedViewAction.AddFromFile -> addFromFile(action.inputStream)
|
||||
is FeedViewAction.ChangeFeedVisible -> changeFeedVisible(action.index)
|
||||
is FeedViewAction.ChangeGroupVisible -> changeGroupVisible()
|
||||
is FeedViewAction.ChangeGroupVisible -> changeGroupVisible(action.visible)
|
||||
is FeedViewAction.ScrollToItem -> scrollToItem(action.index)
|
||||
is FeedViewAction.ChangeSubscribeDialogVisible -> changeAddFeedDialogVisible(action.visible)
|
||||
is FeedViewAction.InputSubscribeFeedLink -> inputSubscribeFeedLink(action.subscribeFeedLink)
|
||||
}
|
||||
}
|
||||
|
||||
private fun inputSubscribeFeedLink(subscribeFeedLink: String) {
|
||||
viewModelScope.launch {
|
||||
_viewState.update {
|
||||
it.copy(
|
||||
subscribeDialogFeedLink = subscribeFeedLink
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeAddFeedDialogVisible(visible: Boolean) {
|
||||
viewModelScope.launch {
|
||||
_viewState.update {
|
||||
it.copy(
|
||||
subscribeDialogVisible = visible
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,10 +143,10 @@ class FeedViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun changeGroupVisible() {
|
||||
private fun changeGroupVisible(visible: Boolean) {
|
||||
_viewState.update {
|
||||
it.copy(
|
||||
groupsVisible = !_viewState.value.groupsVisible
|
||||
groupsVisible = visible
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -143,6 +165,8 @@ data class FeedViewState(
|
|||
val feedsVisible: List<Boolean> = emptyList(),
|
||||
val listState: LazyListState = LazyListState(),
|
||||
val groupsVisible: Boolean = true,
|
||||
var subscribeDialogVisible: Boolean = false,
|
||||
var subscribeDialogFeedLink: String = "",
|
||||
)
|
||||
|
||||
sealed class FeedViewAction {
|
||||
|
@ -163,8 +187,19 @@ sealed class FeedViewAction {
|
|||
val index: Int
|
||||
) : FeedViewAction()
|
||||
|
||||
object ChangeGroupVisible : FeedViewAction()
|
||||
data class ChangeGroupVisible(
|
||||
val visible: Boolean
|
||||
) : FeedViewAction()
|
||||
|
||||
data class ScrollToItem(
|
||||
val index: Int
|
||||
) : FeedViewAction()
|
||||
|
||||
data class ChangeSubscribeDialogVisible(
|
||||
val visible: Boolean
|
||||
) : FeedViewAction()
|
||||
|
||||
data class InputSubscribeFeedLink(
|
||||
val subscribeFeedLink: String
|
||||
) : FeedViewAction()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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文件")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
67
app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt
Normal file
67
app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +1,26 @@
|
|||
package me.ash.reader.ui.page.home.read
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.MoreHoriz
|
||||
import androidx.compose.material.icons.rounded.Share
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import me.ash.reader.DateTimeExt
|
||||
import me.ash.reader.DateTimeExt.toString
|
||||
import me.ash.reader.data.article.Article
|
||||
import me.ash.reader.data.feed.Feed
|
||||
import me.ash.reader.ui.util.collectAsStateValue
|
||||
import me.ash.reader.ui.util.paddingFixedHorizontal
|
||||
import me.ash.reader.ui.util.roundClick
|
||||
import me.ash.reader.ui.extension.collectAsStateValue
|
||||
import me.ash.reader.ui.extension.paddingFixedHorizontal
|
||||
import me.ash.reader.ui.widget.WebView
|
||||
|
||||
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun ReadPage(
|
||||
navController: NavHostController,
|
||||
|
@ -69,38 +48,7 @@ fun ReadPage(
|
|||
Column(
|
||||
modifier.fillMaxSize()
|
||||
) {
|
||||
|
||||
SmallTopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { btnBackOnClickListener() }) {
|
||||
Icon(
|
||||
modifier = Modifier.size(28.dp),
|
||||
imageVector = Icons.Rounded.Close,
|
||||
contentDescription = "Back",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { /*TODO*/ }) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.Rounded.Share,
|
||||
contentDescription = "Add",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { /*TODO*/ }) {
|
||||
Icon(
|
||||
modifier = Modifier.size(28.dp),
|
||||
imageVector = Icons.Rounded.MoreHoriz,
|
||||
contentDescription = "Add",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
ReadPageTopBar(btnBackOnClickListener)
|
||||
|
||||
val composition by rememberLottieComposition(
|
||||
LottieCompositionSpec.Url(
|
||||
|
@ -158,52 +106,3 @@ fun ReadPage(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(
|
||||
context: Context,
|
||||
article: Article,
|
||||
feed: Feed
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.roundClick {
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(article.link))
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
Text(
|
||||
text = article.date.toString(DateTimeExt.YYYY_MM_DD_HH_MM, true),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = article.title,
|
||||
fontSize = 27.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
lineHeight = 34.sp,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
article.author?.let {
|
||||
Text(
|
||||
text = article.author,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = feed.name,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package me.ash.reader.ui.page.settings
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
@ -20,15 +18,10 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import me.ash.reader.ui.util.paddingFixedHorizontal
|
||||
import me.ash.reader.ui.util.roundClick
|
||||
import me.ash.reader.ui.extension.paddingFixedHorizontal
|
||||
import me.ash.reader.ui.extension.roundClick
|
||||
import me.ash.reader.ui.widget.TopTitleBox
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun SettingsPage(
|
||||
navController: NavHostController,
|
||||
|
@ -156,10 +149,6 @@ fun Item(
|
|||
}
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalMaterial3Api
|
||||
@ExperimentalPagerApi
|
||||
@ExperimentalFoundationApi
|
||||
@Preview
|
||||
@Composable
|
||||
fun SettingsPreview() {
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
package me.ash.reader.ui.theme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
//val md_theme_light_primary = Color(0xFF4D4D4D)
|
||||
val md_theme_light_primary = Color(0xFF6750A4)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_primaryContainer = Color(0xFFEADDFF)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
|
||||
|
||||
//val md_theme_light_secondary = Color(0xFF868686)
|
||||
val md_theme_light_secondary = Color(0xFF625B71)
|
||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
|
||||
//val md_theme_light_secondaryContainer = Color(0xFFEAEAEA)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
|
||||
|
||||
//val md_theme_light_tertiary = Color(0xFFC1C1C1)
|
||||
val md_theme_light_tertiary = Color(0xFF7D5260)
|
||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
|
@ -22,13 +25,16 @@ val md_theme_light_error = Color(0xFFB3261E)
|
|||
val md_theme_light_errorContainer = Color(0xFFF9DEDC)
|
||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410E0B)
|
||||
|
||||
//val md_theme_light_background = Color(0xFFF7F5F4)
|
||||
val md_theme_light_background = Color(0xFFFFFBFE)
|
||||
val md_theme_light_onBackground = Color(0xFF1C1B1F)
|
||||
|
||||
//val md_theme_light_surface = Color(0xFFF7F5F4)
|
||||
val md_theme_light_surface = Color(0xFFFFFBFE)
|
||||
val md_theme_light_onSurface = Color(0xFF1C1B1F)
|
||||
val md_theme_light_surfaceVariant = Color(0xFFE7E0EC)
|
||||
|
||||
//val md_theme_light_onSurfaceVariant = md_theme_light_secondary
|
||||
val md_theme_light_onSurfaceVariant = Color(0xFF49454F)
|
||||
val md_theme_light_outline = Color(0xFF79747E)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package me.ash.reader.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
//Replace with your font locations
|
||||
val Roboto = FontFamily.Default
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import androidx.recyclerview.widget.ListUpdateCallback
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Suppress("UpdateTransitionLabel", "TransitionPropertiesLabel")
|
||||
@SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter")
|
||||
/**
|
||||
|
@ -63,7 +62,6 @@ fun <T> updateAnimatedItemsState(
|
|||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
oldList[oldItemPosition].item == newList[newItemPosition]
|
||||
|
||||
}
|
||||
val diffResult = calculateDiff(false, diffCb)
|
||||
val compositeList = oldList.toMutableList()
|
||||
|
|
|
@ -18,7 +18,7 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun AnimatedText(
|
||||
text: String,
|
||||
|
|
|
@ -269,8 +269,12 @@ private class PagerState {
|
|||
val extra = if (remainder > itemDimension / 2f) 1 else 0
|
||||
val lastVisibleIndex =
|
||||
(targetOffset.absoluteValue / itemDimension.toFloat()).toInt() + extra
|
||||
targetOffset = (lastVisibleIndex * (itemDimension + itemSpacing) * targetOffset.sign)
|
||||
.coerceIn(0f, (numberOfItems - 1).toFloat() * (itemDimension + itemSpacing))
|
||||
targetOffset =
|
||||
(lastVisibleIndex * (itemDimension + itemSpacing) * targetOffset.sign)
|
||||
.coerceIn(
|
||||
0f,
|
||||
(numberOfItems - 1).toFloat() * (itemDimension + itemSpacing)
|
||||
)
|
||||
dragOffset.animateTo(
|
||||
animationSpec = animationSpec,
|
||||
targetValue = targetOffset,
|
||||
|
|
|
@ -16,7 +16,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi
|
|||
import com.google.accompanist.pager.PagerState
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@ExperimentalPagerApi
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun BoxScope.MaskBox(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
59
app/src/main/java/me/ash/reader/ui/widget/SelectionChip.kt
Normal file
59
app/src/main/java/me/ash/reader/ui/widget/SelectionChip.kt
Normal 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,
|
||||
)
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package me.ash.reader.ui.widget
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
|
@ -17,9 +16,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import me.ash.reader.ui.util.calculateTopBarAnimateValue
|
||||
import me.ash.reader.ui.extension.calculateTopBarAnimateValue
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun BoxScope.TopTitleBox(
|
||||
title: String,
|
||||
|
|
33
app/src/main/java/me/ash/reader/ui/widget/ViewPager.kt
Normal file
33
app/src/main/java/me/ash/reader/ui/widget/ViewPager.kt
Normal 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]()
|
||||
}
|
||||
}
|
|
@ -13,9 +13,9 @@ import androidx.compose.ui.graphics.toArgb
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.ash.reader.ui.extension.collectAsStateValue
|
||||
import me.ash.reader.ui.page.home.read.ReadViewAction
|
||||
import me.ash.reader.ui.page.home.read.ReadViewModel
|
||||
import me.ash.reader.ui.util.collectAsStateValue
|
||||
|
||||
@Composable
|
||||
fun WebView(
|
||||
|
|
Loading…
Reference in New Issue
Block a user