Improve FeedOptionView

This commit is contained in:
Ash 2022-04-03 23:25:23 +08:00
parent 43bbb87280
commit efba776db3
17 changed files with 501 additions and 287 deletions

View File

@ -14,11 +14,11 @@
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-renamesourcefileattribute SourceFile
-dontobfuscate

View File

@ -2,6 +2,7 @@ package me.ash.reader.ui.component
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetDefaults
@ -12,6 +13,7 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
@ -55,7 +57,8 @@ fun BottomDrawer(
) {
Row(
modifier = modifier
.size(38.dp, 4.dp)
.size(30.dp, 4.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
.zIndex(1f)
) {}

View File

@ -0,0 +1,83 @@
package me.ash.reader.ui.component
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActionScope
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
@Composable
fun ClipboardTextField(
modifier: Modifier = Modifier,
readOnly: Boolean = false,
value: String = "",
onValueChange: (String) -> Unit = {},
placeholder: String = "",
errorText: String = "",
imeAction: ImeAction = ImeAction.Done,
focusManager: FocusManager? = null,
onConfirm: (String) -> Unit = {},
) {
Column(modifier = modifier) {
Spacer(modifier = Modifier.height(10.dp))
TextField(
readOnly = readOnly,
value = value,
onValueChange = onValueChange,
placeholder = placeholder,
errorMessage = errorText,
keyboardActions = KeyboardActions(
onDone = if (imeAction == ImeAction.Done)
action(focusManager, onConfirm, value) else null,
onGo = if (imeAction == ImeAction.Go)
action(focusManager, onConfirm, value) else null,
onNext = if (imeAction == ImeAction.Next)
action(focusManager, onConfirm, value) else null,
onPrevious = if (imeAction == ImeAction.Previous)
action(focusManager, onConfirm, value) else null,
onSearch = if (imeAction == ImeAction.Search)
action(focusManager, onConfirm, value) else null,
onSend = if (imeAction == ImeAction.Send)
action(focusManager, onConfirm, value) else null,
),
keyboardOptions = KeyboardOptions(
imeAction = imeAction
),
)
if (errorText.isNotEmpty()) {
SelectionContainer {
Text(
modifier = Modifier
.padding(start = 16.dp)
.horizontalScroll(rememberScrollState()),
text = errorText,
color = MaterialTheme.colorScheme.error,
maxLines = 1,
softWrap = false,
)
}
}
Spacer(modifier = Modifier.height(10.dp))
}
}
private fun action(
focusManager: FocusManager?,
onConfirm: (String) -> Unit,
value: String
): KeyboardActionScope.() -> Unit = {
focusManager?.clearFocus()
onConfirm(value)
}

View File

@ -44,7 +44,7 @@ fun SelectionChip(
val focusManager = LocalFocusManager.current
FilterChip(
modifier = modifier,
modifier = modifier.defaultMinSize(minHeight = 36.dp),
colors = ChipDefaults.filterChipColors(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,

View File

@ -0,0 +1,90 @@
package me.ash.reader.ui.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.ContentPaste
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import kotlinx.coroutines.delay
import me.ash.reader.R
@Composable
fun TextField(
readOnly: Boolean,
value: String,
onValueChange: (String) -> Unit,
placeholder: String,
errorMessage: String,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
) {
val clipboardManager = LocalClipboardManager.current
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
delay(100) // ???
focusRequester.requestFocus()
}
androidx.compose.material.TextField(
modifier = Modifier.focusRequester(focusRequester),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.onSurface,
textColor = MaterialTheme.colorScheme.onSurface,
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
),
enabled = !readOnly,
value = value,
onValueChange = {
if (!readOnly) onValueChange(it)
},
placeholder = {
Text(
text = placeholder,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
)
},
isError = errorMessage.isNotEmpty(),
singleLine = true,
trailingIcon = {
if (value.isNotEmpty()) {
IconButton(onClick = {
if (!readOnly) onValueChange("")
}) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = stringResource(R.string.clear),
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
)
}
} else {
IconButton(onClick = {
onValueChange(clipboardManager.getText()?.text ?: "")
}) {
Icon(
imageVector = Icons.Rounded.ContentPaste,
contentDescription = stringResource(R.string.paste),
tint = MaterialTheme.colorScheme.primary
)
}
}
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
}

View File

@ -0,0 +1,92 @@
package me.ash.reader.ui.component
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.window.DialogProperties
import com.google.accompanist.pager.ExperimentalPagerApi
import me.ash.reader.R
@OptIn(ExperimentalPagerApi::class)
@Composable
fun TextFieldDialog(
modifier: Modifier = Modifier,
properties: DialogProperties = DialogProperties(),
visible: Boolean = false,
readOnly: Boolean = false,
title: String = "",
icon: ImageVector? = null,
value: String = "",
placeholder: String = "",
errorText: String = "",
dismissText: String = stringResource(R.string.cancel),
confirmText: String = stringResource(R.string.confirm),
onValueChange: (String) -> Unit = {},
onDismissRequest: () -> Unit = {},
onConfirm: (String) -> Unit = {},
imeAction: ImeAction = ImeAction.Done,
) {
val focusManager = LocalFocusManager.current
Dialog(
modifier = modifier,
visible = visible,
onDismissRequest = onDismissRequest,
icon = {
icon?.let {
Icon(
imageVector = icon,
contentDescription = title,
)
}
},
title = {
Text(text = title, maxLines = 1, overflow = TextOverflow.Ellipsis)
},
text = {
ClipboardTextField(
modifier = modifier,
readOnly = readOnly,
value = value,
onValueChange = onValueChange,
placeholder = placeholder,
errorText = errorText,
imeAction = imeAction,
focusManager = focusManager,
onConfirm = onConfirm,
)
},
confirmButton = {
TextButton(
enabled = value.isNotBlank(),
onClick = {
focusManager.clearFocus()
onConfirm(value)
}
) {
Text(
text = confirmText,
color = if (value.isNotBlank()) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
}
)
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = dismissText)
}
},
)
}

View File

@ -67,6 +67,10 @@ fun HomePage(
BackHandler(true) {
val currentPage = viewState.pagerState.currentPage
if (currentPage == 0) {
context.findActivity()?.moveTaskToBack(false)
return@BackHandler
}
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,

View File

@ -2,7 +2,7 @@ package me.ash.reader.ui.page.home.drawer.feed
import android.widget.Toast
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DeleteOutline
import androidx.compose.material.icons.outlined.DeleteForever
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -36,7 +36,7 @@ fun DeleteFeedDialog(
},
icon = {
Icon(
imageVector = Icons.Rounded.DeleteOutline,
imageVector = Icons.Outlined.DeleteForever,
contentDescription = stringResource(R.string.subscribe),
)
},

View File

@ -1,16 +1,19 @@
package me.ash.reader.ui.page.home.drawer.feed
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DeleteOutline
import androidx.compose.material.icons.outlined.CreateNewFolder
import androidx.compose.material.icons.rounded.RssFeed
import androidx.compose.material3.*
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -18,7 +21,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.R
import me.ash.reader.ui.component.BottomDrawer
import me.ash.reader.ui.component.Subtitle
import me.ash.reader.ui.component.TextFieldDialog
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.roundClick
import me.ash.reader.ui.page.home.feeds.subscribe.ResultView
@ -27,10 +30,10 @@ import me.ash.reader.ui.page.home.feeds.subscribe.ResultView
@Composable
fun FeedOptionDrawer(
modifier: Modifier = Modifier,
viewModel: FeedOptionViewModel = hiltViewModel(),
feedOptionViewModel: FeedOptionViewModel = hiltViewModel(),
content: @Composable () -> Unit = {},
) {
val viewState = viewModel.viewState.collectAsStateValue()
val viewState = feedOptionViewModel.viewState.collectAsStateValue()
val feed = viewState.feed
BottomDrawer(
@ -65,53 +68,24 @@ fun FeedOptionDrawer(
groups = viewState.groups,
selectedAllowNotificationPreset = viewState.feed?.isNotification ?: false,
selectedParseFullContentPreset = viewState.feed?.isFullContent ?: false,
showUnsubscribe = true,
selectedGroupId = viewState.feed?.groupId ?: "",
newGroupContent = viewState.newGroupContent,
onNewGroupValueChange = {
viewModel.dispatch(FeedOptionViewAction.InputNewGroup(it))
},
newGroupSelected = viewState.newGroupSelected,
changeNewGroupSelected = {
viewModel.dispatch(FeedOptionViewAction.SelectedNewGroup(it))
},
allowNotificationPresetOnClick = {
viewModel.dispatch(FeedOptionViewAction.ChangeAllowNotificationPreset)
feedOptionViewModel.dispatch(FeedOptionViewAction.ChangeAllowNotificationPreset)
},
parseFullContentPresetOnClick = {
viewModel.dispatch(FeedOptionViewAction.ChangeParseFullContentPreset)
feedOptionViewModel.dispatch(FeedOptionViewAction.ChangeParseFullContentPreset)
},
unsubscribeOnClick = {
feedOptionViewModel.dispatch(FeedOptionViewAction.ShowDeleteDialog)
},
onGroupClick = {
viewModel.dispatch(FeedOptionViewAction.SelectedGroup(it))
feedOptionViewModel.dispatch(FeedOptionViewAction.SelectedGroup(it))
},
onKeyboardAction = { },
)
Spacer(modifier = Modifier.height(20.dp))
Subtitle(text = stringResource(R.string.options))
Spacer(modifier = Modifier.height(10.dp))
Button(
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.error,
),
border = BorderStroke(
width = 1.dp,
color = MaterialTheme.colorScheme.error,
),
onClick = {
viewModel.dispatch(FeedOptionViewAction.ShowDeleteDialog)
onAddNewGroup = {
feedOptionViewModel.dispatch(FeedOptionViewAction.ShowNewGroupDialog)
}
) {
Icon(
modifier = Modifier.size(ButtonDefaults.IconSize),
imageVector = Icons.Rounded.DeleteOutline,
contentDescription = stringResource(R.string.delete),
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(
text = stringResource(R.string.unsubscribe),
style = MaterialTheme.typography.titleSmall,
)
}
)
}
}
) {
@ -119,4 +93,21 @@ fun FeedOptionDrawer(
}
DeleteFeedDialog(feedName = feed?.name ?: "")
TextFieldDialog(
visible = viewState.newGroupDialogVisible,
title = stringResource(R.string.create_new_group),
icon = Icons.Outlined.CreateNewFolder,
value = viewState.newGroupContent,
placeholder = stringResource(R.string.name),
onValueChange = {
feedOptionViewModel.dispatch(FeedOptionViewAction.InputNewGroup(it))
},
onDismissRequest = {
feedOptionViewModel.dispatch(FeedOptionViewAction.HideNewGroupDialog)
},
onConfirm = {
feedOptionViewModel.dispatch(FeedOptionViewAction.AddNewGroup)
}
)
}

View File

@ -49,12 +49,14 @@ class FeedOptionViewModel @Inject constructor(
is FeedOptionViewAction.Hide -> hide(action.scope)
is FeedOptionViewAction.SelectedGroup -> selectedGroup(action.groupId)
is FeedOptionViewAction.InputNewGroup -> inputNewGroup(action.content)
is FeedOptionViewAction.SelectedNewGroup -> selectedNewGroup(action.selected)
is FeedOptionViewAction.ChangeAllowNotificationPreset -> changeAllowNotificationPreset()
is FeedOptionViewAction.ChangeParseFullContentPreset -> changeParseFullContentPreset()
is FeedOptionViewAction.ShowDeleteDialog -> showDeleteDialog()
is FeedOptionViewAction.HideDeleteDialog -> hideDeleteDialog()
is FeedOptionViewAction.Delete -> delete(action.callback)
is FeedOptionViewAction.AddNewGroup -> addNewGroup()
is FeedOptionViewAction.ShowNewGroupDialog -> changeNewGroupDialogVisible(true)
is FeedOptionViewAction.HideNewGroupDialog -> changeNewGroupDialogVisible(false)
}
}
@ -81,6 +83,15 @@ class FeedOptionViewModel @Inject constructor(
}
}
private fun changeNewGroupDialogVisible(visible: Boolean) {
_viewState.update {
it.copy(
newGroupDialogVisible = visible,
newGroupContent = "",
)
}
}
private fun inputNewGroup(content: String) {
_viewState.update {
it.copy(
@ -89,6 +100,15 @@ class FeedOptionViewModel @Inject constructor(
}
}
private fun addNewGroup() {
if (_viewState.value.newGroupContent.isNotBlank()) {
viewModelScope.launch {
selectedGroup(rssRepository.get().addGroup(_viewState.value.newGroupContent))
changeNewGroupDialogVisible(false)
}
}
}
private fun selectedGroup(groupId: String) {
viewModelScope.launch(Dispatchers.IO) {
_viewState.value.feed?.let {
@ -102,14 +122,6 @@ class FeedOptionViewModel @Inject constructor(
}
}
private fun selectedNewGroup(selected: Boolean) {
_viewState.update {
it.copy(
newGroupSelected = selected,
)
}
}
private fun changeParseFullContentPreset() {
viewModelScope.launch(Dispatchers.IO) {
_viewState.value.feed?.let {
@ -170,7 +182,7 @@ data class FeedOptionViewState(
val feed: Feed? = null,
val selectedGroupId: String = "",
val newGroupContent: String = "",
val newGroupSelected: Boolean = false,
val newGroupDialogVisible: Boolean = false,
val groups: List<Group> = emptyList(),
val deleteDialogVisible: Boolean = false,
)
@ -196,14 +208,14 @@ sealed class FeedOptionViewAction {
val content: String
) : FeedOptionViewAction()
data class SelectedNewGroup(
val selected: Boolean
) : FeedOptionViewAction()
data class Delete(
val callback: () -> Unit = {}
) : FeedOptionViewAction()
object ShowDeleteDialog : FeedOptionViewAction()
object HideDeleteDialog : FeedOptionViewAction()
object ShowNewGroupDialog : FeedOptionViewAction()
object HideNewGroupDialog : FeedOptionViewAction()
object AddNewGroup : FeedOptionViewAction()
}

View File

@ -10,9 +10,9 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.KeyboardArrowRight
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.Tune
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@ -106,7 +106,8 @@ fun FeedsPage(
navigationIcon = {
FeedbackIconButton(
isHaptic = false,
imageVector = Icons.Rounded.Tune,
modifier = Modifier.size(20.dp),
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(R.string.settings),
tint = MaterialTheme.colorScheme.onSurface,
) {

View File

@ -3,18 +3,27 @@ package me.ash.reader.ui.page.home.feeds.subscribe
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@ -24,7 +33,6 @@ import com.google.accompanist.flowlayout.MainAxisAlignment
import me.ash.reader.R
import me.ash.reader.data.entity.Group
import me.ash.reader.ui.component.SelectionChip
import me.ash.reader.ui.component.SelectionEditorChip
import me.ash.reader.ui.component.Subtitle
import me.ash.reader.ui.ext.roundClick
@ -35,41 +43,39 @@ fun ResultView(
groups: List<Group> = emptyList(),
selectedAllowNotificationPreset: Boolean = false,
selectedParseFullContentPreset: Boolean = false,
showUnsubscribe: Boolean = false,
selectedGroupId: String = "",
newGroupContent: String = "",
newGroupSelected: Boolean,
onNewGroupValueChange: (String) -> Unit = {},
changeNewGroupSelected: (Boolean) -> Unit = {},
allowNotificationPresetOnClick: () -> Unit = {},
parseFullContentPresetOnClick: () -> Unit = {},
unsubscribeOnClick: () -> Unit = {},
onGroupClick: (groupId: String) -> Unit = {},
onKeyboardAction: () -> Unit = {},
onAddNewGroup: () -> Unit = {},
) {
LaunchedEffect(Unit) {
if (groups.isNotEmpty()) onGroupClick(groups.first().id)
}
Column(
modifier = modifier.verticalScroll(rememberScrollState())
) {
Link(
text = link
)
Link(text = link)
Spacer(modifier = Modifier.height(26.dp))
Preset(
selectedAllowNotificationPreset = selectedAllowNotificationPreset,
selectedParseFullContentPreset = selectedParseFullContentPreset,
showUnsubscribe = showUnsubscribe,
allowNotificationPresetOnClick = allowNotificationPresetOnClick,
parseFullContentPresetOnClick = parseFullContentPresetOnClick,
unsubscribeOnClick = unsubscribeOnClick,
)
Spacer(modifier = Modifier.height(26.dp))
AddToGroup(
groups = groups,
selectedGroupId = selectedGroupId,
newGroupContent = newGroupContent,
newGroupSelected = newGroupSelected,
onNewGroupValueChange = onNewGroupValueChange,
changeNewGroupSelected = changeNewGroupSelected,
onGroupClick = onGroupClick,
onKeyboardAction = onKeyboardAction,
onAddNewGroup = onAddNewGroup,
)
Spacer(modifier = Modifier.height(6.dp))
}
@ -105,8 +111,10 @@ private fun Link(
private fun Preset(
selectedAllowNotificationPreset: Boolean = false,
selectedParseFullContentPreset: Boolean = false,
showUnsubscribe: Boolean = false,
allowNotificationPresetOnClick: () -> Unit = {},
parseFullContentPresetOnClick: () -> Unit = {},
unsubscribeOnClick: () -> Unit = {},
) {
Subtitle(text = stringResource(R.string.preset))
Spacer(modifier = Modifier.height(10.dp))
@ -147,6 +155,15 @@ private fun Preset(
) {
parseFullContentPresetOnClick()
}
if (showUnsubscribe) {
SelectionChip(
modifier = Modifier.animateContentSize(),
content = stringResource(R.string.unsubscribe),
selected = false,
) {
unsubscribeOnClick()
}
}
}
}
@ -154,39 +171,61 @@ private fun Preset(
private fun AddToGroup(
groups: List<Group>,
selectedGroupId: String,
newGroupContent: String,
newGroupSelected: Boolean,
onNewGroupValueChange: (String) -> Unit = {},
changeNewGroupSelected: (Boolean) -> Unit = {},
onGroupClick: (groupId: String) -> Unit = {},
onKeyboardAction: () -> Unit = {},
onAddNewGroup: () -> Unit = {},
) {
Subtitle(text = stringResource(R.string.add_to_group))
Spacer(modifier = Modifier.height(10.dp))
FlowRow(
mainAxisAlignment = MainAxisAlignment.Start,
crossAxisSpacing = 10.dp,
mainAxisSpacing = 10.dp,
) {
groups.forEach {
SelectionChip(
modifier = Modifier.animateContentSize(),
content = it.name,
selected = !newGroupSelected && it.id == selectedGroupId,
) {
changeNewGroupSelected(false)
onGroupClick(it.id)
}
}
SelectionEditorChip(
modifier = Modifier.animateContentSize(),
content = newGroupContent,
onValueChange = onNewGroupValueChange,
selected = newGroupSelected,
onKeyboardAction = onKeyboardAction,
if (groups.size > 6) {
LazyRow {
items(groups) {
SelectionChip(
modifier = Modifier.animateContentSize(),
content = it.name,
selected = it.id == selectedGroupId,
) {
onGroupClick(it.id)
}
Spacer(modifier = Modifier.width(10.dp))
}
item { NewGroupButton(onAddNewGroup) }
}
} else {
FlowRow(
mainAxisAlignment = MainAxisAlignment.Start,
crossAxisSpacing = 10.dp,
mainAxisSpacing = 10.dp,
) {
changeNewGroupSelected(true)
groups.forEach {
SelectionChip(
modifier = Modifier.animateContentSize(),
content = it.name,
selected = it.id == selectedGroupId,
) {
onGroupClick(it.id)
}
}
NewGroupButton(onAddNewGroup)
}
}
}
@Composable
private fun NewGroupButton(onAddNewGroup: () -> Unit) {
Box(
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
.clickable { onAddNewGroup() },
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = Icons.Outlined.Add,
contentDescription = stringResource(R.string.create_new_group),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
)
}
}

View File

@ -1,125 +0,0 @@
package me.ash.reader.ui.page.home.feeds.subscribe
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.ContentPaste
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import me.ash.reader.R
@Composable
fun SearchView(
modifier: Modifier = Modifier,
readOnly: Boolean = false,
inputLink: String = "",
errorMessage: String = "",
onLinkValueChange: (String) -> Unit = {},
onKeyboardAction: () -> Unit = {},
) {
val focusManager = LocalFocusManager.current
val clipboardManager = LocalClipboardManager.current
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
delay(100) // ???
focusRequester.requestFocus()
}
Column(modifier = modifier) {
Spacer(modifier = Modifier.height(10.dp))
TextField(
modifier = Modifier.focusRequester(focusRequester),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.onSurface,
textColor = MaterialTheme.colorScheme.onSurface,
focusedIndicatorColor = MaterialTheme.colorScheme.primary,
),
enabled = !readOnly,
value = inputLink,
onValueChange = {
if (!readOnly) onLinkValueChange(it)
},
placeholder = {
Text(
text = stringResource(R.string.feed_or_site_url),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
)
},
isError = errorMessage.isNotEmpty(),
singleLine = true,
trailingIcon = {
if (inputLink.isNotEmpty()) {
IconButton(onClick = {
if (!readOnly) onLinkValueChange("")
}) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = stringResource(R.string.clear),
tint = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
)
}
} else {
IconButton(onClick = {
onLinkValueChange(clipboardManager.getText()?.text ?: "")
}) {
Icon(
imageVector = Icons.Rounded.ContentPaste,
contentDescription = stringResource(R.string.paste),
tint = MaterialTheme.colorScheme.primary
)
}
}
},
keyboardActions = KeyboardActions(
onSearch = {
focusManager.clearFocus()
onKeyboardAction()
}
),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search
),
)
if (errorMessage.isNotEmpty()) {
SelectionContainer {
Text(
modifier = Modifier
.padding(start = 16.dp)
.horizontalScroll(rememberScrollState()),
text = errorMessage,
color = MaterialTheme.colorScheme.error,
maxLines = 1,
softWrap = false,
)
}
}
Spacer(modifier = Modifier.height(10.dp))
}
}

View File

@ -5,6 +5,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CreateNewFolder
import androidx.compose.material.icons.rounded.RssFeed
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -18,12 +19,16 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.R
import me.ash.reader.ui.component.ClipboardTextField
import me.ash.reader.ui.component.Dialog
import me.ash.reader.ui.ext.*
import me.ash.reader.ui.component.TextFieldDialog
import me.ash.reader.ui.ext.collectAsStateValue
@OptIn(
androidx.compose.ui.ExperimentalComposeUiApi::class,
@ -45,15 +50,9 @@ fun SubscribeDialog(
}
}
}
val readYouString = stringResource(R.string.read_you)
val defaultString = stringResource(R.string.defaults)
LaunchedEffect(viewState.visible) {
if (viewState.visible) {
val defaultGroupId = context.dataStore
.get(DataStoreKeys.CurrentAccountId)!!
.spacerDollar(readYouString + defaultString)
subscribeViewModel.dispatch(SubscribeViewAction.SelectedGroup(defaultGroupId))
subscribeViewModel.dispatch(SubscribeViewAction.Init)
} else {
subscribeViewModel.dispatch(SubscribeViewAction.Reset)
@ -77,11 +76,13 @@ fun SubscribeDialog(
},
title = {
Text(
if (viewState.isSearchPage) {
text = if (viewState.isSearchPage) {
viewState.title
} else {
viewState.feed?.name ?: stringResource(R.string.unknown)
}
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
text = {
@ -93,14 +94,17 @@ fun SubscribeDialog(
}
) { targetExpanded ->
if (targetExpanded) {
SearchView(
ClipboardTextField(
readOnly = viewState.lockLinkInput,
inputLink = viewState.linkContent,
errorMessage = viewState.errorMessage,
onLinkValueChange = {
value = viewState.linkContent,
onValueChange = {
subscribeViewModel.dispatch(SubscribeViewAction.InputLink(it))
},
onKeyboardAction = {
placeholder = stringResource(R.string.feed_or_site_url),
errorText = viewState.errorMessage,
imeAction = ImeAction.Search,
focusManager = focusManager,
onConfirm = {
subscribeViewModel.dispatch(SubscribeViewAction.Search)
},
)
@ -111,14 +115,6 @@ fun SubscribeDialog(
selectedAllowNotificationPreset = viewState.allowNotificationPreset,
selectedParseFullContentPreset = viewState.parseFullContentPreset,
selectedGroupId = viewState.selectedGroupId,
newGroupContent = viewState.newGroupContent,
onNewGroupValueChange = {
subscribeViewModel.dispatch(SubscribeViewAction.InputNewGroup(it))
},
newGroupSelected = viewState.newGroupSelected,
changeNewGroupSelected = {
subscribeViewModel.dispatch(SubscribeViewAction.SelectedNewGroup(it))
},
allowNotificationPresetOnClick = {
subscribeViewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset)
},
@ -128,8 +124,8 @@ fun SubscribeDialog(
onGroupClick = {
subscribeViewModel.dispatch(SubscribeViewAction.SelectedGroup(it))
},
onKeyboardAction = {
subscribeViewModel.dispatch(SubscribeViewAction.Subscribe)
onAddNewGroup = {
subscribeViewModel.dispatch(SubscribeViewAction.ShowNewGroupDialog)
},
)
}
@ -138,7 +134,7 @@ fun SubscribeDialog(
confirmButton = {
if (viewState.isSearchPage) {
TextButton(
enabled = viewState.linkContent.isNotEmpty()
enabled = viewState.linkContent.isNotBlank()
&& viewState.title != stringResource(R.string.searching),
onClick = {
focusManager.clearFocus()
@ -147,7 +143,7 @@ fun SubscribeDialog(
) {
Text(
text = stringResource(R.string.search),
color = if (viewState.linkContent.isNotEmpty()) {
color = if (viewState.linkContent.isNotBlank()) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
@ -188,4 +184,21 @@ fun SubscribeDialog(
}
},
)
TextFieldDialog(
visible = viewState.newGroupDialogVisible,
title = stringResource(R.string.create_new_group),
icon = Icons.Outlined.CreateNewFolder,
value = viewState.newGroupContent,
placeholder = stringResource(R.string.name),
onValueChange = {
subscribeViewModel.dispatch(SubscribeViewAction.InputNewGroup(it))
},
onDismissRequest = {
subscribeViewModel.dispatch(SubscribeViewAction.HideNewGroupDialog)
},
onConfirm = {
subscribeViewModel.dispatch(SubscribeViewAction.AddNewGroup)
}
)
}

View File

@ -40,6 +40,8 @@ class SubscribeViewModel @Inject constructor(
is SubscribeViewAction.Reset -> reset()
is SubscribeViewAction.Show -> changeVisible(true)
is SubscribeViewAction.Hide -> changeVisible(false)
is SubscribeViewAction.ShowNewGroupDialog -> changeNewGroupDialogVisible(true)
is SubscribeViewAction.HideNewGroupDialog -> changeNewGroupDialogVisible(false)
is SubscribeViewAction.SwitchPage -> switchPage(action.isSearchPage)
is SubscribeViewAction.ImportFromInputStream -> importFromInputStream(action.inputStream)
is SubscribeViewAction.InputLink -> inputLink(action.content)
@ -50,7 +52,7 @@ class SubscribeViewModel @Inject constructor(
changeParseFullContentPreset()
is SubscribeViewAction.SelectedGroup -> selectedGroup(action.groupId)
is SubscribeViewAction.InputNewGroup -> inputNewGroup(action.content)
is SubscribeViewAction.SelectedNewGroup -> selectedNewGroup(action.selected)
is SubscribeViewAction.AddNewGroup -> addNewGroup()
is SubscribeViewAction.Subscribe -> subscribe()
}
}
@ -90,14 +92,7 @@ class SubscribeViewModel @Inject constructor(
val articles = _viewState.value.articles
viewModelScope.launch(Dispatchers.IO) {
val groupId = async {
if (
_viewState.value.newGroupSelected &&
_viewState.value.newGroupContent.isNotBlank()
) {
rssRepository.get().addGroup(_viewState.value.newGroupContent)
} else {
_viewState.value.selectedGroupId
}
_viewState.value.selectedGroupId
}
rssRepository.get().subscribe(
feed.copy(
@ -118,11 +113,17 @@ class SubscribeViewModel @Inject constructor(
}
}
private fun selectedNewGroup(selected: Boolean) {
_viewState.update {
it.copy(
newGroupSelected = selected,
)
private fun addNewGroup() {
if (_viewState.value.newGroupContent.isNotBlank()) {
viewModelScope.launch {
selectedGroup(rssRepository.get().addGroup(_viewState.value.newGroupContent))
changeNewGroupDialogVisible(false)
_viewState.update {
it.copy(
newGroupContent = "",
)
}
}
}
}
@ -224,6 +225,14 @@ class SubscribeViewModel @Inject constructor(
}
}
private fun changeNewGroupDialogVisible(visible: Boolean) {
_viewState.update {
it.copy(
newGroupDialogVisible = visible,
)
}
}
private fun switchPage(isSearchPage: Boolean) {
_viewState.update {
it.copy(
@ -245,8 +254,8 @@ data class SubscribeViewState(
val allowNotificationPreset: Boolean = false,
val parseFullContentPreset: Boolean = false,
val selectedGroupId: String = "",
val newGroupDialogVisible: Boolean = false,
val newGroupContent: String = "",
val newGroupSelected: Boolean = false,
val groups: Flow<List<Group>> = emptyFlow(),
val isSearchPage: Boolean = true,
)
@ -258,6 +267,10 @@ sealed class SubscribeViewAction {
object Show : SubscribeViewAction()
object Hide : SubscribeViewAction()
object ShowNewGroupDialog : SubscribeViewAction()
object HideNewGroupDialog : SubscribeViewAction()
object AddNewGroup : SubscribeViewAction()
data class SwitchPage(
val isSearchPage: Boolean
) : SubscribeViewAction()
@ -270,7 +283,7 @@ sealed class SubscribeViewAction {
val content: String
) : SubscribeViewAction()
object Search: SubscribeViewAction()
object Search : SubscribeViewAction()
object ChangeAllowNotificationPreset : SubscribeViewAction()
object ChangeParseFullContentPreset : SubscribeViewAction()
@ -283,9 +296,5 @@ sealed class SubscribeViewAction {
val content: String
) : SubscribeViewAction()
data class SelectedNewGroup(
val selected: Boolean
) : SubscribeViewAction()
object Subscribe : SubscribeViewAction()
}

View File

@ -31,13 +31,14 @@
<string name="allow_notification">允许通知</string>
<string name="parse_full_content">全文解析</string>
<string name="add_to_group">添加到组</string>
<string name="new_group">新建分组</string>
<string name="create_new_group">新建分组</string>
<string name="name">名称</string>
<string name="open_with">打开 %1$s</string>
<string name="options">选项</string>
<string name="delete">删除</string>
<string name="has_been_deleted">“%1$s” 已被删除</string>
<string name="has_been_deleted">\"%1$s\" 已被删除</string>
<string name="unsubscribe">取消订阅</string>
<string name="unsubscribe_tip">不再订阅 “%1$s”,同时删除其所有已归档的文章。</string>
<string name="unsubscribe_tip">不再订阅 \"%1$s\",同时删除其所有已归档的文章。</string>
<string name="today">今天</string>
<string name="yesterday">昨天</string>
<string name="date_at_time">%1$s %2$s</string>

View File

@ -31,13 +31,14 @@
<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="create_new_group">Create New Group</string>
<string name="name">Name</string>
<string name="open_with">Open %1$s</string>
<string name="options">Options</string>
<string name="delete">Delete</string>
<string name="has_been_deleted">"%1$s" has been deleted</string>
<string name="has_been_deleted">\"%1$s\" has been deleted</string>
<string name="unsubscribe">Unsubscribe</string>
<string name="unsubscribe_tip">Unsubscribe "%1$s" and delete all its archived articles.</string>
<string name="unsubscribe_tip">Unsubscribe \"%1$s\" and delete all its archived articles.</string>
<string name="today">Today</string>
<string name="yesterday">Yesterday</string>
<string name="date_at_time">%1$s At %2$s</string>