Refactor Material You design for FilterBar and Add subscribe feature

This commit is contained in:
Ash 2022-03-23 17:28:53 +08:00
parent c7c708d92a
commit b2fe0674c8
27 changed files with 562 additions and 246 deletions

View File

@ -2,7 +2,8 @@ package me.ash.reader
import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.*
import androidx.work.Configuration
import androidx.work.WorkManager
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
@ -12,7 +13,6 @@ import me.ash.reader.data.repository.*
import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.data.source.ReaderDatabase
import me.ash.reader.data.source.RssNetworkDataSource
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@DelicateCoroutinesApi
@ -54,7 +54,7 @@ class App : Application(), Configuration.Provider {
@Inject
lateinit var rssRepository: RssRepository
private val applicationScope = CoroutineScope(Dispatchers.IO)
private val applicationScope = CoroutineScope(Dispatchers.Default)
override fun onCreate() {
super.onCreate()
@ -73,19 +73,7 @@ class App : Application(), Configuration.Provider {
}
private fun workerInit() {
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
).addTag(SyncWorker.WORK_NAME).build()
workManager.enqueueUniquePeriodicWork(
SyncWorker.WORK_NAME,
ExistingPeriodicWorkPolicy.REPLACE,
repeatingRequest
)
rssRepository.get().doSync()
}
override fun getWorkManagerConfiguration(): Configuration =

View File

@ -279,7 +279,7 @@ interface ArticleDao {
WHERE id = :id
"""
)
suspend fun queryById(id: Int): ArticleWithFeed?
suspend fun queryById(id: String): ArticleWithFeed?
@Insert
suspend fun insert(article: Article): Long

View File

@ -1,7 +1,9 @@
package me.ash.reader.data.constant
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FiberManualRecord
import androidx.compose.material.icons.outlined.FiberManualRecord
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Subject
import androidx.compose.ui.graphics.vector.ImageVector
@ -10,6 +12,7 @@ class Filter(
var index: Int,
var important: Int,
var icon: ImageVector,
var filledIcon: ImageVector,
) {
fun isStarred(): Boolean = this == Starred
fun isUnread(): Boolean = this == Unread
@ -20,16 +23,19 @@ class Filter(
index = 0,
important = 13,
icon = Icons.Rounded.StarOutline,
filledIcon = Icons.Rounded.Star,
)
val Unread = Filter(
index = 1,
important = 666,
icon = Icons.Outlined.FiberManualRecord,
filledIcon = Icons.Filled.FiberManualRecord,
)
val All = Filter(
index = 2,
important = 666,
icon = Icons.Rounded.Subject,
filledIcon = Icons.Rounded.Subject,
)
}
}

View File

@ -19,7 +19,7 @@ interface FeedDao {
and url = :url
"""
)
fun queryByLink(accountId: Int, url: String): List<Feed>
suspend fun queryByLink(accountId: Int, url: String): List<Feed>
@Insert
suspend fun insert(feed: Feed): Long

View File

@ -4,9 +4,7 @@ import android.content.Context
import android.util.Log
import androidx.hilt.work.HiltWorker
import androidx.paging.PagingSource
import androidx.work.CoroutineWorker
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
@ -28,6 +26,7 @@ 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(
private val context: Context,
@ -51,8 +50,18 @@ abstract class AbstractRssRepository constructor(
abstract suspend fun subscribe(feed: Feed, articles: List<Article>)
abstract suspend fun addGroup(name: String): String
abstract suspend fun sync()
fun doSync() {
workManager.enqueueUniquePeriodicWork(
SyncWorker.WORK_NAME,
ExistingPeriodicWorkPolicy.REPLACE,
SyncWorker.repeatingRequest
)
}
fun pullGroups(): Flow<MutableList<Group>> {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
return groupDao.queryAllGroup(accountId)
@ -118,11 +127,11 @@ abstract class AbstractRssRepository constructor(
}
}
suspend fun findArticleById(id: Int): ArticleWithFeed? {
suspend fun findArticleById(id: String): ArticleWithFeed? {
return articleDao.queryById(id)
}
fun isExist(url: String): Boolean {
suspend fun isExist(url: String): Boolean {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
return feedDao.queryByLink(accountId, url).isNotEmpty()
}
@ -158,5 +167,13 @@ class SyncWorker @AssistedInject constructor(
companion object {
const val WORK_NAME = "article.sync"
val repeatingRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
).addTag(WORK_NAME).build()
}
}

View File

@ -48,6 +48,19 @@ 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
)
)
}
}
override suspend fun sync() {
mutex.withLock {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)

View File

@ -20,6 +20,7 @@ import me.ash.reader.data.article.Article
import me.ash.reader.data.article.ArticleDao
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.ui.page.common.ExtraName
@ -35,7 +36,7 @@ class LocalRssRepository @Inject constructor(
private val rssHelper: RssHelper,
private val rssNetworkDataSource: RssNetworkDataSource,
private val accountDao: AccountDao,
groupDao: GroupDao,
private val groupDao: GroupDao,
workManager: WorkManager,
) : AbstractRssRepository(
context, accountDao, articleDao, groupDao,
@ -52,6 +53,19 @@ 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
)
)
}
}
override suspend fun sync() {
mutex.withLock {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
@ -136,7 +150,7 @@ class LocalRssRepository @Inject constructor(
Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(
ExtraName.ARTICLE_ID,
ids[index].toInt()
ids[index]
)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT

View File

@ -1,6 +1,7 @@
package me.ash.reader.data.repository
import android.util.Log
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.source.OpmlLocalDataSource
@ -18,11 +19,14 @@ class OpmlRepository @Inject constructor(
val groupWithFeedList = opmlLocalDataSource.parseFileInputStream(inputStream)
groupWithFeedList.forEach { groupWithFeed ->
groupDao.insert(groupWithFeed.group)
groupWithFeed.feeds.forEach { it.groupId = groupWithFeed.group.id }
groupWithFeed.feeds.removeIf {
rssRepository.get().isExist(it.url)
val repeatList = mutableListOf<Feed>()
groupWithFeed.feeds.forEach {
it.groupId = groupWithFeed.group.id
if (rssRepository.get().isExist(it.url)) {
repeatList.add(it)
}
feedDao.insertList(groupWithFeed.feeds)
}
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList())
}
} catch (e: Exception) {
Log.e("saveToDatabase", "${e.message}")

View File

@ -11,8 +11,6 @@ import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.dataStore
import me.ash.reader.get
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
import java.io.InputStream
import java.util.*
import javax.inject.Inject
@ -21,7 +19,7 @@ class OpmlLocalDataSource @Inject constructor(
@ApplicationContext
private val context: Context,
) {
@Throws(XmlPullParserException::class, IOException::class)
// @Throws(XmlPullParserException::class, IOException::class)
fun parseFileInputStream(inputStream: InputStream): List<GroupWithFeed> {
val groupWithFeedList = mutableListOf<GroupWithFeed>()
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0

View File

@ -12,7 +12,9 @@ fun PagerState.animateScrollToPage(
callback: () -> Unit = {}
) {
scope.launch {
if (pageCount > targetPage) {
animateScrollToPage(targetPage)
callback()
}
}
}

View File

@ -1,6 +1,8 @@
package me.ash.reader.ui.page.home
import android.util.Log
import android.view.HapticFeedbackConstants
import android.view.SoundEffectConstants
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutLinearInEasing
@ -10,9 +12,10 @@ import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.FilterChip
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.outlined.Circle
@ -26,13 +29,14 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowCrossAxisAlignment
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.flowlayout.MainAxisAlignment
import com.google.accompanist.flowlayout.SizeMode
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import me.ash.reader.R
@ -89,7 +93,7 @@ fun HomeBottomNavBar(
}
Divider(
modifier = Modifier.alpha(0.3f),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f)
)
Box(
modifier = modifier
@ -107,7 +111,7 @@ fun HomeBottomNavBar(
.animateContentSize()
.alpha(1 - readerBarAlpha),
) {
// Log.i("RLog", "AppNavigationBar: ${readerBarAlpha}, ${1f - readerBarAlpha}")
Log.i("RLog", "AppNavigationBar: ${readerBarAlpha}, ${1f - readerBarAlpha}")
FilterBar(
modifier = modifier,
filter = filter,
@ -148,85 +152,152 @@ private fun FilterBar(
filter: Filter,
onSelected: (Filter) -> Unit = {},
) {
val view = LocalView.current
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
FlowRow(
mainAxisSize = SizeMode.Expand,
mainAxisAlignment = MainAxisAlignment.Center,
crossAxisAlignment = FlowCrossAxisAlignment.Center,
crossAxisSpacing = 0.dp,
mainAxisSpacing = 20.dp,
) {
listOf(
Filter.Starred,
Filter.Unread,
Filter.All
).forEach { item ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.clip(CircleShape)
.animateContentSize(),
Item(
icon = if (filter == item) item.filledIcon else item.icon,
name = item.getName(),
selected = filter == item,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.height(30.dp)
.defaultMinSize(
minWidth = 82.dp
)
.clip(CircleShape)
.clickable(onClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
onSelected(item)
})
.background(
if (filter == item) {
MaterialTheme.colorScheme.inverseOnSurface
} else {
Color.Unspecified
}
)
) {
if (filter == item) {
Spacer(modifier = Modifier.width(10.dp))
Icon(
modifier = Modifier.size(
if (filter == item) {
15.dp
} else {
19.dp
}
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Item(
modifier: Modifier = Modifier,
icon: ImageVector,
name: String,
selected: Boolean = false,
onClick: () -> Unit = {},
) {
val view = LocalView.current
FilterChip(
modifier = Modifier
.height(36.dp)
.animateContentSize(),
colors = ChipDefaults.filterChipColors(
backgroundColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.outline,
leadingIconColor = MaterialTheme.colorScheme.outline,
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
),
imageVector = item.icon,
contentDescription = item.getName(),
tint = MaterialTheme.colorScheme.primary,
selected = selected,
selectedIcon = {
Icon(
imageVector = icon,
contentDescription = name,
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.width(4.dp))
},
onClick = {
view.playSoundEffect(SoundEffectConstants.CLICK)
onClick()
},
content = {
if (selected) {
Text(
text = item.getName().uppercase(),
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(
start = 0.dp,
top = 8.dp,
end = 8.dp,
bottom = 8.dp
),
text = if (selected) name.uppercase() else "",
style = MaterialTheme.typography.titleSmall,
color = if (selected) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.outline
},
)
Spacer(modifier = Modifier.width(10.dp))
} else {
Icon(
modifier = Modifier.size(
if (item.isUnread()) {
15
} else {
19
}.dp
),
imageVector = item.icon,
contentDescription = item.getName(),
imageVector = icon,
contentDescription = name,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.outline,
)
}
}
}
}
}
},
)
// Row(
// modifier = Modifier
// .animateContentSize()
// .height(40.dp)
// .width(if (selected) Dp.Unspecified else 40.dp)
// .padding(vertical = if (selected) 2.dp else 0.dp)
// .clip(CircleShape)
// .pointerInput(Unit) {
// detectTapGestures(
// onTap = {
// view.playSoundEffect(SoundEffectConstants.CLICK)
// onClick()
// }
// )
// }
// .background(
// if (selected) {
// MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.54f)
// } else {
// Color.Transparent
// }
// ),
// horizontalArrangement = Arrangement.Center,
// verticalAlignment = Alignment.CenterVertically,
// ) {
// Spacer(modifier = Modifier.width(8.dp))
// Icon(
// modifier = Modifier.size(20.dp),
// imageVector = icon,
// contentDescription = name,
// tint = if (selected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
// )
// if (selected) {
// Spacer(modifier = Modifier.width(8.dp))
// Text(
// modifier = Modifier.padding(horizontal = 8.dp),
// text = name.uppercase(),
// style = MaterialTheme.typography.titleSmall,
// color = if (selected) {
// MaterialTheme.colorScheme.onSurface
// } else {
// MaterialTheme.colorScheme.outline
// },
// )
// Spacer(modifier = Modifier.width(8.dp))
// }
// }
}
@Composable

View File

@ -47,7 +47,7 @@ fun HomePage(
scope.launch {
val article = readViewModel
.rssRepository.get()
.findArticleById(it.toString().toInt()) ?: return@launch
.findArticleById(it.toString()) ?: return@launch
readViewModel.dispatch(ReadViewAction.InitData(article))
if (article.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)

View File

@ -47,8 +47,8 @@ class HomeViewModel @Inject constructor(
}
private fun sync(callback: () -> Unit = {}) {
viewModelScope.launch(Dispatchers.IO) {
rssRepository.get().sync()
viewModelScope.launch {
rssRepository.get().doSync()
callback()
}
}

View File

@ -106,6 +106,7 @@ fun FeedsPage(
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
)
},
@ -138,7 +139,6 @@ fun FeedsPage(
Icon(
imageVector = Icons.Outlined.KeyboardArrowRight,
contentDescription = stringResource(R.string.go_to),
tint = MaterialTheme.colorScheme.onSurface,
)
},
) {
@ -161,7 +161,7 @@ fun FeedsPage(
item {
Spacer(modifier = Modifier.height(24.dp))
Subtitle(
modifier = Modifier.padding(start = 28.dp),
modifier = Modifier.padding(start = 26.dp),
text = stringResource(R.string.feeds)
)
Spacer(modifier = Modifier.height(8.dp))
@ -207,6 +207,9 @@ fun FeedsPage(
Spacer(modifier = Modifier.height(8.dp))
}
}
item {
Spacer(modifier = Modifier.height(48.dp))
}
}
}
)

View File

@ -5,7 +5,6 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import me.ash.reader.data.account.Account
@ -48,14 +47,14 @@ class FeedsViewModel @Inject constructor(
}
private fun addFromFile(inputStream: InputStream) {
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch {
opmlRepository.saveToDatabase(inputStream)
rssRepository.get().sync()
rssRepository.get().doSync()
}
}
private fun fetchData(filterState: FilterState) {
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch {
pullFeeds(
isStarred = filterState.filter.isStarred(),
isUnread = filterState.filter.isUnread(),

View File

@ -35,7 +35,7 @@ fun GroupItem(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(32.dp))
.background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.14f))
.clickable { groupOnClick() }
@ -78,9 +78,6 @@ fun GroupItem(
exit = fadeOut() + shrinkVertically(),
) {
Column {
if (feeds.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
}
feeds.forEach { feed ->
FeedItem(
modifier = Modifier.padding(horizontal = 20.dp),
@ -90,6 +87,9 @@ fun GroupItem(
feedOnClick(feed)
}
}
if (feeds.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}

View File

@ -6,8 +6,8 @@ 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.outlined.Article
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.filled.AddAlert
import androidx.compose.material.icons.filled.Article
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -25,18 +25,23 @@ import me.ash.reader.ui.widget.Subtitle
@Composable
fun ResultViewPage(
modifier: Modifier = Modifier,
link: String = "",
groups: List<Group> = emptyList(),
selectedAllowNotificationPreset: Boolean = false,
selectedParseFullContentPreset: Boolean = false,
selectedGroupId: String = "",
newGroupContent: String = "",
newGroupSelected: Boolean,
onNewGroupValueChange: (String) -> Unit = {},
changeNewGroupSelected: (Boolean) -> Unit = {},
allowNotificationPresetOnClick: () -> Unit = {},
parseFullContentPresetOnClick: () -> Unit = {},
groupOnClick: (groupId: String) -> Unit = {},
onKeyboardAction: () -> Unit = {},
) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
modifier = modifier.verticalScroll(rememberScrollState())
) {
Link(
text = link
@ -54,6 +59,10 @@ fun ResultViewPage(
AddToGroup(
groups = groups,
selectedGroupId = selectedGroupId,
newGroupContent = newGroupContent,
newGroupSelected = newGroupSelected,
onNewGroupValueChange = onNewGroupValueChange,
changeNewGroupSelected = changeNewGroupSelected,
groupOnClick = groupOnClick,
onKeyboardAction = onKeyboardAction,
)
@ -98,11 +107,12 @@ private fun Preset(
selected = selectedAllowNotificationPreset,
selectedIcon = {
Icon(
imageVector = Icons.Outlined.Notifications,
imageVector = Icons.Filled.AddAlert,
contentDescription = stringResource(R.string.allow_notification),
modifier = Modifier
.padding(start = 8.dp)
.size(18.dp),
.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
},
) {
@ -114,11 +124,12 @@ private fun Preset(
selected = selectedParseFullContentPreset,
selectedIcon = {
Icon(
imageVector = Icons.Outlined.Article,
imageVector = Icons.Filled.Article,
contentDescription = stringResource(R.string.parse_full_content),
modifier = Modifier
.padding(start = 8.dp)
.size(18.dp),
.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
},
) {
@ -131,6 +142,10 @@ private fun Preset(
private fun AddToGroup(
groups: List<Group>,
selectedGroupId: String,
newGroupContent: String,
newGroupSelected: Boolean,
onNewGroupValueChange: (String) -> Unit = {},
changeNewGroupSelected: (Boolean) -> Unit = {},
groupOnClick: (groupId: String) -> Unit = {},
onKeyboardAction: () -> Unit = {},
) {
@ -145,19 +160,21 @@ private fun AddToGroup(
SelectionChip(
modifier = Modifier.animateContentSize(),
content = it.name,
selected = it.id == selectedGroupId,
selected = !newGroupSelected && it.id == selectedGroupId,
) {
changeNewGroupSelected(false)
groupOnClick(it.id)
}
}
SelectionEditorChip(
modifier = Modifier.animateContentSize(),
content = stringResource(R.string.new_group),
selected = false,
content = newGroupContent,
onValueChange = onNewGroupValueChange,
selected = newGroupSelected,
onKeyboardAction = onKeyboardAction,
) {
changeNewGroupSelected(true)
}
}
}

View File

@ -1,10 +1,13 @@
package me.ash.reader.ui.page.home.feeds.subscribe
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
@ -22,7 +25,9 @@ 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.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
@ -32,12 +37,15 @@ import me.ash.reader.R
@OptIn(ExperimentalPagerApi::class)
@Composable
fun SearchViewPage(
modifier: Modifier = Modifier,
pagerState: PagerState,
inputContent: String = "",
readOnly: Boolean = false,
inputLink: String = "",
errorMessage: String = "",
onValueChange: (String) -> Unit = {},
onLinkValueChange: (String) -> Unit = {},
onKeyboardAction: () -> Unit = {},
) {
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
@ -45,7 +53,7 @@ fun SearchViewPage(
focusRequester.requestFocus()
}
Column {
Column(modifier = modifier) {
Spacer(modifier = Modifier.height(10.dp))
TextField(
modifier = Modifier.focusRequester(focusRequester),
@ -55,9 +63,10 @@ fun SearchViewPage(
textColor = MaterialTheme.colorScheme.onSurface,
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
),
value = inputContent,
enabled = !readOnly,
value = inputLink,
onValueChange = {
if (pagerState.currentPage == 0) onValueChange(it)
if (!readOnly) onLinkValueChange(it)
},
placeholder = {
Text(
@ -68,9 +77,9 @@ fun SearchViewPage(
isError = errorMessage.isNotEmpty(),
singleLine = true,
trailingIcon = {
if (inputContent.isNotEmpty()) {
if (inputLink.isNotEmpty()) {
IconButton(onClick = {
onValueChange("")
if (!readOnly) onLinkValueChange("")
}) {
Icon(
imageVector = Icons.Rounded.Close,
@ -90,17 +99,23 @@ fun SearchViewPage(
}
},
keyboardActions = KeyboardActions(
onDone = {
onSearch = {
focusManager.clearFocus()
onKeyboardAction()
}
)
),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search
),
)
if (errorMessage.isNotEmpty()) {
SelectionContainer {
Text(
modifier = Modifier
.padding(start = 16.dp)
.horizontalScroll(rememberScrollState()),
text = errorMessage,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(start = 16.dp),
maxLines = 1,
softWrap = false,
)

View File

@ -2,6 +2,8 @@ package me.ash.reader.ui.page.home.feeds.subscribe
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.RssFeed
import androidx.compose.material3.Icon
@ -9,9 +11,13 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
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.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.*
@ -23,10 +29,12 @@ import java.io.InputStream
@OptIn(ExperimentalPagerApi::class)
@Composable
fun SubscribeDialog(
modifier: Modifier = Modifier,
viewModel: SubscribeViewModel = hiltViewModel(),
openInputStreamCallback: (InputStream) -> Unit,
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val scope = rememberCoroutineScope()
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { uri ->
@ -37,13 +45,14 @@ fun SubscribeDialog(
}
val viewState = viewModel.viewState.collectAsStateValue()
val groupsState = viewState.groups.collectAsState(initial = emptyList())
var height by remember { mutableStateOf(0) }
var dialogHeight by remember { mutableStateOf(280.dp) }
val readYouString = stringResource(R.string.read_you)
val defaultString = stringResource(R.string.defaults)
LaunchedEffect(viewState.visible) {
if (viewState.visible) {
val defaultGroupId = context.dataStore
.get(DataStoreKeys.CurrentAccountId)!!
.spacerDollar("0")
.spacerDollar(readYouString + defaultString)
viewModel.dispatch(SubscribeViewAction.SelectedGroup(defaultGroupId))
viewModel.dispatch(SubscribeViewAction.Init)
} else {
@ -52,9 +61,21 @@ fun SubscribeDialog(
}
}
LaunchedEffect(viewState.pagerState.currentPage) {
focusManager.clearFocus()
when (viewState.pagerState.currentPage) {
0 -> dialogHeight = 280.dp
1 -> dialogHeight = Dp.Unspecified
}
}
Dialog(
modifier = Modifier
.padding(horizontal = 44.dp)
.height(dialogHeight),
visible = viewState.visible,
onDismissRequest = {
focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Hide)
},
icon = {
@ -66,30 +87,35 @@ fun SubscribeDialog(
title = {
Text(
when (viewState.pagerState.currentPage) {
0 -> stringResource(R.string.subscribe)
0 -> viewState.title
else -> viewState.feed?.name ?: stringResource(R.string.unknown)
}
)
},
text = {
SubscribeViewPager(
// height = when (viewState.pagerState.currentPage) {
// 0 -> 84.dp
// else -> Dp.Unspecified
// },
inputContent = viewState.inputContent,
readOnly = viewState.lockLinkInput,
inputLink = viewState.linkContent,
errorMessage = viewState.errorMessage,
onValueChange = {
viewModel.dispatch(SubscribeViewAction.Input(it))
onLinkValueChange = {
viewModel.dispatch(SubscribeViewAction.InputLink(it))
},
onSearchKeyboardAction = {
viewModel.dispatch(SubscribeViewAction.Search(scope))
},
link = viewState.inputContent,
link = viewState.linkContent,
groups = groupsState.value,
selectedAllowNotificationPreset = viewState.allowNotificationPreset,
selectedParseFullContentPreset = viewState.parseFullContentPreset,
selectedGroupId = viewState.selectedGroupId,
newGroupContent = viewState.newGroupContent,
onNewGroupValueChange = {
viewModel.dispatch(SubscribeViewAction.InputNewGroup(it))
},
newGroupSelected = viewState.newGroupSelected,
changeNewGroupSelected = {
viewModel.dispatch(SubscribeViewAction.SelectedNewGroup(it))
},
pagerState = viewState.pagerState,
allowNotificationPresetOnClick = {
viewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset)
@ -109,14 +135,15 @@ fun SubscribeDialog(
when (viewState.pagerState.currentPage) {
0 -> {
TextButton(
enabled = viewState.inputContent.isNotEmpty(),
enabled = viewState.linkContent.isNotEmpty(),
onClick = {
focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Search(scope))
}
) {
Text(
text = stringResource(R.string.search),
color = if (viewState.inputContent.isNotEmpty()) {
color = if (viewState.linkContent.isNotEmpty()) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
@ -127,6 +154,7 @@ fun SubscribeDialog(
1 -> {
TextButton(
onClick = {
focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Subscribe)
}
) {
@ -140,6 +168,7 @@ fun SubscribeDialog(
0 -> {
TextButton(
onClick = {
focusManager.clearFocus()
launcher.launch("*/*")
viewModel.dispatch(SubscribeViewAction.Hide)
}
@ -150,6 +179,7 @@ fun SubscribeDialog(
1 -> {
TextButton(
onClick = {
focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Hide)
}
) {

View File

@ -6,7 +6,8 @@ import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import me.ash.reader.R
@ -29,6 +30,7 @@ class SubscribeViewModel @Inject constructor(
) : ViewModel() {
private val _viewState = MutableStateFlow(SubscribeViewState())
val viewState: StateFlow<SubscribeViewState> = _viewState.asStateFlow()
private var searchJob: Job? = null
fun dispatch(action: SubscribeViewAction) {
when (action) {
@ -36,13 +38,15 @@ class SubscribeViewModel @Inject constructor(
is SubscribeViewAction.Reset -> reset()
is SubscribeViewAction.Show -> changeVisible(true)
is SubscribeViewAction.Hide -> changeVisible(false)
is SubscribeViewAction.Input -> inputLink(action.content)
is SubscribeViewAction.InputLink -> inputLink(action.content)
is SubscribeViewAction.Search -> search(action.scope)
is SubscribeViewAction.ChangeAllowNotificationPreset ->
changeAllowNotificationPreset()
is SubscribeViewAction.ChangeParseFullContentPreset ->
changeParseFullContentPreset()
is SubscribeViewAction.SelectedGroup -> selectedGroup(action.groupId)
is SubscribeViewAction.InputNewGroup -> inputNewGroup(action.content)
is SubscribeViewAction.SelectedNewGroup -> selectedNewGroup(action.selected)
is SubscribeViewAction.Subscribe -> subscribe()
}
}
@ -51,24 +55,17 @@ class SubscribeViewModel @Inject constructor(
_viewState.update {
it.copy(
title = stringsRepository.getString(R.string.subscribe),
groups = rssRepository.get().pullGroups()
groups = rssRepository.get().pullGroups(),
)
}
}
private fun reset() {
searchJob?.cancel()
searchJob = null
_viewState.update {
it.copy(
visible = false,
SubscribeViewState().copy(
title = stringsRepository.getString(R.string.subscribe),
errorMessage = "",
inputContent = "",
feed = null,
articles = emptyList(),
allowNotificationPreset = false,
parseFullContentPreset = false,
selectedGroupId = "",
groups = emptyFlow(),
)
}
}
@ -76,11 +73,20 @@ class SubscribeViewModel @Inject constructor(
private fun subscribe() {
val feed = _viewState.value.feed ?: return
val articles = _viewState.value.articles
val groupId = _viewState.value.selectedGroupId
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch {
val groupId = async {
if (
_viewState.value.newGroupSelected &&
_viewState.value.newGroupContent.isNotBlank()
) {
rssRepository.get().addGroup(_viewState.value.newGroupContent)
} else {
_viewState.value.selectedGroupId
}
}
rssRepository.get().subscribe(
feed.copy(
groupId = groupId,
groupId = groupId.await(),
isNotification = _viewState.value.allowNotificationPreset,
isFullContent = _viewState.value.parseFullContentPreset,
), articles
@ -97,6 +103,14 @@ class SubscribeViewModel @Inject constructor(
}
}
private fun selectedNewGroup(selected: Boolean) {
_viewState.update {
it.copy(
newGroupSelected = selected,
)
}
}
private fun changeParseFullContentPreset() {
_viewState.update {
it.copy(
@ -114,11 +128,19 @@ class SubscribeViewModel @Inject constructor(
}
private fun search(scope: CoroutineScope) {
_viewState.value.inputContent.formatUrl().let { str ->
if (str != _viewState.value.inputContent) {
searchJob?.cancel()
viewModelScope.launch {
try {
_viewState.update {
it.copy(
inputContent = str
errorMessage = "",
)
}
_viewState.value.linkContent.formatUrl().let { str ->
if (str != _viewState.value.linkContent) {
_viewState.update {
it.copy(
linkContent = str
)
}
}
@ -126,19 +148,20 @@ class SubscribeViewModel @Inject constructor(
_viewState.update {
it.copy(
title = stringsRepository.getString(R.string.searching),
lockLinkInput = true,
)
}
viewModelScope.launch(Dispatchers.IO) {
try {
if (rssRepository.get().isExist(_viewState.value.inputContent)) {
if (rssRepository.get().isExist(_viewState.value.linkContent)) {
_viewState.update {
it.copy(
title = stringsRepository.getString(R.string.subscribe),
errorMessage = stringsRepository.getString(R.string.already_subscribed),
lockLinkInput = false,
)
}
return@launch
}
val feedWithArticle = rssHelper.searchFeed(_viewState.value.inputContent)
val feedWithArticle = rssHelper.searchFeed(_viewState.value.linkContent)
_viewState.update {
it.copy(
feed = feedWithArticle.feed,
@ -152,16 +175,27 @@ class SubscribeViewModel @Inject constructor(
it.copy(
title = stringsRepository.getString(R.string.subscribe),
errorMessage = e.message ?: stringsRepository.getString(R.string.unknown),
lockLinkInput = false,
)
}
}
}.also {
searchJob = it
}
}
private fun inputLink(content: String) {
_viewState.update {
it.copy(
inputContent = content
linkContent = content
)
}
}
private fun inputNewGroup(content: String) {
_viewState.update {
it.copy(
newGroupContent = content
)
}
}
@ -180,12 +214,15 @@ data class SubscribeViewState(
val visible: Boolean = false,
val title: String = "",
val errorMessage: String = "",
val inputContent: String = "",
val linkContent: String = "",
val lockLinkInput: Boolean = false,
val feed: Feed? = null,
val articles: List<Article> = emptyList(),
val allowNotificationPreset: Boolean = false,
val parseFullContentPreset: Boolean = false,
val selectedGroupId: String = "",
val newGroupContent: String = "",
val newGroupSelected: Boolean = false,
val groups: Flow<List<Group>> = emptyFlow(),
val pagerState: PagerState = PagerState(),
)
@ -197,7 +234,7 @@ sealed class SubscribeViewAction {
object Show : SubscribeViewAction()
object Hide : SubscribeViewAction()
data class Input(
data class InputLink(
val content: String
) : SubscribeViewAction()
@ -212,5 +249,13 @@ sealed class SubscribeViewAction {
val groupId: String
) : SubscribeViewAction()
data class InputNewGroup(
val content: String
) : SubscribeViewAction()
data class SelectedNewGroup(
val selected: Boolean
) : SubscribeViewAction()
object Subscribe : SubscribeViewAction()
}

View File

@ -1,9 +1,10 @@
package me.ash.reader.ui.page.home.feeds.subscribe
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import me.ash.reader.data.group.Group
@ -12,33 +13,47 @@ import me.ash.reader.ui.widget.ViewPager
@OptIn(ExperimentalPagerApi::class)
@Composable
fun SubscribeViewPager(
height: Dp = Dp.Unspecified,
inputContent: String = "",
modifier: Modifier = Modifier,
readOnly: Boolean = false,
inputLink: String = "",
errorMessage: String = "",
onValueChange: (String) -> Unit = {},
onLinkValueChange: (String) -> Unit = {},
onSearchKeyboardAction: () -> Unit = {},
link: String = "",
groups: List<Group> = emptyList(),
selectedAllowNotificationPreset: Boolean = false,
selectedParseFullContentPreset: Boolean = false,
selectedGroupId: String = "",
newGroupContent: String = "",
onNewGroupValueChange: (String) -> Unit = {},
newGroupSelected: Boolean,
changeNewGroupSelected: (Boolean) -> Unit = {},
pagerState: PagerState = com.google.accompanist.pager.rememberPagerState(),
allowNotificationPresetOnClick: () -> Unit = {},
parseFullContentPresetOnClick: () -> Unit = {},
groupOnClick: (groupId: String) -> Unit = {},
onResultKeyboardAction: () -> Unit = {},
) {
val focusManager = LocalFocusManager.current
ViewPager(
modifier = Modifier.height(height),
modifier = modifier.pointerInput(Unit) {
detectTapGestures(
onTap = {
focusManager.clearFocus()
}
)
},
state = pagerState,
userScrollEnabled = false,
composableList = listOf(
{
SearchViewPage(
pagerState = pagerState,
inputContent = inputContent,
readOnly = readOnly,
inputLink = inputLink,
errorMessage = errorMessage,
onValueChange = onValueChange,
onLinkValueChange = onLinkValueChange,
onKeyboardAction = onSearchKeyboardAction,
)
},
@ -49,6 +64,10 @@ fun SubscribeViewPager(
selectedAllowNotificationPreset = selectedAllowNotificationPreset,
selectedParseFullContentPreset = selectedParseFullContentPreset,
selectedGroupId = selectedGroupId,
newGroupContent = newGroupContent,
onNewGroupValueChange = onNewGroupValueChange,
newGroupSelected = newGroupSelected,
changeNewGroupSelected = changeNewGroupSelected,
allowNotificationPresetOnClick = allowNotificationPresetOnClick,
parseFullContentPresetOnClick = parseFullContentPresetOnClick,
groupOnClick = groupOnClick,

View File

@ -27,10 +27,10 @@ fun ArticleItem(
val context = LocalContext.current
Column(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 8.dp)
.padding(horizontal = 12.dp)
.clip(RoundedCornerShape(12.dp))
.clickable { onClick(articleWithFeed) }
.padding(horizontal = 12.dp, vertical = 8.dp)
.padding(horizontal = 12.dp, vertical = 12.dp)
.alpha(if (articleWithFeed.article.isUnread) 1f else 0.5f),
) {
Row(
@ -72,7 +72,7 @@ fun ArticleItem(
)
Text(
text = articleWithFeed.article.shortDescription,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis,

View File

@ -8,7 +8,6 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import me.ash.reader.data.article.ArticleWithFeed
@ -41,7 +40,7 @@ class FlowViewModel @Inject constructor(
}
private fun fetchData(filterState: FilterState) {
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch {
rssRepository.get().pullImportant(filterState.filter.isStarred(), true)
.collect { importantList ->
_viewState.update {

View File

@ -4,8 +4,9 @@ import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalContext
import me.ash.reader.ui.theme.*
private val LightThemeColors = lightColorScheme(
@ -37,6 +38,7 @@ private val LightThemeColors = lightColorScheme(
inversePrimary = md_theme_light_inversePrimary,
// shadow = md_theme_light_shadow,
)
private val DarkThemeColors = darkColorScheme(
primary = md_theme_dark_primary,
@ -68,6 +70,10 @@ private val DarkThemeColors = darkColorScheme(
// shadow = md_theme_dark_shadow,
)
val LocalLightThemeColors = staticCompositionLocalOf { LightThemeColors }
val LocalDarkThemeColors = staticCompositionLocalOf { DarkThemeColors }
@Composable
fun AppTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(),
@ -75,15 +81,27 @@ fun AppTheme(
) {
// Dynamic color is available on Android 12+
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {
dynamicColor && useDarkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !useDarkTheme -> dynamicLightColorScheme(LocalContext.current)
useDarkTheme -> DarkThemeColors
val light = when {
dynamicColor -> dynamicLightColorScheme(LocalContext.current)
else -> LightThemeColors
}
val dark = when {
dynamicColor -> dynamicDarkColorScheme(LocalContext.current)
else -> DarkThemeColors
}
val colorScheme = when {
useDarkTheme -> dark
else -> light
}
CompositionLocalProvider(
LocalLightThemeColors provides light,
LocalDarkThemeColors provides dark,
) {
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}
}

View File

@ -4,17 +4,16 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.ash.reader.ui.theme.LocalLightThemeColors
@Composable
fun Banner(
@ -25,6 +24,10 @@ fun Banner(
action: (@Composable () -> Unit)? = null,
onClick: () -> Unit = {},
) {
val lightThemeColors = LocalLightThemeColors.current
val lightPrimaryContainer = lightThemeColors.primaryContainer
val lightOnSurface = lightThemeColors.onSurface
Surface(
modifier = modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface,
@ -34,7 +37,7 @@ fun Banner(
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(32.dp))
.background(MaterialTheme.colorScheme.primaryContainer)
.background(lightPrimaryContainer)
.clickable { onClick() }
.padding(16.dp, 20.dp),
verticalAlignment = Alignment.CenterVertically
@ -44,7 +47,7 @@ fun Banner(
imageVector = it,
contentDescription = null,
modifier = Modifier.padding(end = 16.dp),
tint = MaterialTheme.colorScheme.onSurface,
tint = lightOnSurface,
)
}
Column(modifier = Modifier.weight(1f)) {
@ -52,22 +55,24 @@ fun Banner(
text = title,
maxLines = if (desc == null) 2 else 1,
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp),
color = MaterialTheme.colorScheme.onSurface,
color = lightOnSurface,
)
desc?.let {
Text(
text = it,
maxLines = 1,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
color = lightOnSurface.copy(alpha = 0.7f),
)
}
}
action?.let {
Box(Modifier.padding(start = 16.dp)) {
CompositionLocalProvider(LocalContentColor provides lightOnSurface) {
it()
}
}
}
}
}
}

View File

@ -2,9 +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,
onDismissRequest: () -> Unit = {},
icon: @Composable (() -> Unit)? = null,
@ -13,13 +18,10 @@ fun Dialog(
confirmButton: @Composable () -> Unit,
dismissButton: @Composable (() -> Unit)? = null,
) {
// AnimatedVisibility(
// visible = visible,
// enter = fadeIn() + expandVertically(),
// exit = fadeOut() + shrinkVertically(),
// ) {
if (visible) {
AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
modifier = modifier,
onDismissRequest = onDismissRequest,
icon = icon,
title = title,

View File

@ -1,25 +1,32 @@
package me.ash.reader.ui.widget
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.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
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.rounded.Check
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import me.ash.reader.R
@ -34,20 +41,23 @@ fun SelectionChip(
shape: Shape = CircleShape,
selectedIcon: @Composable () -> Unit = {
Icon(
imageVector = Icons.Rounded.Check,
imageVector = Icons.Filled.CheckCircle,
contentDescription = stringResource(R.string.selected),
modifier = Modifier
.padding(start = 8.dp)
.size(18.dp)
.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface
)
},
onClick: () -> Unit,
) {
val focusManager = LocalFocusManager.current
FilterChip(
modifier = modifier,
colors = ChipDefaults.filterChipColors(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurface,
contentColor = MaterialTheme.colorScheme.outline,
leadingIconColor = MaterialTheme.colorScheme.onSurface,
disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
@ -61,7 +71,10 @@ fun SelectionChip(
selected = selected,
selectedIcon = selectedIcon,
shape = shape,
onClick = onClick,
onClick = {
focusManager.clearFocus()
onClick()
},
content = {
Text(
modifier = modifier.padding(
@ -72,6 +85,11 @@ fun SelectionChip(
),
text = content,
style = MaterialTheme.typography.titleSmall,
color = if (selected) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.outline
},
)
},
)
@ -80,26 +98,30 @@ fun SelectionChip(
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SelectionEditorChip(
content: String,
selected: Boolean,
modifier: Modifier = Modifier,
content: String,
onValueChange: (String) -> Unit = {},
selected: Boolean,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = CircleShape,
selectedIcon: @Composable () -> Unit = {
Icon(
imageVector = Icons.Rounded.Check,
imageVector = Icons.Filled.CheckCircle,
contentDescription = stringResource(R.string.selected),
modifier = Modifier
.padding(start = 8.dp)
.size(16.dp)
.size(20.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
},
onKeyboardAction: () -> Unit = {},
onClick: () -> Unit,
) {
val focusManager = LocalFocusManager.current
val placeholder = stringResource(R.string.add_to_group)
FilterChip(
modifier = modifier,
colors = ChipDefaults.filterChipColors(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
@ -123,21 +145,50 @@ fun SelectionEditorChip(
.padding(
start = if (selected) 0.dp else 8.dp,
top = 8.dp,
end = 8.dp,
end = if (content.isEmpty()) 0.dp else 8.dp,
bottom = 8.dp
)
.width(56.dp),
.onFocusChanged {
if (it.isFocused) {
onClick()
} else {
focusManager.clearFocus()
}
},
value = content,
onValueChange = {},
onValueChange = { onValueChange(it) },
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSecondaryContainer),
textStyle = MaterialTheme.typography.titleSmall.copy(
color = MaterialTheme.colorScheme.onSurface
color = if (selected) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.outline
},
),
singleLine = true,
decorationBox = { innerTextField ->
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
if (content.isEmpty()) {
Text(
text = placeholder,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.titleSmall,
)
}
}
innerTextField()
},
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
onKeyboardAction()
}
)
),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done
),
)
},
)