Use localized language

This commit is contained in:
Ash 2022-03-21 00:07:45 +08:00
parent e9f7ac85ab
commit 582e0f8148
33 changed files with 343 additions and 240 deletions

View File

@ -21,9 +21,9 @@
- [x] 全文解析 - [x] 全文解析
- [x] 过滤未读、星标 - [x] 过滤未读、星标
- [x] Feed 分组 - [x] Feed 分组
- [x] 本地化
- [ ] 文章搜索 - [ ] 文章搜索
- [ ] 偏好设置 - [ ] 偏好设置
- [ ] 本地化
- [ ] 发布 APK - [ ] 发布 APK
- [ ] 小组件 - [ ] 小组件
- [ ] ... - [ ] ...

View File

@ -21,9 +21,9 @@ The following are the progress made so far and the goals to be worked on in the
- [x] Full Content Parsing - [x] Full Content Parsing
- [x] Filter unread and starred - [x] Filter unread and starred
- [x] Feed Grouping - [x] Feed Grouping
- [x] Localization
- [ ] Search for articles - [ ] Search for articles
- [ ] Preference settings - [ ] Preference settings
- [ ] Localization
- [ ] Release APK - [ ] Release APK
- [ ] Widget - [ ] Widget
- [ ] ... - [ ] ...

View File

@ -8,7 +8,7 @@
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/read_you"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true" android:supportsRtl="true"

View File

