Add subscribe feed UI
This commit is contained in:
parent
5fff554bba
commit
11ca1f1ae8
|
@ -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'
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ interface AccountDao {
|
||||||
)
|
)
|
||||||
suspend fun queryAll(): List<Account>
|
suspend fun queryAll(): List<Account>
|
||||||
|
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM account
|
SELECT * FROM account
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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.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
|
|
||||||
}
|
|
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
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
@ -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(),
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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 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 = {},
|
|
@ -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
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.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()
|
||||||
}
|
}
|
|
@ -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
|
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
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() {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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
|
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,
|
||||||
|
|
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.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(
|
||||||
|
|
Loading…
Reference in New Issue
Block a user