Refactor Material You design for ReadPage and Add feed option feature

This commit is contained in:
Ash 2022-03-27 02:25:54 +08:00
parent 518dd6b59c
commit 435eb67c55
37 changed files with 1044 additions and 682 deletions

View File

@ -15,6 +15,8 @@ import kotlinx.coroutines.runBlocking
import java.io.IOException
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val Context.currentAccountId: Int
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
this.edit {

View File

@ -7,6 +7,15 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface ArticleDao {
@Query(
"""
DELETE FROM article
WHERE accountId = :accountId
AND feedId = :feedId
"""
)
suspend fun deleteByFeedId(accountId: Int, feedId: String)
@Transaction
@Query(
"""

View File

@ -4,6 +4,14 @@ import androidx.room.*
@Dao
interface FeedDao {
@Query(
"""
SELECT * FROM feed
WHERE id = :id
"""
)
suspend fun queryById(id: String): Feed?
@Query(
"""
SELECT * FROM feed

View File

@ -8,13 +8,10 @@ import androidx.work.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
import me.ash.reader.DataStoreKeys
import me.ash.reader.currentAccountId
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.Article
import me.ash.reader.data.article.ArticleDao
@ -26,8 +23,6 @@ import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.dataStore
import me.ash.reader.get
import java.util.concurrent.TimeUnit
abstract class AbstractRssRepository constructor(
@ -65,14 +60,11 @@ abstract class AbstractRssRepository constructor(
}
fun pullGroups(): Flow<MutableList<Group>> {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
return groupDao.queryAllGroup(accountId)
return groupDao.queryAllGroup(context.currentAccountId).flowOn(Dispatchers.IO)
}
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
return groupDao.queryAllGroupWithFeed(
context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
)//.flowOn(Dispatchers.IO)
return groupDao.queryAllGroupWithFeed(context.currentAccountId).flowOn(Dispatchers.IO)
}
fun pullArticles(
@ -82,7 +74,7 @@ abstract class AbstractRssRepository constructor(
isUnread: Boolean = false,
): PagingSource<Int, ArticleWithFeed> {
Log.i("RLog", "thread:pullArticles ${Thread.currentThread().name}")
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
val accountId = context.currentAccountId
Log.i(
"RLog",
"pullArticles: accountId: ${accountId}, groupId: ${groupId}, feedId: ${feedId}, isStarred: ${isStarred}, isUnread: ${isUnread}"
@ -118,7 +110,7 @@ abstract class AbstractRssRepository constructor(
): Flow<List<ImportantCount>> {
return withContext(Dispatchers.IO) {
Log.i("RLog", "thread:pullImportant ${Thread.currentThread().name}")
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
val accountId = context.currentAccountId
Log.i(
"RLog",
"pullImportant: accountId: ${accountId}, isStarred: ${isStarred}, isUnread: ${isUnread}"
@ -130,7 +122,11 @@ abstract class AbstractRssRepository constructor(
.queryImportantCountWhenIsUnread(accountId, isUnread)
else -> articleDao.queryImportantCountWhenIsAll(accountId)
}
}//.flowOn(Dispatchers.IO)
}.flowOn(Dispatchers.IO)
}
suspend fun findFeedById(id: String): Feed? {
return feedDao.queryById(id)
}
suspend fun findArticleById(id: String): ArticleWithFeed? {
@ -138,14 +134,32 @@ abstract class AbstractRssRepository constructor(
}
suspend fun isExist(url: String): Boolean {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
return feedDao.queryByLink(accountId, url).isNotEmpty()
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
}
fun peekWork(): String {
return workManager.getWorkInfosByTag("sync").get().size.toString()
}
suspend fun updateGroup(group: Group) {
groupDao.update(group)
}
suspend fun updateFeed(feed: Feed) {
feedDao.update(feed)
}
suspend fun deleteGroup(group: Group) {
groupDao.update(group)
}
suspend fun deleteFeed(feed: Feed) {
withContext(Dispatchers.IO) {
articleDao.deleteByFeedId(context.currentAccountId, feed.id)
feedDao.delete(feed)
}
}
companion object {
val mutex = Mutex()

View File

@ -2,11 +2,13 @@ package me.ash.reader.data.repository
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.*
import me.ash.reader.R
import me.ash.reader.currentAccountId
import me.ash.reader.data.account.Account
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao
import me.ash.reader.spacerDollar
import javax.inject.Inject
class AccountRepository @Inject constructor(
@ -17,8 +19,7 @@ class AccountRepository @Inject constructor(
) {
suspend fun getCurrentAccount(): Account? {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
return accountDao.queryById(accountId)
return accountDao.queryById(context.currentAccountId)
}
suspend fun isNoAccount(): Boolean {

View File

@ -5,7 +5,7 @@ import android.util.Log
import androidx.work.WorkManager
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.sync.withLock
import me.ash.reader.DataStoreKeys
import me.ash.reader.*
import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.Article
import me.ash.reader.data.article.ArticleDao
@ -15,9 +15,6 @@ import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.source.FeverApiDataSource
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.dataStore
import me.ash.reader.get
import me.ash.reader.spacerDollar
import net.dankito.readability4j.extended.Readability4JExtended
import java.util.*
import javax.inject.Inject
@ -49,13 +46,12 @@ class FeverRssRepository @Inject constructor(
}
override suspend fun addGroup(name: String): String {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
return UUID.randomUUID().toString().also {
groupDao.insert(
Group(
id = it,
name = name,
accountId = accountId
accountId = context.currentAccountId
)
)
}
@ -63,8 +59,7 @@ class FeverRssRepository @Inject constructor(
override suspend fun sync() {
mutex.withLock {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
?: return
val accountId = context.currentAccountId
updateSyncState {
it.copy(

View File

@ -73,13 +73,12 @@ class LocalRssRepository @Inject constructor(
}
override suspend fun addGroup(name: String): String {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
return UUID.randomUUID().toString().also {
groupDao.insert(
Group(
id = it,
name = name,
accountId = accountId
accountId = context.currentAccountId
)
)
}
@ -89,8 +88,7 @@ class LocalRssRepository @Inject constructor(
mutex.withLock {
withContext(Dispatchers.IO) {
val preTime = System.currentTimeMillis()
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
?: return@withContext
val accountId = context.currentAccountId
val feeds = async { feedDao.queryAll(accountId) }
val articles = feeds.await().also { feed ->
updateSyncState {

View File

@ -3,14 +3,12 @@ package me.ash.reader.data.repository
import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.DataStoreKeys
import me.ash.reader.currentAccountId
import me.ash.reader.data.article.Article
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.feed.FeedWithArticle
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.dataStore
import me.ash.reader.get
import me.ash.reader.spacerDollar
import net.dankito.readability4j.Readability4J
import net.dankito.readability4j.extended.Readability4JExtended
@ -26,7 +24,7 @@ class RssHelper @Inject constructor(
) {
@Throws(Exception::class)
suspend fun searchFeed(feedLink: String): FeedWithArticle {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
val accountId = context.currentAccountId
val parseRss = rssNetworkDataSource.parseRss(feedLink)
val feed = Feed(
id = accountId.spacerDollar(UUID.randomUUID().toString()),

View File

@ -4,12 +4,10 @@ import android.content.Context
import android.util.Log
import android.util.Xml
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.DataStoreKeys
import me.ash.reader.currentAccountId
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.dataStore
import me.ash.reader.get
import org.xmlpull.v1.XmlPullParser
import java.io.InputStream
import java.util.*
@ -22,7 +20,7 @@ class OpmlLocalDataSource @Inject constructor(
// @Throws(XmlPullParserException::class, IOException::class)
fun parseFileInputStream(inputStream: InputStream): List<GroupWithFeed> {
val groupWithFeedList = mutableListOf<GroupWithFeed>()
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
val accountId = context.currentAccountId
inputStream.use {
val parser: XmlPullParser = Xml.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)

View File

@ -1,12 +1,23 @@
package me.ash.reader.ui.extension
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.reflect.KProperty
@Composable
fun <T> StateFlow<T>.collectAsStateValue(
context: CoroutineContext = EmptyCoroutineContext
context: CoroutineContext = Dispatchers.Default
): T = collectAsState(context).value
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}

View File

@ -6,15 +6,12 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.google.accompanist.insets.ProvideWindowInsets
@ -24,16 +21,10 @@ import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.ui.page.home.FeedOptionDrawer
import me.ash.reader.ui.page.home.HomePage
import me.ash.reader.ui.page.settings.SettingsPage
import me.ash.reader.ui.theme.AppTheme
@OptIn(ExperimentalMaterialApi::class)
val LocalDrawerState = staticCompositionLocalOf<ModalBottomSheetState> {
error("CompositionLocal LocalDrawerState not present")
}
@OptIn(ExperimentalAnimationApi::class, androidx.compose.material.ExperimentalMaterialApi::class)
@Composable
fun HomeEntry() {
@ -46,113 +37,105 @@ fun HomeEntry() {
setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme())
setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme())
}
Box {
CompositionLocalProvider(
LocalDrawerState provides rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
) {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
Row(
modifier = Modifier
.weight(1f)
.statusBarsPadding()
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
Row(
modifier = Modifier
.weight(1f)
.statusBarsPadding()
) {
AnimatedNavHost(
navController = navController,
startDestination = RouteName.HOME,
) {
AnimatedNavHost(
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
navController = navController,
startDestination = 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))
},
) {
composable(
route = RouteName.HOME,
enterTransition = {
slideInHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
initialOffsetX = { -it }
) + fadeIn(animationSpec = tween(220, delayMillis = 90))
},
exitTransition = {
slideOutHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
targetOffsetX = { it }
) + fadeOut(animationSpec = tween(220, delayMillis = 90))
},
popEnterTransition = {
slideInHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
initialOffsetX = { -it }
) + fadeIn(animationSpec = tween(220, delayMillis = 90))
},
popExitTransition = {
slideOutHorizontally(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
dampingRatio = Spring.DampingRatioNoBouncy
),
targetOffsetX = { it }
) + fadeOut(animationSpec = tween(220, delayMillis = 90))
},
) {
HomePage(navController)
}
composable(
route = RouteName.SETTINGS,
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)
}
HomePage(navController)
}
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)
}
}
Spacer(
modifier = Modifier
.navigationBarsHeight()
.fillMaxWidth()
)
}
FeedOptionDrawer(drawerState = LocalDrawerState.current)
}
Spacer(
modifier = Modifier
.navigationBarsHeight()
.fillMaxWidth()
)
}
}
}

View File

@ -1,116 +0,0 @@
package me.ash.reader.ui.page.home
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ChipDefaults
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
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.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import me.ash.reader.R
import me.ash.reader.ui.page.home.feeds.subscribe.ResultViewPage
import me.ash.reader.ui.widget.BottomDrawer
import me.ash.reader.ui.widget.Subtitle
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FeedOptionDrawer(
modifier: Modifier = Modifier,
drawerState: ModalBottomSheetState = androidx.compose.material.rememberModalBottomSheetState(
ModalBottomSheetValue.Hidden
),
) {
BottomDrawer(
drawerState = drawerState,
) {
Column {
Icon(
modifier = modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally),
imageVector = Icons.Rounded.RssFeed,
contentDescription = stringResource(R.string.subscribe),
)
Spacer(modifier = modifier.height(16.dp))
Text(
modifier = Modifier.fillMaxWidth(),
text = "Feed",
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
)
Spacer(modifier = modifier.height(16.dp))
ResultViewPage(
link = "https://joeycz.github.io/weekly/rss.xml",
groups = emptyList(),
selectedAllowNotificationPreset = true,
selectedParseFullContentPreset = true,
selectedGroupId = "selectedGroupId",
newGroupContent = "",
onNewGroupValueChange = { },
newGroupSelected = false,
changeNewGroupSelected = { },
allowNotificationPresetOnClick = { },
parseFullContentPresetOnClick = { },
groupOnClick = { },
onKeyboardAction = { },
)
Spacer(modifier = Modifier.height(20.dp))
Subtitle(text = "More")
Spacer(modifier = Modifier.height(10.dp))
androidx.compose.material.FilterChip(
modifier = modifier,
colors = ChipDefaults.filterChipColors(
backgroundColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.error,
leadingIconColor = MaterialTheme.colorScheme.error,
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 = Color.Transparent,
selectedContentColor = MaterialTheme.colorScheme.error,
selectedLeadingIconColor = MaterialTheme.colorScheme.error
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
selected = false,
shape = CircleShape,
onClick = {
// focusManager.clearFocus()
// onClick()
},
content = {
Text(
modifier = modifier.padding(
start = if (false) 0.dp else 8.dp,
top = 8.dp,
end = 8.dp,
bottom = 8.dp
),
text = "Delete",
style = MaterialTheme.typography.titleSmall,
color = if (true) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.error
},
)
},
)
}
}
}

View File

@ -29,7 +29,7 @@ fun FilterBar2(
NavigationBarItem(
icon = {
Icon(
imageVector = if (filter == item) item.filledIcon else item.icon,
imageVector = item.icon,
contentDescription = item.getName()
)
},

View File

@ -17,10 +17,14 @@ 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.filled.FiberManualRecord
import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.outlined.Circle
import androidx.compose.material.icons.outlined.Sell
import androidx.compose.material.icons.rounded.*
import androidx.compose.material.icons.outlined.FiberManualRecord
import androidx.compose.material.icons.outlined.TextFormat
import androidx.compose.material.icons.rounded.Article
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -138,7 +142,7 @@ fun HomeBottomNavBar(
.alpha(readerBarAlpha),
) {
ReaderBar(
modifier = modifier,
modifier = Modifier,
disabled = disabled,
isUnread = isUnread,
isStarred = isStarred,
@ -323,67 +327,78 @@ private fun ReaderBar(
val view = LocalView.current
var fullContent by remember { mutableStateOf(isFullContent) }
Row(
horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
modifier = modifier.fillMaxWidth()
) {
CanBeDisabledIconButton(
modifier = Modifier.size(18.dp),
modifier = Modifier.size(40.dp),
disabled = disabled,
imageVector = if (isUnread) {
Icons.Rounded.Circle
Icons.Filled.FiberManualRecord
} else {
Icons.Outlined.Circle
Icons.Outlined.FiberManualRecord
},
contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread),
tint = MaterialTheme.colorScheme.primary,
tint = if (isUnread) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
unreadOnClick(!isUnread)
}
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled,
modifier = Modifier.size(28.dp),
imageVector = if (isStarred) {
Icons.Rounded.Star
} else {
Icons.Rounded.StarBorder
Icons.Rounded.StarOutline
},
contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred),
tint = MaterialTheme.colorScheme.primary,
tint = if (isStarred) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
starredOnClick(!isStarred)
}
CanBeDisabledIconButton(
disabled = disabled,
modifier = Modifier.size(30.dp),
modifier = Modifier.size(40.dp),
imageVector = Icons.Rounded.ExpandMore,
contentDescription = "Next Article",
tint = MaterialTheme.colorScheme.primary,
tint = MaterialTheme.colorScheme.outline,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled,
imageVector = Icons.Outlined.Sell,
imageVector = Icons.Outlined.TextFormat,
contentDescription = "Add Tag",
tint = MaterialTheme.colorScheme.primary,
tint = MaterialTheme.colorScheme.outline,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
disabled = disabled,
modifier = Modifier.size(26.dp),
modifier = Modifier.size(40.dp),
imageVector = if (fullContent) {
Icons.Rounded.Article
} else {
Icons.Outlined.Article
},
contentDescription = stringResource(R.string.parse_full_content),
tint = MaterialTheme.colorScheme.primary,
tint = if (fullContent) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
val afterIsFullContent = !fullContent

View File

@ -2,12 +2,9 @@ package me.ash.reader.ui.page.home
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
@ -17,11 +14,13 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.findActivity
import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionDrawer
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewAction
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewModel
import me.ash.reader.ui.page.home.feeds.FeedsPage
import me.ash.reader.ui.page.home.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage
@ -35,13 +34,13 @@ fun HomePage(
navController: NavHostController,
viewModel: HomeViewModel = hiltViewModel(),
readViewModel: ReadViewModel = hiltViewModel(),
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue()
val filterState = viewModel.filterState.collectAsStateValue()
val readState = readViewModel.viewState.collectAsStateValue()
val scope = rememberCoroutineScope()
val drawerState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
LaunchedEffect(Unit) {
context.findActivity()?.let { activity ->
@ -82,6 +81,9 @@ fun HomePage(
if (currentPage == 2) {
readViewModel.dispatch(ReadViewAction.ClearArticle)
}
if (currentPage == 0) {
feedOptionViewModel.dispatch(FeedOptionViewAction.Hide(scope))
}
}
)
)
@ -96,69 +98,55 @@ fun HomePage(
}
}
Box {
Column {
ViewPager(
modifier = Modifier.weight(1f),
state = viewState.pagerState,
composableList = listOf(
{
FeedsPage(navController = navController)
},
{
FlowPage(navController = navController)
},
{
ReadPage(
navController = navController,
btnBackOnClickListener = {
viewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 1,
callback = {
readViewModel.dispatch(ReadViewAction.ClearArticle)
}
)
)
},
)
},
),
)
HomeBottomNavBar(
modifier = Modifier
.height(60.dp)
.fillMaxWidth(),
pagerState = viewState.pagerState,
disabled = readState.articleWithFeed == null,
isUnread = readState.articleWithFeed?.article?.isUnread ?: false,
isStarred = readState.articleWithFeed?.article?.isStarred ?: false,
isFullContent = readState.articleWithFeed?.feed?.isFullContent ?: false,
unreadOnClick = {
readViewModel.dispatch(ReadViewAction.MarkUnread(it))
Column {
ViewPager(
modifier = Modifier.weight(1f),
state = viewState.pagerState,
composableList = listOf(
{
FeedsPage(navController = navController)
},
starredOnClick = {
readViewModel.dispatch(ReadViewAction.MarkStarred(it))
{
FlowPage(navController = navController)
},
fullContentOnClick = { afterIsFullContent ->
readState.articleWithFeed?.let {
if (afterIsFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
}
{
ReadPage(navController = navController)
},
filter = filterState.filter,
filterOnClick = {
viewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
filter = it
)
),
)
HomeBottomNavBar(
modifier = Modifier
.height(60.dp)
.fillMaxWidth(),
pagerState = viewState.pagerState,
disabled = readState.articleWithFeed == null,
isUnread = readState.articleWithFeed?.article?.isUnread ?: false,
isStarred = readState.articleWithFeed?.article?.isStarred ?: false,
isFullContent = readState.articleWithFeed?.feed?.isFullContent ?: false,
unreadOnClick = {
readViewModel.dispatch(ReadViewAction.MarkUnread(it))
},
starredOnClick = {
readViewModel.dispatch(ReadViewAction.MarkStarred(it))
},
fullContentOnClick = { afterIsFullContent ->
readState.articleWithFeed?.let {
if (afterIsFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
}
},
filter = filterState.filter,
filterOnClick = {
viewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
filter = it
)
)
},
)
}
FeedOptionDrawer(drawerState = drawerState)
)
},
)
}
FeedOptionDrawer()
}

View File

@ -0,0 +1,76 @@
package me.ash.reader.ui.page.home.drawer.feed
import android.widget.Toast
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DeleteOutline
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.widget.Dialog
@OptIn(ExperimentalPagerApi::class)
@Composable
fun DeleteFeedDialog(
modifier: Modifier = Modifier,
feedName: String,
viewModel: FeedOptionViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue()
val scope = rememberCoroutineScope()
val deletedTip = stringResource(R.string.has_been_deleted, feedName)
Dialog(
visible = viewState.deleteDialogVisible,
onDismissRequest = {
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog)
},
icon = {
Icon(
imageVector = Icons.Rounded.DeleteOutline,
contentDescription = stringResource(R.string.subscribe),
)
},
title = {
Text(text = stringResource(R.string.unsubscribe))
},
text = {
Text(text = stringResource(R.string.unsubscribe_tip, feedName))
},
confirmButton = {
TextButton(
onClick = {
viewModel.dispatch(FeedOptionViewAction.Delete(){
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog)
viewModel.dispatch(FeedOptionViewAction.Hide(scope))
Toast.makeText(context, deletedTip, Toast.LENGTH_SHORT).show()
})
}
) {
Text(
text = stringResource(R.string.unsubscribe),
)
}
},
dismissButton = {
TextButton(
onClick = {
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog)
}
) {
Text(
text = stringResource(R.string.cancel),
)
}
},
)
}

View File

@ -0,0 +1,124 @@
package me.ash.reader.ui.page.home.drawer.feed
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DeleteOutline
import androidx.compose.material.icons.rounded.RssFeed
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.page.home.feeds.subscribe.ResultViewPage
import me.ash.reader.ui.widget.BottomDrawer
import me.ash.reader.ui.widget.Subtitle
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FeedOptionDrawer(
modifier: Modifier = Modifier,
viewModel: FeedOptionViewModel = hiltViewModel(),
content: @Composable () -> Unit = {},
) {
val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue()
val feed = viewState.feed
BottomDrawer(
drawerState = viewState.drawerState,
sheetContent = {
Column {
Icon(
modifier = modifier
.roundClick { }
.fillMaxWidth()
.align(Alignment.CenterHorizontally),
imageVector = Icons.Rounded.RssFeed,
contentDescription = feed?.name
?: stringResource(R.string.unknown),
tint = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = modifier.height(16.dp))
Text(
modifier = Modifier
.roundClick {}
.fillMaxWidth(),
text = feed?.name ?: stringResource(R.string.unknown),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = modifier.height(16.dp))
ResultViewPage(
link = feed?.url ?: stringResource(R.string.unknown),
groups = viewState.groups,
selectedAllowNotificationPreset = viewState.feed?.isNotification ?: false,
selectedParseFullContentPreset = viewState.feed?.isFullContent ?: false,
selectedGroupId = viewState.feed?.groupId ?: "",
newGroupContent = viewState.newGroupContent,
onNewGroupValueChange = {
viewModel.dispatch(FeedOptionViewAction.InputNewGroup(it))
},
newGroupSelected = viewState.newGroupSelected,
changeNewGroupSelected = {
viewModel.dispatch(FeedOptionViewAction.SelectedNewGroup(it))
},
allowNotificationPresetOnClick = {
viewModel.dispatch(FeedOptionViewAction.ChangeAllowNotificationPreset)
},
parseFullContentPresetOnClick = {
viewModel.dispatch(FeedOptionViewAction.ChangeParseFullContentPreset)
},
groupOnClick = {
viewModel.dispatch(FeedOptionViewAction.SelectedGroup(it))
},
onKeyboardAction = { },
)
Spacer(modifier = Modifier.height(20.dp))
Subtitle(text = stringResource(R.string.options))
Spacer(modifier = Modifier.height(10.dp))
Button(
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.error,
),
border = BorderStroke(
width = 1.dp,
color = MaterialTheme.colorScheme.error,
),
onClick = {
viewModel.dispatch(FeedOptionViewAction.ShowDeleteDialog)
}
) {
Icon(
modifier = Modifier.size(ButtonDefaults.IconSize),
imageVector = Icons.Rounded.DeleteOutline,
contentDescription = stringResource(R.string.delete),
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(
text = stringResource(R.string.unsubscribe),
style = MaterialTheme.typography.titleSmall,
)
}
}
}
) {
content()
}
DeleteFeedDialog(feedName = feed?.name ?: "")
}

View File

@ -0,0 +1,206 @@
package me.ash.reader.ui.page.home.drawer.feed
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.accompanist.pager.ExperimentalPagerApi
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group
import me.ash.reader.data.repository.RssRepository
import javax.inject.Inject
@OptIn(
ExperimentalPagerApi::class,
ExperimentalMaterialApi::class
)
@HiltViewModel
class FeedOptionViewModel @Inject constructor(
private val rssRepository: RssRepository,
) : ViewModel() {
private val _viewState = MutableStateFlow(FeedOptionViewState())
val viewState: StateFlow<FeedOptionViewState> = _viewState.asStateFlow()
init {
viewModelScope.launch {
rssRepository.get().pullGroups().collect { groups ->
_viewState.update {
it.copy(
groups = groups
)
}
}
}
}
fun dispatch(action: FeedOptionViewAction) {
when (action) {
is FeedOptionViewAction.Show -> show(action.scope, action.feedId)
is FeedOptionViewAction.Hide -> hide(action.scope)
is FeedOptionViewAction.SelectedGroup -> selectedGroup(action.groupId)
is FeedOptionViewAction.InputNewGroup -> inputNewGroup(action.content)
is FeedOptionViewAction.SelectedNewGroup -> selectedNewGroup(action.selected)
is FeedOptionViewAction.ChangeAllowNotificationPreset -> changeAllowNotificationPreset()
is FeedOptionViewAction.ChangeParseFullContentPreset -> changeParseFullContentPreset()
is FeedOptionViewAction.ShowDeleteDialog -> showDeleteDialog()
is FeedOptionViewAction.HideDeleteDialog -> hideDeleteDialog()
is FeedOptionViewAction.Delete -> delete(action.callback)
}
}
private suspend fun fetchFeed(feedId: String) {
val feed = rssRepository.get().findFeedById(feedId)
_viewState.update {
it.copy(
feed = feed,
selectedGroupId = feed?.groupId ?: "",
)
}
}
private fun show(scope: CoroutineScope, feedId: String) {
scope.launch {
fetchFeed(feedId)
_viewState.value.drawerState.show()
}
}
private fun hide(scope: CoroutineScope) {
scope.launch {
_viewState.value.drawerState.hide()
}
}
private fun inputNewGroup(content: String) {
_viewState.update {
it.copy(
newGroupContent = content
)
}
}
private fun selectedGroup(groupId: String) {
viewModelScope.launch {
_viewState.value.feed?.let {
rssRepository.get().updateFeed(
it.copy(
groupId = groupId
)
)
fetchFeed(it.id)
}
}
}
private fun selectedNewGroup(selected: Boolean) {
_viewState.update {
it.copy(
newGroupSelected = selected,
)
}
}
private fun changeParseFullContentPreset() {
viewModelScope.launch {
_viewState.value.feed?.let {
rssRepository.get().updateFeed(
it.copy(
isFullContent = !it.isFullContent
)
)
fetchFeed(it.id)
}
}
}
private fun changeAllowNotificationPreset() {
viewModelScope.launch {
_viewState.value.feed?.let {
rssRepository.get().updateFeed(
it.copy(
isNotification = !it.isNotification
)
)
fetchFeed(it.id)
}
}
}
private fun delete(callback: () -> Unit = {}) {
_viewState.value.feed?.let {
viewModelScope.launch {
rssRepository.get().deleteFeed(it)
}.invokeOnCompletion {
callback()
}
}
}
private fun hideDeleteDialog() {
_viewState.update {
it.copy(
deleteDialogVisible = false,
)
}
}
private fun showDeleteDialog() {
_viewState.update {
it.copy(
deleteDialogVisible = true,
)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
data class FeedOptionViewState(
var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden),
val feed: Feed? = null,
val selectedGroupId: String = "",
val newGroupContent: String = "",
val newGroupSelected: Boolean = false,
val groups: List<Group> = emptyList(),
val deleteDialogVisible: Boolean = false,
)
sealed class FeedOptionViewAction {
data class Show(
val scope: CoroutineScope,
val feedId: String
) : FeedOptionViewAction()
data class Hide(
val scope: CoroutineScope,
) : FeedOptionViewAction()
object ChangeAllowNotificationPreset : FeedOptionViewAction()
object ChangeParseFullContentPreset : FeedOptionViewAction()
data class SelectedGroup(
val groupId: String
) : FeedOptionViewAction()
data class InputNewGroup(
val content: String
) : FeedOptionViewAction()
data class SelectedNewGroup(
val selected: Boolean
) : FeedOptionViewAction()
data class Delete(
val callback: () -> Unit = {}
) : FeedOptionViewAction()
object ShowDeleteDialog: FeedOptionViewAction()
object HideDeleteDialog: FeedOptionViewAction()
}

View File

@ -1,7 +1,8 @@
package me.ash.reader.ui.page.home.feeds
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
@ -9,24 +10,45 @@ import androidx.compose.material3.Badge
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.data.feed.Feed
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewAction
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewModel
@OptIn(
androidx.compose.foundation.ExperimentalFoundationApi::class,
androidx.compose.material.ExperimentalMaterialApi::class,
)
@Composable
fun FeedItem(
modifier: Modifier = Modifier,
name: String,
important: Int,
feed: Feed,
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
onClick: () -> Unit = {},
) {
val view = LocalView.current
val scope = rememberCoroutineScope()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp)
.clip(RoundedCornerShape(32.dp))
.clickable { onClick() }
.combinedClickable(
onClick = {
onClick()
},
onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
feedOptionViewModel.dispatch(FeedOptionViewAction.Show(scope, feed.id))
}
)
.padding(vertical = 14.dp),
) {
Row(
@ -43,19 +65,19 @@ fun FeedItem(
) {}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = name,
text = feed.name,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (important != 0) {
if (feed.important ?: 0 != 0) {
Badge(
modifier = Modifier.padding(end = 6.dp),
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f),
contentColor = MaterialTheme.colorScheme.outline,
content = {
Text(
text = important.toString(),
text = feed.important.toString(),
style = MaterialTheme.typography.labelSmall
)
},

View File

@ -1,7 +1,9 @@
package me.ash.reader.ui.page.home.feeds
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -23,7 +25,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.collect
import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.getDesc
@ -167,44 +168,48 @@ fun FeedsPage(
Spacer(modifier = Modifier.height(8.dp))
}
itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed ->
GroupItem(
text = groupWithFeed.group.name,
feeds = groupWithFeed.feeds,
groupOnClick = {
homeViewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
group = groupWithFeed.group,
feed = null
Crossfade(targetState = groupWithFeed) { groupWithFeed ->
Column {
GroupItem(
text = groupWithFeed.group.name,
feeds = groupWithFeed.feeds,
groupOnClick = {
homeViewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
group = groupWithFeed.group,
feed = null
)
)
)
)
)
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 1,
)
)
},
feedOnClick = { feed ->
homeViewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
group = null,
feed = feed
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 1,
)
)
)
)
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 1,
)
},
feedOnClick = { feed ->
homeViewModel.dispatch(
HomeViewAction.ChangeFilter(
filterState.copy(
group = null,
feed = feed
)
)
)
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 1,
)
)
}
)
if (index != viewState.groupWithFeedList.lastIndex) {
Spacer(modifier = Modifier.height(8.dp))
}
}
)
if (index != viewState.groupWithFeedList.lastIndex) {
Spacer(modifier = Modifier.height(8.dp))
}
}
item {

View File

@ -20,10 +20,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.data.feed.Feed
import me.ash.reader.ui.page.common.LocalDrawerState
@OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable
@ -36,8 +34,6 @@ fun GroupItem(
feedOnClick: (feed: Feed) -> Unit = {},
) {
var expanded by remember { mutableStateOf(isExpanded) }
val scope = rememberCoroutineScope()
val drawerState = LocalDrawerState.current
Column(
modifier = Modifier
@ -50,9 +46,6 @@ fun GroupItem(
groupOnClick()
},
onLongClick = {
scope.launch {
drawerState.show()
}
}
)
.padding(top = 22.dp)
@ -97,8 +90,7 @@ fun GroupItem(
feeds.forEach { feed ->
FeedItem(
modifier = Modifier.padding(horizontal = 20.dp),
name = feed.name,
important = feed.important ?: 0,
feed = feed,
) {
feedOnClick(feed)
}

View File

@ -1,24 +1,29 @@
package me.ash.reader.ui.page.home.feeds.subscribe
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddAlert
import androidx.compose.material.icons.filled.Article
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.flowlayout.MainAxisAlignment
import me.ash.reader.R
import me.ash.reader.data.group.Group
import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.widget.SelectionChip
import me.ash.reader.ui.widget.SelectionEditorChip
import me.ash.reader.ui.widget.Subtitle
@ -74,15 +79,23 @@ fun ResultViewPage(
private fun Link(
text: String,
) {
val context = LocalContext.current
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
SelectionContainer {
Text(
modifier = Modifier.roundClick {
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(text))
)
},
text = text,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@ -108,12 +121,11 @@ private fun Preset(
selected = selectedAllowNotificationPreset,
selectedIcon = {
Icon(
imageVector = Icons.Filled.AddAlert,
imageVector = Icons.Outlined.Notifications,
contentDescription = stringResource(R.string.allow_notification),
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
},
) {
@ -125,12 +137,11 @@ private fun Preset(
selected = selectedParseFullContentPreset,
selectedIcon = {
Icon(
imageVector = Icons.Filled.Article,
imageVector = Icons.Outlined.Article,
contentDescription = stringResource(R.string.parse_full_content),
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
},
) {

View File

@ -25,6 +25,7 @@ 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.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@ -46,6 +47,7 @@ fun SearchViewPage(
onKeyboardAction: () -> Unit = {},
) {
val focusManager = LocalFocusManager.current
val clipboardManager = LocalClipboardManager.current
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
@ -89,6 +91,7 @@ fun SearchViewPage(
}
} else {
IconButton(onClick = {
onLinkValueChange(clipboardManager.getText()?.text ?: "")
}) {
Icon(
imageVector = Icons.Rounded.ContentPaste,

View File

@ -18,15 +18,17 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.Dispatchers
import me.ash.reader.*
import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.widget.Dialog
import java.io.InputStream
@OptIn(ExperimentalPagerApi::class)
@OptIn(ExperimentalPagerApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
@Composable
fun SubscribeDialog(
modifier: Modifier = Modifier,
@ -44,8 +46,9 @@ fun SubscribeDialog(
}
}
val viewState = viewModel.viewState.collectAsStateValue()
val groupsState = viewState.groups.collectAsState(initial = emptyList())
var dialogHeight by remember { mutableStateOf(280.dp) }
val groupsState =
viewState.groups.collectAsState(initial = emptyList(), context = Dispatchers.IO)
var dialogHeight by remember { mutableStateOf(300.dp) }
val readYouString = stringResource(R.string.read_you)
val defaultString = stringResource(R.string.defaults)
LaunchedEffect(viewState.visible) {
@ -64,7 +67,7 @@ fun SubscribeDialog(
LaunchedEffect(viewState.pagerState.currentPage) {
focusManager.clearFocus()
when (viewState.pagerState.currentPage) {
0 -> dialogHeight = 280.dp
0 -> dialogHeight = 300.dp
1 -> dialogHeight = Dp.Unspecified
}
}
@ -74,6 +77,7 @@ fun SubscribeDialog(
.padding(horizontal = 44.dp)
.height(dialogHeight),
visible = viewState.visible,
properties = DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest = {
focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Hide)

View File

@ -187,7 +187,8 @@ class SubscribeViewModel @Inject constructor(
private fun inputLink(content: String) {
_viewState.update {
it.copy(
linkContent = content
linkContent = content,
errorMessage = "",
)
}
}

View File

@ -1,6 +1,7 @@
package me.ash.reader.ui.page.home.flow
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
@ -79,7 +80,11 @@ fun FlowPage(
actions = {
IconButton(onClick = {
viewModel.dispatch(FlowViewAction.PeekSyncWork)
Toast.makeText(context, viewState.syncWorkInfo.length.toString(), Toast.LENGTH_SHORT)
Toast.makeText(
context,
viewState.syncWorkInfo.length.toString(),
Toast.LENGTH_SHORT
)
.show()
}) {
Icon(
@ -99,31 +104,33 @@ fun FlowPage(
)
},
content = {
LazyColumn(
state = viewState.listState,
) {
item {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
start = if (true) 54.dp else 24.dp,
top = 48.dp,
end = 24.dp,
bottom = 24.dp
),
text = when {
filterState.group != null -> filterState.group.name
filterState.feed != null -> filterState.feed.name
else -> filterState.filter.getName()
},
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Crossfade(targetState = pagingItems) { pagingItems ->
LazyColumn(
state = viewState.listState,
) {
item {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
start = if (true) 54.dp else 24.dp,
top = 48.dp,
end = 24.dp,
bottom = 24.dp
),
text = when {
filterState.group != null -> filterState.group.name
filterState.feed != null -> filterState.feed.name
else -> filterState.filter.getName()
},
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
generateArticleList(context, pagingItems, readViewModel, homeViewModel, scope)
}
generateArticleList(context, pagingItems, readViewModel, homeViewModel, scope)
}
}
)

View File

@ -8,9 +8,7 @@ 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.data.article.Article
import me.ash.reader.data.feed.Feed
import me.ash.reader.formatToString
@ -30,37 +28,31 @@ fun Header(
Intent(Intent.ACTION_VIEW, Uri.parse(article.link))
)
}
.padding(12.dp)
) {
Column(modifier = Modifier.padding(10.dp)) {
Text(
text = article.date.formatToString(context, atHourMinute = true),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = article.title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineLarge,
)
Spacer(modifier = Modifier.height(4.dp))
article.author?.let {
Text(
text = article.date.formatToString(context, atHourMinute = 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,
text = article.author,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)
}
Text(
text = feed.name,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium,
)
}
}

View File

@ -1,34 +1,51 @@
package me.ash.reader.ui.page.home.read
import androidx.compose.animation.*
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Headphones
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import kotlinx.coroutines.flow.collect
import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.paddingFixedHorizontal
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.widget.WebView
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReadPage(
navController: NavHostController,
modifier: Modifier = Modifier,
viewModel: ReadViewModel = hiltViewModel(),
btnBackOnClickListener: () -> Unit,
homeViewModel: HomeViewModel = hiltViewModel(),
readViewModel: ReadViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val viewState = viewModel.viewState.collectAsStateValue()
val composition by rememberLottieComposition(
LottieCompositionSpec.Url(
"https://assets5.lottiefiles.com/packages/lf20_9tvcldy3.json"
)
)
LaunchedEffect(viewModel.viewState) {
viewModel.viewState.collect {
@ -43,18 +60,52 @@ fun ReadPage(
}
}
Box {
Column(
modifier.fillMaxSize()
) {
ReadPageTopBar(btnBackOnClickListener)
val composition by rememberLottieComposition(
LottieCompositionSpec.Url(
"https://assets5.lottiefiles.com/packages/lf20_9tvcldy3.json"
)
Scaffold(
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
topBar = {
SmallTopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = {
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 1,
callback = {
readViewModel.dispatch(ReadViewAction.ClearArticle)
}
)
)
}) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
)
}
},
actions = {
viewState.articleWithFeed?.let {
IconButton(onClick = {}) {
Icon(
modifier = Modifier.size(22.dp),
imageVector = Icons.Outlined.Headphones,
contentDescription = stringResource(R.string.mark_all_as_read),
tint = MaterialTheme.colorScheme.onSurface,
)
}
IconButton(onClick = {}) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
)
},
content = {
if (viewState.articleWithFeed == null) {
LottieAnimation(
composition = composition,
@ -65,41 +116,34 @@ fun ReadPage(
restartOnPlay = true,
iterations = Int.MAX_VALUE
)
}
AnimatedVisibility(
modifier = modifier.fillMaxSize(),
visible = viewState.articleWithFeed != null,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
if (viewState.articleWithFeed == null) return@AnimatedVisibility
} else {
LazyColumn(
state = viewState.listState,
modifier = Modifier
.weight(1f),
) {
val article = viewState.articleWithFeed.article
val feed = viewState.articleWithFeed.feed
item {
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(2.dp))
Column(
modifier = Modifier
.weight(1f)
.paddingFixedHorizontal()
.padding(horizontal = 12.dp)
) {
Header(context, article, feed)
}
}
item {
Spacer(modifier = Modifier.height(40.dp))
WebView(
content = viewState.content ?: "",
)
Spacer(modifier = Modifier.height(50.dp))
Spacer(modifier = Modifier.height(22.dp))
Crossfade(targetState = viewState.content) { content ->
WebView(
content = content ?: "",
)
Spacer(modifier = Modifier.height(50.dp))
}
}
}
}
}
}
)
}

View File

@ -1,63 +0,0 @@
package me.ash.reader.ui.page.home.read
import android.view.HapticFeedbackConstants
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.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import me.ash.reader.R
@Composable
fun ReadPageTopBar(
btnBackOnClickListener: () -> Unit = {},
) {
val view = LocalView.current
SmallTopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
btnBackOnClickListener()
}) {
Icon(
modifier = Modifier.size(28.dp),
imageVector = Icons.Rounded.Close,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.primary
)
}
},
actions = {
IconButton(onClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}) {
Icon(
// modifier = Modifier.size(20.dp),
imageVector = Icons.Rounded.Share,
contentDescription = "Share",
tint = MaterialTheme.colorScheme.primary,
)
}
IconButton(onClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}) {
Icon(
// modifier = Modifier.size(28.dp),
imageVector = Icons.Rounded.MoreHoriz,
contentDescription = "More",
tint = MaterialTheme.colorScheme.primary,
)
}
},
)
}

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.widget
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -42,13 +43,15 @@ fun Banner(
.padding(16.dp, 20.dp),
verticalAlignment = Alignment.CenterVertically
) {
icon?.let {
Icon(
imageVector = it,
contentDescription = null,
modifier = Modifier.padding(end = 16.dp),
tint = lightOnSurface,
)
icon?.let { icon ->
Crossfade(targetState = icon) {
Icon(
imageVector = it,
contentDescription = null,
modifier = Modifier.padding(end = 16.dp),
tint = lightOnSurface,
)
}
}
Column(modifier = Modifier.weight(1f)) {
Text(

View File

@ -8,11 +8,10 @@ import androidx.compose.material.ModalBottomSheetDefaults
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
@ -23,51 +22,53 @@ fun BottomDrawer(
drawerState: ModalBottomSheetState = androidx.compose.material.rememberModalBottomSheetState(
ModalBottomSheetValue.Hidden
),
sheetContent: @Composable ColumnScope.() -> Unit = {},
content: @Composable () -> Unit = {},
) {
androidx.compose.material.ModalBottomSheetLayout(
modifier = modifier,
sheetShape = RoundedCornerShape(
topStart = 28.0.dp,
topEnd = 28.0.dp,
bottomEnd = 0.0.dp,
bottomStart = 0.0.dp
),
sheetState = drawerState,
sheetBackgroundColor = Color.Transparent,
sheetBackgroundColor = MaterialTheme.colorScheme.surface,
sheetElevation = if (drawerState.isVisible) ModalBottomSheetDefaults.Elevation else 0.dp,
sheetContent = {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(
RoundedCornerShape(
topStart = 28.0.dp,
topEnd = 28.0.dp,
bottomEnd = 0.0.dp,
bottomStart = 0.0.dp
)
)
.background(MaterialTheme.colorScheme.surface)
.padding(horizontal = 28.dp)
.navigationBarsPadding()
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 6.dp,
) {
Box {
Row(
modifier = modifier
.padding(top = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier.padding(horizontal = 28.dp)
) {
Box {
Row(
modifier = modifier
.size(38.dp, 4.dp)
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
.zIndex(1f)
) {}
}
Column {
Spacer(modifier = Modifier.height(40.dp))
content()
Spacer(modifier = Modifier.height(28.dp))
.padding(top = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = modifier
.size(38.dp, 4.dp)
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
.zIndex(1f)
) {}
}
Column {
Spacer(modifier = Modifier.height(40.dp))
sheetContent()
Spacer(modifier = Modifier.height(28.dp))
}
}
}
}
},
content = {}
content = content,
)
}

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.widget
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
@ -9,20 +10,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun CanBeDisabledIconButton(
modifier: Modifier = Modifier,
disabled: Boolean,
imageVector: ImageVector,
size: Dp = 24.dp,
contentDescription: String?,
tint: Color = LocalContentColor.current,
onClick: () -> Unit = {},
) {
IconButton(
modifier = Modifier.alpha(
modifier = modifier.alpha(
if (disabled) {
0.7f
0.5f
} else {
1f
}
@ -31,7 +35,7 @@ fun CanBeDisabledIconButton(
onClick = onClick,
) {
Icon(
modifier = modifier,
modifier = Modifier.size(size),
imageVector = imageVector,
contentDescription = contentDescription,
tint = if (disabled) MaterialTheme.colorScheme.outline else tint,

View File

@ -2,15 +2,14 @@ package me.ash.reader.ui.widget
import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.DialogProperties
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun Dialog(
modifier: Modifier = Modifier,
visible: Boolean,
properties: DialogProperties = DialogProperties(),
onDismissRequest: () -> Unit = {},
icon: @Composable (() -> Unit)? = null,
title: @Composable (() -> Unit)? = null,
@ -20,7 +19,7 @@ fun Dialog(
) {
if (visible) {
AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
properties = properties,
modifier = modifier,
onDismissRequest = onDismissRequest,
icon = icon,

View File

@ -2,10 +2,7 @@ package me.ash.reader.ui.widget
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
@ -14,7 +11,7 @@ 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.filled.CheckCircle
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -41,16 +38,7 @@ fun SelectionChip(
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = CircleShape,
border: BorderStroke? = null,
selectedIcon: @Composable () -> Unit = {
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = stringResource(R.string.selected),
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface
)
},
selectedIcon: @Composable (() -> Unit)? = null,
onClick: () -> Unit,
) {
val focusManager = LocalFocusManager.current
@ -59,20 +47,28 @@ fun SelectionChip(
modifier = modifier,
colors = ChipDefaults.filterChipColors(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.outline,
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),
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
leadingIconColor = MaterialTheme.colorScheme.surfaceVariant,
disabledBackgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer,
selectedContentColor = MaterialTheme.colorScheme.onSurface,
selectedLeadingIconColor = MaterialTheme.colorScheme.onSurface
selectedContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedLeadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer
),
border = border,
interactionSource = interactionSource,
enabled = enabled,
selected = selected,
selectedIcon = selectedIcon,
selectedIcon = selectedIcon ?: {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = stringResource(R.string.selected),
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
)
},
shape = shape,
onClick = {
focusManager.clearFocus()
@ -88,11 +84,6 @@ fun SelectionChip(
),
text = content,
style = MaterialTheme.typography.titleSmall,
color = if (selected) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.outline
},
)
},
)
@ -108,16 +99,7 @@ fun SelectionEditorChip(
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = CircleShape,
selectedIcon: @Composable () -> Unit = {
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = stringResource(R.string.selected),
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
},
selectedIcon: @Composable (() -> Unit)? = null,
onKeyboardAction: () -> Unit = {},
onClick: () -> Unit,
) {
@ -125,21 +107,30 @@ fun SelectionEditorChip(
val placeholder = stringResource(R.string.add_to_group)
FilterChip(
modifier = modifier.defaultMinSize(minHeight = 36.dp),
colors = ChipDefaults.filterChipColors(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
leadingIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
leadingIconColor = MaterialTheme.colorScheme.surfaceVariant,
disabledBackgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer,
selectedContentColor = MaterialTheme.colorScheme.onSecondaryContainer,
selectedLeadingIconColor = MaterialTheme.colorScheme.onSecondaryContainer
selectedContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedLeadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer
),
interactionSource = interactionSource,
enabled = enabled,
selected = selected,
selectedIcon = selectedIcon,
selectedIcon = selectedIcon ?: {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = stringResource(R.string.selected),
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
)
},
shape = shape,
onClick = onClick,
content = {
@ -160,13 +151,9 @@ fun SelectionEditorChip(
},
value = content,
onValueChange = { onValueChange(it) },
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSecondaryContainer),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
textStyle = MaterialTheme.typography.titleSmall.copy(
color = if (selected) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.outline
},
color = MaterialTheme.colorScheme.onSurfaceVariant,
),
decorationBox = { innerTextField ->
Row(
@ -176,7 +163,7 @@ fun SelectionEditorChip(
if (content.isEmpty()) {
Text(
text = placeholder,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
style = MaterialTheme.typography.titleSmall,
)
}

View File

@ -17,6 +17,8 @@ import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.page.home.read.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel
const val INJECTION_TOKEN = "/android_asset_font/"
@Composable
fun WebView(
modifier: Modifier = Modifier,
@ -26,11 +28,30 @@ fun WebView(
onReceivedError: (error: WebResourceError?) -> Unit = {}
) {
val context = LocalContext.current
val color = MaterialTheme.colorScheme.secondary.toArgb()
val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb()
val backgroundColor = MaterialTheme.colorScheme.surface.toArgb()
val viewState = viewModel.viewState.collectAsStateValue()
val webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? {
if (url != null && url.contains(INJECTION_TOKEN)) {
try {
val assetPath = url.substring(
url.indexOf(INJECTION_TOKEN) + INJECTION_TOKEN.length,
url.length
)
return WebResourceResponse(
"text/HTML",
"UTF-8",
context.assets.open(assetPath)
)
} catch (e: Exception) {
Log.e("RLog", "WebView shouldInterceptRequest: $e")
}
}
return super.shouldInterceptRequest(view, url);
}
override fun onPageStarted(
view: WebView?,
url: String?,
@ -131,23 +152,29 @@ fun getStyle(argb: Int): String = """
*{
padding: 0;
margin: 0;
color: ${argbToCssColor(argb)}
color: ${argbToCssColor(argb)};
font-family: url('/android_asset_font/font/google_sans_text_regular.TTF'),
url('/android_asset_font/font/google_sans_text_medium_italic.TTF'),
url('/android_asset_font/font/google_sans_text_medium.TTF'),
url('/android_asset_font/font/google_sans_text_italic.TTF'),
url('/android_asset_font/font/google_sans_text_bold_italic.TTF'),
url('/android_asset_font/font/google_sans_text_bold.TTF');
}
.page {
padding: 0 20px;
padding: 0 24px;
}
img {
margin: 0 -20px 20px;
width: calc(100% + 40px);
margin: 0 -24px 20px;
width: calc(100% + 48px);
height: auto;
}
p,span,a,ol,ul,blockquote,article,section {
text-align: justify;
font-size: 18px;
line-height: 32px;
text-align: left;
font-size: 16px;
line-height: 24px;
margin-bottom: 20px;
}
@ -186,6 +213,7 @@ hr {
}
h1,h2,h3,h4,h5,h6,figure,br {
font-size: large;
margin-bottom: 20px;
}

View File

@ -30,6 +30,12 @@
<string name="parse_full_content">全文解析</string>
<string name="add_to_group">添加到组</string>
<string name="new_group">新建分组</string>
<string name="open_with">打开 %1$s</string>
<string name="options">选项</string>
<string name="delete">删除</string>
<string name="has_been_deleted">%1$s 已被删除</string>
<string name="unsubscribe">取消订阅</string>
<string name="unsubscribe_tip">不再订阅 %1$s同时删除其所有已归档的文章。</string>
<string name="today">今天</string>
<string name="yesterday">昨天</string>
<string name="date_at_time">%1$s %2$s</string>

View File

@ -30,6 +30,12 @@
<string name="parse_full_content">Parse Full Content</string>
<string name="add_to_group">Add to Group</string>
<string name="new_group">New Group</string>
<string name="open_with">Open %1$s</string>
<string name="options">Options</string>
<string name="delete">Delete</string>
<string name="has_been_deleted">%1$s has been deleted</string>
<string name="unsubscribe">Unsubscribe</string>
<string name="unsubscribe_tip">Unsubscribe %1$s and delete all its archived articles.</string>
<string name="today">Today</string>
<string name="yesterday">Yesterday</string>
<string name="date_at_time">%1$s At %2$s</string>