@ -26,6 +26,9 @@ class App : Application() {
@Inject @Inject
lateinit var rssHelper: RssHelper lateinit var rssHelper: RssHelper
@Inject
lateinit var stringsRepository: StringsRepository
@Inject @Inject
lateinit var accountRepository: AccountRepository lateinit var accountRepository: AccountRepository

View File

@ -0,0 +1,42 @@
package me.ash.reader
import android.content.Context
import androidx.core.os.ConfigurationCompat
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
fun Date.formatToString(
context: Context,
onlyHourMinute: Boolean? = false,
atHourMinute: Boolean? = false,
): String {
val locale = ConfigurationCompat.getLocales(context.resources.configuration)[0]
val df = DateFormat.getDateInstance(DateFormat.FULL, locale)
return when {
onlyHourMinute == true -> {
SimpleDateFormat("HH:mm", locale).format(this)
}
atHourMinute == true -> {
context.getString(
R.string.date_at_time,
df.format(this),
SimpleDateFormat("HH:mm", locale).format(this),
)
}
else -> {
df.format(this).run {
when (this) {
df.format(Date()) -> context.getString(R.string.today)
df.format(
Calendar.getInstance().apply {
time = Date()
add(Calendar.DAY_OF_MONTH, -1)
}.time
) -> context.getString(R.string.yesterday)
else -> this
}
}
}
}
}

View File

@ -1,53 +0,0 @@
package me.ash.reader
import java.text.SimpleDateFormat
import java.util.*
object DateTimeExt {
const val HH_MM_SS = "HH:mm:ss"
const val HH_MM = "HH:mm"
const val MM_SS = "mm:ss"
const val YYYY_MM_DD_HH_MM_SS = "yyyy年MM月dd日 HH:mm:ss"
const val YYYY_MM_DD_HH_MM = "yyyy年MM月dd日 HH:mm"
const val YYYY_MM_DD = "yyyy年MM月dd日"
const val YYYY_MM = "yyyy年MM月"
const val YYYY = "yyyy年"
const val MM = "MM月"
const val DD = "dd日"
/**
* Returns a date-time [String] format from a [Date] object.
*/
fun Date.toString(pattern: String, simpleDate: Boolean? = false): String {
return if (simpleDate == true) {
val format = if (pattern == YYYY_MM_DD) {
""
} else {
SimpleDateFormat(
pattern.replace(YYYY_MM_DD, "")
).format(this)
}
when (this.toString(YYYY_MM_DD)) {
Date().toString(YYYY_MM_DD) -> {
"今天${format}"
}
Calendar.getInstance().apply {
time = Date()
add(Calendar.DAY_OF_MONTH, -1)
}.time.toString(YYYY_MM_DD) -> {
"昨天${format}"
}
else -> SimpleDateFormat(pattern).format(this)
}
} else {
SimpleDateFormat(pattern).format(this)
}
}
/**
* Returns a [Date] object parsed from a date-time [String].
*/
fun String.toDate(pattern: String? = null): Date =
SimpleDateFormat((pattern ?: YYYY_MM_DD_HH_MM_SS)).parse(this)
}

View File

@ -8,8 +8,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
class Filter( class Filter(
var index: Int, var index: Int,
var name: String,
var description: String,
var important: Int, var important: Int,
var icon: ImageVector, var icon: ImageVector,
) { ) {
@ -20,22 +18,16 @@ class Filter(
companion object { companion object {
val Starred = Filter( val Starred = Filter(
index = 0, index = 0,
name = "Starred",
description = " Starred Items",
important = 13, important = 13,
icon = Icons.Rounded.StarOutline, icon = Icons.Rounded.StarOutline,
) )
val Unread = Filter( val Unread = Filter(
index = 1, index = 1,
name = "Unread",
description = " Unread Items",
important = 666, important = 666,
icon = Icons.Outlined.FiberManualRecord, icon = Icons.Outlined.FiberManualRecord,
) )
val All = Filter( val All = Filter(
index = 2, index = 2,
name = "All",
description = " Unread Items",
important = 666, important = 666,
icon = Icons.Rounded.Subject, icon = Icons.Rounded.Subject,
) )

View File

@ -1,18 +0,0 @@
package me.ash.reader.data.constant
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.FiberManualRecord
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.Subject
import androidx.compose.ui.graphics.vector.ImageVector
class NavigationBarItem(
var title: String,
var icon: ImageVector,
) {
companion object {
val Starred = NavigationBarItem("STARRED", Icons.Rounded.Star)
val Unread = NavigationBarItem("UNREAD", Icons.Rounded.FiberManualRecord)
val All = NavigationBarItem("ALL", Icons.Rounded.Subject)
}
}

View File

@ -1,8 +0,0 @@
package me.ash.reader.data.constant
object Symbol {
const val NOTHING: String = "Null"
const val Unknown: String = "Unknown"
const val NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE: String = "article.update"
const val EXTRA_ARTICLE_ID: String = "article.id"
}

View File

@ -2,14 +2,11 @@ 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.DataStoreKeys import me.ash.reader.*
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.dataStore
import me.ash.reader.get
import me.ash.reader.spacerDollar
import javax.inject.Inject import javax.inject.Inject
class AccountRepository @Inject constructor( class AccountRepository @Inject constructor(
@ -29,8 +26,10 @@ class AccountRepository @Inject constructor(
} }
suspend fun addDefaultAccount(): Account { suspend fun addDefaultAccount(): Account {
val readYouString = context.getString(R.string.read_you)
val defaultString = context.getString(R.string.defaults)
return Account( return Account(
name = "Read You", name = readYouString,
type = Account.Type.LOCAL, type = Account.Type.LOCAL,
).apply { ).apply {
id = accountDao.insert(this).toInt() id = accountDao.insert(this).toInt()
@ -38,8 +37,8 @@ class AccountRepository @Inject constructor(
if (groupDao.queryAll(it.id!!).isEmpty()) { if (groupDao.queryAll(it.id!!).isEmpty()) {
groupDao.insert( groupDao.insert(
Group( Group(
id = it.id!!.spacerDollar("0"), id = it.id!!.spacerDollar(readYouString + defaultString),
name = "默认", name = defaultString,
accountId = it.id!!, accountId = it.id!!,
) )
) )

View File

@ -19,11 +19,12 @@ 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
import me.ash.reader.data.constant.Symbol
import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.Feed
import me.ash.reader.data.feed.FeedDao import me.ash.reader.data.feed.FeedDao
import me.ash.reader.data.group.GroupDao import me.ash.reader.data.group.GroupDao
import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.ui.page.common.ExtraName
import me.ash.reader.ui.page.common.NotificationGroupName
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -119,7 +120,7 @@ class LocalRssRepository @Inject constructor(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel( notificationManager.createNotificationChannel(
NotificationChannel( NotificationChannel(
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE, NotificationGroupName.ARTICLE_UPDATE,
"文章更新", "文章更新",
NotificationManager.IMPORTANCE_DEFAULT NotificationManager.IMPORTANCE_DEFAULT
) )
@ -132,9 +133,9 @@ class LocalRssRepository @Inject constructor(
if (feedNotificationMap[article.feedId] == true) { if (feedNotificationMap[article.feedId] == true) {
val builder = NotificationCompat.Builder( val builder = NotificationCompat.Builder(
context, context,
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE NotificationGroupName.ARTICLE_UPDATE
).setSmallIcon(R.drawable.ic_launcher_foreground) ).setSmallIcon(R.drawable.ic_launcher_foreground)
.setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE) .setGroup(NotificationGroupName.ARTICLE_UPDATE)
.setContentTitle(article.title) .setContentTitle(article.title)
.setContentText(article.shortDescription) .setContentText(article.shortDescription)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
@ -146,7 +147,7 @@ class LocalRssRepository @Inject constructor(
flags = Intent.FLAG_ACTIVITY_NEW_TASK or flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra( putExtra(
Symbol.EXTRA_ARTICLE_ID, ExtraName.ARTICLE_ID,
ids[index].toInt() ids[index].toInt()
) )
}, },

View File

@ -0,0 +1,12 @@
package me.ash.reader.data.repository
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class StringsRepository @Inject constructor(
@ApplicationContext
private val context: Context,
) {
fun getString(resId: Int) = context.getString(resId)
}

View File

@ -0,0 +1,20 @@
package me.ash.reader.ui.extension
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import me.ash.reader.R
import me.ash.reader.data.constant.Filter
@Composable
fun Filter.getName(): String = when (this) {
Filter.Unread -> stringResource(R.string.unread)
Filter.Starred -> stringResource(R.string.starred)
else -> stringResource(R.string.all)
}
@Composable
fun Filter.getDesc(): String = when (this) {
Filter.Unread -> stringResource(R.string.unread_desc, this.important)
Filter.Starred -> stringResource(R.string.starred_desc, this.important)
else -> stringResource(R.string.unread_desc, this.important)
}

View File

@ -0,0 +1,5 @@
package me.ash.reader.ui.page.common
object ExtraName {
const val ARTICLE_ID: String = "article.id"
}

View File

@ -0,0 +1,5 @@
package me.ash.reader.ui.page.common
object NotificationGroupName {
const val ARTICLE_UPDATE: String = "article.update"
}

View File

@ -29,13 +29,15 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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 androidx.compose.ui.unit.sp
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import me.ash.reader.R
import me.ash.reader.data.constant.Filter import me.ash.reader.data.constant.Filter
import me.ash.reader.data.constant.NavigationBarItem import me.ash.reader.ui.extension.getName
import me.ash.reader.ui.widget.CanBeDisabledIconButton import me.ash.reader.ui.widget.CanBeDisabledIconButton
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -153,10 +155,10 @@ private fun FilterBar(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
listOf( listOf(
NavigationBarItem.Starred, Filter.Starred,
NavigationBarItem.Unread, Filter.Unread,
NavigationBarItem.All Filter.All
).forEachIndexed { index, item -> ).forEach { item ->
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
@ -175,39 +177,33 @@ private fun FilterBar(
.clip(CircleShape) .clip(CircleShape)
.clickable(onClick = { .clickable(onClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
onSelected( onSelected(item)
when (index) {
0 -> Filter.Starred
1 -> Filter.Unread
else -> Filter.All
}
)
}) })
.background( .background(
if (filter.index == index) { if (filter == item) {
MaterialTheme.colorScheme.inverseOnSurface MaterialTheme.colorScheme.inverseOnSurface
} else { } else {
Color.Unspecified Color.Unspecified
} }
) )
) { ) {
if (filter.index == index) { if (filter == item) {
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Icon( Icon(
modifier = Modifier.size( modifier = Modifier.size(
if (Filter.Unread.index == index) { if (filter == item) {
15 15.dp
} else { } else {
19 19.dp
}.dp }
), ),
imageVector = item.icon, imageVector = item.icon,
contentDescription = item.title, contentDescription = item.getName(),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = item.title, text = item.getName().uppercase(),
fontSize = 11.sp, fontSize = 11.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
@ -216,14 +212,14 @@ private fun FilterBar(
} else { } else {
Icon( Icon(
modifier = Modifier.size( modifier = Modifier.size(
if (Filter.Unread.index == index) { if (item.isUnread()) {
15 15
} else { } else {
19 19
}.dp }.dp
), ),
imageVector = item.icon, imageVector = item.icon,
contentDescription = item.title, contentDescription = item.getName(),
tint = MaterialTheme.colorScheme.outline, tint = MaterialTheme.colorScheme.outline,
) )
} }
@ -261,7 +257,7 @@ private fun ReaderBar(
} else { } else {
Icons.Outlined.Circle Icons.Outlined.Circle
}, },
contentDescription = "Mark Unread", contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) { ) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
@ -275,7 +271,7 @@ private fun ReaderBar(
} else { } else {
Icons.Rounded.StarBorder Icons.Rounded.StarBorder
}, },
contentDescription = "Starred", contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) { ) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
@ -306,7 +302,7 @@ private fun ReaderBar(
} else { } else {
Icons.Outlined.Article Icons.Outlined.Article
}, },
contentDescription = "Full Content Parsing", contentDescription = stringResource(R.string.parse_full_content),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) { ) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)

View File

@ -16,9 +16,10 @@ import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.constant.Symbol import me.ash.reader.ui.page.common.NotificationGroupName
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.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
@ -42,7 +43,7 @@ fun HomePage(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
context.findActivity()?.let { activity -> context.findActivity()?.let { activity ->
activity.intent?.let { intent -> activity.intent?.let { intent ->
intent.extras?.get(Symbol.EXTRA_ARTICLE_ID)?.let { intent.extras?.get(ExtraName.ARTICLE_ID)?.let {
readViewModel.dispatch(ReadViewAction.ScrollToItem(2)) readViewModel.dispatch(ReadViewAction.ScrollToItem(2))
scope.launch { scope.launch {
val article = val article =
@ -60,7 +61,7 @@ fun HomePage(
) )
} }
} }
intent.extras?.remove(Symbol.EXTRA_ARTICLE_ID) intent.extras?.remove(ExtraName.ARTICLE_ID)
} }
} }
} }

View File

@ -19,12 +19,15 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
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 kotlinx.coroutines.flow.collect
import me.ash.reader.data.constant.Symbol 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.getName
import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
@ -77,7 +80,7 @@ fun FeedsPage(
IconButton(onClick = { }) { IconButton(onClick = { }) {
Icon( Icon(
imageVector = Icons.Rounded.ArrowBack, imageVector = Icons.Rounded.ArrowBack,
contentDescription = "Back", contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurface
) )
} }
@ -90,7 +93,7 @@ fun FeedsPage(
Icon( Icon(
modifier = Modifier.rotate(if (syncState.isSyncing) angle else 0f), modifier = Modifier.rotate(if (syncState.isSyncing) angle else 0f),
imageVector = Icons.Rounded.Refresh, imageVector = Icons.Rounded.Refresh,
contentDescription = "Refresh", contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) )
} }
@ -99,7 +102,7 @@ fun FeedsPage(
}) { }) {
Icon( Icon(
imageVector = Icons.Rounded.Add, imageVector = Icons.Rounded.Add,
contentDescription = "Subscribe", contentDescription = stringResource(R.string.subscribe),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) )
} }
@ -121,20 +124,20 @@ fun FeedsPage(
end = 24.dp, end = 24.dp,
bottom = 24.dp bottom = 24.dp
), ),
text = viewState.account?.name ?: Symbol.Unknown, text = viewState.account?.name ?: stringResource(R.string.unknown),
style = MaterialTheme.typography.displaySmall, style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
} }
item { item {
Banner( Banner(
title = viewState.filter.name, title = filterState.filter.getName(),
desc = "${viewState.filter.important}${viewState.filter.description}", desc = filterState.filter.getDesc(),
icon = viewState.filter.icon, icon = viewState.filter.icon,
action = { action = {
Icon( Icon(
imageVector = Icons.Outlined.KeyboardArrowRight, imageVector = Icons.Outlined.KeyboardArrowRight,
contentDescription = "Goto", contentDescription = stringResource(R.string.go_to),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) )
}, },
@ -159,7 +162,7 @@ fun FeedsPage(
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Subtitle( Subtitle(
modifier = Modifier.padding(start = 28.dp), modifier = Modifier.padding(start = 28.dp),
text = "Feeds" text = stringResource(R.string.feeds)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }

View File

@ -16,7 +16,9 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.ash.reader.R
import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.Feed
@Composable @Composable
@ -37,7 +39,7 @@ fun GroupItem(
.clip(RoundedCornerShape(32.dp)) .clip(RoundedCornerShape(32.dp))
.background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.14f)) .background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.14f))
.clickable { groupOnClick() } .clickable { groupOnClick() }
.padding(vertical = 22.dp) .padding(top = 22.dp)
) { ) {
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
@ -64,12 +66,12 @@ fun GroupItem(
) { ) {
Icon( Icon(
imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore, imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
contentDescription = if (expanded) "Expand Less" else "Expand More", contentDescription = stringResource(if (expanded) R.string.expand_less else R.string.expand_more),
tint = MaterialTheme.colorScheme.onSecondaryContainer, tint = MaterialTheme.colorScheme.onSecondaryContainer,
) )
} }
} }
Spacer(modifier = Modifier.height(22.dp))
AnimatedVisibility( AnimatedVisibility(
visible = expanded, visible = expanded,
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),

View File

@ -13,9 +13,11 @@ 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.res.stringResource
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.data.group.Group import me.ash.reader.data.group.Group
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
@ -25,11 +27,11 @@ import me.ash.reader.ui.widget.Subtitle
fun ResultViewPage( fun ResultViewPage(
link: String = "", link: String = "",
groups: List<Group> = emptyList(), groups: List<Group> = emptyList(),
selectedNotificationPreset: Boolean = false, selectedAllowNotificationPreset: Boolean = false,
selectedFullContentParsePreset: Boolean = false, selectedParseFullContentPreset: Boolean = false,
selectedGroupId: String = "", selectedGroupId: String = "",
notificationPresetOnClick: () -> Unit = {}, allowNotificationPresetOnClick: () -> Unit = {},
fullContentParsePresetOnClick: () -> Unit = {}, parseFullContentPresetOnClick: () -> Unit = {},
groupOnClick: (groupId: String) -> Unit = {}, groupOnClick: (groupId: String) -> Unit = {},
onKeyboardAction: () -> Unit = {}, onKeyboardAction: () -> Unit = {},
) { ) {
@ -42,10 +44,10 @@ fun ResultViewPage(
Spacer(modifier = Modifier.height(26.dp)) Spacer(modifier = Modifier.height(26.dp))
Preset( Preset(
selectedNotificationPreset = selectedNotificationPreset, selectedAllowNotificationPreset = selectedAllowNotificationPreset,
selectedFullContentParsePreset = selectedFullContentParsePreset, selectedParseFullContentPreset = selectedParseFullContentPreset,
notificationPresetOnClick = notificationPresetOnClick, allowNotificationPresetOnClick = allowNotificationPresetOnClick,
fullContentParsePresetOnClick = fullContentParsePresetOnClick, parseFullContentPresetOnClick = parseFullContentPresetOnClick,
) )
Spacer(modifier = Modifier.height(26.dp)) Spacer(modifier = Modifier.height(26.dp))
@ -78,12 +80,12 @@ private fun Link(
@Composable @Composable
private fun Preset( private fun Preset(
selectedNotificationPreset: Boolean = false, selectedAllowNotificationPreset: Boolean = false,
selectedFullContentParsePreset: Boolean = false, selectedParseFullContentPreset: Boolean = false,
notificationPresetOnClick: () -> Unit = {}, allowNotificationPresetOnClick: () -> Unit = {},
fullContentParsePresetOnClick: () -> Unit = {}, parseFullContentPresetOnClick: () -> Unit = {},
) { ) {
Subtitle(text = "预设") Subtitle(text = stringResource(R.string.preset))
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
FlowRow( FlowRow(
mainAxisAlignment = MainAxisAlignment.Start, mainAxisAlignment = MainAxisAlignment.Start,
@ -92,35 +94,35 @@ private fun Preset(
) { ) {
SelectionChip( SelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = "接收通知", content = stringResource(R.string.allow_notification),
selected = selectedNotificationPreset, selected = selectedAllowNotificationPreset,
selectedIcon = { selectedIcon = {
Icon( Icon(
imageVector = Icons.Outlined.Notifications, imageVector = Icons.Outlined.Notifications,
contentDescription = "Check", contentDescription = stringResource(R.string.allow_notification),
modifier = Modifier modifier = Modifier
.padding(start = 8.dp) .padding(start = 8.dp)
.size(18.dp), .size(18.dp),
) )
}, },
) { ) {
notificationPresetOnClick() allowNotificationPresetOnClick()
} }
SelectionChip( SelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = "全文解析", content = stringResource(R.string.parse_full_content),
selected = selectedFullContentParsePreset, selected = selectedParseFullContentPreset,
selectedIcon = { selectedIcon = {
Icon( Icon(
imageVector = Icons.Outlined.Article, imageVector = Icons.Outlined.Article,
contentDescription = "Check", contentDescription = stringResource(R.string.parse_full_content),
modifier = Modifier modifier = Modifier
.padding(start = 8.dp) .padding(start = 8.dp)
.size(18.dp), .size(18.dp),
) )
}, },
) { ) {
fullContentParsePresetOnClick() parseFullContentPresetOnClick()
} }
} }
} }
@ -132,7 +134,7 @@ private fun AddToGroup(
groupOnClick: (groupId: String) -> Unit = {}, groupOnClick: (groupId: String) -> Unit = {},
onKeyboardAction: () -> Unit = {}, onKeyboardAction: () -> Unit = {},
) { ) {
Subtitle(text = "添加到组") Subtitle(text = stringResource(R.string.add_to_group))
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
FlowRow( FlowRow(
mainAxisAlignment = MainAxisAlignment.Start, mainAxisAlignment = MainAxisAlignment.Start,
@ -151,7 +153,7 @@ private fun AddToGroup(
SelectionEditorChip( SelectionEditorChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = "新建分组", content = stringResource(R.string.new_group),
selected = false, selected = false,
onKeyboardAction = onKeyboardAction, onKeyboardAction = onKeyboardAction,
) { ) {

View File

@ -22,11 +22,17 @@ 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import me.ash.reader.R
@OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun SearchViewPage( fun SearchViewPage(
pagerState: PagerState,
inputContent: String = "", inputContent: String = "",
errorMessage: String = "", errorMessage: String = "",
onValueChange: (String) -> Unit = {}, onValueChange: (String) -> Unit = {},
@ -51,11 +57,11 @@ fun SearchViewPage(
), ),
value = inputContent, value = inputContent,
onValueChange = { onValueChange = {
onValueChange(it) if (pagerState.currentPage == 0) onValueChange(it)
}, },
placeholder = { placeholder = {
Text( Text(
text = "订阅源或站点链接", text = stringResource(R.string.feed_or_site_url),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f) color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
) )
}, },
@ -68,7 +74,7 @@ fun SearchViewPage(
}) { }) {
Icon( Icon(
imageVector = Icons.Rounded.Close, imageVector = Icons.Rounded.Close,
contentDescription = "Clear", contentDescription = stringResource(R.string.clear),
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f), tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
) )
} }
@ -77,7 +83,7 @@ fun SearchViewPage(
}) { }) {
Icon( Icon(
imageVector = Icons.Rounded.ContentPaste, imageVector = Icons.Rounded.ContentPaste,
contentDescription = "Paste", contentDescription = stringResource(R.string.paste),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} }

View File

@ -11,12 +11,11 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.DataStoreKeys import me.ash.reader.*
import me.ash.reader.dataStore import me.ash.reader.R
import me.ash.reader.get
import me.ash.reader.spacerDollar
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
@ -61,14 +60,14 @@ fun SubscribeDialog(
icon = { icon = {
Icon( Icon(
imageVector = Icons.Rounded.RssFeed, imageVector = Icons.Rounded.RssFeed,
contentDescription = "Subscribe", contentDescription = stringResource(R.string.subscribe),
) )
}, },
title = { title = {
Text( Text(
when (viewState.pagerState.currentPage) { when (viewState.pagerState.currentPage) {
0 -> "订阅" 0 -> stringResource(R.string.subscribe)
else -> viewState.feed?.name ?: "未知" else -> viewState.feed?.name ?: stringResource(R.string.unknown)
} }
) )
}, },
@ -88,15 +87,15 @@ fun SubscribeDialog(
}, },
link = viewState.inputContent, link = viewState.inputContent,
groups = groupsState.value, groups = groupsState.value,
selectedNotificationPreset = viewState.notificationPreset, selectedAllowNotificationPreset = viewState.allowNotificationPreset,
selectedFullContentParsePreset = viewState.fullContentParsePreset, selectedParseFullContentPreset = viewState.parseFullContentPreset,
selectedGroupId = viewState.selectedGroupId, selectedGroupId = viewState.selectedGroupId,
pagerState = viewState.pagerState, pagerState = viewState.pagerState,
notificationPresetOnClick = { allowNotificationPresetOnClick = {
viewModel.dispatch(SubscribeViewAction.ChangeNotificationPreset) viewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset)
}, },
fullContentParsePresetOnClick = { parseFullContentPresetOnClick = {
viewModel.dispatch(SubscribeViewAction.ChangeFullContentParsePreset) viewModel.dispatch(SubscribeViewAction.ChangeParseFullContentPreset)
}, },
groupOnClick = { groupOnClick = {
viewModel.dispatch(SubscribeViewAction.SelectedGroup(it)) viewModel.dispatch(SubscribeViewAction.SelectedGroup(it))
@ -116,7 +115,7 @@ fun SubscribeDialog(
} }
) { ) {
Text( Text(
text = "搜索", text = stringResource(R.string.search),
color = if (viewState.inputContent.isNotEmpty()) { color = if (viewState.inputContent.isNotEmpty()) {
Color.Unspecified Color.Unspecified
} else { } else {
@ -131,7 +130,7 @@ fun SubscribeDialog(
viewModel.dispatch(SubscribeViewAction.Subscribe) viewModel.dispatch(SubscribeViewAction.Subscribe)
} }
) { ) {
Text("订阅") Text(stringResource(R.string.subscribe))
} }
} }
} }
@ -145,7 +144,7 @@ fun SubscribeDialog(
viewModel.dispatch(SubscribeViewAction.Hide) viewModel.dispatch(SubscribeViewAction.Hide)
} }
) { ) {
Text("导入OPML文件") Text(text = stringResource(R.string.import_from_opml))
} }
} }
1 -> { 1 -> {
@ -154,7 +153,7 @@ fun SubscribeDialog(
viewModel.dispatch(SubscribeViewAction.Hide) viewModel.dispatch(SubscribeViewAction.Hide)
} }
) { ) {
Text("取消") Text(text = stringResource(R.string.cancel))
} }
} }
} }

