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] Feed 分组
- [x] 本地化
- [ ] 文章搜索
- [ ] 偏好设置
- [ ] 本地化
- [ ] 发布 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] Filter unread and starred
- [x] Feed Grouping
- [x] Localization
- [ ] Search for articles
- [ ] Preference settings
- [ ] Localization
- [ ] Release APK
- [ ] Widget
- [ ] ...

View File

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

View File

@ -26,6 +26,9 @@ class App : Application() {
@Inject
lateinit var rssHelper: RssHelper
@Inject
lateinit var stringsRepository: StringsRepository
@Inject
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(
var index: Int,
var name: String,
var description: String,
var important: Int,
var icon: ImageVector,
) {
@ -20,22 +18,16 @@ class Filter(
companion object {
val Starred = Filter(
index = 0,
name = "Starred",
description = " Starred Items",
important = 13,
icon = Icons.Rounded.StarOutline,
)
val Unread = Filter(
index = 1,
name = "Unread",
description = " Unread Items",
important = 666,
icon = Icons.Outlined.FiberManualRecord,
)
val All = Filter(
index = 2,
name = "All",
description = " Unread Items",
important = 666,
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 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.AccountDao
import me.ash.reader.data.group.Group
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
class AccountRepository @Inject constructor(
@ -29,8 +26,10 @@ class AccountRepository @Inject constructor(
}
suspend fun addDefaultAccount(): Account {
val readYouString = context.getString(R.string.read_you)
val defaultString = context.getString(R.string.defaults)
return Account(
name = "Read You",
name = readYouString,
type = Account.Type.LOCAL,
).apply {
id = accountDao.insert(this).toInt()
@ -38,8 +37,8 @@ class AccountRepository @Inject constructor(
if (groupDao.queryAll(it.id!!).isEmpty()) {
groupDao.insert(
Group(
id = it.id!!.spacerDollar("0"),
name = "默认",
id = it.id!!.spacerDollar(readYouString + defaultString),
name = defaultString,
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.article.Article
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.FeedDao
import me.ash.reader.data.group.GroupDao
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 javax.inject.Inject
@ -119,7 +120,7 @@ class LocalRssRepository @Inject constructor(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(
NotificationChannel(
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE,
NotificationGroupName.ARTICLE_UPDATE,
"文章更新",
NotificationManager.IMPORTANCE_DEFAULT
)
@ -132,9 +133,9 @@ class LocalRssRepository @Inject constructor(
if (feedNotificationMap[article.feedId] == true) {
val builder = NotificationCompat.Builder(
context,
Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE
NotificationGroupName.ARTICLE_UPDATE
).setSmallIcon(R.drawable.ic_launcher_foreground)
.setGroup(Symbol.NOTIFICATION_CHANNEL_GROUP_ARTICLE_UPDATE)
.setGroup(NotificationGroupName.ARTICLE_UPDATE)
.setContentTitle(article.title)
.setContentText(article.shortDescription)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
@ -146,7 +147,7 @@ class LocalRssRepository @Inject constructor(
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(
Symbol.EXTRA_ARTICLE_ID,
ExtraName.ARTICLE_ID,
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.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import me.ash.reader.R
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 kotlin.math.absoluteValue
@ -153,10 +155,10 @@ private fun FilterBar(
verticalAlignment = Alignment.CenterVertically,
) {
listOf(
NavigationBarItem.Starred,
NavigationBarItem.Unread,
NavigationBarItem.All
).forEachIndexed { index, item ->
Filter.Starred,
Filter.Unread,
Filter.All
).forEach { item ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
@ -175,39 +177,33 @@ private fun FilterBar(
.clip(CircleShape)
.clickable(onClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
onSelected(
when (index) {
0 -> Filter.Starred
1 -> Filter.Unread
else -> Filter.All
}
)
onSelected(item)
})
.background(
if (filter.index == index) {
if (filter == item) {
MaterialTheme.colorScheme.inverseOnSurface
} else {
Color.Unspecified
}
)
) {
if (filter.index == index) {
if (filter == item) {
Spacer(modifier = Modifier.width(10.dp))
Icon(
modifier = Modifier.size(
if (Filter.Unread.index == index) {
15
if (filter == item) {
15.dp
} else {
19
}.dp
19.dp
}
),
imageVector = item.icon,
contentDescription = item.title,
contentDescription = item.getName(),
tint = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = item.title,
text = item.getName().uppercase(),
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
@ -216,14 +212,14 @@ private fun FilterBar(
} else {
Icon(
modifier = Modifier.size(
if (Filter.Unread.index == index) {
if (item.isUnread()) {
15
} else {
19
}.dp
),
imageVector = item.icon,
contentDescription = item.title,
contentDescription = item.getName(),
tint = MaterialTheme.colorScheme.outline,
)
}
@ -261,7 +257,7 @@ private fun ReaderBar(
} else {
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,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
@ -275,7 +271,7 @@ private fun ReaderBar(
} else {
Icons.Rounded.StarBorder
},
contentDescription = "Starred",
contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred),
tint = MaterialTheme.colorScheme.primary,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
@ -306,7 +302,7 @@ private fun ReaderBar(
} else {
Icons.Outlined.Article
},
contentDescription = "Full Content Parsing",
contentDescription = stringResource(R.string.parse_full_content),
tint = MaterialTheme.colorScheme.primary,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)

View File

@ -16,9 +16,10 @@ import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.flow.collect
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.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.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage
@ -42,7 +43,7 @@ fun HomePage(
LaunchedEffect(Unit) {
context.findActivity()?.let { activity ->
activity.intent?.let { intent ->
intent.extras?.get(Symbol.EXTRA_ARTICLE_ID)?.let {
intent.extras?.get(ExtraName.ARTICLE_ID)?.let {
readViewModel.dispatch(ReadViewAction.ScrollToItem(2))
scope.launch {
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.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.collect
import me.ash.reader.data.constant.Symbol
import me.ash.reader.R
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.HomeViewModel
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeDialog
@ -77,7 +80,7 @@ fun FeedsPage(
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = "Back",
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
)
}
@ -90,7 +93,7 @@ fun FeedsPage(
Icon(
modifier = Modifier.rotate(if (syncState.isSyncing) angle else 0f),
imageVector = Icons.Rounded.Refresh,
contentDescription = "Refresh",
contentDescription = stringResource(R.string.refresh),
tint = MaterialTheme.colorScheme.onSurface,
)
}
@ -99,7 +102,7 @@ fun FeedsPage(
}) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = "Subscribe",
contentDescription = stringResource(R.string.subscribe),
tint = MaterialTheme.colorScheme.onSurface,
)
}
@ -121,20 +124,20 @@ fun FeedsPage(
end = 24.dp,
bottom = 24.dp
),
text = viewState.account?.name ?: Symbol.Unknown,
text = viewState.account?.name ?: stringResource(R.string.unknown),
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
item {
Banner(
title = viewState.filter.name,
desc = "${viewState.filter.important}${viewState.filter.description}",
title = filterState.filter.getName(),
desc = filterState.filter.getDesc(),
icon = viewState.filter.icon,
action = {
Icon(
imageVector = Icons.Outlined.KeyboardArrowRight,
contentDescription = "Goto",
contentDescription = stringResource(R.string.go_to),
tint = MaterialTheme.colorScheme.onSurface,
)
},
@ -159,7 +162,7 @@ fun FeedsPage(
Spacer(modifier = Modifier.height(24.dp))
Subtitle(
modifier = Modifier.padding(start = 28.dp),
text = "Feeds"
text = stringResource(R.string.feeds)
)
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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import me.ash.reader.R
import me.ash.reader.data.feed.Feed
@Composable
@ -37,7 +39,7 @@ fun GroupItem(
.clip(RoundedCornerShape(32.dp))
.background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.14f))
.clickable { groupOnClick() }
.padding(vertical = 22.dp)
.padding(top = 22.dp)
) {
Row(
modifier = modifier.fillMaxWidth(),
@ -64,12 +66,12 @@ fun GroupItem(
) {
Icon(
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,
)
}
}
Spacer(modifier = Modifier.height(22.dp))
AnimatedVisibility(
visible = expanded,
enter = fadeIn() + expandVertically(),

View File

@ -13,9 +13,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.flowlayout.MainAxisAlignment
import me.ash.reader.R
import me.ash.reader.data.group.Group
import me.ash.reader.ui.widget.SelectionChip
import me.ash.reader.ui.widget.SelectionEditorChip
@ -25,11 +27,11 @@ import me.ash.reader.ui.widget.Subtitle
fun ResultViewPage(
link: String = "",
groups: List<Group> = emptyList(),
selectedNotificationPreset: Boolean = false,
selectedFullContentParsePreset: Boolean = false,
selectedAllowNotificationPreset: Boolean = false,
selectedParseFullContentPreset: Boolean = false,
selectedGroupId: String = "",
notificationPresetOnClick: () -> Unit = {},
fullContentParsePresetOnClick: () -> Unit = {},
allowNotificationPresetOnClick: () -> Unit = {},
parseFullContentPresetOnClick: () -> Unit = {},
groupOnClick: (groupId: String) -> Unit = {},
onKeyboardAction: () -> Unit = {},
) {
@ -42,10 +44,10 @@ fun ResultViewPage(
Spacer(modifier = Modifier.height(26.dp))
Preset(
selectedNotificationPreset = selectedNotificationPreset,
selectedFullContentParsePreset = selectedFullContentParsePreset,
notificationPresetOnClick = notificationPresetOnClick,
fullContentParsePresetOnClick = fullContentParsePresetOnClick,
selectedAllowNotificationPreset = selectedAllowNotificationPreset,
selectedParseFullContentPreset = selectedParseFullContentPreset,
allowNotificationPresetOnClick = allowNotificationPresetOnClick,
parseFullContentPresetOnClick = parseFullContentPresetOnClick,
)
Spacer(modifier = Modifier.height(26.dp))
@ -78,12 +80,12 @@ private fun Link(
@Composable
private fun Preset(
selectedNotificationPreset: Boolean = false,
selectedFullContentParsePreset: Boolean = false,
notificationPresetOnClick: () -> Unit = {},
fullContentParsePresetOnClick: () -> Unit = {},
selectedAllowNotificationPreset: Boolean = false,
selectedParseFullContentPreset: Boolean = false,
allowNotificationPresetOnClick: () -> Unit = {},
parseFullContentPresetOnClick: () -> Unit = {},
) {
Subtitle(text = "预设")
Subtitle(text = stringResource(R.string.preset))
Spacer(modifier = Modifier.height(10.dp))
FlowRow(
mainAxisAlignment = MainAxisAlignment.Start,
@ -92,35 +94,35 @@ private fun Preset(
) {
SelectionChip(
modifier = Modifier.animateContentSize(),
content = "接收通知",
selected = selectedNotificationPreset,
content = stringResource(R.string.allow_notification),
selected = selectedAllowNotificationPreset,
selectedIcon = {
Icon(
imageVector = Icons.Outlined.Notifications,
contentDescription = "Check",
contentDescription = stringResource(R.string.allow_notification),
modifier = Modifier
.padding(start = 8.dp)
.size(18.dp),
)
},
) {
notificationPresetOnClick()
allowNotificationPresetOnClick()
}
SelectionChip(
modifier = Modifier.animateContentSize(),
content = "全文解析",
selected = selectedFullContentParsePreset,
content = stringResource(R.string.parse_full_content),
selected = selectedParseFullContentPreset,
selectedIcon = {
Icon(
imageVector = Icons.Outlined.Article,
contentDescription = "Check",
contentDescription = stringResource(R.string.parse_full_content),
modifier = Modifier
.padding(start = 8.dp)
.size(18.dp),
)
},
) {
fullContentParsePresetOnClick()
parseFullContentPresetOnClick()
}
}
}
@ -132,7 +134,7 @@ private fun AddToGroup(
groupOnClick: (groupId: String) -> Unit = {},
onKeyboardAction: () -> Unit = {},
) {
Subtitle(text = "添加到组")
Subtitle(text = stringResource(R.string.add_to_group))
Spacer(modifier = Modifier.height(10.dp))
FlowRow(
mainAxisAlignment = MainAxisAlignment.Start,
@ -151,7 +153,7 @@ private fun AddToGroup(
SelectionEditorChip(
modifier = Modifier.animateContentSize(),
content = "新建分组",
content = stringResource(R.string.new_group),
selected = false,
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.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import kotlinx.coroutines.delay
import me.ash.reader.R
@OptIn(ExperimentalPagerApi::class)
@Composable
fun SearchViewPage(
pagerState: PagerState,
inputContent: String = "",
errorMessage: String = "",
onValueChange: (String) -> Unit = {},
@ -51,11 +57,11 @@ fun SearchViewPage(
),
value = inputContent,
onValueChange = {
onValueChange(it)
if (pagerState.currentPage == 0) onValueChange(it)
},
placeholder = {
Text(
text = "订阅源或站点链接",
text = stringResource(R.string.feed_or_site_url),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
)
},
@ -68,7 +74,7 @@ fun SearchViewPage(
}) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = "Clear",
contentDescription = stringResource(R.string.clear),
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
)
}
@ -77,7 +83,7 @@ fun SearchViewPage(
}) {
Icon(
imageVector = Icons.Rounded.ContentPaste,
contentDescription = "Paste",
contentDescription = stringResource(R.string.paste),
tint = MaterialTheme.colorScheme.primary
)
}

View File

@ -11,12 +11,11 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
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.DataStoreKeys
import me.ash.reader.dataStore
import me.ash.reader.get
import me.ash.reader.spacerDollar
import me.ash.reader.*
import me.ash.reader.R
import me.ash.reader.ui.extension.collectAsStateValue
import me.ash.reader.ui.widget.Dialog
import java.io.InputStream
@ -61,14 +60,14 @@ fun SubscribeDialog(
icon = {
Icon(
imageVector = Icons.Rounded.RssFeed,
contentDescription = "Subscribe",
contentDescription = stringResource(R.string.subscribe),
)
},
title = {
Text(
when (viewState.pagerState.currentPage) {
0 -> "订阅"
else -> viewState.feed?.name ?: "未知"
0 -> stringResource(R.string.subscribe)
else -> viewState.feed?.name ?: stringResource(R.string.unknown)
}
)
},
@ -88,15 +87,15 @@ fun SubscribeDialog(
},
link = viewState.inputContent,
groups = groupsState.value,
selectedNotificationPreset = viewState.notificationPreset,
selectedFullContentParsePreset = viewState.fullContentParsePreset,
selectedAllowNotificationPreset = viewState.allowNotificationPreset,
selectedParseFullContentPreset = viewState.parseFullContentPreset,
selectedGroupId = viewState.selectedGroupId,
pagerState = viewState.pagerState,
notificationPresetOnClick = {
viewModel.dispatch(SubscribeViewAction.ChangeNotificationPreset)
allowNotificationPresetOnClick = {
viewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset)
},
fullContentParsePresetOnClick = {
viewModel.dispatch(SubscribeViewAction.ChangeFullContentParsePreset)
parseFullContentPresetOnClick = {
viewModel.dispatch(SubscribeViewAction.ChangeParseFullContentPreset)
},
groupOnClick = {
viewModel.dispatch(SubscribeViewAction.SelectedGroup(it))
@ -116,7 +115,7 @@ fun SubscribeDialog(
}
) {
Text(
text = "搜索",
text = stringResource(R.string.search),
color = if (viewState.inputContent.isNotEmpty()) {
Color.Unspecified
} else {
@ -131,7 +130,7 @@ fun SubscribeDialog(
viewModel.dispatch(SubscribeViewAction.Subscribe)
}
) {
Text("订阅")
Text(stringResource(R.string.subscribe))
}
}
}
@ -145,7 +144,7 @@ fun SubscribeDialog(
viewModel.dispatch(SubscribeViewAction.Hide)
}
) {
Text("导入OPML文件")
Text(text = stringResource(R.string.import_from_opml))
}
}
1 -> {
@ -154,7 +153,7 @@ fun SubscribeDialog(
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.flow.*
import kotlinx.coroutines.launch
import me.ash.reader.R
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.group.Group
import me.ash.reader.data.repository.RssHelper
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.StringsRepository
import me.ash.reader.formatUrl
import me.ash.reader.ui.extension.animateScrollToPage
import javax.inject.Inject
@ -24,9 +25,10 @@ import javax.inject.Inject
class SubscribeViewModel @Inject constructor(
private val rssRepository: RssRepository,
private val rssHelper: RssHelper,
private val stringsRepository: StringsRepository,
) : ViewModel() {
private val _viewState = MutableStateFlow(SubScribeViewState())
val viewState: StateFlow<SubScribeViewState> = _viewState.asStateFlow()
private val _viewState = MutableStateFlow(SubscribeViewState())
val viewState: StateFlow<SubscribeViewState> = _viewState.asStateFlow()
fun dispatch(action: SubscribeViewAction) {
when (action) {
@ -36,10 +38,10 @@ class SubscribeViewModel @Inject constructor(
is SubscribeViewAction.Hide -> changeVisible(false)
is SubscribeViewAction.Input -> inputLink(action.content)
is SubscribeViewAction.Search -> search(action.scope)
is SubscribeViewAction.ChangeNotificationPreset ->
changeNotificationPreset()
is SubscribeViewAction.ChangeFullContentParsePreset ->
changeFullContentParsePreset()
is SubscribeViewAction.ChangeAllowNotificationPreset ->
changeAllowNotificationPreset()
is SubscribeViewAction.ChangeParseFullContentPreset ->
changeParseFullContentPreset()
is SubscribeViewAction.SelectedGroup -> selectedGroup(action.groupId)
is SubscribeViewAction.Subscribe -> subscribe()
}
@ -48,6 +50,7 @@ class SubscribeViewModel @Inject constructor(
private fun init() {
_viewState.update {
it.copy(
title = stringsRepository.getString(R.string.subscribe),
groups = rssRepository.get().pullGroups()
)
}
@ -57,13 +60,13 @@ class SubscribeViewModel @Inject constructor(
_viewState.update {
it.copy(
visible = false,
title = "订阅",
title = stringsRepository.getString(R.string.subscribe),
errorMessage = "",
inputContent = "",
feed = null,
articles = emptyList(),
notificationPreset = false,
fullContentParsePreset = false,
allowNotificationPreset = false,
parseFullContentPreset = false,
selectedGroupId = "",
groups = emptyFlow(),
)
@ -78,8 +81,8 @@ class SubscribeViewModel @Inject constructor(
rssRepository.get().subscribe(
feed.copy(
groupId = groupId,
isNotification = _viewState.value.notificationPreset,
isFullContent = _viewState.value.fullContentParsePreset,
isNotification = _viewState.value.allowNotificationPreset,
isFullContent = _viewState.value.parseFullContentPreset,
), articles
)
changeVisible(false)
@ -94,18 +97,18 @@ class SubscribeViewModel @Inject constructor(
}
}
private fun changeFullContentParsePreset() {
private fun changeParseFullContentPreset() {
_viewState.update {
it.copy(
fullContentParsePreset = !_viewState.value.fullContentParsePreset
parseFullContentPreset = !_viewState.value.parseFullContentPreset
)
}
}
private fun changeNotificationPreset() {
private fun changeAllowNotificationPreset() {
_viewState.update {
it.copy(
notificationPreset = !_viewState.value.notificationPreset
allowNotificationPreset = !_viewState.value.allowNotificationPreset
)
}
}
@ -122,7 +125,7 @@ class SubscribeViewModel @Inject constructor(
}
_viewState.update {
it.copy(
title = "搜索中",
title = stringsRepository.getString(R.string.searching),
)
}
viewModelScope.launch(Dispatchers.IO) {
@ -130,7 +133,7 @@ class SubscribeViewModel @Inject constructor(
if (rssRepository.get().isExist(_viewState.value.inputContent)) {
_viewState.update {
it.copy(
errorMessage = "已订阅",
errorMessage = stringsRepository.getString(R.string.already_subscribed),
)
}
return@launch
@ -147,8 +150,8 @@ class SubscribeViewModel @Inject constructor(
e.printStackTrace()
_viewState.update {
it.copy(
title = "订阅",
errorMessage = e.message ?: Symbol.Unknown,
title = stringsRepository.getString(R.string.subscribe),
errorMessage = e.message ?: stringsRepository.getString(R.string.unknown),
)
}
}
@ -173,15 +176,15 @@ class SubscribeViewModel @Inject constructor(
}
@OptIn(ExperimentalPagerApi::class)
data class SubScribeViewState(
data class SubscribeViewState(
val visible: Boolean = false,
val title: String = "订阅",
val title: String = "",
val errorMessage: String = "",
val inputContent: String = "",
val feed: Feed? = null,
val articles: List<Article> = emptyList(),
val notificationPreset: Boolean = false,
val fullContentParsePreset: Boolean = false,
val allowNotificationPreset: Boolean = false,
val parseFullContentPreset: Boolean = false,
val selectedGroupId: String = "",
val groups: Flow<List<Group>> = emptyFlow(),
val pagerState: PagerState = PagerState(),
@ -202,8 +205,8 @@ sealed class SubscribeViewAction {
val scope: CoroutineScope,
) : SubscribeViewAction()
object ChangeNotificationPreset : SubscribeViewAction()
object ChangeFullContentParsePreset : SubscribeViewAction()
object ChangeAllowNotificationPreset : SubscribeViewAction()
object ChangeParseFullContentPreset : SubscribeViewAction()
data class SelectedGroup(
val groupId: String

View File

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

View File

@ -12,11 +12,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
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.formatToString
@Composable
fun ArticleItem(
@ -24,6 +24,7 @@ fun ArticleItem(
articleWithFeed: ArticleWithFeed,
onClick: (ArticleWithFeed) -> Unit = {},
) {
val context = LocalContext.current
Column(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 8.dp)
@ -44,7 +45,7 @@ fun ArticleItem(
style = MaterialTheme.typography.labelMedium,
)
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),
style = MaterialTheme.typography.labelMedium,
)

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.page.home.flow
import android.content.Context
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
@ -8,9 +9,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
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.formatToString
import me.ash.reader.ui.page.home.HomeViewAction
import me.ash.reader.ui.page.home.HomeViewModel
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)
fun LazyListScope.generateArticleList(
context: Context,
pagingItems: LazyPagingItems<ArticleWithFeed>?,
readViewModel: ReadViewModel,
homeViewModel: HomeViewModel,
@ -27,8 +28,7 @@ fun LazyListScope.generateArticleList(
var lastItemDay: String? = null
for (itemIndex in 0 until pagingItems.itemCount) {
val currentItem = pagingItems.peek(itemIndex) ?: continue
val currentItemDay = currentItem.article.date
.toString(DateTimeExt.YYYY_MM_DD, true)
val currentItemDay = currentItem.article.date.formatToString(context)
if (lastItemDay != currentItemDay) {
if (itemIndex != 0) {
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.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.flow.collect
import me.ash.reader.R
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.HomeViewModel
import me.ash.reader.ui.page.home.read.ReadViewModel
@ -37,6 +41,7 @@ fun FlowPage(
homeViewModel: HomeViewModel = hiltViewModel(),
readViewModel: ReadViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val viewState = viewModel.viewState.collectAsStateValue()
val filterState = homeViewModel.filterState.collectAsStateValue()
@ -66,7 +71,7 @@ fun FlowPage(
}) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = "Back",
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
)
}
@ -75,14 +80,14 @@ fun FlowPage(
IconButton(onClick = {}) {
Icon(
imageVector = Icons.Rounded.DoneAll,
contentDescription = "Read All",
contentDescription = stringResource(R.string.mark_all_as_read),
tint = MaterialTheme.colorScheme.onSurface,
)
}
IconButton(onClick = {}) {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = "Search",
contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onSurface,
)
}
@ -106,7 +111,7 @@ fun FlowPage(
text = when {
filterState.group != null -> filterState.group.name
filterState.feed != null -> filterState.feed.name
else -> filterState.filter.name
else -> filterState.filter.getName()
},
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onSurface,
@ -114,7 +119,7 @@ fun FlowPage(
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.unit.dp
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.feed.Feed
import me.ash.reader.formatToString
import me.ash.reader.ui.extension.roundClick
@Composable
@ -34,7 +33,7 @@ fun Header(
) {
Column(modifier = Modifier.padding(10.dp)) {
Text(
text = article.date.toString(DateTimeExt.YYYY_MM_DD_HH_MM, true),
text = article.date.formatToString(context, atHourMinute = true),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.outline,
fontWeight = FontWeight.Medium,

View File

@ -13,7 +13,9 @@ 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(
@ -30,7 +32,7 @@ fun ReadPageTopBar(
Icon(
modifier = Modifier.size(28.dp),
imageVector = Icons.Rounded.Close,
contentDescription = "Back",
contentDescription = stringResource(R.string.back),
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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.ui.extension.paddingFixedHorizontal
import me.ash.reader.ui.extension.roundClick
import me.ash.reader.ui.widget.TopTitleBox
@ -49,7 +51,7 @@ fun SettingsPage(
IconButton(onClick = { navController.popBackStack() }) {
Icon(
imageVector = Icons.Rounded.ArrowBackIosNew,
contentDescription = "Back",
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.primary
)
}

View File

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