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

View File

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

View File

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

View File

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

View File

@ -4,9 +4,7 @@ import android.content.Context
import android.util.Log import android.util.Log
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.work.CoroutineWorker import androidx.work.*
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow 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.data.source.RssNetworkDataSource
import me.ash.reader.dataStore import me.ash.reader.dataStore
import me.ash.reader.get import me.ash.reader.get
import java.util.concurrent.TimeUnit
abstract class AbstractRssRepository constructor( abstract class AbstractRssRepository constructor(
private val context: Context, private val context: Context,
@ -51,8 +50,18 @@ abstract class AbstractRssRepository constructor(
abstract suspend fun subscribe(feed: Feed, articles: List<Article>) abstract suspend fun subscribe(feed: Feed, articles: List<Article>)
abstract suspend fun addGroup(name: String): String
abstract suspend fun sync() abstract suspend fun sync()
fun doSync() {
workManager.enqueueUniquePeriodicWork(
SyncWorker.WORK_NAME,
ExistingPeriodicWorkPolicy.REPLACE,
SyncWorker.repeatingRequest
)
}
fun pullGroups(): Flow<MutableList<Group>> { fun pullGroups(): Flow<MutableList<Group>> {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
return groupDao.queryAllGroup(accountId) 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) return articleDao.queryById(id)
} }
fun isExist(url: String): Boolean { suspend fun isExist(url: String): Boolean {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!! val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
return feedDao.queryByLink(accountId, url).isNotEmpty() return feedDao.queryByLink(accountId, url).isNotEmpty()
} }
@ -158,5 +167,13 @@ class SyncWorker @AssistedInject constructor(
companion object { companion object {
const val WORK_NAME = "article.sync" 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() { override suspend fun sync() {
mutex.withLock { mutex.withLock {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) 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.article.ArticleDao
import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao 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.group.GroupDao
import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.ui.page.common.ExtraName import me.ash.reader.ui.page.common.ExtraName
@ -35,7 +36,7 @@ class LocalRssRepository @Inject constructor(
private val rssHelper: RssHelper, private val rssHelper: RssHelper,
private val rssNetworkDataSource: RssNetworkDataSource, private val rssNetworkDataSource: RssNetworkDataSource,
private val accountDao: AccountDao, private val accountDao: AccountDao,
groupDao: GroupDao, private val groupDao: GroupDao,
workManager: WorkManager, workManager: WorkManager,
) : AbstractRssRepository( ) : AbstractRssRepository(
context, accountDao, articleDao, groupDao, 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() { override suspend fun sync() {
mutex.withLock { mutex.withLock {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
@ -136,7 +150,7 @@ class LocalRssRepository @Inject constructor(
Intent.FLAG_ACTIVITY_CLEAR_TASK Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra( putExtra(
ExtraName.ARTICLE_ID, ExtraName.ARTICLE_ID,
ids[index].toInt() ids[index]
) )
}, },
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT

View File

@ -1,6 +1,7 @@
package me.ash.reader.data.repository package me.ash.reader.data.repository
import android.util.Log import android.util.Log
import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.GroupDao import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.source.OpmlLocalDataSource import me.ash.reader.data.source.OpmlLocalDataSource
@ -18,11 +19,14 @@ class OpmlRepository @Inject constructor(
val groupWithFeedList = opmlLocalDataSource.parseFileInputStream(inputStream) val groupWithFeedList = opmlLocalDataSource.parseFileInputStream(inputStream)
groupWithFeedList.forEach { groupWithFeed -> groupWithFeedList.forEach { groupWithFeed ->
groupDao.insert(groupWithFeed.group) groupDao.insert(groupWithFeed.group)
groupWithFeed.feeds.forEach { it.groupId = groupWithFeed.group.id } val repeatList = mutableListOf<Feed>()
groupWithFeed.feeds.removeIf { groupWithFeed.feeds.forEach {
rssRepository.get().isExist(it.url) 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) { } catch (e: Exception) {
Log.e("saveToDatabase", "${e.message}") 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.dataStore
import me.ash.reader.get import me.ash.reader.get
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -21,7 +19,7 @@ class OpmlLocalDataSource @Inject constructor(
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
) { ) {
@Throws(XmlPullParserException::class, IOException::class) // @Throws(XmlPullParserException::class, IOException::class)
fun parseFileInputStream(inputStream: InputStream): List<GroupWithFeed> { fun parseFileInputStream(inputStream: InputStream): List<GroupWithFeed> {
val groupWithFeedList = mutableListOf<GroupWithFeed>() val groupWithFeedList = mutableListOf<GroupWithFeed>()
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0

View File

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

View File

@ -1,6 +1,8 @@
package me.ash.reader.ui.page.home package me.ash.reader.ui.page.home
import android.util.Log
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.SoundEffectConstants
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutLinearInEasing 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.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* 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.Icons
import androidx.compose.material.icons.outlined.Article import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.outlined.Circle import androidx.compose.material.icons.outlined.Circle
@ -26,13 +29,14 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import 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.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import me.ash.reader.R import me.ash.reader.R
@ -89,7 +93,7 @@ fun HomeBottomNavBar(
} }
Divider( Divider(
modifier = Modifier.alpha(0.3f), color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f)
) )
Box( Box(
modifier = modifier modifier = modifier
@ -107,7 +111,7 @@ fun HomeBottomNavBar(
.animateContentSize() .animateContentSize()
.alpha(1 - readerBarAlpha), .alpha(1 - readerBarAlpha),
) { ) {
// Log.i("RLog", "AppNavigationBar: ${readerBarAlpha}, ${1f - readerBarAlpha}") Log.i("RLog", "AppNavigationBar: ${readerBarAlpha}, ${1f - readerBarAlpha}")
FilterBar( FilterBar(
modifier = modifier, modifier = modifier,
filter = filter, filter = filter,
@ -148,87 +152,154 @@ private fun FilterBar(
filter: Filter, filter: Filter,
onSelected: (Filter) -> Unit = {}, onSelected: (Filter) -> Unit = {},
) { ) {
val view = LocalView.current
Row( Row(
modifier = modifier, modifier = modifier,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
listOf( FlowRow(
Filter.Starred, mainAxisSize = SizeMode.Expand,
Filter.Unread, mainAxisAlignment = MainAxisAlignment.Center,
Filter.All crossAxisAlignment = FlowCrossAxisAlignment.Center,
).forEach { item -> crossAxisSpacing = 0.dp,
Row( mainAxisSpacing = 20.dp,
verticalAlignment = Alignment.CenterVertically, ) {
horizontalArrangement = Arrangement.Center, listOf(
modifier = Modifier Filter.Starred,
.clip(CircleShape) Filter.Unread,
.animateContentSize(), Filter.All
) { ).forEach { item ->
Row( Item(
verticalAlignment = Alignment.CenterVertically, icon = if (filter == item) item.filledIcon else item.icon,
horizontalArrangement = Arrangement.Center, name = item.getName(),
modifier = Modifier selected = filter == item,
.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) { onSelected(item)
Spacer(modifier = Modifier.width(10.dp))
Icon(
modifier = Modifier.size(
if (filter == item) {
15.dp
} else {
19.dp
}
),
imageVector = item.icon,
contentDescription = item.getName(),
tint = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = item.getName().uppercase(),
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.width(10.dp))
} else {
Icon(
modifier = Modifier.size(
if (item.isUnread()) {
15
} else {
19
}.dp
),
imageVector = item.icon,
contentDescription = item.getName(),
tint = MaterialTheme.colorScheme.outline,
)
}
} }
} }
} }
} }
} }
@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
),
selected = selected,
selectedIcon = {
Icon(
imageVector = icon,
contentDescription = name,
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
},
onClick = {
view.playSoundEffect(SoundEffectConstants.CLICK)
onClick()
},
content = {
if (selected) {
Text(
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
},
)
} else {
Icon(
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 @Composable
private fun ReaderBar( private fun ReaderBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View File

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

View File

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

View File

@ -106,6 +106,7 @@ fun FeedsPage(
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) )
} }
} }
) )
}, },
@ -138,7 +139,6 @@ fun FeedsPage(
Icon( Icon(
imageVector = Icons.Outlined.KeyboardArrowRight, imageVector = Icons.Outlined.KeyboardArrowRight,
contentDescription = stringResource(R.string.go_to), contentDescription = stringResource(R.string.go_to),
tint = MaterialTheme.colorScheme.onSurface,
) )
}, },
) { ) {
@ -161,7 +161,7 @@ fun FeedsPage(
item { item {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Subtitle( Subtitle(
modifier = Modifier.padding(start = 28.dp), modifier = Modifier.padding(start = 26.dp),
text = stringResource(R.string.feeds) text = stringResource(R.string.feeds)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -207,6 +207,9 @@ fun FeedsPage(
Spacer(modifier = Modifier.height(8.dp)) 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.account.Account import me.ash.reader.data.account.Account
@ -48,14 +47,14 @@ class FeedsViewModel @Inject constructor(
} }
private fun addFromFile(inputStream: InputStream) { private fun addFromFile(inputStream: InputStream) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch {
opmlRepository.saveToDatabase(inputStream) opmlRepository.saveToDatabase(inputStream)
rssRepository.get().sync() rssRepository.get().doSync()
} }
} }
private fun fetchData(filterState: FilterState) { private fun fetchData(filterState: FilterState) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch {
pullFeeds( pullFeeds(
isStarred = filterState.filter.isStarred(), isStarred = filterState.filter.isStarred(),
isUnread = filterState.filter.isUnread(), isUnread = filterState.filter.isUnread(),

View File

@ -35,7 +35,7 @@ fun GroupItem(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 20.dp) .padding(horizontal = 16.dp)
.clip(RoundedCornerShape(32.dp)) .clip(RoundedCornerShape(32.dp))
.background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.14f)) .background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.14f))
.clickable { groupOnClick() } .clickable { groupOnClick() }
@ -78,9 +78,6 @@ fun GroupItem(
exit = fadeOut() + shrinkVertically(), exit = fadeOut() + shrinkVertically(),
) { ) {
Column { Column {
if (feeds.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
}
feeds.forEach { feed -> feeds.forEach { feed ->
FeedItem( FeedItem(
modifier = Modifier.padding(horizontal = 20.dp), modifier = Modifier.padding(horizontal = 20.dp),
@ -90,6 +87,9 @@ fun GroupItem(
feedOnClick(feed) 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.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Article import androidx.compose.material.icons.filled.AddAlert
import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.filled.Article
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -25,18 +25,23 @@ import me.ash.reader.ui.widget.Subtitle
@Composable @Composable
fun ResultViewPage( fun ResultViewPage(
modifier: Modifier = Modifier,
link: String = "", link: String = "",
groups: List<Group> = emptyList(), groups: List<Group> = emptyList(),
selectedAllowNotificationPreset: Boolean = false, selectedAllowNotificationPreset: Boolean = false,
selectedParseFullContentPreset: Boolean = false, selectedParseFullContentPreset: Boolean = false,
selectedGroupId: String = "", selectedGroupId: String = "",
newGroupContent: String = "",
newGroupSelected: Boolean,
onNewGroupValueChange: (String) -> Unit = {},
changeNewGroupSelected: (Boolean) -> Unit = {},
allowNotificationPresetOnClick: () -> Unit = {}, allowNotificationPresetOnClick: () -> Unit = {},
parseFullContentPresetOnClick: () -> Unit = {}, parseFullContentPresetOnClick: () -> Unit = {},
groupOnClick: (groupId: String) -> Unit = {}, groupOnClick: (groupId: String) -> Unit = {},
onKeyboardAction: () -> Unit = {}, onKeyboardAction: () -> Unit = {},
) { ) {
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()) modifier = modifier.verticalScroll(rememberScrollState())
) { ) {
Link( Link(
text = link text = link
@ -54,6 +59,10 @@ fun ResultViewPage(
AddToGroup( AddToGroup(
groups = groups, groups = groups,
selectedGroupId = selectedGroupId, selectedGroupId = selectedGroupId,
newGroupContent = newGroupContent,
newGroupSelected = newGroupSelected,
onNewGroupValueChange = onNewGroupValueChange,
changeNewGroupSelected = changeNewGroupSelected,
groupOnClick = groupOnClick, groupOnClick = groupOnClick,
onKeyboardAction = onKeyboardAction, onKeyboardAction = onKeyboardAction,
) )
@ -98,11 +107,12 @@ private fun Preset(
selected = selectedAllowNotificationPreset, selected = selectedAllowNotificationPreset,
selectedIcon = { selectedIcon = {
Icon( Icon(
imageVector = Icons.Outlined.Notifications, imageVector = Icons.Filled.AddAlert,
contentDescription = stringResource(R.string.allow_notification), contentDescription = stringResource(R.string.allow_notification),
modifier = Modifier modifier = Modifier
.padding(start = 8.dp) .padding(start = 8.dp)
.size(18.dp), .size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
) )
}, },
) { ) {
@ -114,11 +124,12 @@ private fun Preset(
selected = selectedParseFullContentPreset, selected = selectedParseFullContentPreset,
selectedIcon = { selectedIcon = {
Icon( Icon(
imageVector = Icons.Outlined.Article, imageVector = Icons.Filled.Article,
contentDescription = stringResource(R.string.parse_full_content), contentDescription = stringResource(R.string.parse_full_content),
modifier = Modifier modifier = Modifier
.padding(start = 8.dp) .padding(start = 8.dp)
.size(18.dp), .size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
) )
}, },
) { ) {
@ -131,6 +142,10 @@ private fun Preset(
private fun AddToGroup( private fun AddToGroup(
groups: List<Group>, groups: List<Group>,
selectedGroupId: String, selectedGroupId: String,
newGroupContent: String,
newGroupSelected: Boolean,
onNewGroupValueChange: (String) -> Unit = {},
changeNewGroupSelected: (Boolean) -> Unit = {},
groupOnClick: (groupId: String) -> Unit = {}, groupOnClick: (groupId: String) -> Unit = {},
onKeyboardAction: () -> Unit = {}, onKeyboardAction: () -> Unit = {},
) { ) {
@ -145,19 +160,21 @@ private fun AddToGroup(
SelectionChip( SelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = it.name, content = it.name,
selected = it.id == selectedGroupId, selected = !newGroupSelected && it.id == selectedGroupId,
) { ) {
changeNewGroupSelected(false)
groupOnClick(it.id) groupOnClick(it.id)
} }
} }
SelectionEditorChip( SelectionEditorChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = stringResource(R.string.new_group), content = newGroupContent,
selected = false, onValueChange = onNewGroupValueChange,
selected = newGroupSelected,
onKeyboardAction = onKeyboardAction, onKeyboardAction = onKeyboardAction,
) { ) {
changeNewGroupSelected(true)
} }
} }
} }

View File

@ -1,10 +1,13 @@
package me.ash.reader.ui.page.home.feeds.subscribe 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.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.TextField import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults 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.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
@ -32,12 +37,15 @@ import me.ash.reader.R
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun SearchViewPage( fun SearchViewPage(
modifier: Modifier = Modifier,
pagerState: PagerState, pagerState: PagerState,
inputContent: String = "", readOnly: Boolean = false,
inputLink: String = "",
errorMessage: String = "", errorMessage: String = "",
onValueChange: (String) -> Unit = {}, onLinkValueChange: (String) -> Unit = {},
onKeyboardAction: () -> Unit = {}, onKeyboardAction: () -> Unit = {},
) { ) {
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -45,7 +53,7 @@ fun SearchViewPage(
focusRequester.requestFocus() focusRequester.requestFocus()
} }
Column { Column(modifier = modifier) {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
TextField( TextField(
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
@ -55,9 +63,10 @@ fun SearchViewPage(
textColor = MaterialTheme.colorScheme.onSurface, textColor = MaterialTheme.colorScheme.onSurface,
focusedIndicatorColor = MaterialTheme.colorScheme.primary, focusedIndicatorColor = MaterialTheme.colorScheme.primary,
), ),
value = inputContent, enabled = !readOnly,
value = inputLink,
onValueChange = { onValueChange = {
if (pagerState.currentPage == 0) onValueChange(it) if (!readOnly) onLinkValueChange(it)
}, },
placeholder = { placeholder = {
Text( Text(
@ -68,9 +77,9 @@ fun SearchViewPage(
isError = errorMessage.isNotEmpty(), isError = errorMessage.isNotEmpty(),
singleLine = true, singleLine = true,
trailingIcon = { trailingIcon = {
if (inputContent.isNotEmpty()) { if (inputLink.isNotEmpty()) {
IconButton(onClick = { IconButton(onClick = {
onValueChange("") if (!readOnly) onLinkValueChange("")
}) { }) {
Icon( Icon(
imageVector = Icons.Rounded.Close, imageVector = Icons.Rounded.Close,
@ -90,17 +99,23 @@ fun SearchViewPage(
} }
}, },
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onDone = { onSearch = {
focusManager.clearFocus()
onKeyboardAction() onKeyboardAction()
} }
) ),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search
),
) )
if (errorMessage.isNotEmpty()) { if (errorMessage.isNotEmpty()) {
SelectionContainer { SelectionContainer {
Text( Text(
modifier = Modifier
.padding(start = 16.dp)
.horizontalScroll(rememberScrollState()),
text = errorMessage, text = errorMessage,
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(start = 16.dp),
maxLines = 1, maxLines = 1,
softWrap = false, 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.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts 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.Icons
import androidx.compose.material.icons.rounded.RssFeed import androidx.compose.material.icons.rounded.RssFeed
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -9,9 +11,13 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource 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 androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.* import me.ash.reader.*
@ -23,10 +29,12 @@ import java.io.InputStream
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun SubscribeDialog( fun SubscribeDialog(
modifier: Modifier = Modifier,
viewModel: SubscribeViewModel = hiltViewModel(), viewModel: SubscribeViewModel = hiltViewModel(),
openInputStreamCallback: (InputStream) -> Unit, openInputStreamCallback: (InputStream) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { uri -> it?.let { uri ->
@ -37,13 +45,14 @@ fun SubscribeDialog(
} }
val viewState = viewModel.viewState.collectAsStateValue() val viewState = viewModel.viewState.collectAsStateValue()
val groupsState = viewState.groups.collectAsState(initial = emptyList()) 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) { LaunchedEffect(viewState.visible) {
if (viewState.visible) { if (viewState.visible) {
val defaultGroupId = context.dataStore val defaultGroupId = context.dataStore
.get(DataStoreKeys.CurrentAccountId)!! .get(DataStoreKeys.CurrentAccountId)!!
.spacerDollar("0") .spacerDollar(readYouString + defaultString)
viewModel.dispatch(SubscribeViewAction.SelectedGroup(defaultGroupId)) viewModel.dispatch(SubscribeViewAction.SelectedGroup(defaultGroupId))
viewModel.dispatch(SubscribeViewAction.Init) viewModel.dispatch(SubscribeViewAction.Init)
} else { } 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( Dialog(
modifier = Modifier
.padding(horizontal = 44.dp)
.height(dialogHeight),
visible = viewState.visible, visible = viewState.visible,
onDismissRequest = { onDismissRequest = {
focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Hide) viewModel.dispatch(SubscribeViewAction.Hide)
}, },
icon = { icon = {
@ -66,30 +87,35 @@ fun SubscribeDialog(
title = { title = {
Text( Text(
when (viewState.pagerState.currentPage) { when (viewState.pagerState.currentPage) {
0 -> stringResource(R.string.subscribe) 0 -> viewState.title
else -> viewState.feed?.name ?: stringResource(R.string.unknown) else -> viewState.feed?.name ?: stringResource(R.string.unknown)
} }
) )
}, },
text = { text = {
SubscribeViewPager( SubscribeViewPager(
// height = when (viewState.pagerState.currentPage) { readOnly = viewState.lockLinkInput,
// 0 -> 84.dp inputLink = viewState.linkContent,
// else -> Dp.Unspecified
// },
inputContent = viewState.inputContent,
errorMessage = viewState.errorMessage, errorMessage = viewState.errorMessage,
onValueChange = { onLinkValueChange = {
viewModel.dispatch(SubscribeViewAction.Input(it)) viewModel.dispatch(SubscribeViewAction.InputLink(it))
}, },
onSearchKeyboardAction = { onSearchKeyboardAction = {
viewModel.dispatch(SubscribeViewAction.Search(scope)) viewModel.dispatch(SubscribeViewAction.Search(scope))
}, },
link = viewState.inputContent, link = viewState.linkContent,
groups = groupsState.value, groups = groupsState.value,
selectedAllowNotificationPreset = viewState.allowNotificationPreset, selectedAllowNotificationPreset = viewState.allowNotificationPreset,
selectedParseFullContentPreset = viewState.parseFullContentPreset, selectedParseFullContentPreset = viewState.parseFullContentPreset,
selectedGroupId = viewState.selectedGroupId, selectedGroupId = viewState.selectedGroupId,
newGroupContent = viewState.newGroupContent,
onNewGroupValueChange = {
viewModel.dispatch(SubscribeViewAction.InputNewGroup(it))
},
newGroupSelected = viewState.newGroupSelected,
changeNewGroupSelected = {
viewModel.dispatch(SubscribeViewAction.SelectedNewGroup(it))
},
pagerState = viewState.pagerState, pagerState = viewState.pagerState,
allowNotificationPresetOnClick = { allowNotificationPresetOnClick = {
viewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset) viewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset)
@ -109,14 +135,15 @@ fun SubscribeDialog(
when (viewState.pagerState.currentPage) { when (viewState.pagerState.currentPage) {
0 -> { 0 -> {
TextButton( TextButton(
enabled = viewState.inputContent.isNotEmpty(), enabled = viewState.linkContent.isNotEmpty(),
onClick = { onClick = {
focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Search(scope)) viewModel.dispatch(SubscribeViewAction.Search(scope))
} }
) { ) {
Text( Text(
text = stringResource(R.string.search), text = stringResource(R.string.search),
color = if (viewState.inputContent.isNotEmpty()) { color = if (viewState.linkContent.isNotEmpty()) {
Color.Unspecified Color.Unspecified
} else { } else {
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f) MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
@ -127,6 +154,7 @@ fun SubscribeDialog(
1 -> { 1 -> {
TextButton( TextButton(
onClick = { onClick = {
focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Subscribe) viewModel.dispatch(SubscribeViewAction.Subscribe)
} }
) { ) {
@ -140,6 +168,7 @@ fun SubscribeDialog(
0 -> { 0 -> {
TextButton( TextButton(
onClick = { onClick = {
focusManager.clearFocus()
launcher.launch("*/*") launcher.launch("*/*")
viewModel.dispatch(SubscribeViewAction.Hide) viewModel.dispatch(SubscribeViewAction.Hide)
} }
@ -150,6 +179,7 @@ fun SubscribeDialog(
1 -> { 1 -> {
TextButton( TextButton(
onClick = { onClick = {
focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Hide) viewModel.dispatch(SubscribeViewAction.Hide)
} }
) { ) {

View File

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

View File

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

View File

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

View File

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

View File

@ -4,8 +4,9 @@ import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import me.ash.reader.ui.theme.*
private val LightThemeColors = lightColorScheme( private val LightThemeColors = lightColorScheme(
@ -37,6 +38,7 @@ private val LightThemeColors = lightColorScheme(
inversePrimary = md_theme_light_inversePrimary, inversePrimary = md_theme_light_inversePrimary,
// shadow = md_theme_light_shadow, // shadow = md_theme_light_shadow,
) )
private val DarkThemeColors = darkColorScheme( private val DarkThemeColors = darkColorScheme(
primary = md_theme_dark_primary, primary = md_theme_dark_primary,
@ -68,6 +70,10 @@ private val DarkThemeColors = darkColorScheme(
// shadow = md_theme_dark_shadow, // shadow = md_theme_dark_shadow,
) )
val LocalLightThemeColors = staticCompositionLocalOf { LightThemeColors }
val LocalDarkThemeColors = staticCompositionLocalOf { DarkThemeColors }
@Composable @Composable
fun AppTheme( fun AppTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(), useDarkTheme: Boolean = isSystemInDarkTheme(),
@ -75,15 +81,27 @@ fun AppTheme(
) { ) {
// Dynamic color is available on Android 12+ // Dynamic color is available on Android 12+
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when { val light = when {
dynamicColor && useDarkTheme -> dynamicDarkColorScheme(LocalContext.current) dynamicColor -> dynamicLightColorScheme(LocalContext.current)
dynamicColor && !useDarkTheme -> dynamicLightColorScheme(LocalContext.current)
useDarkTheme -> DarkThemeColors
else -> LightThemeColors else -> LightThemeColors
} }
MaterialTheme( val dark = when {
colorScheme = colorScheme, dynamicColor -> dynamicDarkColorScheme(LocalContext.current)
typography = AppTypography, else -> DarkThemeColors
content = content }
) 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.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import me.ash.reader.ui.theme.LocalLightThemeColors
@Composable @Composable
fun Banner( fun Banner(
@ -25,6 +24,10 @@ fun Banner(
action: (@Composable () -> Unit)? = null, action: (@Composable () -> Unit)? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
val lightThemeColors = LocalLightThemeColors.current
val lightPrimaryContainer = lightThemeColors.primaryContainer
val lightOnSurface = lightThemeColors.onSurface
Surface( Surface(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
@ -34,7 +37,7 @@ fun Banner(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.clip(RoundedCornerShape(32.dp)) .clip(RoundedCornerShape(32.dp))
.background(MaterialTheme.colorScheme.primaryContainer) .background(lightPrimaryContainer)
.clickable { onClick() } .clickable { onClick() }
.padding(16.dp, 20.dp), .padding(16.dp, 20.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -44,7 +47,7 @@ fun Banner(
imageVector = it, imageVector = it,
contentDescription = null, contentDescription = null,
modifier = Modifier.padding(end = 16.dp), modifier = Modifier.padding(end = 16.dp),
tint = MaterialTheme.colorScheme.onSurface, tint = lightOnSurface,
) )
} }
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
@ -52,20 +55,22 @@ fun Banner(
text = title, text = title,
maxLines = if (desc == null) 2 else 1, maxLines = if (desc == null) 2 else 1,
style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp), style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp),
color = MaterialTheme.colorScheme.onSurface, color = lightOnSurface,
) )
desc?.let { desc?.let {
Text( Text(
text = it, text = it,
maxLines = 1, maxLines = 1,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), color = lightOnSurface.copy(alpha = 0.7f),
) )
} }
} }
action?.let { action?.let {
Box(Modifier.padding(start = 16.dp)) { Box(Modifier.padding(start = 16.dp)) {
it() 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.material3.AlertDialog
import androidx.compose.runtime.Composable 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 @Composable
fun Dialog( fun Dialog(
modifier: Modifier = Modifier,
visible: Boolean, visible: Boolean,
onDismissRequest: () -> Unit = {}, onDismissRequest: () -> Unit = {},
icon: @Composable (() -> Unit)? = null, icon: @Composable (() -> Unit)? = null,
@ -13,13 +18,10 @@ fun Dialog(
confirmButton: @Composable () -> Unit, confirmButton: @Composable () -> Unit,
dismissButton: @Composable (() -> Unit)? = null, dismissButton: @Composable (() -> Unit)? = null,
) { ) {
// AnimatedVisibility(
// visible = visible,
// enter = fadeIn() + expandVertically(),
// exit = fadeOut() + shrinkVertically(),
// ) {
if (visible) { if (visible) {
AlertDialog( AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
modifier = modifier,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
icon = icon, icon = icon,
title = title, title = title,

View File

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