View File

@ -9,12 +9,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R
import me.ash.reader.data.article.Article import me.ash.reader.data.article.Article
import me.ash.reader.data.constant.Symbol
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.repository.RssHelper import me.ash.reader.data.repository.RssHelper
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.StringsRepository
import me.ash.reader.formatUrl import me.ash.reader.formatUrl
import me.ash.reader.ui.extension.animateScrollToPage import me.ash.reader.ui.extension.animateScrollToPage
import javax.inject.Inject import javax.inject.Inject
@ -24,9 +25,10 @@ import javax.inject.Inject
class SubscribeViewModel @Inject constructor( class SubscribeViewModel @Inject constructor(
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
private val rssHelper: RssHelper, private val rssHelper: RssHelper,
private val stringsRepository: StringsRepository,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(SubScribeViewState()) private val _viewState = MutableStateFlow(SubscribeViewState())
val viewState: StateFlow<SubScribeViewState> = _viewState.asStateFlow() val viewState: StateFlow<SubscribeViewState> = _viewState.asStateFlow()
fun dispatch(action: SubscribeViewAction) { fun dispatch(action: SubscribeViewAction) {
when (action) { when (action) {
@ -36,10 +38,10 @@ class SubscribeViewModel @Inject constructor(
is SubscribeViewAction.Hide -> changeVisible(false) is SubscribeViewAction.Hide -> changeVisible(false)
is SubscribeViewAction.Input -> inputLink(action.content) is SubscribeViewAction.Input -> inputLink(action.content)
is SubscribeViewAction.Search -> search(action.scope) is SubscribeViewAction.Search -> search(action.scope)
is SubscribeViewAction.ChangeNotificationPreset -> is SubscribeViewAction.ChangeAllowNotificationPreset ->
changeNotificationPreset() changeAllowNotificationPreset()
is SubscribeViewAction.ChangeFullContentParsePreset -> is SubscribeViewAction.ChangeParseFullContentPreset ->
changeFullContentParsePreset() changeParseFullContentPreset()
is SubscribeViewAction.SelectedGroup -> selectedGroup(action.groupId) is SubscribeViewAction.SelectedGroup -> selectedGroup(action.groupId)
is SubscribeViewAction.Subscribe -> subscribe() is SubscribeViewAction.Subscribe -> subscribe()
} }
@ -48,6 +50,7 @@ class SubscribeViewModel @Inject constructor(
private fun init() { private fun init() {
_viewState.update { _viewState.update {
it.copy( it.copy(
title = stringsRepository.getString(R.string.subscribe),
groups = rssRepository.get().pullGroups() groups = rssRepository.get().pullGroups()
) )
} }
@ -57,13 +60,13 @@ class SubscribeViewModel @Inject constructor(
_viewState.update { _viewState.update {
it.copy( it.copy(
visible = false, visible = false,
title = "订阅", title = stringsRepository.getString(R.string.subscribe),
errorMessage = "", errorMessage = "",
inputContent = "", inputContent = "",
feed = null, feed = null,
articles = emptyList(), articles = emptyList(),
notificationPreset = false, allowNotificationPreset = false,
fullContentParsePreset = false, parseFullContentPreset = false,
selectedGroupId = "", selectedGroupId = "",
groups = emptyFlow(), groups = emptyFlow(),
) )
@ -78,8 +81,8 @@ class SubscribeViewModel @Inject constructor(
rssRepository.get().subscribe( rssRepository.get().subscribe(
feed.copy( feed.copy(
groupId = groupId, groupId = groupId,
isNotification = _viewState.value.notificationPreset, isNotification = _viewState.value.allowNotificationPreset,
isFullContent = _viewState.value.fullContentParsePreset, isFullContent = _viewState.value.parseFullContentPreset,
), articles ), articles
) )
changeVisible(false) changeVisible(false)
@ -94,18 +97,18 @@ class SubscribeViewModel @Inject constructor(
} }
} }
private fun changeFullContentParsePreset() { private fun changeParseFullContentPreset() {
_viewState.update { _viewState.update {
it.copy( it.copy(
fullContentParsePreset = !_viewState.value.fullContentParsePreset parseFullContentPreset = !_viewState.value.parseFullContentPreset
) )
} }
} }
private fun changeNotificationPreset() { private fun changeAllowNotificationPreset() {
_viewState.update { _viewState.update {
it.copy( it.copy(
notificationPreset = !_viewState.value.notificationPreset allowNotificationPreset = !_viewState.value.allowNotificationPreset
) )
} }
} }
@ -122,7 +125,7 @@ class SubscribeViewModel @Inject constructor(
} }
_viewState.update { _viewState.update {
it.copy( it.copy(
title = "搜索中", title = stringsRepository.getString(R.string.searching),
) )
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -130,7 +133,7 @@ class SubscribeViewModel @Inject constructor(
if (rssRepository.get().isExist(_viewState.value.inputContent)) { if (rssRepository.get().isExist(_viewState.value.inputContent)) {
_viewState.update { _viewState.update {
it.copy( it.copy(
errorMessage = "已订阅", errorMessage = stringsRepository.getString(R.string.already_subscribed),
) )
} }
return@launch return@launch
@ -147,8 +150,8 @@ class SubscribeViewModel @Inject constructor(
e.printStackTrace() e.printStackTrace()
_viewState.update { _viewState.update {
it.copy( it.copy(
title = "订阅", title = stringsRepository.getString(R.string.subscribe),
errorMessage = e.message ?: Symbol.Unknown, errorMessage = e.message ?: stringsRepository.getString(R.string.unknown),
) )
} }
} }
@ -173,15 +176,15 @@ class SubscribeViewModel @Inject constructor(
} }
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
data class SubScribeViewState( data class SubscribeViewState(
val visible: Boolean = false, val visible: Boolean = false,
val title: String = "订阅", val title: String = "",
val errorMessage: String = "", val errorMessage: String = "",
val inputContent: String = "", val inputContent: String = "",
val feed: Feed? = null, val feed: Feed? = null,
val articles: List<Article> = emptyList(), val articles: List<Article> = emptyList(),
val notificationPreset: Boolean = false, val allowNotificationPreset: Boolean = false,
val fullContentParsePreset: Boolean = false, val parseFullContentPreset: Boolean = false,
val selectedGroupId: String = "", val selectedGroupId: String = "",
val groups: Flow<List<Group>> = emptyFlow(), val groups: Flow<List<Group>> = emptyFlow(),
val pagerState: PagerState = PagerState(), val pagerState: PagerState = PagerState(),
@ -202,8 +205,8 @@ sealed class SubscribeViewAction {
val scope: CoroutineScope, val scope: CoroutineScope,
) : SubscribeViewAction() ) : SubscribeViewAction()
object ChangeNotificationPreset : SubscribeViewAction() object ChangeAllowNotificationPreset : SubscribeViewAction()
object ChangeFullContentParsePreset : SubscribeViewAction() object ChangeParseFullContentPreset : SubscribeViewAction()
data class SelectedGroup( data class SelectedGroup(
val groupId: String val groupId: String

View File

@ -19,12 +19,12 @@ fun SubscribeViewPager(
onSearchKeyboardAction: () -> Unit = {}, onSearchKeyboardAction: () -> Unit = {},
link: String = "", link: String = "",
groups: List<Group> = emptyList(), groups: List<Group> = emptyList(),
selectedNotificationPreset: Boolean = false, selectedAllowNotificationPreset: Boolean = false,
selectedFullContentParsePreset: Boolean = false, selectedParseFullContentPreset: Boolean = false,
selectedGroupId: String = "", selectedGroupId: String = "",
pagerState: PagerState = com.google.accompanist.pager.rememberPagerState(), pagerState: PagerState = com.google.accompanist.pager.rememberPagerState(),
notificationPresetOnClick: () -> Unit = {}, allowNotificationPresetOnClick: () -> Unit = {},
fullContentParsePresetOnClick: () -> Unit = {}, parseFullContentPresetOnClick: () -> Unit = {},
groupOnClick: (groupId: String) -> Unit = {}, groupOnClick: (groupId: String) -> Unit = {},
onResultKeyboardAction: () -> Unit = {}, onResultKeyboardAction: () -> Unit = {},
) { ) {
@ -35,6 +35,7 @@ fun SubscribeViewPager(
composableList = listOf( composableList = listOf(
{ {
SearchViewPage( SearchViewPage(
pagerState = pagerState,
inputContent = inputContent, inputContent = inputContent,
errorMessage = errorMessage, errorMessage = errorMessage,
onValueChange = onValueChange, onValueChange = onValueChange,
@ -45,11 +46,11 @@ fun SubscribeViewPager(
ResultViewPage( ResultViewPage(
link = link, link = link,
groups = groups, groups = groups,
selectedNotificationPreset = selectedNotificationPreset, selectedAllowNotificationPreset = selectedAllowNotificationPreset,
selectedFullContentParsePreset = selectedFullContentParsePreset, selectedParseFullContentPreset = selectedParseFullContentPreset,
selectedGroupId = selectedGroupId, selectedGroupId = selectedGroupId,
notificationPresetOnClick = notificationPresetOnClick, allowNotificationPresetOnClick = allowNotificationPresetOnClick,
fullContentParsePresetOnClick = fullContentParsePresetOnClick, parseFullContentPresetOnClick = parseFullContentPresetOnClick,
groupOnClick = groupOnClick, groupOnClick = groupOnClick,
onKeyboardAction = onResultKeyboardAction, onKeyboardAction = onResultKeyboardAction,
) )

View File

@ -12,11 +12,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.ash.reader.DateTimeExt
import me.ash.reader.DateTimeExt.toString
import me.ash.reader.data.article.ArticleWithFeed import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.formatToString
@Composable @Composable
fun ArticleItem( fun ArticleItem(
@ -24,6 +24,7 @@ fun ArticleItem(
articleWithFeed: ArticleWithFeed, articleWithFeed: ArticleWithFeed,
onClick: (ArticleWithFeed) -> Unit = {}, onClick: (ArticleWithFeed) -> Unit = {},
) { ) {
val context = LocalContext.current
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = 12.dp, vertical = 8.dp) .padding(horizontal = 12.dp, vertical = 8.dp)
@ -44,7 +45,7 @@ fun ArticleItem(
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
) )
Text( Text(
text = articleWithFeed.article.date.toString(DateTimeExt.HH_MM), text = articleWithFeed.article.date.formatToString(context, onlyHourMinute = true),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
) )

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.page.home.flow package me.ash.reader.ui.page.home.flow
import android.content.Context
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -8,9 +9,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import me.ash.reader.DateTimeExt
import me.ash.reader.DateTimeExt.toString
import me.ash.reader.data.article.ArticleWithFeed import me.ash.reader.data.article.ArticleWithFeed
import me.ash.reader.formatToString
import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.read.ReadViewAction import me.ash.reader.ui.page.home.read.ReadViewAction
@ -18,6 +18,7 @@ import me.ash.reader.ui.page.home.read.ReadViewModel
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.generateArticleList( fun LazyListScope.generateArticleList(
context: Context,
pagingItems: LazyPagingItems<ArticleWithFeed>?, pagingItems: LazyPagingItems<ArticleWithFeed>?,
readViewModel: ReadViewModel, readViewModel: ReadViewModel,
homeViewModel: HomeViewModel, homeViewModel: HomeViewModel,
@ -27,8 +28,7 @@ fun LazyListScope.generateArticleList(
var lastItemDay: String? = null var lastItemDay: String? = null
for (itemIndex in 0 until pagingItems.itemCount) { for (itemIndex in 0 until pagingItems.itemCount) {
val currentItem = pagingItems.peek(itemIndex) ?: continue val currentItem = pagingItems.peek(itemIndex) ?: continue
val currentItemDay = currentItem.article.date val currentItemDay = currentItem.article.date.formatToString(context)
.toString(DateTimeExt.YYYY_MM_DD, true)
if (lastItemDay != currentItemDay) { if (lastItemDay != currentItemDay) {
if (itemIndex != 0) { if (itemIndex != 0) {
item { Spacer(modifier = Modifier.height(40.dp)) } item { Spacer(modifier = Modifier.height(40.dp)) }

View File

@ -14,13 +14,17 @@ 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
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.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
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 androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.flow.collect 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.getName
import me.ash.reader.ui.page.home.HomeViewAction import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.read.ReadViewModel import me.ash.reader.ui.page.home.read.ReadViewModel
@ -37,6 +41,7 @@ fun FlowPage(
homeViewModel: HomeViewModel = hiltViewModel(), homeViewModel: HomeViewModel = hiltViewModel(),
readViewModel: ReadViewModel = hiltViewModel(), readViewModel: ReadViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val viewState = viewModel.viewState.collectAsStateValue() val viewState = viewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue() val filterState = homeViewModel.filterState.collectAsStateValue()
@ -66,7 +71,7 @@ fun FlowPage(
}) { }) {
Icon( Icon(
imageVector = Icons.Rounded.ArrowBack, imageVector = Icons.Rounded.ArrowBack,
contentDescription = "Back", contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurface
) )
} }
@ -75,14 +80,14 @@ fun FlowPage(
IconButton(onClick = {}) { IconButton(onClick = {}) {
Icon( Icon(
imageVector = Icons.Rounded.DoneAll, imageVector = Icons.Rounded.DoneAll,
contentDescription = "Read All", contentDescription = stringResource(R.string.mark_all_as_read),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) )
} }
IconButton(onClick = {}) { IconButton(onClick = {}) {
Icon( Icon(
imageVector = Icons.Rounded.Search, imageVector = Icons.Rounded.Search,
contentDescription = "Search", contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) )
} }
@ -106,7 +111,7 @@ fun FlowPage(
text = when { text = when {
filterState.group != null -> filterState.group.name filterState.group != null -> filterState.group.name
filterState.feed != null -> filterState.feed.name filterState.feed != null -> filterState.feed.name
else -> filterState.filter.name else -> filterState.filter.getName()
}, },
style = MaterialTheme.typography.displaySmall, style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
@ -114,7 +119,7 @@ fun FlowPage(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
} }
generateArticleList(pagingItems, readViewModel, homeViewModel, scope) generateArticleList(context, pagingItems, readViewModel, homeViewModel, scope)
} }
} }
) )

View File

@ -11,10 +11,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight 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 androidx.compose.ui.unit.sp
import me.ash.reader.DateTimeExt
import me.ash.reader.DateTimeExt.toString
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.ui.extension.roundClick import me.ash.reader.ui.extension.roundClick
@Composable @Composable
@ -34,7 +33,7 @@ fun Header(
) { ) {
Column(modifier = Modifier.padding(10.dp)) { Column(modifier = Modifier.padding(10.dp)) {
Text( Text(
text = article.date.toString(DateTimeExt.YYYY_MM_DD_HH_MM, true), text = article.date.formatToString(context, atHourMinute = true),
fontSize = 12.sp, fontSize = 12.sp,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,

View File

@ -13,7 +13,9 @@ import androidx.compose.material3.SmallTopAppBar
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.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.ash.reader.R
@Composable @Composable
fun ReadPageTopBar( fun ReadPageTopBar(
@ -30,7 +32,7 @@ fun ReadPageTopBar(
Icon( Icon(
modifier = Modifier.size(28.dp), modifier = Modifier.size(28.dp),
imageVector = Icons.Rounded.Close, imageVector = Icons.Rounded.Close,
contentDescription = "Back", contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} }

View File

@ -14,10 +14,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.ui.extension.paddingFixedHorizontal import me.ash.reader.ui.extension.paddingFixedHorizontal
import me.ash.reader.ui.extension.roundClick import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.widget.TopTitleBox import me.ash.reader.ui.widget.TopTitleBox
@ -49,7 +51,7 @@ fun SettingsPage(
IconButton(onClick = { navController.popBackStack() }) { IconButton(onClick = { navController.popBackStack() }) {
Icon( Icon(
imageVector = Icons.Rounded.ArrowBackIosNew, imageVector = Icons.Rounded.ArrowBackIosNew,
contentDescription = "Back", contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} }

View File

@ -19,7 +19,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.ash.reader.R
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@ -33,7 +35,7 @@ fun SelectionChip(
selectedIcon: @Composable () -> Unit = { selectedIcon: @Composable () -> Unit = {
Icon( Icon(
imageVector = Icons.Rounded.Check, imageVector = Icons.Rounded.Check,
contentDescription = "Check", contentDescription = stringResource(R.string.selected),
modifier = Modifier modifier = Modifier
.padding(start = 8.dp) .padding(start = 8.dp)
.size(18.dp) .size(18.dp)
@ -87,7 +89,7 @@ fun SelectionEditorChip(
selectedIcon: @Composable () -> Unit = { selectedIcon: @Composable () -> Unit = {
Icon( Icon(
imageVector = Icons.Rounded.Check, imageVector = Icons.Rounded.Check,
contentDescription = "Check", contentDescription = stringResource(R.string.selected),
modifier = Modifier modifier = Modifier
.padding(start = 8.dp) .padding(start = 8.dp)
.size(16.dp) .size(16.dp)

View File

@ -0,0 +1,41 @@
<resources>
<string name="read_you">Read You</string>
<string name="all">全部</string>
<string name="all_desc">共 %1$d 项</string>
<string name="unread">未读</string>
<string name="unread_desc">%1$d 项未读</string>
<string name="starred">已加星标</string>
<string name="starred_desc">%1$d 项已加星标</string>
<string name="feeds">分组</string>
<string name="expand_less">收缩</string>
<string name="expand_more">展开</string>
<string name="confirm">确认</string>
<string name="cancel">取消</string>
<string name="defaults">默认</string>
<string name="unknown">未知</string>
<string name="back">返回</string>
<string name="go_to">转到</string>
<string name="refresh">刷新</string>
<string name="search">搜索</string>
<string name="searching">搜索中…</string>
<string name="subscribe">订阅</string>
<string name="already_subscribed">已有订阅</string>
<string name="clear">清空</string>
<string name="paste">粘贴</string>
<string name="feed_or_site_url">订阅源或站点链接</string>
<string name="import_from_opml">导入 OPML 文件</string>
<string name="preset">预设</string>
<string name="selected">已选择</string>
<string name="allow_notification">允许通知</string>
<string name="parse_full_content">全文解析</string>
<string name="add_to_group">添加到组</string>
<string name="new_group">新建分组</string>
<string name="today">今天</string>
<string name="yesterday">昨天</string>
<string name="date_at_time">%1$s %2$s</string>
<string name="mark_as_read">标记为已读</string>
<string name="mark_all_as_read">全部标记为已读</string>
<string name="mark_as_unread">标记为未读</string>
<string name="mark_as_starred">标记为已加星标</string>
<string name="mark_as_unstar">标记为未加星标</string>
</resources>

View File

@ -1,3 +1,41 @@
<resources> <resources>
<string name="app_name">Reader</string> <string name="read_you">Read You</string>
<string name="all">All</string>
<string name="all_desc">%1$d All Items</string>
<string name="unread">Unread</string>
<string name="unread_desc"> %1$d Unread Items</string>
<string name="starred">Starred</string>
<string name="starred_desc">%1$d Starred Items</string>
<string name="feeds">Feeds</string>
<string name="expand_less">Expand Less</string>
<string name="expand_more">Expand More</string>
<string name="confirm">Confirm</string>
<string name="cancel">Cancel</string>
<string name="defaults">Default</string>
<string name="unknown">Unknown</string>
<string name="back">Back</string>
<string name="go_to">Goto</string>
<string name="refresh">Refresh</string>
<string name="search">Search</string>
<string name="searching">Searching…</string>
<string name="subscribe">Subscribe</string>
<string name="already_subscribed">Already subscribed</string>
<string name="clear">Clear</string>
<string name="paste">Paste</string>
<string name="feed_or_site_url">Feed or Site URL</string>
<string name="import_from_opml">Import from OPML</string>
<string name="preset">Preset</string>
<string name="selected">Selected</string>
<string name="allow_notification">Allow Notification</string>
<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="today">Today</string>
<string name="yesterday">Yesterday</string>
<string name="date_at_time">%1$s At %2$s</string>
<string name="mark_as_read">Mark as Read</string>
<string name="mark_all_as_read">Mark All as Read</string>
<string name="mark_as_unread">Mark as Unread</string>
<string name="mark_as_starred">Mark as Starred</string>
<string name="mark_as_unstar">Mark as Unstar</string>
</resources> </resources>