Feat(ToolStarScreen): Support star tool
This commit is contained in:
@@ -31,6 +31,15 @@ interface ToolDao {
|
||||
"ORDER BY updateTime DESC")
|
||||
fun selectAllTools(searchValue: String): Flow<List<ToolEntity>>
|
||||
|
||||
@Query("SELECT * FROM tools " +
|
||||
"WHERE isStar = 1 " +
|
||||
"AND (:searchValue = '' " +
|
||||
"OR name LIKE '%' || :searchValue || '%' COLLATE NOCASE " +
|
||||
"OR keywords LIKE '%\"%' || :searchValue || '%\"%' COLLATE NOCASE" +
|
||||
") " +
|
||||
"ORDER BY updateTime DESC")
|
||||
fun selectStarTools(searchValue: String): Flow<List<ToolEntity>>
|
||||
|
||||
@Query("SELECT * FROM tools " +
|
||||
"WHERE authorUsername = :username " +
|
||||
"and toolId = :toolId LIMIT 1")
|
||||
|
||||
@@ -45,7 +45,9 @@ fun OxygenNavHost(
|
||||
onBackClick = navController::popBackStack
|
||||
)
|
||||
starScreen(
|
||||
isVertical = isVertical
|
||||
isVertical = isVertical,
|
||||
searchValue = searchValue,
|
||||
onNavigateToToolView = navController::navigateToToolView
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,9 @@ const val STAR_ROUTE = "star_route"
|
||||
fun NavController.navigateToStar(navOptions: NavOptions) = navigate(STAR_ROUTE, navOptions)
|
||||
|
||||
fun NavGraphBuilder.starScreen(
|
||||
isVertical: Boolean
|
||||
isVertical: Boolean,
|
||||
searchValue: String,
|
||||
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit
|
||||
) {
|
||||
composable(
|
||||
route = STAR_ROUTE,
|
||||
@@ -38,6 +40,9 @@ fun NavGraphBuilder.starScreen(
|
||||
}
|
||||
}
|
||||
) {
|
||||
StarRoute()
|
||||
StarRoute(
|
||||
searchValue = searchValue,
|
||||
onNavigateToToolView = onNavigateToToolView
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ interface ToolRepository {
|
||||
|
||||
fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>>
|
||||
|
||||
fun getStarToolsStream(searchValue: String): Flow<List<ToolEntity>>
|
||||
|
||||
fun getToolById(id: Long): Flow<ToolEntity?>
|
||||
|
||||
fun getToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?>
|
||||
|
||||
@@ -17,6 +17,9 @@ class OfflineToolRepository @Inject constructor(
|
||||
override fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>> =
|
||||
toolDao.selectAllTools(searchValue)
|
||||
|
||||
override fun getStarToolsStream(searchValue: String): Flow<List<ToolEntity>> =
|
||||
toolDao.selectStarTools(searchValue)
|
||||
|
||||
override fun getToolById(id: Long): Flow<ToolEntity?> =
|
||||
toolDao.selectToolById(id)
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.star
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
internal fun StarRoute(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
StarScreen(
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun StarScreen(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.star
|
||||
|
||||
import androidx.activity.compose.ReportDrawnWhen
|
||||
import androidx.compose.animation.core.Ease
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.items
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import top.fatweb.oxygen.toolbox.R
|
||||
import top.fatweb.oxygen.toolbox.icon.Loading
|
||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||
import top.fatweb.oxygen.toolbox.ui.component.DialogClickerRow
|
||||
import top.fatweb.oxygen.toolbox.ui.component.DialogSectionGroup
|
||||
import top.fatweb.oxygen.toolbox.ui.component.DialogTitle
|
||||
import top.fatweb.oxygen.toolbox.ui.component.ToolCard
|
||||
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar
|
||||
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.rememberDraggableScroller
|
||||
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
|
||||
|
||||
@Composable
|
||||
internal fun StarRoute(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: StarScreenViewModel = hiltViewModel(),
|
||||
searchValue: String,
|
||||
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit
|
||||
) {
|
||||
val starScreenUiState by viewModel.starScreenUiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(searchValue) {
|
||||
viewModel.onSearchValueChange(searchValue)
|
||||
}
|
||||
|
||||
StarScreen(
|
||||
modifier = modifier,
|
||||
starScreenUiState = starScreenUiState,
|
||||
onNavigateToToolView = onNavigateToToolView,
|
||||
onUnstar = viewModel::unstar
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun StarScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
starScreenUiState: StarScreenUiState,
|
||||
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit,
|
||||
onUnstar: (ToolEntity) -> Unit
|
||||
) {
|
||||
ReportDrawnWhen { starScreenUiState !is StarScreenUiState.Loading }
|
||||
|
||||
val itemsAvailable = howManyTools(starScreenUiState)
|
||||
|
||||
val state = rememberLazyStaggeredGridState()
|
||||
val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable)
|
||||
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
|
||||
|
||||
var selectedTool by remember { mutableStateOf<ToolEntity?>(null) }
|
||||
var isShowMenu by remember { mutableStateOf(true) }
|
||||
|
||||
Box(
|
||||
modifier.fillMaxSize()
|
||||
) {
|
||||
when (starScreenUiState) {
|
||||
StarScreenUiState.Loading -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
val angle by infiniteTransition.animateFloat(
|
||||
initialValue = 0F, targetValue = 360F, animationSpec = infiniteRepeatable(
|
||||
animation = tween(800, easing = Ease)
|
||||
), label = "angle"
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.graphicsLayer { rotationZ = angle },
|
||||
imageVector = OxygenIcons.Loading,
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StarScreenUiState.Nothing -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(text = stringResource(R.string.feature_star_no_tools_starred))
|
||||
}
|
||||
}
|
||||
|
||||
is StarScreenUiState.Success -> {
|
||||
LazyVerticalStaggeredGrid(
|
||||
columns = StaggeredGridCells.Adaptive(160.dp),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalItemSpacing = 24.dp,
|
||||
state = state
|
||||
) {
|
||||
toolsPanel(
|
||||
toolItems = starScreenUiState.tools,
|
||||
onClick = onNavigateToToolView,
|
||||
onLongClick = {
|
||||
selectedTool = it
|
||||
isShowMenu = true
|
||||
}
|
||||
)
|
||||
|
||||
item(span = StaggeredGridItemSpan.FullLine) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.DraggableScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.windowInsetsPadding(WindowInsets.systemBars)
|
||||
.padding(horizontal = 2.dp)
|
||||
.align(Alignment.CenterEnd),
|
||||
state = scrollbarState,
|
||||
orientation = Orientation.Vertical,
|
||||
onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable)
|
||||
)
|
||||
}
|
||||
|
||||
if (isShowMenu && selectedTool != null) {
|
||||
ToolMenu(
|
||||
onDismiss = { isShowMenu = false },
|
||||
selectedTool = selectedTool!!,
|
||||
onUnstar = {
|
||||
isShowMenu = false
|
||||
onUnstar(selectedTool!!)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyStaggeredGridScope.toolsPanel(
|
||||
toolItems: List<ToolEntity>,
|
||||
onClick: (username: String, toolId: String, preview: Boolean) -> Unit,
|
||||
onLongClick: (ToolEntity) -> Unit
|
||||
) {
|
||||
items(
|
||||
items = toolItems,
|
||||
key = { it.id }
|
||||
) {
|
||||
ToolCard(
|
||||
tool = it,
|
||||
actionIcon = if (it.isStar) OxygenIcons.Star else null,
|
||||
actionIconContentDescription = stringResource(R.string.core_star),
|
||||
onClick = { onClick(it.authorUsername, it.toolId, false) },
|
||||
onLongClick = { onLongClick(it) },
|
||||
onAction = { onClick(it.authorUsername, it.toolId, false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ToolMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
onDismiss: () -> Unit,
|
||||
selectedTool: ToolEntity,
|
||||
onUnstar: () -> Unit
|
||||
) {
|
||||
ModalBottomSheet(onDismissRequest = onDismiss, dragHandle = {}) {
|
||||
Column(
|
||||
modifier = modifier.padding(16.dp)
|
||||
){
|
||||
DialogTitle(text = selectedTool.name)
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
DialogSectionGroup {
|
||||
DialogClickerRow(
|
||||
icon = OxygenIcons.StarBorder,
|
||||
text = stringResource(R.string.core_unstar),
|
||||
onClick = onUnstar
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun howManyTools(starScreenUiState: StarScreenUiState) =
|
||||
when (starScreenUiState) {
|
||||
StarScreenUiState.Loading, StarScreenUiState.Nothing -> 0
|
||||
is StarScreenUiState.Success -> starScreenUiState.tools.size
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package top.fatweb.oxygen.toolbox.ui.star
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class StarScreenViewModel @Inject constructor(
|
||||
private val toolRepository: ToolRepository,
|
||||
private val savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val starScreenUiState: StateFlow<StarScreenUiState> =
|
||||
searchValue.flatMapLatest { searchValue ->
|
||||
toolRepository.getStarToolsStream(searchValue).map {
|
||||
if (it.isEmpty()) {
|
||||
StarScreenUiState.Nothing
|
||||
} else {
|
||||
StarScreenUiState.Success(it)
|
||||
}
|
||||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = StarScreenUiState.Loading,
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
|
||||
fun onSearchValueChange(value: String) {
|
||||
savedStateHandle[SEARCH_VALUE] = value
|
||||
}
|
||||
|
||||
fun unstar(tool: ToolEntity) {
|
||||
viewModelScope.launch {
|
||||
toolRepository.updateTool(tool.copy(isStar = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface StarScreenUiState {
|
||||
data object Loading : StarScreenUiState
|
||||
data object Nothing : StarScreenUiState
|
||||
data class Success(val tools: List<ToolEntity>) : StarScreenUiState
|
||||
}
|
||||
|
||||
private const val SEARCH_VALUE = "searchValue"
|
||||
@@ -86,19 +86,21 @@ internal fun ToolsRoute(
|
||||
onNavigateToToolStore = onNavigateToToolStore,
|
||||
toolsScreenUiState = toolsScreenUiStateState,
|
||||
onUninstall = viewModel::uninstall,
|
||||
onUndo = viewModel::undo
|
||||
onUndo = viewModel::undo,
|
||||
onChangeStar = viewModel::changeStar
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ToolsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
toolsScreenUiState: ToolsScreenUiState,
|
||||
onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
|
||||
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit,
|
||||
onNavigateToToolStore: () -> Unit,
|
||||
toolsScreenUiState: ToolsScreenUiState,
|
||||
onUninstall: (ToolEntity) -> Unit,
|
||||
onUndo: (ToolEntity) -> Unit
|
||||
onUndo: (ToolEntity) -> Unit,
|
||||
onChangeStar: (ToolEntity, Boolean) -> Unit
|
||||
) {
|
||||
val localContext = LocalContext.current
|
||||
|
||||
@@ -164,16 +166,18 @@ internal fun ToolsScreen(
|
||||
state = state
|
||||
) {
|
||||
|
||||
toolsPanel(toolItems = toolsScreenUiState.tools,
|
||||
toolsPanel(
|
||||
toolItems = toolsScreenUiState.tools,
|
||||
onClick = onNavigateToToolView,
|
||||
onLongClick = {
|
||||
selectedTool = it
|
||||
isShowMenu = true
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
item(span = StaggeredGridItemSpan.FullLine) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
||||
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,6 +211,10 @@ internal fun ToolsScreen(
|
||||
onUndo(selectedTool!!)
|
||||
}
|
||||
}
|
||||
},
|
||||
onChangeStar = {
|
||||
isShowMenu = false
|
||||
onChangeStar(selectedTool!!, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -219,11 +227,16 @@ private fun LazyStaggeredGridScope.toolsPanel(
|
||||
) {
|
||||
items(
|
||||
items = toolItems,
|
||||
key = { it.id },
|
||||
key = { it.id }
|
||||
) {
|
||||
ToolCard(tool = it,
|
||||
ToolCard(
|
||||
tool = it,
|
||||
actionIcon = if (it.isStar) OxygenIcons.Star else null,
|
||||
actionIconContentDescription = stringResource(R.string.core_star),
|
||||
onClick = { onClick(it.authorUsername, it.toolId, false) },
|
||||
onLongClick = { onLongClick(it) })
|
||||
onLongClick = { onLongClick(it) },
|
||||
onAction = { onClick(it.authorUsername, it.toolId, false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +246,8 @@ private fun ToolMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
onDismiss: () -> Unit,
|
||||
selectedTool: ToolEntity,
|
||||
onUninstall: () -> Unit
|
||||
onUninstall: () -> Unit,
|
||||
onChangeStar: (Boolean) -> Unit
|
||||
) {
|
||||
ModalBottomSheet(onDismissRequest = onDismiss, dragHandle = {}) {
|
||||
Column(
|
||||
@@ -248,6 +262,11 @@ private fun ToolMenu(
|
||||
text = stringResource(R.string.core_uninstall),
|
||||
onClick = onUninstall
|
||||
)
|
||||
DialogClickerRow(
|
||||
icon = if (selectedTool.isStar) OxygenIcons.StarBorder else OxygenIcons.Star,
|
||||
text = stringResource(if (selectedTool.isStar) R.string.core_unstar else R.string.core_star),
|
||||
onClick = { onChangeStar(!selectedTool.isStar) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,12 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||
import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository
|
||||
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class ToolsScreenViewModel @Inject constructor(
|
||||
private val storeRepository: StoreRepository,
|
||||
private val toolRepository: ToolRepository,
|
||||
private val savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
@@ -57,6 +55,12 @@ class ToolsScreenViewModel @Inject constructor(
|
||||
toolRepository.saveTool(tool)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeStar(tool: ToolEntity, star: Boolean) {
|
||||
viewModelScope.launch {
|
||||
toolRepository.updateTool(tool.copy(isStar = star))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ToolsScreenUiState {
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
<string name="core_upgrading">更新中……</string>
|
||||
<string name="core_uninstall">卸载</string>
|
||||
<string name="core_uninstall_success">卸载成功</string>
|
||||
<string name="core_star">收藏</string>
|
||||
<string name="core_unstar">取消收藏</string>
|
||||
<string name="core_cancel">取消</string>
|
||||
<string name="core_undo">撤消</string>
|
||||
|
||||
@@ -44,6 +46,7 @@
|
||||
<string name="feature_tool_view_preview_suffix">%1$s (预览)</string>
|
||||
|
||||
<string name="feature_star_title">收藏</string>
|
||||
<string name="feature_star_no_tools_starred">暂无工具已收藏</string>
|
||||
|
||||
<string name="feature_settings_title">设置</string>
|
||||
<string name="feature_settings_language">语言</string>
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<string name="core_upgrading">Upgrading…</string>
|
||||
<string name="core_uninstall">Uninstall</string>
|
||||
<string name="core_uninstall_success">Uninstalled successfully</string>
|
||||
<string name="core_star">Star</string>
|
||||
<string name="core_unstar">Unstar</string>
|
||||
<string name="core_cancel">Cancel</string>
|
||||
<string name="core_undo">Undo</string>
|
||||
|
||||
@@ -43,6 +45,7 @@
|
||||
<string name="feature_tool_view_preview_suffix">%1$s (Preview)</string>
|
||||
|
||||
<string name="feature_star_title">Star</string>
|
||||
<string name="feature_star_no_tools_starred">No tools starred yet</string>
|
||||
|
||||
<string name="feature_settings_title">Settings</string>
|
||||
<string name="feature_settings_language">Language</string>
|
||||
|
||||
Reference in New Issue
Block a user