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