Refactor Material You design for ReadPage and Add feed option feature

This commit is contained in:
Ash 2022-03-27 02:25:54 +08:00
parent 518dd6b59c
commit 435eb67c55
37 changed files with 1044 additions and 682 deletions

View File

@ -15,6 +15,8 @@ import kotlinx.coroutines.runBlocking
import java.io.IOException import java.io.IOException
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val Context.currentAccountId: Int
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) { suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
this.edit { this.edit {

View File

@ -7,6 +7,15 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface ArticleDao { interface ArticleDao {
@Query(
"""
DELETE FROM article
WHERE accountId = :accountId
AND feedId = :feedId
"""
)
suspend fun deleteByFeedId(accountId: Int, feedId: String)
@Transaction @Transaction
@Query( @Query(
""" """

View File

@ -4,6 +4,14 @@ import androidx.room.*
@Dao @Dao
interface FeedDao { interface FeedDao {
@Query(
"""
SELECT * FROM feed
WHERE id = :id
"""
)
suspend fun queryById(id: String): Feed?
@Query( @Query(
""" """
SELECT * FROM feed SELECT * FROM feed

View File

@ -8,13 +8,10 @@ import androidx.work.*
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.ash.reader.DataStoreKeys import me.ash.reader.currentAccountId
import me.ash.reader.data.account.AccountDao import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.Article import me.ash.reader.data.article.Article
import me.ash.reader.data.article.ArticleDao import me.ash.reader.data.article.ArticleDao
@ -26,8 +23,6 @@ import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.group.GroupWithFeed import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.dataStore
import me.ash.reader.get
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
abstract class AbstractRssRepository constructor( abstract class AbstractRssRepository constructor(
@ -65,14 +60,11 @@ abstract class AbstractRssRepository constructor(
} }
fun pullGroups(): Flow<MutableList<Group>> { fun pullGroups(): Flow<MutableList<Group>> {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 return groupDao.queryAllGroup(context.currentAccountId).flowOn(Dispatchers.IO)
return groupDao.queryAllGroup(accountId)
} }
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> { fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
return groupDao.queryAllGroupWithFeed( return groupDao.queryAllGroupWithFeed(context.currentAccountId).flowOn(Dispatchers.IO)
context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0
)//.flowOn(Dispatchers.IO)
} }
fun pullArticles( fun pullArticles(
@ -82,7 +74,7 @@ abstract class AbstractRssRepository constructor(
isUnread: Boolean = false, isUnread: Boolean = false,
): PagingSource<Int, ArticleWithFeed> { ): PagingSource<Int, ArticleWithFeed> {
Log.i("RLog", "thread:pullArticles ${Thread.currentThread().name}") Log.i("RLog", "thread:pullArticles ${Thread.currentThread().name}")
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 val accountId = context.currentAccountId
Log.i( Log.i(
"RLog", "RLog",
"pullArticles: accountId: ${accountId}, groupId: ${groupId}, feedId: ${feedId}, isStarred: ${isStarred}, isUnread: ${isUnread}" "pullArticles: accountId: ${accountId}, groupId: ${groupId}, feedId: ${feedId}, isStarred: ${isStarred}, isUnread: ${isUnread}"
@ -118,7 +110,7 @@ abstract class AbstractRssRepository constructor(
): Flow<List<ImportantCount>> { ): Flow<List<ImportantCount>> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
Log.i("RLog", "thread:pullImportant ${Thread.currentThread().name}") Log.i("RLog", "thread:pullImportant ${Thread.currentThread().name}")
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!! val accountId = context.currentAccountId
Log.i( Log.i(
"RLog", "RLog",
"pullImportant: accountId: ${accountId}, isStarred: ${isStarred}, isUnread: ${isUnread}" "pullImportant: accountId: ${accountId}, isStarred: ${isStarred}, isUnread: ${isUnread}"
@ -130,7 +122,11 @@ abstract class AbstractRssRepository constructor(
.queryImportantCountWhenIsUnread(accountId, isUnread) .queryImportantCountWhenIsUnread(accountId, isUnread)
else -> articleDao.queryImportantCountWhenIsAll(accountId) else -> articleDao.queryImportantCountWhenIsAll(accountId)
} }
}//.flowOn(Dispatchers.IO) }.flowOn(Dispatchers.IO)
}
suspend fun findFeedById(id: String): Feed? {
return feedDao.queryById(id)
} }
suspend fun findArticleById(id: String): ArticleWithFeed? { suspend fun findArticleById(id: String): ArticleWithFeed? {
@ -138,14 +134,32 @@ abstract class AbstractRssRepository constructor(
} }
suspend fun isExist(url: String): Boolean { suspend fun isExist(url: String): Boolean {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!! return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
return feedDao.queryByLink(accountId, url).isNotEmpty()
} }
fun peekWork(): String { fun peekWork(): String {
return workManager.getWorkInfosByTag("sync").get().size.toString() return workManager.getWorkInfosByTag("sync").get().size.toString()
} }
suspend fun updateGroup(group: Group) {
groupDao.update(group)
}
suspend fun updateFeed(feed: Feed) {
feedDao.update(feed)
}
suspend fun deleteGroup(group: Group) {
groupDao.update(group)
}
suspend fun deleteFeed(feed: Feed) {
withContext(Dispatchers.IO) {
articleDao.deleteByFeedId(context.currentAccountId, feed.id)
feedDao.delete(feed)
}
}
companion object { companion object {
val mutex = Mutex() val mutex = Mutex()

View File

@ -2,11 +2,13 @@ package me.ash.reader.data.repository
import android.content.Context import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.* import me.ash.reader.R
import me.ash.reader.currentAccountId
import me.ash.reader.data.account.Account import me.ash.reader.data.account.Account
import me.ash.reader.data.account.AccountDao import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.group.Group import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao import me.ash.reader.data.group.GroupDao
import me.ash.reader.spacerDollar
import javax.inject.Inject import javax.inject.Inject
class AccountRepository @Inject constructor( class AccountRepository @Inject constructor(
@ -17,8 +19,7 @@ class AccountRepository @Inject constructor(
) { ) {
suspend fun getCurrentAccount(): Account? { suspend fun getCurrentAccount(): Account? {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 return accountDao.queryById(context.currentAccountId)
return accountDao.queryById(accountId)
} }
suspend fun isNoAccount(): Boolean { suspend fun isNoAccount(): Boolean {

View File

@ -5,7 +5,7 @@ import android.util.Log
import androidx.work.WorkManager import androidx.work.WorkManager
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import me.ash.reader.DataStoreKeys import me.ash.reader.*
import me.ash.reader.data.account.AccountDao import me.ash.reader.data.account.AccountDao
import me.ash.reader.data.article.Article import me.ash.reader.data.article.Article
import me.ash.reader.data.article.ArticleDao import me.ash.reader.data.article.ArticleDao
@ -15,9 +15,6 @@ import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupDao import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.source.FeverApiDataSource import me.ash.reader.data.source.FeverApiDataSource
import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.dataStore
import me.ash.reader.get
import me.ash.reader.spacerDollar
import net.dankito.readability4j.extended.Readability4JExtended import net.dankito.readability4j.extended.Readability4JExtended
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -49,13 +46,12 @@ class FeverRssRepository @Inject constructor(
} }
override suspend fun addGroup(name: String): String { override suspend fun addGroup(name: String): String {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
return UUID.randomUUID().toString().also { return UUID.randomUUID().toString().also {
groupDao.insert( groupDao.insert(
Group( Group(
id = it, id = it,
name = name, name = name,
accountId = accountId accountId = context.currentAccountId
) )
) )
} }
@ -63,8 +59,7 @@ class FeverRssRepository @Inject constructor(
override suspend fun sync() { override suspend fun sync() {
mutex.withLock { mutex.withLock {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) val accountId = context.currentAccountId
?: return
updateSyncState { updateSyncState {
it.copy( it.copy(

View File

@ -73,13 +73,12 @@ class LocalRssRepository @Inject constructor(
} }
override suspend fun addGroup(name: String): String { override suspend fun addGroup(name: String): String {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!!
return UUID.randomUUID().toString().also { return UUID.randomUUID().toString().also {
groupDao.insert( groupDao.insert(
Group( Group(
id = it, id = it,
name = name, name = name,
accountId = accountId accountId = context.currentAccountId
) )
) )
} }
@ -89,8 +88,7 @@ class LocalRssRepository @Inject constructor(
mutex.withLock { mutex.withLock {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val preTime = System.currentTimeMillis() val preTime = System.currentTimeMillis()
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) val accountId = context.currentAccountId
?: return@withContext
val feeds = async { feedDao.queryAll(accountId) } val feeds = async { feedDao.queryAll(accountId) }
val articles = feeds.await().also { feed -> val articles = feeds.await().also { feed ->
updateSyncState { updateSyncState {

View File

@ -3,14 +3,12 @@ package me.ash.reader.data.repository
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.DataStoreKeys import me.ash.reader.currentAccountId
import me.ash.reader.data.article.Article import me.ash.reader.data.article.Article
import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.feed.FeedWithArticle import me.ash.reader.data.feed.FeedWithArticle
import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.dataStore
import me.ash.reader.get
import me.ash.reader.spacerDollar import me.ash.reader.spacerDollar
import net.dankito.readability4j.Readability4J import net.dankito.readability4j.Readability4J
import net.dankito.readability4j.extended.Readability4JExtended import net.dankito.readability4j.extended.Readability4JExtended
@ -26,7 +24,7 @@ class RssHelper @Inject constructor(
) { ) {
@Throws(Exception::class) @Throws(Exception::class)
suspend fun searchFeed(feedLink: String): FeedWithArticle { suspend fun searchFeed(feedLink: String): FeedWithArticle {
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 val accountId = context.currentAccountId
val parseRss = rssNetworkDataSource.parseRss(feedLink) val parseRss = rssNetworkDataSource.parseRss(feedLink)
val feed = Feed( val feed = Feed(
id = accountId.spacerDollar(UUID.randomUUID().toString()), id = accountId.spacerDollar(UUID.randomUUID().toString()),

View File

@ -4,12 +4,10 @@ import android.content.Context
import android.util.Log import android.util.Log
import android.util.Xml import android.util.Xml
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.DataStoreKeys import me.ash.reader.currentAccountId
import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.Feed
import me.ash.reader.data.group.Group import me.ash.reader.data.group.Group
import me.ash.reader.data.group.GroupWithFeed import me.ash.reader.data.group.GroupWithFeed
import me.ash.reader.dataStore
import me.ash.reader.get
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
@ -22,7 +20,7 @@ class OpmlLocalDataSource @Inject constructor(
// @Throws(XmlPullParserException::class, IOException::class) // @Throws(XmlPullParserException::class, IOException::class)
fun parseFileInputStream(inputStream: InputStream): List<GroupWithFeed> { fun parseFileInputStream(inputStream: InputStream): List<GroupWithFeed> {
val groupWithFeedList = mutableListOf<GroupWithFeed>() val groupWithFeedList = mutableListOf<GroupWithFeed>()
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 val accountId = context.currentAccountId
inputStream.use { inputStream.use {
val parser: XmlPullParser = Xml.newPullParser() val parser: XmlPullParser = Xml.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)

View File

@ -1,12 +1,23 @@
package me.ash.reader.ui.extension package me.ash.reader.ui.extension
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.reflect.KProperty
@Composable @Composable
fun <T> StateFlow<T>.collectAsStateValue( fun <T> StateFlow<T>.collectAsStateValue(
context: CoroutineContext = EmptyCoroutineContext context: CoroutineContext = Dispatchers.Default
): T = collectAsState(context).value ): T = collectAsState(context).value
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}

View File

@ -6,15 +6,12 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.foundation.layout.Row
import androidx.compose.material.ModalBottomSheetState import androidx.compose.foundation.layout.Spacer
import androidx.compose.material.ModalBottomSheetValue import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.ProvideWindowInsets
@ -24,16 +21,10 @@ import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import me.ash.reader.ui.page.home.FeedOptionDrawer
import me.ash.reader.ui.page.home.HomePage import me.ash.reader.ui.page.home.HomePage
import me.ash.reader.ui.page.settings.SettingsPage import me.ash.reader.ui.page.settings.SettingsPage
import me.ash.reader.ui.theme.AppTheme import me.ash.reader.ui.theme.AppTheme
@OptIn(ExperimentalMaterialApi::class)
val LocalDrawerState = staticCompositionLocalOf<ModalBottomSheetState> {
error("CompositionLocal LocalDrawerState not present")
}
@OptIn(ExperimentalAnimationApi::class, androidx.compose.material.ExperimentalMaterialApi::class) @OptIn(ExperimentalAnimationApi::class, androidx.compose.material.ExperimentalMaterialApi::class)
@Composable @Composable
fun HomeEntry() { fun HomeEntry() {
@ -46,10 +37,6 @@ fun HomeEntry() {
setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme()) setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme())
setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme()) setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme())
} }
Box {
CompositionLocalProvider(
LocalDrawerState provides rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
) {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -57,7 +44,6 @@ fun HomeEntry() {
.statusBarsPadding() .statusBarsPadding()
) { ) {
AnimatedNavHost( AnimatedNavHost(
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
navController = navController, navController = navController,
startDestination = RouteName.HOME, startDestination = RouteName.HOME,
) { ) {
@ -151,9 +137,6 @@ fun HomeEntry() {
.fillMaxWidth() .fillMaxWidth()
) )
} }
FeedOptionDrawer(drawerState = LocalDrawerState.current)
}
}
} }
} }
} }

View File

@ -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
},
)
},
)
}
}
}

View File

@ -29,7 +29,7 @@ fun FilterBar2(
NavigationBarItem( NavigationBarItem(
icon = { icon = {
Icon( Icon(
imageVector = if (filter == item) item.filledIcon else item.icon, imageVector = item.icon,
contentDescription = item.getName() contentDescription = item.getName()
) )
}, },

View File

@ -17,10 +17,14 @@ import androidx.compose.material.ChipDefaults
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FilterChip import androidx.compose.material.FilterChip
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FiberManualRecord
import androidx.compose.material.icons.outlined.Article import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.outlined.Circle import androidx.compose.material.icons.outlined.FiberManualRecord
import androidx.compose.material.icons.outlined.Sell import androidx.compose.material.icons.outlined.TextFormat
import androidx.compose.material.icons.rounded.* import androidx.compose.material.icons.rounded.Article
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -138,7 +142,7 @@ fun HomeBottomNavBar(
.alpha(readerBarAlpha), .alpha(readerBarAlpha),
) { ) {
ReaderBar( ReaderBar(
modifier = modifier, modifier = Modifier,
disabled = disabled, disabled = disabled,
isUnread = isUnread, isUnread = isUnread,
isStarred = isStarred, isStarred = isStarred,
@ -323,67 +327,78 @@ private fun ReaderBar(
val view = LocalView.current val view = LocalView.current
var fullContent by remember { mutableStateOf(isFullContent) } var fullContent by remember { mutableStateOf(isFullContent) }
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier.fillMaxWidth()
.fillMaxWidth()
.padding(horizontal = 8.dp)
) { ) {
CanBeDisabledIconButton( CanBeDisabledIconButton(
modifier = Modifier.size(18.dp), modifier = Modifier.size(40.dp),
disabled = disabled, disabled = disabled,
imageVector = if (isUnread) { imageVector = if (isUnread) {
Icons.Rounded.Circle Icons.Filled.FiberManualRecord
} else { } else {
Icons.Outlined.Circle Icons.Outlined.FiberManualRecord
}, },
contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread), contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread),
tint = MaterialTheme.colorScheme.primary, tint = if (isUnread) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) { ) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
unreadOnClick(!isUnread) unreadOnClick(!isUnread)
} }
CanBeDisabledIconButton( CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled, disabled = disabled,
modifier = Modifier.size(28.dp),
imageVector = if (isStarred) { imageVector = if (isStarred) {
Icons.Rounded.Star Icons.Rounded.Star
} else { } else {
Icons.Rounded.StarBorder Icons.Rounded.StarOutline
}, },
contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred), contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred),
tint = MaterialTheme.colorScheme.primary, tint = if (isStarred) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) { ) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
starredOnClick(!isStarred) starredOnClick(!isStarred)
} }
CanBeDisabledIconButton( CanBeDisabledIconButton(
disabled = disabled, disabled = disabled,
modifier = Modifier.size(30.dp), modifier = Modifier.size(40.dp),
imageVector = Icons.Rounded.ExpandMore, imageVector = Icons.Rounded.ExpandMore,
contentDescription = "Next Article", contentDescription = "Next Article",
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.outline,
) { ) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
} }
CanBeDisabledIconButton( CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled, disabled = disabled,
imageVector = Icons.Outlined.Sell, imageVector = Icons.Outlined.TextFormat,
contentDescription = "Add Tag", contentDescription = "Add Tag",
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.outline,
) { ) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
} }
CanBeDisabledIconButton( CanBeDisabledIconButton(
disabled = disabled, disabled = disabled,
modifier = Modifier.size(26.dp), modifier = Modifier.size(40.dp),
imageVector = if (fullContent) { imageVector = if (fullContent) {
Icons.Rounded.Article Icons.Rounded.Article
} else { } else {
Icons.Outlined.Article Icons.Outlined.Article
}, },
contentDescription = stringResource(R.string.parse_full_content), contentDescription = stringResource(R.string.parse_full_content),
tint = MaterialTheme.colorScheme.primary, tint = if (fullContent) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) { ) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
val afterIsFullContent = !fullContent val afterIsFullContent = !fullContent

View File

@ -2,12 +2,9 @@ package me.ash.reader.ui.page.home
import android.util.Log import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -17,11 +14,13 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.findActivity import me.ash.reader.ui.extension.findActivity
import me.ash.reader.ui.page.common.ExtraName import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionDrawer
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewAction
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewModel
import me.ash.reader.ui.page.home.feeds.FeedsPage import me.ash.reader.ui.page.home.feeds.FeedsPage
import me.ash.reader.ui.page.home.flow.FlowPage import me.ash.reader.ui.page.home.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage import me.ash.reader.ui.page.home.read.ReadPage
@ -35,13 +34,13 @@ fun HomePage(
navController: NavHostController, navController: NavHostController,
viewModel: HomeViewModel = hiltViewModel(), viewModel: HomeViewModel = hiltViewModel(),
readViewModel: ReadViewModel = hiltViewModel(), readViewModel: ReadViewModel = hiltViewModel(),
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue() val viewState = viewModel.viewState.collectAsStateValue()
val filterState = viewModel.filterState.collectAsStateValue() val filterState = viewModel.filterState.collectAsStateValue()
val readState = readViewModel.viewState.collectAsStateValue() val readState = readViewModel.viewState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val drawerState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
context.findActivity()?.let { activity -> context.findActivity()?.let { activity ->
@ -82,6 +81,9 @@ fun HomePage(
if (currentPage == 2) { if (currentPage == 2) {
readViewModel.dispatch(ReadViewAction.ClearArticle) readViewModel.dispatch(ReadViewAction.ClearArticle)
} }
if (currentPage == 0) {
feedOptionViewModel.dispatch(FeedOptionViewAction.Hide(scope))
}
} }
) )
) )
@ -96,7 +98,6 @@ fun HomePage(
} }
} }
Box {
Column { Column {
ViewPager( ViewPager(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -109,20 +110,7 @@ fun HomePage(
FlowPage(navController = navController) FlowPage(navController = navController)
}, },
{ {
ReadPage( ReadPage(navController = navController)
navController = navController,
btnBackOnClickListener = {
viewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 1,
callback = {
readViewModel.dispatch(ReadViewAction.ClearArticle)
}
)
)
},
)
}, },
), ),
) )
@ -159,6 +147,6 @@ fun HomePage(
}, },
) )
} }
FeedOptionDrawer(drawerState = drawerState)
} FeedOptionDrawer()
} }

View File

@ -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),
)
}
},
)
}

View File

@ -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 ?: "")
}

View File

@ -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()
}

View File

@ -1,7 +1,8 @@
package me.ash.reader.ui.page.home.feeds package me.ash.reader.ui.page.home.feeds
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -9,24 +10,45 @@ import androidx.compose.material3.Badge
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.data.feed.Feed
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewAction
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewModel
@OptIn(
androidx.compose.foundation.ExperimentalFoundationApi::class,
androidx.compose.material.ExperimentalMaterialApi::class,
)
@Composable @Composable
fun FeedItem( fun FeedItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
name: String, feed: Feed,
important: Int, feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
val view = LocalView.current
val scope = rememberCoroutineScope()
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 14.dp) .padding(horizontal = 14.dp)
.clip(RoundedCornerShape(32.dp)) .clip(RoundedCornerShape(32.dp))
.clickable { onClick() } .combinedClickable(
onClick = {
onClick()
},
onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
feedOptionViewModel.dispatch(FeedOptionViewAction.Show(scope, feed.id))
}
)
.padding(vertical = 14.dp), .padding(vertical = 14.dp),
) { ) {
Row( Row(
@ -43,19 +65,19 @@ fun FeedItem(
) {} ) {}
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Text( Text(
text = name, text = feed.name,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
if (important != 0) { if (feed.important ?: 0 != 0) {
Badge( Badge(
modifier = Modifier.padding(end = 6.dp), modifier = Modifier.padding(end = 6.dp),
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f), containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f),
contentColor = MaterialTheme.colorScheme.outline, contentColor = MaterialTheme.colorScheme.outline,
content = { content = {
Text( Text(
text = important.toString(), text = feed.important.toString(),
style = MaterialTheme.typography.labelSmall style = MaterialTheme.typography.labelSmall
) )
}, },

View File

@ -1,7 +1,9 @@
package me.ash.reader.ui.page.home.feeds package me.ash.reader.ui.page.home.feeds
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -23,7 +25,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.collect
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.getDesc import me.ash.reader.ui.extension.getDesc
@ -167,6 +168,8 @@ fun FeedsPage(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed -> itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed ->
Crossfade(targetState = groupWithFeed) { groupWithFeed ->
Column {
GroupItem( GroupItem(
text = groupWithFeed.group.name, text = groupWithFeed.group.name,
feeds = groupWithFeed.feeds, feeds = groupWithFeed.feeds,
@ -207,6 +210,8 @@ fun FeedsPage(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
} }
}
}
item { item {
Spacer(modifier = Modifier.height(48.dp)) Spacer(modifier = Modifier.height(48.dp))
} }

View File

@ -20,10 +20,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.Feed
import me.ash.reader.ui.page.common.LocalDrawerState
@OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable @Composable
@ -36,8 +34,6 @@ fun GroupItem(
feedOnClick: (feed: Feed) -> Unit = {}, feedOnClick: (feed: Feed) -> Unit = {},
) { ) {
var expanded by remember { mutableStateOf(isExpanded) } var expanded by remember { mutableStateOf(isExpanded) }
val scope = rememberCoroutineScope()
val drawerState = LocalDrawerState.current
Column( Column(
modifier = Modifier modifier = Modifier
@ -50,9 +46,6 @@ fun GroupItem(
groupOnClick() groupOnClick()
}, },
onLongClick = { onLongClick = {
scope.launch {
drawerState.show()
}
} }
) )
.padding(top = 22.dp) .padding(top = 22.dp)
@ -97,8 +90,7 @@ fun GroupItem(
feeds.forEach { feed -> feeds.forEach { feed ->
FeedItem( FeedItem(
modifier = Modifier.padding(horizontal = 20.dp), modifier = Modifier.padding(horizontal = 20.dp),
name = feed.name, feed = feed,
important = feed.important ?: 0,
) { ) {
feedOnClick(feed) feedOnClick(feed)
} }

View File

@ -1,24 +1,29 @@
package me.ash.reader.ui.page.home.feeds.subscribe package me.ash.reader.ui.page.home.feeds.subscribe
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddAlert import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.filled.Article import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.flowlayout.MainAxisAlignment import com.google.accompanist.flowlayout.MainAxisAlignment
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.group.Group import me.ash.reader.data.group.Group
import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.widget.SelectionChip import me.ash.reader.ui.widget.SelectionChip
import me.ash.reader.ui.widget.SelectionEditorChip import me.ash.reader.ui.widget.SelectionEditorChip
import me.ash.reader.ui.widget.Subtitle import me.ash.reader.ui.widget.Subtitle
@ -74,15 +79,23 @@ fun ResultViewPage(
private fun Link( private fun Link(
text: String, text: String,
) { ) {
val context = LocalContext.current
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
SelectionContainer { SelectionContainer {
Text( Text(
modifier = Modifier.roundClick {
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(text))
)
},
text = text, text = text,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} }
} }
@ -108,12 +121,11 @@ private fun Preset(
selected = selectedAllowNotificationPreset, selected = selectedAllowNotificationPreset,
selectedIcon = { selectedIcon = {
Icon( Icon(
imageVector = Icons.Filled.AddAlert, imageVector = Icons.Outlined.Notifications,
contentDescription = stringResource(R.string.allow_notification), contentDescription = stringResource(R.string.allow_notification),
modifier = Modifier modifier = Modifier
.padding(start = 8.dp) .padding(start = 8.dp)
.size(20.dp), .size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
) )
}, },
) { ) {
@ -125,12 +137,11 @@ private fun Preset(
selected = selectedParseFullContentPreset, selected = selectedParseFullContentPreset,
selectedIcon = { selectedIcon = {
Icon( Icon(
imageVector = Icons.Filled.Article, imageVector = Icons.Outlined.Article,
contentDescription = stringResource(R.string.parse_full_content), contentDescription = stringResource(R.string.parse_full_content),
modifier = Modifier modifier = Modifier
.padding(start = 8.dp) .padding(start = 8.dp)
.size(20.dp), .size(20.dp),
tint = MaterialTheme.colorScheme.onSurface,
) )
}, },
) { ) {

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
@ -46,6 +47,7 @@ fun SearchViewPage(
onKeyboardAction: () -> Unit = {}, onKeyboardAction: () -> Unit = {},
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val clipboardManager = LocalClipboardManager.current
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -89,6 +91,7 @@ fun SearchViewPage(
} }
} else { } else {
IconButton(onClick = { IconButton(onClick = {
onLinkValueChange(clipboardManager.getText()?.text ?: "")
}) { }) {
Icon( Icon(
imageVector = Icons.Rounded.ContentPaste, imageVector = Icons.Rounded.ContentPaste,

View File

@ -18,15 +18,17 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.Dispatchers
import me.ash.reader.* import me.ash.reader.*
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.widget.Dialog import me.ash.reader.ui.widget.Dialog
import java.io.InputStream import java.io.InputStream
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
@Composable @Composable
fun SubscribeDialog( fun SubscribeDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -44,8 +46,9 @@ fun SubscribeDialog(
} }
} }
val viewState = viewModel.viewState.collectAsStateValue() val viewState = viewModel.viewState.collectAsStateValue()
val groupsState = viewState.groups.collectAsState(initial = emptyList()) val groupsState =
var dialogHeight by remember { mutableStateOf(280.dp) } viewState.groups.collectAsState(initial = emptyList(), context = Dispatchers.IO)
var dialogHeight by remember { mutableStateOf(300.dp) }
val readYouString = stringResource(R.string.read_you) val readYouString = stringResource(R.string.read_you)
val defaultString = stringResource(R.string.defaults) val defaultString = stringResource(R.string.defaults)
LaunchedEffect(viewState.visible) { LaunchedEffect(viewState.visible) {
@ -64,7 +67,7 @@ fun SubscribeDialog(
LaunchedEffect(viewState.pagerState.currentPage) { LaunchedEffect(viewState.pagerState.currentPage) {
focusManager.clearFocus() focusManager.clearFocus()
when (viewState.pagerState.currentPage) { when (viewState.pagerState.currentPage) {
0 -> dialogHeight = 280.dp 0 -> dialogHeight = 300.dp
1 -> dialogHeight = Dp.Unspecified 1 -> dialogHeight = Dp.Unspecified
} }
} }
@ -74,6 +77,7 @@ fun SubscribeDialog(
.padding(horizontal = 44.dp) .padding(horizontal = 44.dp)
.height(dialogHeight), .height(dialogHeight),
visible = viewState.visible, visible = viewState.visible,
properties = DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest = { onDismissRequest = {
focusManager.clearFocus() focusManager.clearFocus()
viewModel.dispatch(SubscribeViewAction.Hide) viewModel.dispatch(SubscribeViewAction.Hide)

View File

@ -187,7 +187,8 @@ class SubscribeViewModel @Inject constructor(
private fun inputLink(content: String) { private fun inputLink(content: String) {
_viewState.update { _viewState.update {
it.copy( it.copy(
linkContent = content linkContent = content,
errorMessage = "",
) )
} }
} }

View File

@ -1,6 +1,7 @@
package me.ash.reader.ui.page.home.flow package me.ash.reader.ui.page.home.flow
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -79,7 +80,11 @@ fun FlowPage(
actions = { actions = {
IconButton(onClick = { IconButton(onClick = {
viewModel.dispatch(FlowViewAction.PeekSyncWork) viewModel.dispatch(FlowViewAction.PeekSyncWork)
Toast.makeText(context, viewState.syncWorkInfo.length.toString(), Toast.LENGTH_SHORT) Toast.makeText(
context,
viewState.syncWorkInfo.length.toString(),
Toast.LENGTH_SHORT
)
.show() .show()
}) { }) {
Icon( Icon(
@ -99,6 +104,7 @@ fun FlowPage(
) )
}, },
content = { content = {
Crossfade(targetState = pagingItems) { pagingItems ->
LazyColumn( LazyColumn(
state = viewState.listState, state = viewState.listState,
) { ) {
@ -126,5 +132,6 @@ fun FlowPage(
generateArticleList(context, pagingItems, readViewModel, homeViewModel, scope) generateArticleList(context, pagingItems, readViewModel, homeViewModel, scope)
} }
} }
}
) )
} }

View File

@ -8,9 +8,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.ash.reader.data.article.Article import me.ash.reader.data.article.Article
import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.Feed
import me.ash.reader.formatToString import me.ash.reader.formatToString
@ -30,37 +28,31 @@ fun Header(
Intent(Intent.ACTION_VIEW, Uri.parse(article.link)) Intent(Intent.ACTION_VIEW, Uri.parse(article.link))
) )
} }
.padding(12.dp)
) { ) {
Column(modifier = Modifier.padding(10.dp)) {
Text( Text(
text = article.date.formatToString(context, atHourMinute = true), text = article.date.formatToString(context, atHourMinute = true),
fontSize = 12.sp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Medium,
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = article.title, text = article.title,
fontSize = 27.sp, color = MaterialTheme.colorScheme.onSurface,
color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
lineHeight = 34.sp,
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
article.author?.let { article.author?.let {
Text( Text(
text = article.author, text = article.author,
fontSize = 12.sp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Medium,
) )
} }
Text( Text(
text = feed.name, text = feed.name,
fontSize = 12.sp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Medium,
) )
} }
} }
}

View File

@ -1,34 +1,51 @@
package me.ash.reader.ui.page.home.read package me.ash.reader.ui.page.home.read
import androidx.compose.animation.* import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Headphones
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition import com.airbnb.lottie.compose.rememberLottieComposition
import kotlinx.coroutines.flow.collect import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.extension.paddingFixedHorizontal import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.widget.WebView import me.ash.reader.ui.widget.WebView
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ReadPage( fun ReadPage(
navController: NavHostController, navController: NavHostController,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: ReadViewModel = hiltViewModel(), viewModel: ReadViewModel = hiltViewModel(),
btnBackOnClickListener: () -> Unit, homeViewModel: HomeViewModel = hiltViewModel(),
readViewModel: ReadViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope()
val viewState = viewModel.viewState.collectAsStateValue() val viewState = viewModel.viewState.collectAsStateValue()
val composition by rememberLottieComposition(
LottieCompositionSpec.Url(
"https://assets5.lottiefiles.com/packages/lf20_9tvcldy3.json"
)
)
LaunchedEffect(viewModel.viewState) { LaunchedEffect(viewModel.viewState) {
viewModel.viewState.collect { viewModel.viewState.collect {
@ -43,18 +60,52 @@ fun ReadPage(
} }
} }
Box { Scaffold(
Column( modifier = Modifier.background(MaterialTheme.colorScheme.surface),
modifier.fillMaxSize() topBar = {
) { SmallTopAppBar(
ReadPageTopBar(btnBackOnClickListener) title = {},
navigationIcon = {
val composition by rememberLottieComposition( IconButton(onClick = {
LottieCompositionSpec.Url( homeViewModel.dispatch(
"https://assets5.lottiefiles.com/packages/lf20_9tvcldy3.json" HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 1,
callback = {
readViewModel.dispatch(ReadViewAction.ClearArticle)
}
) )
) )
}) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
)
}
},
actions = {
viewState.articleWithFeed?.let {
IconButton(onClick = {}) {
Icon(
modifier = Modifier.size(22.dp),
imageVector = Icons.Outlined.Headphones,
contentDescription = stringResource(R.string.mark_all_as_read),
tint = MaterialTheme.colorScheme.onSurface,
)
}
IconButton(onClick = {}) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
)
},
content = {
if (viewState.articleWithFeed == null) { if (viewState.articleWithFeed == null) {
LottieAnimation( LottieAnimation(
composition = composition, composition = composition,
@ -65,36 +116,28 @@ fun ReadPage(
restartOnPlay = true, restartOnPlay = true,
iterations = Int.MAX_VALUE iterations = Int.MAX_VALUE
) )
} } else {
AnimatedVisibility(
modifier = modifier.fillMaxSize(),
visible = viewState.articleWithFeed != null,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
if (viewState.articleWithFeed == null) return@AnimatedVisibility
LazyColumn( LazyColumn(
state = viewState.listState, state = viewState.listState,
modifier = Modifier
.weight(1f),
) { ) {
val article = viewState.articleWithFeed.article val article = viewState.articleWithFeed.article
val feed = viewState.articleWithFeed.feed val feed = viewState.articleWithFeed.feed
item { item {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(2.dp))
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .padding(horizontal = 12.dp)
.paddingFixedHorizontal()
) { ) {
Header(context, article, feed) Header(context, article, feed)
} }
} }
item { item {
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(22.dp))
Crossfade(targetState = viewState.content) { content ->
WebView( WebView(
content = viewState.content ?: "", content = content ?: "",
) )
Spacer(modifier = Modifier.height(50.dp)) Spacer(modifier = Modifier.height(50.dp))
} }
@ -102,4 +145,5 @@ fun ReadPage(
} }
} }
} }
)
} }

View File

@ -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,
)
}
},
)
}

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.widget package me.ash.reader.ui.widget
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -42,7 +43,8 @@ fun Banner(
.padding(16.dp, 20.dp), .padding(16.dp, 20.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
icon?.let { icon?.let { icon ->
Crossfade(targetState = icon) {
Icon( Icon(
imageVector = it, imageVector = it,
contentDescription = null, contentDescription = null,
@ -50,6 +52,7 @@ fun Banner(
tint = lightOnSurface, tint = lightOnSurface,
) )
} }
}
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = title, text = title,

View File

@ -8,11 +8,10 @@ import androidx.compose.material.ModalBottomSheetDefaults
import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
@ -23,27 +22,28 @@ fun BottomDrawer(
drawerState: ModalBottomSheetState = androidx.compose.material.rememberModalBottomSheetState( drawerState: ModalBottomSheetState = androidx.compose.material.rememberModalBottomSheetState(
ModalBottomSheetValue.Hidden ModalBottomSheetValue.Hidden
), ),
sheetContent: @Composable ColumnScope.() -> Unit = {},
content: @Composable () -> Unit = {}, content: @Composable () -> Unit = {},
) { ) {
androidx.compose.material.ModalBottomSheetLayout( androidx.compose.material.ModalBottomSheetLayout(
sheetState = drawerState, modifier = modifier,
sheetBackgroundColor = Color.Transparent, sheetShape = RoundedCornerShape(
sheetElevation = if (drawerState.isVisible) ModalBottomSheetDefaults.Elevation else 0.dp,
sheetContent = {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(
RoundedCornerShape(
topStart = 28.0.dp, topStart = 28.0.dp,
topEnd = 28.0.dp, topEnd = 28.0.dp,
bottomEnd = 0.0.dp, bottomEnd = 0.0.dp,
bottomStart = 0.0.dp bottomStart = 0.0.dp
) ),
) sheetState = drawerState,
.background(MaterialTheme.colorScheme.surface) sheetBackgroundColor = MaterialTheme.colorScheme.surface,
.padding(horizontal = 28.dp) sheetElevation = if (drawerState.isVisible) ModalBottomSheetDefaults.Elevation else 0.dp,
.navigationBarsPadding() sheetContent = {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 6.dp,
) {
Row(
modifier = Modifier.padding(horizontal = 28.dp)
) { ) {
Box { Box {
Row( Row(
@ -62,12 +62,13 @@ fun BottomDrawer(
} }
Column { Column {
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(40.dp))
content() sheetContent()
Spacer(modifier = Modifier.height(28.dp)) Spacer(modifier = Modifier.height(28.dp))
} }
} }
} }
}
}, },
content = {} content = content,
) )
} }

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.widget package me.ash.reader.ui.widget
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
@ -9,20 +10,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable @Composable
fun CanBeDisabledIconButton( fun CanBeDisabledIconButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
disabled: Boolean, disabled: Boolean,
imageVector: ImageVector, imageVector: ImageVector,
size: Dp = 24.dp,
contentDescription: String?, contentDescription: String?,
tint: Color = LocalContentColor.current, tint: Color = LocalContentColor.current,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
) { ) {
IconButton( IconButton(
modifier = Modifier.alpha( modifier = modifier.alpha(
if (disabled) { if (disabled) {
0.7f 0.5f
} else { } else {
1f 1f
} }
@ -31,7 +35,7 @@ fun CanBeDisabledIconButton(
onClick = onClick, onClick = onClick,
) { ) {
Icon( Icon(
modifier = modifier, modifier = Modifier.size(size),
imageVector = imageVector, imageVector = imageVector,
contentDescription = contentDescription, contentDescription = contentDescription,
tint = if (disabled) MaterialTheme.colorScheme.outline else tint, tint = if (disabled) MaterialTheme.colorScheme.outline else tint,

View File

@ -2,15 +2,14 @@ package me.ash.reader.ui.widget
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun Dialog( fun Dialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
visible: Boolean, visible: Boolean,
properties: DialogProperties = DialogProperties(),
onDismissRequest: () -> Unit = {}, onDismissRequest: () -> Unit = {},
icon: @Composable (() -> Unit)? = null, icon: @Composable (() -> Unit)? = null,
title: @Composable (() -> Unit)? = null, title: @Composable (() -> Unit)? = null,
@ -20,7 +19,7 @@ fun Dialog(
) { ) {
if (visible) { if (visible) {
AlertDialog( AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false), properties = properties,
modifier = modifier, modifier = modifier,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
icon = icon, icon = icon,

View File

@ -2,10 +2,7 @@ package me.ash.reader.ui.widget
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@ -14,7 +11,7 @@ import androidx.compose.material.ChipDefaults
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FilterChip import androidx.compose.material.FilterChip
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.rounded.Check
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -41,16 +38,7 @@ fun SelectionChip(
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = CircleShape, shape: Shape = CircleShape,
border: BorderStroke? = null, border: BorderStroke? = null,
selectedIcon: @Composable () -> Unit = { selectedIcon: @Composable (() -> Unit)? = null,
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = stringResource(R.string.selected),
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
tint = MaterialTheme.colorScheme.onSurface
)
},
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
@ -59,20 +47,28 @@ fun SelectionChip(
modifier = modifier, modifier = modifier,
colors = ChipDefaults.filterChipColors( colors = ChipDefaults.filterChipColors(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant, backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.outline, contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
leadingIconColor = MaterialTheme.colorScheme.onSurface, leadingIconColor = MaterialTheme.colorScheme.surfaceVariant,
disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), disabledBackgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),
disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer, selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer,
selectedContentColor = MaterialTheme.colorScheme.onSurface, selectedContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedLeadingIconColor = MaterialTheme.colorScheme.onSurface selectedLeadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer
), ),
border = border, border = border,
interactionSource = interactionSource, interactionSource = interactionSource,
enabled = enabled, enabled = enabled,
selected = selected, selected = selected,
selectedIcon = selectedIcon, selectedIcon = selectedIcon ?: {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = stringResource(R.string.selected),
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
)
},
shape = shape, shape = shape,
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
@ -88,11 +84,6 @@ fun SelectionChip(
), ),
text = content, text = content,
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
color = if (selected) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.outline
},
) )
}, },
) )
@ -108,16 +99,7 @@ fun SelectionEditorChip(
enabled: Boolean = true, enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = CircleShape, shape: Shape = CircleShape,
selectedIcon: @Composable () -> Unit = { selectedIcon: @Composable (() -> Unit)? = null,
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = stringResource(R.string.selected),
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
},
onKeyboardAction: () -> Unit = {}, onKeyboardAction: () -> Unit = {},
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
@ -125,21 +107,30 @@ fun SelectionEditorChip(
val placeholder = stringResource(R.string.add_to_group) val placeholder = stringResource(R.string.add_to_group)
FilterChip( FilterChip(
modifier = modifier.defaultMinSize(minHeight = 36.dp),
colors = ChipDefaults.filterChipColors( colors = ChipDefaults.filterChipColors(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant, backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer, contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
leadingIconColor = MaterialTheme.colorScheme.onSecondaryContainer, leadingIconColor = MaterialTheme.colorScheme.surfaceVariant,
disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), disabledBackgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),
disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer, selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer,
selectedContentColor = MaterialTheme.colorScheme.onSecondaryContainer, selectedContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedLeadingIconColor = MaterialTheme.colorScheme.onSecondaryContainer selectedLeadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer
), ),
interactionSource = interactionSource, interactionSource = interactionSource,
enabled = enabled, enabled = enabled,
selected = selected, selected = selected,
selectedIcon = selectedIcon, selectedIcon = selectedIcon ?: {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = stringResource(R.string.selected),
modifier = Modifier
.padding(start = 8.dp)
.size(20.dp),
)
},
shape = shape, shape = shape,
onClick = onClick, onClick = onClick,
content = { content = {
@ -160,13 +151,9 @@ fun SelectionEditorChip(
}, },
value = content, value = content,
onValueChange = { onValueChange(it) }, onValueChange = { onValueChange(it) },
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSecondaryContainer), cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
textStyle = MaterialTheme.typography.titleSmall.copy( textStyle = MaterialTheme.typography.titleSmall.copy(
color = if (selected) { color = MaterialTheme.colorScheme.onSurfaceVariant,
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.outline
},
), ),
decorationBox = { innerTextField -> decorationBox = { innerTextField ->
Row( Row(
@ -176,7 +163,7 @@ fun SelectionEditorChip(
if (content.isEmpty()) { if (content.isEmpty()) {
Text( Text(
text = placeholder, text = placeholder,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
) )
} }

View File

@ -17,6 +17,8 @@ import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.page.home.read.ReadViewAction import me.ash.reader.ui.page.home.read.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel import me.ash.reader.ui.page.home.read.ReadViewModel
const val INJECTION_TOKEN = "/android_asset_font/"
@Composable @Composable
fun WebView( fun WebView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -26,11 +28,30 @@ fun WebView(
onReceivedError: (error: WebResourceError?) -> Unit = {} onReceivedError: (error: WebResourceError?) -> Unit = {}
) { ) {
val context = LocalContext.current val context = LocalContext.current
val color = MaterialTheme.colorScheme.secondary.toArgb() val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb()
val backgroundColor = MaterialTheme.colorScheme.surface.toArgb() val backgroundColor = MaterialTheme.colorScheme.surface.toArgb()
val viewState = viewModel.viewState.collectAsStateValue() val viewState = viewModel.viewState.collectAsStateValue()
val webViewClient = object : WebViewClient() { val webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? {
if (url != null && url.contains(INJECTION_TOKEN)) {
try {
val assetPath = url.substring(
url.indexOf(INJECTION_TOKEN) + INJECTION_TOKEN.length,
url.length
)
return WebResourceResponse(
"text/HTML",
"UTF-8",
context.assets.open(assetPath)
)
} catch (e: Exception) {
Log.e("RLog", "WebView shouldInterceptRequest: $e")
}
}
return super.shouldInterceptRequest(view, url);
}
override fun onPageStarted( override fun onPageStarted(
view: WebView?, view: WebView?,
url: String?, url: String?,
@ -131,23 +152,29 @@ fun getStyle(argb: Int): String = """
*{ *{
padding: 0; padding: 0;
margin: 0; margin: 0;
color: ${argbToCssColor(argb)} color: ${argbToCssColor(argb)};
font-family: url('/android_asset_font/font/google_sans_text_regular.TTF'),
url('/android_asset_font/font/google_sans_text_medium_italic.TTF'),
url('/android_asset_font/font/google_sans_text_medium.TTF'),
url('/android_asset_font/font/google_sans_text_italic.TTF'),
url('/android_asset_font/font/google_sans_text_bold_italic.TTF'),
url('/android_asset_font/font/google_sans_text_bold.TTF');
} }
.page { .page {
padding: 0 20px; padding: 0 24px;
} }
img { img {
margin: 0 -20px 20px; margin: 0 -24px 20px;
width: calc(100% + 40px); width: calc(100% + 48px);
height: auto; height: auto;
} }
p,span,a,ol,ul,blockquote,article,section { p,span,a,ol,ul,blockquote,article,section {
text-align: justify; text-align: left;
font-size: 18px; font-size: 16px;
line-height: 32px; line-height: 24px;
margin-bottom: 20px; margin-bottom: 20px;
} }
@ -186,6 +213,7 @@ hr {
} }
h1,h2,h3,h4,h5,h6,figure,br { h1,h2,h3,h4,h5,h6,figure,br {
font-size: large;
margin-bottom: 20px; margin-bottom: 20px;
} }

View File

@ -30,6 +30,12 @@
<string name="parse_full_content">全文解析</string> <string name="parse_full_content">全文解析</string>
<string name="add_to_group">添加到组</string> <string name="add_to_group">添加到组</string>
<string name="new_group">新建分组</string> <string name="new_group">新建分组</string>
<string name="open_with">打开 %1$s</string>
<string name="options">选项</string>
<string name="delete">删除</string>
<string name="has_been_deleted">%1$s 已被删除</string>
<string name="unsubscribe">取消订阅</string>
<string name="unsubscribe_tip">不再订阅 %1$s同时删除其所有已归档的文章。</string>
<string name="today">今天</string> <string name="today">今天</string>
<string name="yesterday">昨天</string> <string name="yesterday">昨天</string>
<string name="date_at_time">%1$s %2$s</string> <string name="date_at_time">%1$s %2$s</string>

View File

@ -30,6 +30,12 @@
<string name="parse_full_content">Parse Full Content</string> <string name="parse_full_content">Parse Full Content</string>
<string name="add_to_group">Add to Group</string> <string name="add_to_group">Add to Group</string>
<string name="new_group">New Group</string> <string name="new_group">New Group</string>
<string name="open_with">Open %1$s</string>
<string name="options">Options</string>
<string name="delete">Delete</string>
<string name="has_been_deleted">%1$s has been deleted</string>
<string name="unsubscribe">Unsubscribe</string>
<string name="unsubscribe_tip">Unsubscribe %1$s and delete all its archived articles.</string>
<string name="today">Today</string> <string name="today">Today</string>
<string name="yesterday">Yesterday</string> <string name="yesterday">Yesterday</string>
<string name="date_at_time">%1$s At %2$s</string> <string name="date_at_time">%1$s At %2$s</string>