Refactor Material You design for ReadPage and Add feed option feature
This commit is contained in:
parent
518dd6b59c
commit
435eb67c55
|
@ -15,6 +15,8 @@ import kotlinx.coroutines.runBlocking
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||||
|
val Context.currentAccountId: Int
|
||||||
|
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
||||||
|
|
||||||
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
|
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
|
||||||
this.edit {
|
this.edit {
|
||||||
|
|
|
@ -7,6 +7,15 @@ import kotlinx.coroutines.flow.Flow
|
||||||
@Dao
|
@Dao
|
||||||
interface ArticleDao {
|
interface ArticleDao {
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM article
|
||||||
|
WHERE accountId = :accountId
|
||||||
|
AND feedId = :feedId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun deleteByFeedId(accountId: Int, feedId: String)
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -4,6 +4,14 @@ import androidx.room.*
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface FeedDao {
|
interface FeedDao {
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM feed
|
||||||
|
WHERE id = :id
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun queryById(id: String): Feed?
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM feed
|
SELECT * FROM feed
|
||||||
|
|
|
@ -8,13 +8,10 @@ import androidx.work.*
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.ash.reader.DataStoreKeys
|
import me.ash.reader.currentAccountId
|
||||||
import me.ash.reader.data.account.AccountDao
|
import me.ash.reader.data.account.AccountDao
|
||||||
import me.ash.reader.data.article.Article
|
import me.ash.reader.data.article.Article
|
||||||
import me.ash.reader.data.article.ArticleDao
|
import me.ash.reader.data.article.ArticleDao
|
||||||
|
@ -26,8 +23,6 @@ import me.ash.reader.data.group.Group
|
||||||
import me.ash.reader.data.group.GroupDao
|
import me.ash.reader.data.group.GroupDao
|
||||||
import me.ash.reader.data.group.GroupWithFeed
|
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.get
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
abstract class AbstractRssRepository constructor(
|
abstract class AbstractRssRepository constructor(
|
||||||
|
@ -65,14 +60,11 @@ abstract class AbstractRssRepository constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullGroups(): Flow<MutableList<Group>> {
|
fun pullGroups(): Flow<MutableList<Group>> {
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
return groupDao.queryAllGroup(context.currentAccountId).flowOn(Dispatchers.IO)
|
||||||
return groupDao.queryAllGroup(accountId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
|
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
|
||||||
return groupDao.queryAllGroupWithFeed(
|
return groupDao.queryAllGroupWithFeed(context.currentAccountId).flowOn(Dispatchers.IO)
|
||||||
context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
|
||||||
)//.flowOn(Dispatchers.IO)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullArticles(
|
fun pullArticles(
|
||||||
|
@ -82,7 +74,7 @@ abstract class AbstractRssRepository constructor(
|
||||||
isUnread: Boolean = false,
|
isUnread: Boolean = false,
|
||||||
): PagingSource<Int, ArticleWithFeed> {
|
): PagingSource<Int, ArticleWithFeed> {
|
||||||
Log.i("RLog", "thread:pullArticles ${Thread.currentThread().name}")
|
Log.i("RLog", "thread:pullArticles ${Thread.currentThread().name}")
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
val accountId = context.currentAccountId
|
||||||
Log.i(
|
Log.i(
|
||||||
"RLog",
|
"RLog",
|
||||||
"pullArticles: accountId: ${accountId}, groupId: ${groupId}, feedId: ${feedId}, isStarred: ${isStarred}, isUnread: ${isUnread}"
|
"pullArticles: accountId: ${accountId}, groupId: ${groupId}, feedId: ${feedId}, isStarred: ${isStarred}, isUnread: ${isUnread}"
|
||||||
|
@ -118,7 +110,7 @@ abstract class AbstractRssRepository constructor(
|
||||||
): Flow<List<ImportantCount>> {
|
): Flow<List<ImportantCount>> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
Log.i("RLog", "thread:pullImportant ${Thread.currentThread().name}")
|
Log.i("RLog", "thread:pullImportant ${Thread.currentThread().name}")
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
val accountId = context.currentAccountId
|
||||||
Log.i(
|
Log.i(
|
||||||
"RLog",
|
"RLog",
|
||||||
"pullImportant: accountId: ${accountId}, isStarred: ${isStarred}, isUnread: ${isUnread}"
|
"pullImportant: accountId: ${accountId}, isStarred: ${isStarred}, isUnread: ${isUnread}"
|
||||||
|
@ -130,7 +122,11 @@ abstract class AbstractRssRepository constructor(
|
||||||
.queryImportantCountWhenIsUnread(accountId, isUnread)
|
.queryImportantCountWhenIsUnread(accountId, isUnread)
|
||||||
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
||||||
}
|
}
|
||||||
}//.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findFeedById(id: String): Feed? {
|
||||||
|
return feedDao.queryById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findArticleById(id: String): ArticleWithFeed? {
|
suspend fun findArticleById(id: String): ArticleWithFeed? {
|
||||||
|
@ -138,14 +134,32 @@ abstract class AbstractRssRepository constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isExist(url: String): Boolean {
|
suspend fun isExist(url: String): Boolean {
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
|
||||||
return feedDao.queryByLink(accountId, url).isNotEmpty()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun peekWork(): String {
|
fun peekWork(): String {
|
||||||
return workManager.getWorkInfosByTag("sync").get().size.toString()
|
return workManager.getWorkInfosByTag("sync").get().size.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateGroup(group: Group) {
|
||||||
|
groupDao.update(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateFeed(feed: Feed) {
|
||||||
|
feedDao.update(feed)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteGroup(group: Group) {
|
||||||
|
groupDao.update(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteFeed(feed: Feed) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
articleDao.deleteByFeedId(context.currentAccountId, feed.id)
|
||||||
|
feedDao.delete(feed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val mutex = Mutex()
|
val mutex = Mutex()
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,13 @@ package me.ash.reader.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import me.ash.reader.*
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.currentAccountId
|
||||||
import me.ash.reader.data.account.Account
|
import me.ash.reader.data.account.Account
|
||||||
import me.ash.reader.data.account.AccountDao
|
import me.ash.reader.data.account.AccountDao
|
||||||
import me.ash.reader.data.group.Group
|
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.spacerDollar
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AccountRepository @Inject constructor(
|
class AccountRepository @Inject constructor(
|
||||||
|
@ -17,8 +19,7 @@ class AccountRepository @Inject constructor(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun getCurrentAccount(): Account? {
|
suspend fun getCurrentAccount(): Account? {
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
return accountDao.queryById(context.currentAccountId)
|
||||||
return accountDao.queryById(accountId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isNoAccount(): Boolean {
|
suspend fun isNoAccount(): Boolean {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import android.util.Log
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import me.ash.reader.DataStoreKeys
|
import me.ash.reader.*
|
||||||
import me.ash.reader.data.account.AccountDao
|
import me.ash.reader.data.account.AccountDao
|
||||||
import me.ash.reader.data.article.Article
|
import me.ash.reader.data.article.Article
|
||||||
import me.ash.reader.data.article.ArticleDao
|
import me.ash.reader.data.article.ArticleDao
|
||||||
|
@ -15,9 +15,6 @@ import me.ash.reader.data.group.Group
|
||||||
import me.ash.reader.data.group.GroupDao
|
import me.ash.reader.data.group.GroupDao
|
||||||
import me.ash.reader.data.source.FeverApiDataSource
|
import me.ash.reader.data.source.FeverApiDataSource
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
import me.ash.reader.dataStore
|
|
||||||
import me.ash.reader.get
|
|
||||||
import me.ash.reader.spacerDollar
|
|
||||||
import net.dankito.readability4j.extended.Readability4JExtended
|
import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -49,13 +46,12 @@ class FeverRssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun addGroup(name: String): String {
|
override suspend fun addGroup(name: String): String {
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
|
||||||
return UUID.randomUUID().toString().also {
|
return UUID.randomUUID().toString().also {
|
||||||
groupDao.insert(
|
groupDao.insert(
|
||||||
Group(
|
Group(
|
||||||
id = it,
|
id = it,
|
||||||
name = name,
|
name = name,
|
||||||
accountId = accountId
|
accountId = context.currentAccountId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -63,8 +59,7 @@ class FeverRssRepository @Inject constructor(
|
||||||
|
|
||||||
override suspend fun sync() {
|
override suspend fun sync() {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
|
val accountId = context.currentAccountId
|
||||||
?: return
|
|
||||||
|
|
||||||
updateSyncState {
|
updateSyncState {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|
|
@ -73,13 +73,12 @@ class LocalRssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun addGroup(name: String): String {
|
override suspend fun addGroup(name: String): String {
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
|
||||||
return UUID.randomUUID().toString().also {
|
return UUID.randomUUID().toString().also {
|
||||||
groupDao.insert(
|
groupDao.insert(
|
||||||
Group(
|
Group(
|
||||||
id = it,
|
id = it,
|
||||||
name = name,
|
name = name,
|
||||||
accountId = accountId
|
accountId = context.currentAccountId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -89,8 +88,7 @@ class LocalRssRepository @Inject constructor(
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val preTime = System.currentTimeMillis()
|
val preTime = System.currentTimeMillis()
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
|
val accountId = context.currentAccountId
|
||||||
?: return@withContext
|
|
||||||
val feeds = async { feedDao.queryAll(accountId) }
|
val feeds = async { feedDao.queryAll(accountId) }
|
||||||
val articles = feeds.await().also { feed ->
|
val articles = feeds.await().also { feed ->
|
||||||
updateSyncState {
|
updateSyncState {
|
||||||
|
|
|
@ -3,14 +3,12 @@ package me.ash.reader.data.repository
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import me.ash.reader.DataStoreKeys
|
import me.ash.reader.currentAccountId
|
||||||
import me.ash.reader.data.article.Article
|
import me.ash.reader.data.article.Article
|
||||||
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.feed.FeedWithArticle
|
import me.ash.reader.data.feed.FeedWithArticle
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
import me.ash.reader.dataStore
|
|
||||||
import me.ash.reader.get
|
|
||||||
import me.ash.reader.spacerDollar
|
import me.ash.reader.spacerDollar
|
||||||
import net.dankito.readability4j.Readability4J
|
import net.dankito.readability4j.Readability4J
|
||||||
import net.dankito.readability4j.extended.Readability4JExtended
|
import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
|
@ -26,7 +24,7 @@ class RssHelper @Inject constructor(
|
||||||
) {
|
) {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
||||||
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
|
val accountId = context.currentAccountId
|
||||||
val parseRss = rssNetworkDataSource.parseRss(feedLink)
|
val parseRss = rssNetworkDataSource.parseRss(feedLink)
|
||||||
val feed = Feed(
|
val feed = Feed(
|
||||||
id = accountId.spacerDollar(UUID.randomUUID().toString()),
|
id = accountId.spacerDollar(UUID.randomUUID().toString()),
|
||||||
|
|
|
@ -4,12 +4,10 @@ import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Xml
|
import android.util.Xml
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import me.ash.reader.DataStoreKeys
|
import me.ash.reader.currentAccountId
|
||||||
import me.ash.reader.data.feed.Feed
|
import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.group.Group
|
import me.ash.reader.data.group.Group
|
||||||
import me.ash.reader.data.group.GroupWithFeed
|
import me.ash.reader.data.group.GroupWithFeed
|
||||||
import me.ash.reader.dataStore
|
|
||||||
import me.ash.reader.get
|
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -22,7 +20,7 @@ class OpmlLocalDataSource @Inject constructor(
|
||||||
// @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.currentAccountId
|
||||||
inputStream.use {
|
inputStream.use {
|
||||||
val parser: XmlPullParser = Xml.newPullParser()
|
val parser: XmlPullParser = Xml.newPullParser()
|
||||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||||
|
|
|
@ -1,12 +1,23 @@
|
||||||
package me.ash.reader.ui.extension
|
package me.ash.reader.ui.extension
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> StateFlow<T>.collectAsStateValue(
|
fun <T> StateFlow<T>.collectAsStateValue(
|
||||||
context: CoroutineContext = EmptyCoroutineContext
|
context: CoroutineContext = Dispatchers.Default
|
||||||
): T = collectAsState(context).value
|
): T = collectAsState(context).value
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
|
||||||
|
this.value = value
|
||||||
|
}
|
|
@ -6,15 +6,12 @@ import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.material.ModalBottomSheetState
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.material.ModalBottomSheetValue
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.google.accompanist.insets.ProvideWindowInsets
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
|
@ -24,16 +21,10 @@ import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||||
import com.google.accompanist.navigation.animation.composable
|
import com.google.accompanist.navigation.animation.composable
|
||||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import me.ash.reader.ui.page.home.FeedOptionDrawer
|
|
||||||
import me.ash.reader.ui.page.home.HomePage
|
import me.ash.reader.ui.page.home.HomePage
|
||||||
import me.ash.reader.ui.page.settings.SettingsPage
|
import me.ash.reader.ui.page.settings.SettingsPage
|
||||||
import me.ash.reader.ui.theme.AppTheme
|
import me.ash.reader.ui.theme.AppTheme
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
val LocalDrawerState = staticCompositionLocalOf<ModalBottomSheetState> {
|
|
||||||
error("CompositionLocal LocalDrawerState not present")
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalAnimationApi::class, androidx.compose.material.ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalAnimationApi::class, androidx.compose.material.ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeEntry() {
|
fun HomeEntry() {
|
||||||
|
@ -46,10 +37,6 @@ fun HomeEntry() {
|
||||||
setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme())
|
setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme())
|
||||||
setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme())
|
setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme())
|
||||||
}
|
}
|
||||||
Box {
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalDrawerState provides rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -57,7 +44,6 @@ fun HomeEntry() {
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
) {
|
) {
|
||||||
AnimatedNavHost(
|
AnimatedNavHost(
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = RouteName.HOME,
|
startDestination = RouteName.HOME,
|
||||||
) {
|
) {
|
||||||
|
@ -151,9 +137,6 @@ fun HomeEntry() {
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
FeedOptionDrawer(drawerState = LocalDrawerState.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,116 +0,0 @@
|
||||||
package me.ash.reader.ui.page.home
|
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.ChipDefaults
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.ModalBottomSheetState
|
|
||||||
import androidx.compose.material.ModalBottomSheetValue
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.RssFeed
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import me.ash.reader.R
|
|
||||||
import me.ash.reader.ui.page.home.feeds.subscribe.ResultViewPage
|
|
||||||
import me.ash.reader.ui.widget.BottomDrawer
|
|
||||||
import me.ash.reader.ui.widget.Subtitle
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
@Composable
|
|
||||||
fun FeedOptionDrawer(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
drawerState: ModalBottomSheetState = androidx.compose.material.rememberModalBottomSheetState(
|
|
||||||
ModalBottomSheetValue.Hidden
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
BottomDrawer(
|
|
||||||
drawerState = drawerState,
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
Icon(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.CenterHorizontally),
|
|
||||||
imageVector = Icons.Rounded.RssFeed,
|
|
||||||
contentDescription = stringResource(R.string.subscribe),
|
|
||||||
)
|
|
||||||
Spacer(modifier = modifier.height(16.dp))
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = "Feed",
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
Spacer(modifier = modifier.height(16.dp))
|
|
||||||
ResultViewPage(
|
|
||||||
link = "https://joeycz.github.io/weekly/rss.xml",
|
|
||||||
groups = emptyList(),
|
|
||||||
selectedAllowNotificationPreset = true,
|
|
||||||
selectedParseFullContentPreset = true,
|
|
||||||
selectedGroupId = "selectedGroupId",
|
|
||||||
newGroupContent = "",
|
|
||||||
onNewGroupValueChange = { },
|
|
||||||
newGroupSelected = false,
|
|
||||||
changeNewGroupSelected = { },
|
|
||||||
allowNotificationPresetOnClick = { },
|
|
||||||
parseFullContentPresetOnClick = { },
|
|
||||||
groupOnClick = { },
|
|
||||||
onKeyboardAction = { },
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
|
||||||
Subtitle(text = "More")
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
|
||||||
androidx.compose.material.FilterChip(
|
|
||||||
modifier = modifier,
|
|
||||||
colors = ChipDefaults.filterChipColors(
|
|
||||||
backgroundColor = Color.Transparent,
|
|
||||||
contentColor = MaterialTheme.colorScheme.error,
|
|
||||||
leadingIconColor = MaterialTheme.colorScheme.error,
|
|
||||||
disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(
|
|
||||||
alpha = 0.7f
|
|
||||||
),
|
|
||||||
disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
|
||||||
disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy(
|
|
||||||
alpha = 0.7f
|
|
||||||
),
|
|
||||||
selectedBackgroundColor = Color.Transparent,
|
|
||||||
selectedContentColor = MaterialTheme.colorScheme.error,
|
|
||||||
selectedLeadingIconColor = MaterialTheme.colorScheme.error
|
|
||||||
),
|
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
|
|
||||||
selected = false,
|
|
||||||
shape = CircleShape,
|
|
||||||
onClick = {
|
|
||||||
// focusManager.clearFocus()
|
|
||||||
// onClick()
|
|
||||||
},
|
|
||||||
content = {
|
|
||||||
Text(
|
|
||||||
modifier = modifier.padding(
|
|
||||||
start = if (false) 0.dp else 8.dp,
|
|
||||||
top = 8.dp,
|
|
||||||
end = 8.dp,
|
|
||||||
bottom = 8.dp
|
|
||||||
),
|
|
||||||
text = "Delete",
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
color = if (true) {
|
|
||||||
MaterialTheme.colorScheme.error
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.error
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -29,7 +29,7 @@ fun FilterBar2(
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (filter == item) item.filledIcon else item.icon,
|
imageVector = item.icon,
|
||||||
contentDescription = item.getName()
|
contentDescription = item.getName()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,10 +17,14 @@ 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.filled.FiberManualRecord
|
||||||
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.FiberManualRecord
|
||||||
import androidx.compose.material.icons.outlined.Sell
|
import androidx.compose.material.icons.outlined.TextFormat
|
||||||
import androidx.compose.material.icons.rounded.*
|
import androidx.compose.material.icons.rounded.Article
|
||||||
|
import androidx.compose.material.icons.rounded.ExpandMore
|
||||||
|
import androidx.compose.material.icons.rounded.Star
|
||||||
|
import androidx.compose.material.icons.rounded.StarOutline
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
@ -138,7 +142,7 @@ fun HomeBottomNavBar(
|
||||||
.alpha(readerBarAlpha),
|
.alpha(readerBarAlpha),
|
||||||
) {
|
) {
|
||||||
ReaderBar(
|
ReaderBar(
|
||||||
modifier = modifier,
|
modifier = Modifier,
|
||||||
disabled = disabled,
|
disabled = disabled,
|
||||||
isUnread = isUnread,
|
isUnread = isUnread,
|
||||||
isStarred = isStarred,
|
isStarred = isStarred,
|
||||||
|
@ -323,67 +327,78 @@ private fun ReaderBar(
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
var fullContent by remember { mutableStateOf(isFullContent) }
|
var fullContent by remember { mutableStateOf(isFullContent) }
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceAround,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = modifier
|
modifier = modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
CanBeDisabledIconButton(
|
CanBeDisabledIconButton(
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(40.dp),
|
||||||
disabled = disabled,
|
disabled = disabled,
|
||||||
imageVector = if (isUnread) {
|
imageVector = if (isUnread) {
|
||||||
Icons.Rounded.Circle
|
Icons.Filled.FiberManualRecord
|
||||||
} else {
|
} else {
|
||||||
Icons.Outlined.Circle
|
Icons.Outlined.FiberManualRecord
|
||||||
},
|
},
|
||||||
contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread),
|
contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = if (isUnread) {
|
||||||
|
MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.outline
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
unreadOnClick(!isUnread)
|
unreadOnClick(!isUnread)
|
||||||
}
|
}
|
||||||
CanBeDisabledIconButton(
|
CanBeDisabledIconButton(
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
disabled = disabled,
|
disabled = disabled,
|
||||||
modifier = Modifier.size(28.dp),
|
|
||||||
imageVector = if (isStarred) {
|
imageVector = if (isStarred) {
|
||||||
Icons.Rounded.Star
|
Icons.Rounded.Star
|
||||||
} else {
|
} else {
|
||||||
Icons.Rounded.StarBorder
|
Icons.Rounded.StarOutline
|
||||||
},
|
},
|
||||||
contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred),
|
contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = if (isStarred) {
|
||||||
|
MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.outline
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
starredOnClick(!isStarred)
|
starredOnClick(!isStarred)
|
||||||
}
|
}
|
||||||
CanBeDisabledIconButton(
|
CanBeDisabledIconButton(
|
||||||
disabled = disabled,
|
disabled = disabled,
|
||||||
modifier = Modifier.size(30.dp),
|
modifier = Modifier.size(40.dp),
|
||||||
imageVector = Icons.Rounded.ExpandMore,
|
imageVector = Icons.Rounded.ExpandMore,
|
||||||
contentDescription = "Next Article",
|
contentDescription = "Next Article",
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.outline,
|
||||||
) {
|
) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
}
|
}
|
||||||
CanBeDisabledIconButton(
|
CanBeDisabledIconButton(
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
disabled = disabled,
|
disabled = disabled,
|
||||||
imageVector = Icons.Outlined.Sell,
|
imageVector = Icons.Outlined.TextFormat,
|
||||||
contentDescription = "Add Tag",
|
contentDescription = "Add Tag",
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.outline,
|
||||||
) {
|
) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
}
|
}
|
||||||
CanBeDisabledIconButton(
|
CanBeDisabledIconButton(
|
||||||
disabled = disabled,
|
disabled = disabled,
|
||||||
modifier = Modifier.size(26.dp),
|
modifier = Modifier.size(40.dp),
|
||||||
imageVector = if (fullContent) {
|
imageVector = if (fullContent) {
|
||||||
Icons.Rounded.Article
|
Icons.Rounded.Article
|
||||||
} else {
|
} else {
|
||||||
Icons.Outlined.Article
|
Icons.Outlined.Article
|
||||||
},
|
},
|
||||||
contentDescription = stringResource(R.string.parse_full_content),
|
contentDescription = stringResource(R.string.parse_full_content),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = if (fullContent) {
|
||||||
|
MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.outline
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
val afterIsFullContent = !fullContent
|
val afterIsFullContent = !fullContent
|
||||||
|
|
|
@ -2,12 +2,9 @@ package me.ash.reader.ui.page.home
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.material.ModalBottomSheetValue
|
|
||||||
import androidx.compose.material.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
@ -17,11 +14,13 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.ui.extension.collectAsStateValue
|
import me.ash.reader.ui.extension.collectAsStateValue
|
||||||
import me.ash.reader.ui.extension.findActivity
|
import me.ash.reader.ui.extension.findActivity
|
||||||
import me.ash.reader.ui.page.common.ExtraName
|
import me.ash.reader.ui.page.common.ExtraName
|
||||||
|
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionDrawer
|
||||||
|
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewAction
|
||||||
|
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewModel
|
||||||
import me.ash.reader.ui.page.home.feeds.FeedsPage
|
import me.ash.reader.ui.page.home.feeds.FeedsPage
|
||||||
import me.ash.reader.ui.page.home.flow.FlowPage
|
import me.ash.reader.ui.page.home.flow.FlowPage
|
||||||
import me.ash.reader.ui.page.home.read.ReadPage
|
import me.ash.reader.ui.page.home.read.ReadPage
|
||||||
|
@ -35,13 +34,13 @@ fun HomePage(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
viewModel: HomeViewModel = hiltViewModel(),
|
viewModel: HomeViewModel = hiltViewModel(),
|
||||||
readViewModel: ReadViewModel = hiltViewModel(),
|
readViewModel: ReadViewModel = hiltViewModel(),
|
||||||
|
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val viewState = viewModel.viewState.collectAsStateValue()
|
val viewState = viewModel.viewState.collectAsStateValue()
|
||||||
val filterState = viewModel.filterState.collectAsStateValue()
|
val filterState = viewModel.filterState.collectAsStateValue()
|
||||||
val readState = readViewModel.viewState.collectAsStateValue()
|
val readState = readViewModel.viewState.collectAsStateValue()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val drawerState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
context.findActivity()?.let { activity ->
|
context.findActivity()?.let { activity ->
|
||||||
|
@ -82,6 +81,9 @@ fun HomePage(
|
||||||
if (currentPage == 2) {
|
if (currentPage == 2) {
|
||||||
readViewModel.dispatch(ReadViewAction.ClearArticle)
|
readViewModel.dispatch(ReadViewAction.ClearArticle)
|
||||||
}
|
}
|
||||||
|
if (currentPage == 0) {
|
||||||
|
feedOptionViewModel.dispatch(FeedOptionViewAction.Hide(scope))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -96,7 +98,6 @@ fun HomePage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box {
|
|
||||||
Column {
|
Column {
|
||||||
ViewPager(
|
ViewPager(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
@ -109,20 +110,7 @@ fun HomePage(
|
||||||
FlowPage(navController = navController)
|
FlowPage(navController = navController)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ReadPage(
|
ReadPage(navController = navController)
|
||||||
navController = navController,
|
|
||||||
btnBackOnClickListener = {
|
|
||||||
viewModel.dispatch(
|
|
||||||
HomeViewAction.ScrollToPage(
|
|
||||||
scope = scope,
|
|
||||||
targetPage = 1,
|
|
||||||
callback = {
|
|
||||||
readViewModel.dispatch(ReadViewAction.ClearArticle)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -159,6 +147,6 @@ fun HomePage(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
FeedOptionDrawer(drawerState = drawerState)
|
|
||||||
}
|
FeedOptionDrawer()
|
||||||
}
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package me.ash.reader.ui.page.home.drawer.feed
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.DeleteOutline
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.ui.extension.collectAsStateValue
|
||||||
|
import me.ash.reader.ui.widget.Dialog
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@Composable
|
||||||
|
fun DeleteFeedDialog(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
feedName: String,
|
||||||
|
viewModel: FeedOptionViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val viewState = viewModel.viewState.collectAsStateValue()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val deletedTip = stringResource(R.string.has_been_deleted, feedName)
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
visible = viewState.deleteDialogVisible,
|
||||||
|
onDismissRequest = {
|
||||||
|
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog)
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.DeleteOutline,
|
||||||
|
contentDescription = stringResource(R.string.subscribe),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.unsubscribe))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(R.string.unsubscribe_tip, feedName))
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.dispatch(FeedOptionViewAction.Delete(){
|
||||||
|
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog)
|
||||||
|
viewModel.dispatch(FeedOptionViewAction.Hide(scope))
|
||||||
|
Toast.makeText(context, deletedTip, Toast.LENGTH_SHORT).show()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.unsubscribe),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.cancel),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package me.ash.reader.ui.page.home.drawer.feed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.DeleteOutline
|
||||||
|
import androidx.compose.material.icons.rounded.RssFeed
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import me.ash.reader.R
|
||||||
|
import me.ash.reader.ui.extension.collectAsStateValue
|
||||||
|
import me.ash.reader.ui.extension.roundClick
|
||||||
|
import me.ash.reader.ui.page.home.feeds.subscribe.ResultViewPage
|
||||||
|
import me.ash.reader.ui.widget.BottomDrawer
|
||||||
|
import me.ash.reader.ui.widget.Subtitle
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FeedOptionDrawer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: FeedOptionViewModel = hiltViewModel(),
|
||||||
|
content: @Composable () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val viewState = viewModel.viewState.collectAsStateValue()
|
||||||
|
val feed = viewState.feed
|
||||||
|
|
||||||
|
BottomDrawer(
|
||||||
|
drawerState = viewState.drawerState,
|
||||||
|
sheetContent = {
|
||||||
|
Column {
|
||||||
|
Icon(
|
||||||
|
modifier = modifier
|
||||||
|
.roundClick { }
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.CenterHorizontally),
|
||||||
|
imageVector = Icons.Rounded.RssFeed,
|
||||||
|
contentDescription = feed?.name
|
||||||
|
?: stringResource(R.string.unknown),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Spacer(modifier = modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.roundClick {}
|
||||||
|
.fillMaxWidth(),
|
||||||
|
text = feed?.name ?: stringResource(R.string.unknown),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Spacer(modifier = modifier.height(16.dp))
|
||||||
|
ResultViewPage(
|
||||||
|
link = feed?.url ?: stringResource(R.string.unknown),
|
||||||
|
groups = viewState.groups,
|
||||||
|
selectedAllowNotificationPreset = viewState.feed?.isNotification ?: false,
|
||||||
|
selectedParseFullContentPreset = viewState.feed?.isFullContent ?: false,
|
||||||
|
selectedGroupId = viewState.feed?.groupId ?: "",
|
||||||
|
newGroupContent = viewState.newGroupContent,
|
||||||
|
onNewGroupValueChange = {
|
||||||
|
viewModel.dispatch(FeedOptionViewAction.InputNewGroup(it))
|
||||||
|
},
|
||||||
|
newGroupSelected = viewState.newGroupSelected,
|
||||||
|
changeNewGroupSelected = {
|
||||||
|
viewModel.dispatch(FeedOptionViewAction.SelectedNewGroup(it))
|
||||||
|
},
|
||||||
|
allowNotificationPresetOnClick = {
|
||||||
|
viewModel.dispatch(FeedOptionViewAction.ChangeAllowNotificationPreset)
|
||||||
|
},
|
||||||
|
parseFullContentPresetOnClick = {
|
||||||
|
viewModel.dispatch(FeedOptionViewAction.ChangeParseFullContentPreset)
|
||||||
|
},
|
||||||
|
groupOnClick = {
|
||||||
|
viewModel.dispatch(FeedOptionViewAction.SelectedGroup(it))
|
||||||
|
},
|
||||||
|
onKeyboardAction = { },
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
Subtitle(text = stringResource(R.string.options))
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
Button(
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = MaterialTheme.colorScheme.error,
|
||||||
|
),
|
||||||
|
border = BorderStroke(
|
||||||
|
width = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
viewModel.dispatch(FeedOptionViewAction.ShowDeleteDialog)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(ButtonDefaults.IconSize),
|
||||||
|
imageVector = Icons.Rounded.DeleteOutline,
|
||||||
|
contentDescription = stringResource(R.string.delete),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.unsubscribe),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteFeedDialog(feedName = feed?.name ?: "")
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
package me.ash.reader.ui.page.home.drawer.feed
|
||||||
|
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.ModalBottomSheetState
|
||||||
|
import androidx.compose.material.ModalBottomSheetValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.ash.reader.data.feed.Feed
|
||||||
|
import me.ash.reader.data.group.Group
|
||||||
|
import me.ash.reader.data.repository.RssRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@OptIn(
|
||||||
|
ExperimentalPagerApi::class,
|
||||||
|
ExperimentalMaterialApi::class
|
||||||
|
)
|
||||||
|
@HiltViewModel
|
||||||
|
class FeedOptionViewModel @Inject constructor(
|
||||||
|
private val rssRepository: RssRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _viewState = MutableStateFlow(FeedOptionViewState())
|
||||||
|
val viewState: StateFlow<FeedOptionViewState> = _viewState.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
rssRepository.get().pullGroups().collect { groups ->
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
groups = groups
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispatch(action: FeedOptionViewAction) {
|
||||||
|
when (action) {
|
||||||
|
is FeedOptionViewAction.Show -> show(action.scope, action.feedId)
|
||||||
|
is FeedOptionViewAction.Hide -> hide(action.scope)
|
||||||
|
is FeedOptionViewAction.SelectedGroup -> selectedGroup(action.groupId)
|
||||||
|
is FeedOptionViewAction.InputNewGroup -> inputNewGroup(action.content)
|
||||||
|
is FeedOptionViewAction.SelectedNewGroup -> selectedNewGroup(action.selected)
|
||||||
|
is FeedOptionViewAction.ChangeAllowNotificationPreset -> changeAllowNotificationPreset()
|
||||||
|
is FeedOptionViewAction.ChangeParseFullContentPreset -> changeParseFullContentPreset()
|
||||||
|
is FeedOptionViewAction.ShowDeleteDialog -> showDeleteDialog()
|
||||||
|
is FeedOptionViewAction.HideDeleteDialog -> hideDeleteDialog()
|
||||||
|
is FeedOptionViewAction.Delete -> delete(action.callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchFeed(feedId: String) {
|
||||||
|
val feed = rssRepository.get().findFeedById(feedId)
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
feed = feed,
|
||||||
|
selectedGroupId = feed?.groupId ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun show(scope: CoroutineScope, feedId: String) {
|
||||||
|
scope.launch {
|
||||||
|
fetchFeed(feedId)
|
||||||
|
_viewState.value.drawerState.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hide(scope: CoroutineScope) {
|
||||||
|
scope.launch {
|
||||||
|
_viewState.value.drawerState.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inputNewGroup(content: String) {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
newGroupContent = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectedGroup(groupId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_viewState.value.feed?.let {
|
||||||
|
rssRepository.get().updateFeed(
|
||||||
|
it.copy(
|
||||||
|
groupId = groupId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fetchFeed(it.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectedNewGroup(selected: Boolean) {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
newGroupSelected = selected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeParseFullContentPreset() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_viewState.value.feed?.let {
|
||||||
|
rssRepository.get().updateFeed(
|
||||||
|
it.copy(
|
||||||
|
isFullContent = !it.isFullContent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fetchFeed(it.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeAllowNotificationPreset() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_viewState.value.feed?.let {
|
||||||
|
rssRepository.get().updateFeed(
|
||||||
|
it.copy(
|
||||||
|
isNotification = !it.isNotification
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fetchFeed(it.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun delete(callback: () -> Unit = {}) {
|
||||||
|
_viewState.value.feed?.let {
|
||||||
|
viewModelScope.launch {
|
||||||
|
rssRepository.get().deleteFeed(it)
|
||||||
|
}.invokeOnCompletion {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideDeleteDialog() {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
deleteDialogVisible = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteDialog() {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
deleteDialogVisible = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
data class FeedOptionViewState(
|
||||||
|
var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden),
|
||||||
|
val feed: Feed? = null,
|
||||||
|
val selectedGroupId: String = "",
|
||||||
|
val newGroupContent: String = "",
|
||||||
|
val newGroupSelected: Boolean = false,
|
||||||
|
val groups: List<Group> = emptyList(),
|
||||||
|
val deleteDialogVisible: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class FeedOptionViewAction {
|
||||||
|
data class Show(
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
val feedId: String
|
||||||
|
) : FeedOptionViewAction()
|
||||||
|
|
||||||
|
data class Hide(
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
) : FeedOptionViewAction()
|
||||||
|
|
||||||
|
object ChangeAllowNotificationPreset : FeedOptionViewAction()
|
||||||
|
object ChangeParseFullContentPreset : FeedOptionViewAction()
|
||||||
|
|
||||||
|
data class SelectedGroup(
|
||||||
|
val groupId: String
|
||||||
|
) : FeedOptionViewAction()
|
||||||
|
|
||||||
|
data class InputNewGroup(
|
||||||
|
val content: String
|
||||||
|
) : FeedOptionViewAction()
|
||||||
|
|
||||||
|
data class SelectedNewGroup(
|
||||||
|
val selected: Boolean
|
||||||
|
) : FeedOptionViewAction()
|
||||||
|
|
||||||
|
data class Delete(
|
||||||
|
val callback: () -> Unit = {}
|
||||||
|
) : FeedOptionViewAction()
|
||||||
|
|
||||||
|
object ShowDeleteDialog: FeedOptionViewAction()
|
||||||
|
object HideDeleteDialog: FeedOptionViewAction()
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
package me.ash.reader.ui.page.home.feeds
|
package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
@ -9,24 +10,45 @@ import androidx.compose.material3.Badge
|
||||||
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.rememberCoroutineScope
|
||||||
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.platform.LocalView
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import me.ash.reader.data.feed.Feed
|
||||||
|
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewAction
|
||||||
|
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewModel
|
||||||
|
|
||||||
|
@OptIn(
|
||||||
|
androidx.compose.foundation.ExperimentalFoundationApi::class,
|
||||||
|
androidx.compose.material.ExperimentalMaterialApi::class,
|
||||||
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun FeedItem(
|
fun FeedItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
name: String,
|
feed: Feed,
|
||||||
important: Int,
|
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 14.dp)
|
.padding(horizontal = 14.dp)
|
||||||
.clip(RoundedCornerShape(32.dp))
|
.clip(RoundedCornerShape(32.dp))
|
||||||
.clickable { onClick() }
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
onClick()
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
|
feedOptionViewModel.dispatch(FeedOptionViewAction.Show(scope, feed.id))
|
||||||
|
}
|
||||||
|
)
|
||||||
.padding(vertical = 14.dp),
|
.padding(vertical = 14.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
@ -43,19 +65,19 @@ fun FeedItem(
|
||||||
) {}
|
) {}
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text(
|
Text(
|
||||||
text = name,
|
text = feed.name,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (important != 0) {
|
if (feed.important ?: 0 != 0) {
|
||||||
Badge(
|
Badge(
|
||||||
modifier = Modifier.padding(end = 6.dp),
|
modifier = Modifier.padding(end = 6.dp),
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f),
|
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f),
|
||||||
contentColor = MaterialTheme.colorScheme.outline,
|
contentColor = MaterialTheme.colorScheme.outline,
|
||||||
content = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = important.toString(),
|
text = feed.important.toString(),
|
||||||
style = MaterialTheme.typography.labelSmall
|
style = MaterialTheme.typography.labelSmall
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package me.ash.reader.ui.page.home.feeds
|
package me.ash.reader.ui.page.home.feeds
|
||||||
|
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
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
|
||||||
|
@ -23,7 +25,6 @@ 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 androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.ui.extension.collectAsStateValue
|
import me.ash.reader.ui.extension.collectAsStateValue
|
||||||
import me.ash.reader.ui.extension.getDesc
|
import me.ash.reader.ui.extension.getDesc
|
||||||
|
@ -167,6 +168,8 @@ fun FeedsPage(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed ->
|
itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed ->
|
||||||
|
Crossfade(targetState = groupWithFeed) { groupWithFeed ->
|
||||||
|
Column {
|
||||||
GroupItem(
|
GroupItem(
|
||||||
text = groupWithFeed.group.name,
|
text = groupWithFeed.group.name,
|
||||||
feeds = groupWithFeed.feeds,
|
feeds = groupWithFeed.feeds,
|
||||||
|
@ -207,6 +210,8 @@ fun FeedsPage(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,8 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
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 kotlinx.coroutines.launch
|
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.feed.Feed
|
import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.ui.page.common.LocalDrawerState
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -36,8 +34,6 @@ fun GroupItem(
|
||||||
feedOnClick: (feed: Feed) -> Unit = {},
|
feedOnClick: (feed: Feed) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
var expanded by remember { mutableStateOf(isExpanded) }
|
var expanded by remember { mutableStateOf(isExpanded) }
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val drawerState = LocalDrawerState.current
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -50,9 +46,6 @@ fun GroupItem(
|
||||||
groupOnClick()
|
groupOnClick()
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
scope.launch {
|
|
||||||
drawerState.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.padding(top = 22.dp)
|
.padding(top = 22.dp)
|
||||||
|
@ -97,8 +90,7 @@ fun GroupItem(
|
||||||
feeds.forEach { feed ->
|
feeds.forEach { feed ->
|
||||||
FeedItem(
|
FeedItem(
|
||||||
modifier = Modifier.padding(horizontal = 20.dp),
|
modifier = Modifier.padding(horizontal = 20.dp),
|
||||||
name = feed.name,
|
feed = feed,
|
||||||
important = feed.important ?: 0,
|
|
||||||
) {
|
) {
|
||||||
feedOnClick(feed)
|
feedOnClick(feed)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
package me.ash.reader.ui.page.home.feeds.subscribe
|
package me.ash.reader.ui.page.home.feeds.subscribe
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
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.filled.AddAlert
|
import androidx.compose.material.icons.outlined.Article
|
||||||
import androidx.compose.material.icons.filled.Article
|
import androidx.compose.material.icons.outlined.Notifications
|
||||||
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.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import com.google.accompanist.flowlayout.MainAxisAlignment
|
import com.google.accompanist.flowlayout.MainAxisAlignment
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.data.group.Group
|
import me.ash.reader.data.group.Group
|
||||||
|
import me.ash.reader.ui.extension.roundClick
|
||||||
import me.ash.reader.ui.widget.SelectionChip
|
import me.ash.reader.ui.widget.SelectionChip
|
||||||
import me.ash.reader.ui.widget.SelectionEditorChip
|
import me.ash.reader.ui.widget.SelectionEditorChip
|
||||||
import me.ash.reader.ui.widget.Subtitle
|
import me.ash.reader.ui.widget.Subtitle
|
||||||
|
@ -74,15 +79,23 @@ fun ResultViewPage(
|
||||||
private fun Link(
|
private fun Link(
|
||||||
text: String,
|
text: String,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
Text(
|
Text(
|
||||||
|
modifier = Modifier.roundClick {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(Intent.ACTION_VIEW, Uri.parse(text))
|
||||||
|
)
|
||||||
|
},
|
||||||
text = text,
|
text = text,
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,12 +121,11 @@ private fun Preset(
|
||||||
selected = selectedAllowNotificationPreset,
|
selected = selectedAllowNotificationPreset,
|
||||||
selectedIcon = {
|
selectedIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.AddAlert,
|
imageVector = Icons.Outlined.Notifications,
|
||||||
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(20.dp),
|
.size(20.dp),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
@ -125,12 +137,11 @@ private fun Preset(
|
||||||
selected = selectedParseFullContentPreset,
|
selected = selectedParseFullContentPreset,
|
||||||
selectedIcon = {
|
selectedIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Article,
|
imageVector = Icons.Outlined.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(20.dp),
|
.size(20.dp),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.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.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
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.text.input.ImeAction
|
||||||
|
@ -46,6 +47,7 @@ fun SearchViewPage(
|
||||||
onKeyboardAction: () -> Unit = {},
|
onKeyboardAction: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
@ -89,6 +91,7 @@ fun SearchViewPage(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
|
onLinkValueChange(clipboardManager.getText()?.text ?: "")
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.ContentPaste,
|
imageVector = Icons.Rounded.ContentPaste,
|
||||||
|
|
|
@ -18,15 +18,17 @@ 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.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
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 kotlinx.coroutines.Dispatchers
|
||||||
import me.ash.reader.*
|
import me.ash.reader.*
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.ui.extension.collectAsStateValue
|
import me.ash.reader.ui.extension.collectAsStateValue
|
||||||
import me.ash.reader.ui.widget.Dialog
|
import me.ash.reader.ui.widget.Dialog
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@OptIn(ExperimentalPagerApi::class)
|
@OptIn(ExperimentalPagerApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SubscribeDialog(
|
fun SubscribeDialog(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -44,8 +46,9 @@ fun SubscribeDialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val viewState = viewModel.viewState.collectAsStateValue()
|
val viewState = viewModel.viewState.collectAsStateValue()
|
||||||
val groupsState = viewState.groups.collectAsState(initial = emptyList())
|
val groupsState =
|
||||||
var dialogHeight by remember { mutableStateOf(280.dp) }
|
viewState.groups.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
||||||
|
var dialogHeight by remember { mutableStateOf(300.dp) }
|
||||||
val readYouString = stringResource(R.string.read_you)
|
val readYouString = stringResource(R.string.read_you)
|
||||||
val defaultString = stringResource(R.string.defaults)
|
val defaultString = stringResource(R.string.defaults)
|
||||||
LaunchedEffect(viewState.visible) {
|
LaunchedEffect(viewState.visible) {
|
||||||
|
@ -64,7 +67,7 @@ fun SubscribeDialog(
|
||||||
LaunchedEffect(viewState.pagerState.currentPage) {
|
LaunchedEffect(viewState.pagerState.currentPage) {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
when (viewState.pagerState.currentPage) {
|
when (viewState.pagerState.currentPage) {
|
||||||
0 -> dialogHeight = 280.dp
|
0 -> dialogHeight = 300.dp
|
||||||
1 -> dialogHeight = Dp.Unspecified
|
1 -> dialogHeight = Dp.Unspecified
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +77,7 @@ fun SubscribeDialog(
|
||||||
.padding(horizontal = 44.dp)
|
.padding(horizontal = 44.dp)
|
||||||
.height(dialogHeight),
|
.height(dialogHeight),
|
||||||
visible = viewState.visible,
|
visible = viewState.visible,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
viewModel.dispatch(SubscribeViewAction.Hide)
|
viewModel.dispatch(SubscribeViewAction.Hide)
|
||||||
|
|
|
@ -187,7 +187,8 @@ class SubscribeViewModel @Inject constructor(
|
||||||
private fun inputLink(content: String) {
|
private fun inputLink(content: String) {
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
linkContent = content
|
linkContent = content,
|
||||||
|
errorMessage = "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package me.ash.reader.ui.page.home.flow
|
package me.ash.reader.ui.page.home.flow
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
@ -79,7 +80,11 @@ fun FlowPage(
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
viewModel.dispatch(FlowViewAction.PeekSyncWork)
|
viewModel.dispatch(FlowViewAction.PeekSyncWork)
|
||||||
Toast.makeText(context, viewState.syncWorkInfo.length.toString(), Toast.LENGTH_SHORT)
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
viewState.syncWorkInfo.length.toString(),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
)
|
||||||
.show()
|
.show()
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -99,6 +104,7 @@ fun FlowPage(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
|
Crossfade(targetState = pagingItems) { pagingItems ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = viewState.listState,
|
state = viewState.listState,
|
||||||
) {
|
) {
|
||||||
|
@ -126,5 +132,6 @@ fun FlowPage(
|
||||||
generateArticleList(context, pagingItems, readViewModel, homeViewModel, scope)
|
generateArticleList(context, pagingItems, readViewModel, homeViewModel, scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -8,9 +8,7 @@ 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.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
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 me.ash.reader.data.article.Article
|
import me.ash.reader.data.article.Article
|
||||||
import me.ash.reader.data.feed.Feed
|
import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.formatToString
|
import me.ash.reader.formatToString
|
||||||
|
@ -30,37 +28,31 @@ fun Header(
|
||||||
Intent(Intent.ACTION_VIEW, Uri.parse(article.link))
|
Intent(Intent.ACTION_VIEW, Uri.parse(article.link))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.padding(12.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(10.dp)) {
|
|
||||||
Text(
|
Text(
|
||||||
text = article.date.formatToString(context, atHourMinute = true),
|
text = article.date.formatToString(context, atHourMinute = true),
|
||||||
fontSize = 12.sp,
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||||
color = MaterialTheme.colorScheme.outline,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = article.title,
|
text = article.title,
|
||||||
fontSize = 27.sp,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
lineHeight = 34.sp,
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
article.author?.let {
|
article.author?.let {
|
||||||
Text(
|
Text(
|
||||||
text = article.author,
|
text = article.author,
|
||||||
fontSize = 12.sp,
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||||
color = MaterialTheme.colorScheme.outline,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = feed.name,
|
text = feed.name,
|
||||||
fontSize = 12.sp,
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||||
color = MaterialTheme.colorScheme.outline,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,34 +1,51 @@
|
||||||
package me.ash.reader.ui.page.home.read
|
package me.ash.reader.ui.page.home.read
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Headphones
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.material.icons.rounded.Close
|
||||||
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.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 androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import com.airbnb.lottie.compose.LottieAnimation
|
import com.airbnb.lottie.compose.LottieAnimation
|
||||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||||
import kotlinx.coroutines.flow.collect
|
import me.ash.reader.R
|
||||||
import me.ash.reader.ui.extension.collectAsStateValue
|
import me.ash.reader.ui.extension.collectAsStateValue
|
||||||
import me.ash.reader.ui.extension.paddingFixedHorizontal
|
import me.ash.reader.ui.page.home.HomeViewAction
|
||||||
|
import me.ash.reader.ui.page.home.HomeViewModel
|
||||||
import me.ash.reader.ui.widget.WebView
|
import me.ash.reader.ui.widget.WebView
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ReadPage(
|
fun ReadPage(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: ReadViewModel = hiltViewModel(),
|
viewModel: ReadViewModel = hiltViewModel(),
|
||||||
btnBackOnClickListener: () -> Unit,
|
homeViewModel: HomeViewModel = hiltViewModel(),
|
||||||
|
readViewModel: ReadViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val viewState = viewModel.viewState.collectAsStateValue()
|
val viewState = viewModel.viewState.collectAsStateValue()
|
||||||
|
val composition by rememberLottieComposition(
|
||||||
|
LottieCompositionSpec.Url(
|
||||||
|
"https://assets5.lottiefiles.com/packages/lf20_9tvcldy3.json"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
LaunchedEffect(viewModel.viewState) {
|
LaunchedEffect(viewModel.viewState) {
|
||||||
viewModel.viewState.collect {
|
viewModel.viewState.collect {
|
||||||
|
@ -43,18 +60,52 @@ fun ReadPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box {
|
Scaffold(
|
||||||
Column(
|
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||||
modifier.fillMaxSize()
|
topBar = {
|
||||||
) {
|
SmallTopAppBar(
|
||||||
ReadPageTopBar(btnBackOnClickListener)
|
title = {},
|
||||||
|
navigationIcon = {
|
||||||
val composition by rememberLottieComposition(
|
IconButton(onClick = {
|
||||||
LottieCompositionSpec.Url(
|
homeViewModel.dispatch(
|
||||||
"https://assets5.lottiefiles.com/packages/lf20_9tvcldy3.json"
|
HomeViewAction.ScrollToPage(
|
||||||
|
scope = scope,
|
||||||
|
targetPage = 1,
|
||||||
|
callback = {
|
||||||
|
readViewModel.dispatch(ReadViewAction.ClearArticle)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Close,
|
||||||
|
contentDescription = stringResource(R.string.back),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
viewState.articleWithFeed?.let {
|
||||||
|
IconButton(onClick = {}) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
imageVector = Icons.Outlined.Headphones,
|
||||||
|
contentDescription = stringResource(R.string.mark_all_as_read),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = {}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.MoreVert,
|
||||||
|
contentDescription = stringResource(R.string.search),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = {
|
||||||
if (viewState.articleWithFeed == null) {
|
if (viewState.articleWithFeed == null) {
|
||||||
LottieAnimation(
|
LottieAnimation(
|
||||||
composition = composition,
|
composition = composition,
|
||||||
|
@ -65,36 +116,28 @@ fun ReadPage(
|
||||||
restartOnPlay = true,
|
restartOnPlay = true,
|
||||||
iterations = Int.MAX_VALUE
|
iterations = Int.MAX_VALUE
|
||||||
)
|
)
|
||||||
}
|
} else {
|
||||||
AnimatedVisibility(
|
|
||||||
modifier = modifier.fillMaxSize(),
|
|
||||||
visible = viewState.articleWithFeed != null,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically(),
|
|
||||||
) {
|
|
||||||
if (viewState.articleWithFeed == null) return@AnimatedVisibility
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = viewState.listState,
|
state = viewState.listState,
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f),
|
|
||||||
) {
|
) {
|
||||||
val article = viewState.articleWithFeed.article
|
val article = viewState.articleWithFeed.article
|
||||||
val feed = viewState.articleWithFeed.feed
|
val feed = viewState.articleWithFeed.feed
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.padding(horizontal = 12.dp)
|
||||||
.paddingFixedHorizontal()
|
|
||||||
) {
|
) {
|
||||||
Header(context, article, feed)
|
Header(context, article, feed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
Spacer(modifier = Modifier.height(22.dp))
|
||||||
|
Crossfade(targetState = viewState.content) { content ->
|
||||||
WebView(
|
WebView(
|
||||||
content = viewState.content ?: "",
|
content = content ?: "",
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(50.dp))
|
Spacer(modifier = Modifier.height(50.dp))
|
||||||
}
|
}
|
||||||
|
@ -102,4 +145,5 @@ fun ReadPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
package me.ash.reader.ui.page.home.read
|
|
||||||
|
|
||||||
import android.view.HapticFeedbackConstants
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Close
|
|
||||||
import androidx.compose.material.icons.rounded.MoreHoriz
|
|
||||||
import androidx.compose.material.icons.rounded.Share
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.SmallTopAppBar
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import me.ash.reader.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ReadPageTopBar(
|
|
||||||
btnBackOnClickListener: () -> Unit = {},
|
|
||||||
) {
|
|
||||||
val view = LocalView.current
|
|
||||||
SmallTopAppBar(
|
|
||||||
title = {},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
btnBackOnClickListener()
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.size(28.dp),
|
|
||||||
imageVector = Icons.Rounded.Close,
|
|
||||||
contentDescription = stringResource(R.string.back),
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
// modifier = Modifier.size(20.dp),
|
|
||||||
imageVector = Icons.Rounded.Share,
|
|
||||||
contentDescription = "Share",
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
// modifier = Modifier.size(28.dp),
|
|
||||||
imageVector = Icons.Rounded.MoreHoriz,
|
|
||||||
contentDescription = "More",
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
package me.ash.reader.ui.widget
|
package me.ash.reader.ui.widget
|
||||||
|
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.background
|
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.*
|
||||||
|
@ -42,7 +43,8 @@ fun Banner(
|
||||||
.padding(16.dp, 20.dp),
|
.padding(16.dp, 20.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
icon?.let {
|
icon?.let { icon ->
|
||||||
|
Crossfade(targetState = icon) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = it,
|
imageVector = it,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
@ -50,6 +52,7 @@ fun Banner(
|
||||||
tint = lightOnSurface,
|
tint = lightOnSurface,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
|
|
|
@ -8,11 +8,10 @@ import androidx.compose.material.ModalBottomSheetDefaults
|
||||||
import androidx.compose.material.ModalBottomSheetState
|
import androidx.compose.material.ModalBottomSheetState
|
||||||
import androidx.compose.material.ModalBottomSheetValue
|
import androidx.compose.material.ModalBottomSheetValue
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
|
|
||||||
|
@ -23,27 +22,28 @@ fun BottomDrawer(
|
||||||
drawerState: ModalBottomSheetState = androidx.compose.material.rememberModalBottomSheetState(
|
drawerState: ModalBottomSheetState = androidx.compose.material.rememberModalBottomSheetState(
|
||||||
ModalBottomSheetValue.Hidden
|
ModalBottomSheetValue.Hidden
|
||||||
),
|
),
|
||||||
|
sheetContent: @Composable ColumnScope.() -> Unit = {},
|
||||||
content: @Composable () -> Unit = {},
|
content: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
androidx.compose.material.ModalBottomSheetLayout(
|
androidx.compose.material.ModalBottomSheetLayout(
|
||||||
sheetState = drawerState,
|
modifier = modifier,
|
||||||
sheetBackgroundColor = Color.Transparent,
|
sheetShape = RoundedCornerShape(
|
||||||
sheetElevation = if (drawerState.isVisible) ModalBottomSheetDefaults.Elevation else 0.dp,
|
|
||||||
sheetContent = {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(
|
|
||||||
RoundedCornerShape(
|
|
||||||
topStart = 28.0.dp,
|
topStart = 28.0.dp,
|
||||||
topEnd = 28.0.dp,
|
topEnd = 28.0.dp,
|
||||||
bottomEnd = 0.0.dp,
|
bottomEnd = 0.0.dp,
|
||||||
bottomStart = 0.0.dp
|
bottomStart = 0.0.dp
|
||||||
)
|
),
|
||||||
)
|
sheetState = drawerState,
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
sheetBackgroundColor = MaterialTheme.colorScheme.surface,
|
||||||
.padding(horizontal = 28.dp)
|
sheetElevation = if (drawerState.isVisible) ModalBottomSheetDefaults.Elevation else 0.dp,
|
||||||
.navigationBarsPadding()
|
sheetContent = {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 6.dp,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 28.dp)
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
Row(
|
Row(
|
||||||
|
@ -62,12 +62,13 @@ fun BottomDrawer(
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
content()
|
sheetContent()
|
||||||
Spacer(modifier = Modifier.height(28.dp))
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
content = {}
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package me.ash.reader.ui.widget
|
package me.ash.reader.ui.widget
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
@ -9,20 +10,23 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
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
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CanBeDisabledIconButton(
|
fun CanBeDisabledIconButton(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
imageVector: ImageVector,
|
imageVector: ImageVector,
|
||||||
|
size: Dp = 24.dp,
|
||||||
contentDescription: String?,
|
contentDescription: String?,
|
||||||
tint: Color = LocalContentColor.current,
|
tint: Color = LocalContentColor.current,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.alpha(
|
modifier = modifier.alpha(
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
0.7f
|
0.5f
|
||||||
} else {
|
} else {
|
||||||
1f
|
1f
|
||||||
}
|
}
|
||||||
|
@ -31,7 +35,7 @@ fun CanBeDisabledIconButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = modifier,
|
modifier = Modifier.size(size),
|
||||||
imageVector = imageVector,
|
imageVector = imageVector,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
tint = if (disabled) MaterialTheme.colorScheme.outline else tint,
|
tint = if (disabled) MaterialTheme.colorScheme.outline else tint,
|
||||||
|
|
|
@ -2,15 +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.Modifier
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Dialog(
|
fun Dialog(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
|
properties: DialogProperties = DialogProperties(),
|
||||||
onDismissRequest: () -> Unit = {},
|
onDismissRequest: () -> Unit = {},
|
||||||
icon: @Composable (() -> Unit)? = null,
|
icon: @Composable (() -> Unit)? = null,
|
||||||
title: @Composable (() -> Unit)? = null,
|
title: @Composable (() -> Unit)? = null,
|
||||||
|
@ -20,7 +19,7 @@ fun Dialog(
|
||||||
) {
|
) {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
properties = properties,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
icon = icon,
|
icon = icon,
|
||||||
|
|
|
@ -2,10 +2,7 @@ package me.ash.reader.ui.widget
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
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
|
||||||
|
@ -14,7 +11,7 @@ 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.filled.CheckCircle
|
import androidx.compose.material.icons.rounded.Check
|
||||||
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
|
||||||
|
@ -41,16 +38,7 @@ fun SelectionChip(
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
shape: Shape = CircleShape,
|
shape: Shape = CircleShape,
|
||||||
border: BorderStroke? = null,
|
border: BorderStroke? = null,
|
||||||
selectedIcon: @Composable () -> Unit = {
|
selectedIcon: @Composable (() -> Unit)? = null,
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.CheckCircle,
|
|
||||||
contentDescription = stringResource(R.string.selected),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 8.dp)
|
|
||||||
.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
@ -59,20 +47,28 @@ fun SelectionChip(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
colors = ChipDefaults.filterChipColors(
|
colors = ChipDefaults.filterChipColors(
|
||||||
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
|
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
contentColor = MaterialTheme.colorScheme.outline,
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
leadingIconColor = MaterialTheme.colorScheme.onSurface,
|
leadingIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
disabledBackgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),
|
||||||
disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer,
|
selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
selectedContentColor = MaterialTheme.colorScheme.onSurface,
|
selectedContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
selectedLeadingIconColor = MaterialTheme.colorScheme.onSurface
|
selectedLeadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
),
|
),
|
||||||
border = border,
|
border = border,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
selected = selected,
|
selected = selected,
|
||||||
selectedIcon = selectedIcon,
|
selectedIcon = selectedIcon ?: {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Check,
|
||||||
|
contentDescription = stringResource(R.string.selected),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.size(20.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
shape = shape,
|
shape = shape,
|
||||||
onClick = {
|
onClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
|
@ -88,11 +84,6 @@ fun SelectionChip(
|
||||||
),
|
),
|
||||||
text = content,
|
text = content,
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
color = if (selected) {
|
|
||||||
MaterialTheme.colorScheme.onSurface
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.outline
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -108,16 +99,7 @@ fun SelectionEditorChip(
|
||||||
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)? = null,
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.CheckCircle,
|
|
||||||
contentDescription = stringResource(R.string.selected),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 8.dp)
|
|
||||||
.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onKeyboardAction: () -> Unit = {},
|
onKeyboardAction: () -> Unit = {},
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
@ -125,21 +107,30 @@ fun SelectionEditorChip(
|
||||||
val placeholder = stringResource(R.string.add_to_group)
|
val placeholder = stringResource(R.string.add_to_group)
|
||||||
|
|
||||||
FilterChip(
|
FilterChip(
|
||||||
|
modifier = modifier.defaultMinSize(minHeight = 36.dp),
|
||||||
colors = ChipDefaults.filterChipColors(
|
colors = ChipDefaults.filterChipColors(
|
||||||
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
|
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
leadingIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
leadingIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
disabledBackgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),
|
||||||
disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer,
|
selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
selectedContentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
selectedContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
selectedLeadingIconColor = MaterialTheme.colorScheme.onSecondaryContainer
|
selectedLeadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
),
|
),
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
selected = selected,
|
selected = selected,
|
||||||
selectedIcon = selectedIcon,
|
selectedIcon = selectedIcon ?: {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Check,
|
||||||
|
contentDescription = stringResource(R.string.selected),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.size(20.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
shape = shape,
|
shape = shape,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
content = {
|
content = {
|
||||||
|
@ -160,13 +151,9 @@ fun SelectionEditorChip(
|
||||||
},
|
},
|
||||||
value = content,
|
value = content,
|
||||||
onValueChange = { onValueChange(it) },
|
onValueChange = { onValueChange(it) },
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSecondaryContainer),
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||||
textStyle = MaterialTheme.typography.titleSmall.copy(
|
textStyle = MaterialTheme.typography.titleSmall.copy(
|
||||||
color = if (selected) {
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
MaterialTheme.colorScheme.onSurface
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.outline
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
decorationBox = { innerTextField ->
|
decorationBox = { innerTextField ->
|
||||||
Row(
|
Row(
|
||||||
|
@ -176,7 +163,7 @@ fun SelectionEditorChip(
|
||||||
if (content.isEmpty()) {
|
if (content.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = placeholder,
|
text = placeholder,
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ import me.ash.reader.ui.extension.collectAsStateValue
|
||||||
import me.ash.reader.ui.page.home.read.ReadViewAction
|
import me.ash.reader.ui.page.home.read.ReadViewAction
|
||||||
import me.ash.reader.ui.page.home.read.ReadViewModel
|
import me.ash.reader.ui.page.home.read.ReadViewModel
|
||||||
|
|
||||||
|
const val INJECTION_TOKEN = "/android_asset_font/"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WebView(
|
fun WebView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -26,11 +28,30 @@ fun WebView(
|
||||||
onReceivedError: (error: WebResourceError?) -> Unit = {}
|
onReceivedError: (error: WebResourceError?) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val color = MaterialTheme.colorScheme.secondary.toArgb()
|
val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb()
|
||||||
val backgroundColor = MaterialTheme.colorScheme.surface.toArgb()
|
val backgroundColor = MaterialTheme.colorScheme.surface.toArgb()
|
||||||
val viewState = viewModel.viewState.collectAsStateValue()
|
val viewState = viewModel.viewState.collectAsStateValue()
|
||||||
val webViewClient = object : WebViewClient() {
|
val webViewClient = object : WebViewClient() {
|
||||||
|
|
||||||
|
override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? {
|
||||||
|
if (url != null && url.contains(INJECTION_TOKEN)) {
|
||||||
|
try {
|
||||||
|
val assetPath = url.substring(
|
||||||
|
url.indexOf(INJECTION_TOKEN) + INJECTION_TOKEN.length,
|
||||||
|
url.length
|
||||||
|
)
|
||||||
|
return WebResourceResponse(
|
||||||
|
"text/HTML",
|
||||||
|
"UTF-8",
|
||||||
|
context.assets.open(assetPath)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("RLog", "WebView shouldInterceptRequest: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.shouldInterceptRequest(view, url);
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPageStarted(
|
override fun onPageStarted(
|
||||||
view: WebView?,
|
view: WebView?,
|
||||||
url: String?,
|
url: String?,
|
||||||
|
@ -131,23 +152,29 @@ fun getStyle(argb: Int): String = """
|
||||||
*{
|
*{
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: ${argbToCssColor(argb)}
|
color: ${argbToCssColor(argb)};
|
||||||
|
font-family: url('/android_asset_font/font/google_sans_text_regular.TTF'),
|
||||||
|
url('/android_asset_font/font/google_sans_text_medium_italic.TTF'),
|
||||||
|
url('/android_asset_font/font/google_sans_text_medium.TTF'),
|
||||||
|
url('/android_asset_font/font/google_sans_text_italic.TTF'),
|
||||||
|
url('/android_asset_font/font/google_sans_text_bold_italic.TTF'),
|
||||||
|
url('/android_asset_font/font/google_sans_text_bold.TTF');
|
||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
padding: 0 20px;
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
margin: 0 -20px 20px;
|
margin: 0 -24px 20px;
|
||||||
width: calc(100% + 40px);
|
width: calc(100% + 48px);
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
p,span,a,ol,ul,blockquote,article,section {
|
p,span,a,ol,ul,blockquote,article,section {
|
||||||
text-align: justify;
|
text-align: left;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
line-height: 32px;
|
line-height: 24px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,6 +213,7 @@ hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,h2,h3,h4,h5,h6,figure,br {
|
h1,h2,h3,h4,h5,h6,figure,br {
|
||||||
|
font-size: large;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,12 @@
|
||||||
<string name="parse_full_content">全文解析</string>
|
<string name="parse_full_content">全文解析</string>
|
||||||
<string name="add_to_group">添加到组</string>
|
<string name="add_to_group">添加到组</string>
|
||||||
<string name="new_group">新建分组</string>
|
<string name="new_group">新建分组</string>
|
||||||
|
<string name="open_with">打开 %1$s</string>
|
||||||
|
<string name="options">选项</string>
|
||||||
|
<string name="delete">删除</string>
|
||||||
|
<string name="has_been_deleted">%1$s 已被删除</string>
|
||||||
|
<string name="unsubscribe">取消订阅</string>
|
||||||
|
<string name="unsubscribe_tip">不再订阅 %1$s,同时删除其所有已归档的文章。</string>
|
||||||
<string name="today">今天</string>
|
<string name="today">今天</string>
|
||||||
<string name="yesterday">昨天</string>
|
<string name="yesterday">昨天</string>
|
||||||
<string name="date_at_time">%1$s %2$s</string>
|
<string name="date_at_time">%1$s %2$s</string>
|
||||||
|
|
|
@ -30,6 +30,12 @@
|
||||||
<string name="parse_full_content">Parse Full Content</string>
|
<string name="parse_full_content">Parse Full Content</string>
|
||||||
<string name="add_to_group">Add to Group</string>
|
<string name="add_to_group">Add to Group</string>
|
||||||
<string name="new_group">New Group</string>
|
<string name="new_group">New Group</string>
|
||||||
|
<string name="open_with">Open %1$s</string>
|
||||||
|
<string name="options">Options</string>
|
||||||
|
<string name="delete">Delete</string>
|
||||||
|
<string name="has_been_deleted">%1$s has been deleted</string>
|
||||||
|
<string name="unsubscribe">Unsubscribe</string>
|
||||||
|
<string name="unsubscribe_tip">Unsubscribe %1$s and delete all its archived articles.</string>
|
||||||
<string name="today">Today</string>
|
<string name="today">Today</string>
|
||||||
<string name="yesterday">Yesterday</string>
|
<string name="yesterday">Yesterday</string>
|
||||||
<string name="date_at_time">%1$s At %2$s</string>
|
<string name="date_at_time">%1$s At %2$s</string>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user