diff --git a/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt b/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt new file mode 100644 index 0000000..748ffcd --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt @@ -0,0 +1,51 @@ +package me.ash.reader.ui.component + +import androidx.compose.animation.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +@Composable +fun DisplayText( + modifier: Modifier = Modifier, + text: String, + desc: String, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding( + start = 24.dp, + top = 48.dp, + end = 24.dp, + bottom = 24.dp, + ) + ) { + Text( + text = text, + style = MaterialTheme.typography.displaySmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + AnimatedVisibility( + visible = desc.isNotEmpty(), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + Text( + text = desc, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/component/FeedbackIconButton.kt b/app/src/main/java/me/ash/reader/ui/component/FeedbackIconButton.kt new file mode 100644 index 0000000..0e00ea6 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/component/FeedbackIconButton.kt @@ -0,0 +1,40 @@ +package me.ash.reader.ui.component + +import android.view.HapticFeedbackConstants +import android.view.SoundEffectConstants +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +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.LocalView + +@Composable +fun FeedbackIconButton( + modifier: Modifier = Modifier, + imageVector: ImageVector, + contentDescription: String?, + tint: Color = LocalContentColor.current, + isHaptic: Boolean? = true, + isSound: Boolean? = true, + onClick: () -> Unit = {}, +) { + val view = LocalView.current + + IconButton( + onClick = { + if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK) + onClick() + }, + ) { + Icon( + modifier = modifier, + imageVector = imageVector, + contentDescription = contentDescription, + tint = tint, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt b/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt index 511f1cd..6d86d30 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt @@ -1,11 +1,21 @@ package me.ash.reader.ui.ext -import androidx.compose.foundation.clickable +import android.annotation.SuppressLint +import android.view.HapticFeedbackConstants +import android.view.SoundEffectConstants +import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp @@ -48,3 +58,70 @@ fun Modifier.roundClick(onClick: () -> Unit = {}) = this fun Modifier.paddingFixedHorizontal(top: Dp = 0.dp, bottom: Dp = 0.dp) = this .padding(horizontal = 10.dp) .padding(top = top, bottom = bottom) + +@OptIn( + ExperimentalFoundationApi::class, + androidx.compose.ui.ExperimentalComposeUiApi::class +) +@Composable +@SuppressLint("ComposableModifierFactory") +fun Modifier.combinedFeedbackClickable( + isHaptic: Boolean? = false, + isSound: Boolean? = false, + onPressDown: (() -> Unit)? = null, + onPressUp: (() -> Unit)? = null, + onLongClick: (() -> Unit)? = null, + onDoubleClick: (() -> Unit)? = null, + onClick: (() -> Unit)? = null, +): Modifier { + val view = LocalView.current + val interactionSource = remember { MutableInteractionSource() } + return if (onPressDown != null || onPressUp != null) { + indication(interactionSource, LocalIndication.current) + .pointerInput(Unit) { + detectTapGestures( + onPress = { offset -> + onPressDown?.let { + if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + val press = PressInteraction.Press(offset) + interactionSource.emit(press) + tryAwaitRelease() + interactionSource.emit(PressInteraction.Release(press)) + it() + } + }, + onTap = { + onPressUp?.let { + if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK) + it() + } + } + ) + } + } else { + combinedClickable( + onClick = { + onClick?.let { + if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK) + it() + } + }, + onLongClick = { + onLongClick?.let { + if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK) + it() + } + }, + onDoubleClick = { + onDoubleClick?.let { + if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK) + it() + } + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/FilterBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/FilterBar.kt index ffe62a4..86a070d 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/FilterBar.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/FilterBar.kt @@ -1,9 +1,11 @@ package me.ash.reader.ui.page.home +import android.view.SoundEffectConstants import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import com.google.accompanist.pager.ExperimentalPagerApi @@ -17,6 +19,8 @@ fun FilterBar( filter: Filter, filterOnClick: (Filter) -> Unit = {}, ) { + val view = LocalView.current + Box( modifier = Modifier.height(60.dp) ) { @@ -45,7 +49,11 @@ fun FilterBar( ) }, selected = filter == item, - onClick = { filterOnClick(item) }, + onClick = { +// view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + view.playSoundEffect(SoundEffectConstants.CLICK) + filterOnClick(item) + }, // colors = NavigationBarItemDefaults.colors( // selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer, // unselectedIconColor = MaterialTheme.colorScheme.outline, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt index bcb236a..cd9591b 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt @@ -2,7 +2,6 @@ package me.ash.reader.ui.page.home.feeds import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures @@ -12,8 +11,8 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.KeyboardArrowRight import androidx.compose.material.icons.rounded.Add -import androidx.compose.material.icons.rounded.ArrowBack 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 @@ -22,7 +21,6 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner 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.lifecycle.LiveData @@ -31,6 +29,8 @@ import androidx.work.WorkInfo import me.ash.reader.R import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing import me.ash.reader.ui.component.Banner +import me.ash.reader.ui.component.DisplayText +import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.component.Subtitle import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.getDesc @@ -104,39 +104,35 @@ fun FeedsPage( SmallTopAppBar( title = {}, navigationIcon = { - IconButton(onClick = { + FeedbackIconButton( + isHaptic = false, + imageVector = Icons.Rounded.Tune, + contentDescription = stringResource(R.string.settings), + tint = MaterialTheme.colorScheme.onSurface, + ) { onScrollToPage(0) - }) { - Icon( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) } }, actions = { - IconButton(onClick = { + FeedbackIconButton( + isHaptic = false, + modifier = Modifier.rotate(if (isSyncing) angle else 0f), + imageVector = Icons.Rounded.Refresh, + contentDescription = stringResource(R.string.refresh), + tint = MaterialTheme.colorScheme.onSurface, + ) { if (!isSyncing) { onSyncClick() } - }) { - Icon( - modifier = Modifier.rotate(if (isSyncing) angle else 0f), - imageVector = Icons.Rounded.Refresh, - contentDescription = stringResource(R.string.refresh), - tint = MaterialTheme.colorScheme.onSurface, - ) } - IconButton(onClick = { + FeedbackIconButton( + isHaptic = false, + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(R.string.subscribe), + tint = MaterialTheme.colorScheme.onSurface, + ) { subscribeViewModel.dispatch(SubscribeViewAction.Show) - }) { - Icon( - imageVector = Icons.Rounded.Add, - contentDescription = stringResource(R.string.subscribe), - tint = MaterialTheme.colorScheme.onSurface, - ) } - } ) }, @@ -144,48 +140,18 @@ fun FeedsPage( SubscribeDialog() LazyColumn { item { - Text( - modifier = Modifier - .padding( - start = 24.dp, - top = 48.dp, - end = 24.dp, -// bottom = 24.dp + DisplayText( + modifier = Modifier.pointerInput(Unit) { + detectTapGestures( + onLongPress = { + launcher.launch("ReadYou.opml") + } ) - .pointerInput(Unit) { - detectTapGestures( - onLongPress = { - launcher.launch("ReadYou.opml") - } - ) - }, + }, text = viewState.account?.name ?: stringResource(R.string.unknown), - style = MaterialTheme.typography.displaySmall, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + desc = if (isSyncing) stringResource(R.string.syncing) else "", ) } - item { - AnimatedVisibility( - visible = isSyncing, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - Text( - modifier = Modifier.padding( - start = 24.dp, - top = 0.dp, - end = 24.dp, - bottom = 0.dp - ), - text = stringResource(R.string.syncing), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), - ) - } - Spacer(modifier = Modifier.height(24.dp)) - } item { Banner( title = filterState.filter.getName(), diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt index 2030d63..a397894 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -10,14 +10,16 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.DoneAll import androidx.compose.material.icons.rounded.Search -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SmallTopAppBar import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner 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.lifecycle.LiveData @@ -29,6 +31,8 @@ import kotlinx.coroutines.launch import me.ash.reader.R import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing +import me.ash.reader.ui.component.DisplayText +import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.getName import me.ash.reader.ui.page.home.FilterBar @@ -87,12 +91,13 @@ fun FlowPage( SmallTopAppBar( title = {}, navigationIcon = { - IconButton(onClick = { onScrollToPage(0) }) { - Icon( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) + FeedbackIconButton( + isHaptic = false, + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { + onScrollToPage(0) } }, actions = { @@ -101,29 +106,28 @@ fun FlowPage( enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically(), ) { - IconButton(onClick = { + FeedbackIconButton( + isHaptic = false, + imageVector = Icons.Rounded.DoneAll, + contentDescription = stringResource(R.string.mark_all_as_read), + tint = if (markAsRead) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + }, + ) { scope.launch { viewState.listState.scrollToItem(0) markAsRead = !markAsRead } - }) { - Icon( - imageVector = Icons.Rounded.DoneAll, - contentDescription = stringResource(R.string.mark_all_as_read), - tint = if (markAsRead) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface - }, - ) } } - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Rounded.Search, - contentDescription = stringResource(R.string.search), - tint = MaterialTheme.colorScheme.onSurface, - ) + FeedbackIconButton( + isHaptic = false, + imageVector = Icons.Rounded.Search, + contentDescription = stringResource(R.string.search), + tint = MaterialTheme.colorScheme.onSurface, + ) { } } ) @@ -142,46 +146,16 @@ fun FlowPage( state = viewState.listState, ) { item { - Text( - modifier = Modifier - .fillMaxWidth() - .padding( - start = if (true) 54.dp else 24.dp, - top = 48.dp, - end = 24.dp, -// bottom = 24.dp - ), + DisplayText( + modifier = Modifier.padding(start = 30.dp), text = when { filterState.group != null -> filterState.group.name filterState.feed != null -> filterState.feed.name else -> filterState.filter.getName() }, - style = MaterialTheme.typography.displaySmall, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + desc = if (isSyncing) stringResource(R.string.syncing) else "", ) } - item { - AnimatedVisibility( - visible = isSyncing, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - Text( - modifier = Modifier.padding( - start = if (true) 54.dp else 24.dp, - top = 0.dp, - end = 24.dp, - bottom = 0.dp - ), - text = stringResource(R.string.syncing), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), - ) - } - Spacer(modifier = Modifier.height(24.dp)) - } item { AnimatedVisibility( visible = markAsRead, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt index a75c175..590acd3 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt @@ -22,6 +22,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import me.ash.reader.R import me.ash.reader.data.entity.ArticleWithFeed +import me.ash.reader.ui.component.FeedbackIconButton import me.ash.reader.ui.component.WebView import me.ash.reader.ui.ext.collectAsStateValue @@ -143,34 +144,33 @@ private fun TopBar( ), title = {}, navigationIcon = { - IconButton(onClick = { + FeedbackIconButton( + isHaptic = false, + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) { onScrollToPage(1) { onClearArticle() } - }) { - Icon( - imageVector = Icons.Rounded.Close, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) } }, actions = { if (isShowActions) { - IconButton(onClick = {}) { - Icon( - modifier = Modifier.size(22.dp), - imageVector = Icons.Outlined.Headphones, - contentDescription = stringResource(R.string.mark_all_as_read), - tint = MaterialTheme.colorScheme.onSurface, - ) + FeedbackIconButton( + isHaptic = false, + modifier = Modifier.size(22.dp), + imageVector = Icons.Outlined.Headphones, + contentDescription = stringResource(R.string.mark_all_as_read), + tint = MaterialTheme.colorScheme.onSurface, + ) { } - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Outlined.MoreVert, - contentDescription = stringResource(R.string.search), - tint = MaterialTheme.colorScheme.onSurface, - ) + FeedbackIconButton( + isHaptic = false, + imageVector = Icons.Outlined.MoreVert, + contentDescription = stringResource(R.string.search), + tint = MaterialTheme.colorScheme.onSurface, + ) { } } } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 5c3b961..5090934 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -16,6 +16,7 @@ 未知 返回 转到 + 设置 刷新 搜索 搜索中… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 66e0a7a..5f8d0d1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,6 +16,7 @@ Unknown Back Goto + Settings Refresh Search Searching…