Refactor Material You design for FilterBar and Add subscribe feature
This commit is contained in:
parent
c7c708d92a
commit
b2fe0674c8
|
@ -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 =
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -12,7 +12,9 @@ fun PagerState.animateScrollToPage(
|
||||||
callback: () -> Unit = {}
|
callback: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
if (pageCount > targetPage) {
|
||||||
animateScrollToPage(targetPage)
|
animateScrollToPage(targetPage)
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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,85 +152,152 @@ 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,
|
||||||
|
) {
|
||||||
|
FlowRow(
|
||||||
|
mainAxisSize = SizeMode.Expand,
|
||||||
|
mainAxisAlignment = MainAxisAlignment.Center,
|
||||||
|
crossAxisAlignment = FlowCrossAxisAlignment.Center,
|
||||||
|
crossAxisSpacing = 0.dp,
|
||||||
|
mainAxisSpacing = 20.dp,
|
||||||
) {
|
) {
|
||||||
listOf(
|
listOf(
|
||||||
Filter.Starred,
|
Filter.Starred,
|
||||||
Filter.Unread,
|
Filter.Unread,
|
||||||
Filter.All
|
Filter.All
|
||||||
).forEach { item ->
|
).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,
|
||||||
.clip(CircleShape)
|
|
||||||
.animateContentSize(),
|
|
||||||
) {
|
) {
|
||||||
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)
|
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,
|
selected = selected,
|
||||||
contentDescription = item.getName(),
|
selectedIcon = {
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
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(
|
||||||
text = item.getName().uppercase(),
|
modifier = modifier.padding(
|
||||||
fontSize = 11.sp,
|
start = 0.dp,
|
||||||
fontWeight = FontWeight.Bold,
|
top = 8.dp,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
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 {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(
|
imageVector = icon,
|
||||||
if (item.isUnread()) {
|
contentDescription = name,
|
||||||
15
|
modifier = Modifier.size(20.dp),
|
||||||
} else {
|
|
||||||
19
|
|
||||||
}.dp
|
|
||||||
),
|
|
||||||
imageVector = item.icon,
|
|
||||||
contentDescription = item.getName(),
|
|
||||||
tint = MaterialTheme.colorScheme.outline,
|
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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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,11 +128,19 @@ 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 {
|
||||||
|
it.copy(
|
||||||
|
linkContent = str
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,19 +148,20 @@ class SubscribeViewModel @Inject constructor(
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
title = stringsRepository.getString(R.string.searching),
|
title = stringsRepository.getString(R.string.searching),
|
||||||
|
lockLinkInput = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
if (rssRepository.get().isExist(_viewState.value.linkContent)) {
|
||||||
try {
|
|
||||||
if (rssRepository.get().isExist(_viewState.value.inputContent)) {
|
|
||||||
_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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
val dark = when {
|
||||||
|
dynamicColor -> dynamicDarkColorScheme(LocalContext.current)
|
||||||
|
else -> DarkThemeColors
|
||||||
|
}
|
||||||
|
val colorScheme = when {
|
||||||
|
useDarkTheme -> dark
|
||||||
|
else -> light
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalLightThemeColors provides light,
|
||||||
|
LocalDarkThemeColors provides dark,
|
||||||
|
) {
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = AppTypography,
|
typography = AppTypography,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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,22 +55,24 @@ 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)) {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides lightOnSurface) {
|
||||||
it()
|
it()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user