diff --git a/app/schemas/com.jerboa.db.AppDB/27.json b/app/schemas/com.jerboa.db.AppDB/27.json new file mode 100644 index 000000000..96d84f5b7 --- /dev/null +++ b/app/schemas/com.jerboa.db.AppDB/27.json @@ -0,0 +1,260 @@ +{ + "formatVersion": 1, + "database": { + "version": 27, + "identityHash": "8e8aa32b002a7ce0d76dfb82e9309bd0", + "entities": [ + { + "tableName": "Account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `current` INTEGER NOT NULL, `instance` TEXT NOT NULL, `name` TEXT NOT NULL, `jwt` TEXT NOT NULL, `default_listing_type` INTEGER NOT NULL DEFAULT 0, `default_sort_type` INTEGER NOT NULL DEFAULT 0, `verification_state` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jwt", + "columnName": "jwt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultListingType", + "columnName": "default_listing_type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "defaultSortType", + "columnName": "default_sort_type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "verificationState", + "columnName": "verification_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AppSettings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `font_size` INTEGER NOT NULL DEFAULT 16, `theme` INTEGER NOT NULL DEFAULT 0, `theme_color` INTEGER NOT NULL DEFAULT 0, `viewed_changelog` INTEGER NOT NULL DEFAULT 0, `post_view_mode` INTEGER NOT NULL DEFAULT 0, `show_bottom_nav` INTEGER NOT NULL DEFAULT 1, `post_navigation_gesture_mode` INTEGER NOT NULL DEFAULT 0, `show_collapsed_comment_content` INTEGER NOT NULL DEFAULT 0, `show_comment_action_bar_by_default` INTEGER NOT NULL DEFAULT 1, `show_voting_arrows_in_list_view` INTEGER NOT NULL DEFAULT 1, `show_parent_comment_navigation_buttons` INTEGER NOT NULL DEFAULT 0, `navigate_parent_comments_with_volume_buttons` INTEGER NOT NULL DEFAULT 0, `use_custom_tabs` INTEGER NOT NULL DEFAULT 1, `use_private_tabs` INTEGER NOT NULL DEFAULT 0, `secure_window` INTEGER NOT NULL DEFAULT 0, `blur_nsfw` INTEGER NOT NULL DEFAULT 1, `show_text_descriptions_in_navbar` INTEGER NOT NULL DEFAULT 1, `markAsReadOnScroll` INTEGER NOT NULL DEFAULT 0, `backConfirmationMode` INTEGER NOT NULL DEFAULT 1, `show_post_link_previews` INTEGER NOT NULL DEFAULT 1, `post_actionbar_mode` INTEGER NOT NULL DEFAULT 0, `auto_play_gifs` INTEGER NOT NULL DEFAULT 0, `swipe_to_action_preset` INTEGER NOT NULL DEFAULT 1)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fontSize", + "columnName": "font_size", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "16" + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "themeColor", + "columnName": "theme_color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "viewedChangelog", + "columnName": "viewed_changelog", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "postViewMode", + "columnName": "post_view_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "showBottomNav", + "columnName": "show_bottom_nav", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "postNavigationGestureMode", + "columnName": "post_navigation_gesture_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "showCollapsedCommentContent", + "columnName": "show_collapsed_comment_content", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "showCommentActionBarByDefault", + "columnName": "show_comment_action_bar_by_default", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showVotingArrowsInListView", + "columnName": "show_voting_arrows_in_list_view", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showParentCommentNavigationButtons", + "columnName": "show_parent_comment_navigation_buttons", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "navigateParentCommentsWithVolumeButtons", + "columnName": "navigate_parent_comments_with_volume_buttons", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "useCustomTabs", + "columnName": "use_custom_tabs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "usePrivateTabs", + "columnName": "use_private_tabs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "secureWindow", + "columnName": "secure_window", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "blurNSFW", + "columnName": "blur_nsfw", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showTextDescriptionsInNavbar", + "columnName": "show_text_descriptions_in_navbar", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "markAsReadOnScroll", + "columnName": "markAsReadOnScroll", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "backConfirmationMode", + "columnName": "backConfirmationMode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showPostLinkPreviews", + "columnName": "show_post_link_previews", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "postActionbarMode", + "columnName": "post_actionbar_mode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "autoPlayGifs", + "columnName": "auto_play_gifs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "swipeToActionPreset", + "columnName": "swipe_to_action_preset", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8e8aa32b002a7ce0d76dfb82e9309bd0')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/jerboa/MainActivity.kt b/app/src/main/java/com/jerboa/MainActivity.kt index 4817a2f2a..8280bae1a 100644 --- a/app/src/main/java/com/jerboa/MainActivity.kt +++ b/app/src/main/java/com/jerboa/MainActivity.kt @@ -237,6 +237,7 @@ class MainActivity : AppCompatActivity() { showPostLinkPreviews = appSettings.showPostLinkPreviews, markAsReadOnScroll = appSettings.markAsReadOnScroll, postActionbarMode = appSettings.postActionbarMode, + swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), ) } @@ -280,6 +281,7 @@ class MainActivity : AppCompatActivity() { showPostLinkPreviews = appSettings.showPostLinkPreviews, markAsReadOnScroll = appSettings.markAsReadOnScroll, postActionbarMode = appSettings.postActionbarMode, + swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), ) } @@ -320,6 +322,7 @@ class MainActivity : AppCompatActivity() { onBack = appState::popBackStack, markAsReadOnScroll = appSettings.markAsReadOnScroll, postActionbarMode = appSettings.postActionbarMode, + swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), ) } @@ -357,6 +360,7 @@ class MainActivity : AppCompatActivity() { drawerState = drawerState, markAsReadOnScroll = appSettings.markAsReadOnScroll, postActionbarMode = appSettings.postActionbarMode, + swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), ) } @@ -468,6 +472,7 @@ class MainActivity : AppCompatActivity() { blurNSFW = appSettings.blurNSFW, showPostLinkPreview = appSettings.showPostLinkPreviews, postActionbarMode = appSettings.postActionbarMode, + swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), ) } } @@ -501,6 +506,7 @@ class MainActivity : AppCompatActivity() { blurNSFW = appSettings.blurNSFW, showPostLinkPreview = appSettings.showPostLinkPreviews, postActionbarMode = appSettings.postActionbarMode, + swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), ) } diff --git a/app/src/main/java/com/jerboa/db/AppDB.kt b/app/src/main/java/com/jerboa/db/AppDB.kt index a5473d5d3..de1d84daa 100644 --- a/app/src/main/java/com/jerboa/db/AppDB.kt +++ b/app/src/main/java/com/jerboa/db/AppDB.kt @@ -12,6 +12,7 @@ import com.jerboa.db.dao.AccountDao import com.jerboa.db.dao.AppSettingsDao import com.jerboa.db.entity.Account import com.jerboa.db.entity.AppSettings +import com.jerboa.feat.SwipeToActionPreset import java.util.concurrent.Executors val APP_SETTINGS_DEFAULT = @@ -39,10 +40,11 @@ val APP_SETTINGS_DEFAULT = showPostLinkPreviews = true, postActionbarMode = 0, autoPlayGifs = false, + swipeToActionPreset = SwipeToActionPreset.DEFAULT.ordinal, ) @Database( - version = 26, + version = 27, entities = [Account::class, AppSettings::class], exportSchema = true, ) diff --git a/app/src/main/java/com/jerboa/db/AppDBMigrations.kt b/app/src/main/java/com/jerboa/db/AppDBMigrations.kt index 719bf964d..4da3f5216 100644 --- a/app/src/main/java/com/jerboa/db/AppDBMigrations.kt +++ b/app/src/main/java/com/jerboa/db/AppDBMigrations.kt @@ -384,6 +384,25 @@ val MIGRATION_26_25 = } } +val MIGRATION_26_27 = + object : Migration(26, 27) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL(UPDATE_APP_CHANGELOG_UNVIEWED) + db.execSQL( + "ALTER TABLE AppSettings ADD COLUMN swipe_to_action_preset INTEGER NOT NULL DEFAULT 1", + ) + } + } + +val MIGRATION_27_26 = + object : Migration(27, 26) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE AppSettings DROP COLUMN swipe_to_action_preset", + ) + } + } + // Don't forget to test your migration with `./gradlew app:connectAndroidTest` val MIGRATIONS_LIST = arrayOf( @@ -417,4 +436,6 @@ val MIGRATIONS_LIST = MIGRATION_25_24, MIGRATION_25_26, MIGRATION_26_25, + MIGRATION_26_27, + MIGRATION_27_26, ) diff --git a/app/src/main/java/com/jerboa/db/entity/AppSettings.kt b/app/src/main/java/com/jerboa/db/entity/AppSettings.kt index 7b6ea91f4..99f3d9731 100644 --- a/app/src/main/java/com/jerboa/db/entity/AppSettings.kt +++ b/app/src/main/java/com/jerboa/db/entity/AppSettings.kt @@ -119,4 +119,9 @@ data class AppSettings( defaultValue = "0", ) val autoPlayGifs: Boolean, + @ColumnInfo( + name = "swipe_to_action_preset", + defaultValue = "1", + ) + val swipeToActionPreset: Int, ) diff --git a/app/src/main/java/com/jerboa/feat/SwipeToAction.kt b/app/src/main/java/com/jerboa/feat/SwipeToAction.kt new file mode 100644 index 000000000..54b2fd577 --- /dev/null +++ b/app/src/main/java/com/jerboa/feat/SwipeToAction.kt @@ -0,0 +1,96 @@ +package com.jerboa.feat + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Comment +import androidx.compose.material.icons.outlined.Bookmark +import androidx.compose.material.icons.outlined.Comment +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import com.jerboa.R + +enum class SwipeToActionType { + Upvote, + Downvote, + Reply, + Save, + ; + + companion object { + const val START_THRESHOLD = 0.05f + + fun getActionToRangeList(actions: List): List, SwipeToActionType>> { + val start = START_THRESHOLD + 0.05f + val delta = if (actions.size > 2) 0.14f else 0.2f + return actions.mapIndexed { index, it -> + (start + delta * index) + .rangeUntil(if (index == actions.size - 1) 1f else (start + delta * (index + 1))) to it + } + } + } + + @Composable + fun getImageVector(): ImageVector { + return when (this) { + Upvote -> ImageVector.vectorResource(id = R.drawable.up_outline) + Downvote -> ImageVector.vectorResource(id = R.drawable.down_outline) + Reply -> Icons.AutoMirrored.Outlined.Comment + Save -> Icons.Outlined.Bookmark + } + } + + @Composable + fun getActionColor(): Color { + return when (this) { + Upvote -> MaterialTheme.colorScheme.secondary + Downvote -> MaterialTheme.colorScheme.error + Reply -> MaterialTheme.colorScheme.inversePrimary + Save -> MaterialTheme.colorScheme.primary + } + } +} + +enum class SwipeToActionPreset( + val leftActions: List, + val rightActions: List, + val resId: Int, +) { + DISABLED(emptyList(), emptyList(), R.string.swipe_action_preset_disabled), + DEFAULT( + listOf(SwipeToActionType.Reply, SwipeToActionType.Save), + listOf(SwipeToActionType.Upvote, SwipeToActionType.Downvote), + R.string.swipe_action_preset_default, + ), + LEFT_DOWNVOTE_RIGHT_UPVOTE( + listOf(SwipeToActionType.Downvote, SwipeToActionType.Reply), + listOf(SwipeToActionType.Upvote, SwipeToActionType.Save), + R.string.swipe_action_preset_downvote_on_left_upvote_on_right, + ), + ONLY_RIGHT( + emptyList(), + listOf( + SwipeToActionType.Upvote, + SwipeToActionType.Downvote, + SwipeToActionType.Reply, + SwipeToActionType.Save, + ), + R.string.only_right_swipe_action_preset, + ), + ONLY_LEFT( + listOf( + SwipeToActionType.Upvote, + SwipeToActionType.Downvote, + SwipeToActionType.Reply, + SwipeToActionType.Save, + ), + emptyList(), + R.string.only_left_swipe_action_preset, + ), + ONLY_VOTES( + listOf(SwipeToActionType.Downvote), + listOf(SwipeToActionType.Upvote), + R.string.swipe_action_preset_only_votes, + ), +} diff --git a/app/src/main/java/com/jerboa/ui/components/comment/CommentNode.kt b/app/src/main/java/com/jerboa/ui/components/comment/CommentNode.kt index 2fd9af04a..81f03a0c0 100644 --- a/app/src/main/java/com/jerboa/ui/components/comment/CommentNode.kt +++ b/app/src/main/java/com/jerboa/ui/components/comment/CommentNode.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope @@ -22,6 +23,7 @@ import androidx.compose.material.icons.automirrored.outlined.Comment import androidx.compose.material.icons.filled.Bookmark import androidx.compose.material.icons.outlined.BookmarkBorder import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -57,16 +59,21 @@ import com.jerboa.datatypes.sampleSecondReplyCommentView import com.jerboa.db.entity.Account import com.jerboa.db.entity.AnonAccount import com.jerboa.feat.InstantScores +import com.jerboa.feat.SwipeToActionPreset +import com.jerboa.feat.SwipeToActionType import com.jerboa.feat.VoteType import com.jerboa.feat.amAdmin import com.jerboa.feat.amMod import com.jerboa.feat.canMod +import com.jerboa.feat.isReadyAndIfNotShowSimplifiedInfoToast import com.jerboa.isPostCreator import com.jerboa.ui.components.common.ActionBarButton import com.jerboa.ui.components.common.CommentOrPostNodeHeader import com.jerboa.ui.components.common.MarkdownHelper import com.jerboa.ui.components.common.MyMarkdownText +import com.jerboa.ui.components.common.SwipeToAction import com.jerboa.ui.components.common.VoteGeneric +import com.jerboa.ui.components.common.rememberSwipeActionState import com.jerboa.ui.components.community.CommunityLink import com.jerboa.ui.theme.LARGE_PADDING import com.jerboa.ui.theme.MEDIUM_PADDING @@ -171,6 +178,7 @@ fun CommentBodyPreview() { ) } +@OptIn(ExperimentalMaterial3Api::class) fun LazyListScope.commentNodeItem( node: CommentNode, admins: List, @@ -211,6 +219,7 @@ fun LazyListScope.commentNodeItem( showAvatar: Boolean, blurNSFW: Int, showScores: Boolean, + swipeToActionPreset: SwipeToActionPreset, ) { val commentView = node.commentView val commentId = commentView.comment.id @@ -241,6 +250,8 @@ fun LazyListScope.commentNodeItem( val borderColor = calculateBorderColor(backgroundColor, node.depth) val border = Border(SMALL_PADDING, borderColor) + val ctx = LocalContext.current + var instantScores by remember { mutableStateOf( @@ -253,119 +264,150 @@ fun LazyListScope.commentNodeItem( ) } - AnimatedVisibility( - visible = !isCollapsedByParent, - enter = expandVertically(), - exit = shrinkVertically(), + val swipeState = rememberSwipeActionState( + swipeToActionPreset = swipeToActionPreset, + enableDownVotes = enableDownVotes, + rememberKey = commentView, ) { - Column( - modifier = - Modifier - .padding( - start = offset, - ), + if (account.isReadyAndIfNotShowSimplifiedInfoToast(ctx)) { + when (it) { + SwipeToActionType.Upvote -> { + instantScores = + instantScores.update(VoteType.Upvote) + onUpvoteClick(commentView) + } + SwipeToActionType.Downvote -> { + instantScores = + instantScores.update(VoteType.Downvote) + onDownvoteClick(commentView) + } + SwipeToActionType.Reply -> { + onReplyClick(commentView) + } + SwipeToActionType.Save -> { + onSaveClick(commentView) + } + } + } + } + + val swipeableContent: @Composable RowScope.() -> Unit = { + AnimatedVisibility( + visible = !isCollapsedByParent, + enter = expandVertically(), + exit = shrinkVertically(), ) { Column( - modifier = Modifier.border(start = border), + modifier = + Modifier + .padding( + start = offset, + ), ) { - HorizontalDivider(modifier = Modifier.padding(start = if (node.depth == 0) 0.dp else border.strokeWidth)) Column( - modifier = - Modifier.padding( - start = offset2, - end = MEDIUM_PADDING, - ), + modifier = Modifier.border(start = border), ) { - if (showPostAndCommunityContext) { - PostAndCommunityContextHeader( - post = commentView.post, - community = commentView.community, - onCommunityClick = onCommunityClick, - onPostClick = onPostClick, - blurNSFW = blurNSFW, - ) - } - CommentNodeHeader( - commentView = commentView, - onPersonClick = onPersonClick, - score = instantScores.score, - myVote = instantScores.myVote, - onClick = { - onHeaderClick(commentView) - }, - onLongClick = { - onHeaderLongClick(commentView) - }, - collapsedCommentsCount = commentView.counts.child_count, - isExpanded = isExpanded(commentId), - showAvatar = showAvatar, - showScores = showScores, - ) - AnimatedVisibility( - visible = isExpanded(commentId) || showCollapsedCommentContent, - enter = expandVertically(), - exit = shrinkVertically(), + HorizontalDivider(modifier = Modifier.padding(start = if (node.depth == 0) 0.dp else border.strokeWidth)) + Column( + modifier = + Modifier.padding( + start = offset2, + end = MEDIUM_PADDING, + ), ) { - Column { - CommentBody( - comment = commentView.comment, - viewSource = viewSource, - onClick = { onCommentClick(commentView) }, - onLongClick = { v -> - if (v is TextView) { - // Also triggers for long click on links, so we check if link was hit - // Can have selection in viewSource but there are no links there - if (viewSource || (v.selectionStart == -1 && v.selectionEnd == -1)) { - toggleActionBar(commentId) - } - } - true - }, + if (showPostAndCommunityContext) { + PostAndCommunityContextHeader( + post = commentView.post, + community = commentView.community, + onCommunityClick = onCommunityClick, + onPostClick = onPostClick, + blurNSFW = blurNSFW, ) - AnimatedVisibility( - visible = showActionBar(commentId), - enter = expandVertically(), - exit = shrinkVertically(), - ) { - CommentFooterLine( - commentView = commentView, - admins = admins, - moderators = moderators, - instantScores = instantScores, - onUpvoteClick = { - instantScores = instantScores.update(VoteType.Upvote) - onUpvoteClick(commentView) - }, - onDownvoteClick = { - instantScores = instantScores.update(VoteType.Downvote) - onDownvoteClick(commentView) - }, - onViewSourceClick = { - viewSource = !viewSource - }, - onEditCommentClick = onEditCommentClick, - onDeleteCommentClick = onDeleteCommentClick, - onReplyClick = onReplyClick, - onSaveClick = onSaveClick, - onReportClick = onReportClick, - onRemoveClick = onRemoveClick, - onBanPersonClick = onBanPersonClick, - onBanFromCommunityClick = onBanFromCommunityClick, - onCommentLinkClick = onCommentLinkClick, - onPersonClick = onPersonClick, - onViewVotesClick = onViewVotesClick, - onBlockCreatorClick = onBlockCreatorClick, - onClick = { - toggleExpanded(commentId) - }, - onLongClick = { - toggleActionBar(commentId) - }, - account = account, - enableDownVotes = enableDownVotes, - showScores = showScores, + } + CommentNodeHeader( + commentView = commentView, + onPersonClick = onPersonClick, + score = instantScores.score, + myVote = instantScores.myVote, + onClick = { + onHeaderClick(commentView) + }, + onLongClick = { + onHeaderLongClick(commentView) + }, + collapsedCommentsCount = commentView.counts.child_count, + isExpanded = isExpanded(commentId), + showAvatar = showAvatar, + showScores = showScores, + ) + AnimatedVisibility( + visible = isExpanded(commentId) || showCollapsedCommentContent, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + Column { + CommentBody( + comment = commentView.comment, viewSource = viewSource, + onClick = { onCommentClick(commentView) }, + onLongClick = { v -> + if (v is TextView) { + // Also triggers for long click on links, so we check if link was hit + // Can have selection in viewSource but there are no links there + if (viewSource || (v.selectionStart == -1 && v.selectionEnd == -1)) { + toggleActionBar(commentId) + } + } + true + }, ) + AnimatedVisibility( + visible = showActionBar(commentId), + enter = expandVertically(), + exit = shrinkVertically(), + ) { + CommentFooterLine( + commentView = commentView, + admins = admins, + moderators = moderators, + instantScores = instantScores, + onUpvoteClick = { + instantScores = + instantScores.update(VoteType.Upvote) + onUpvoteClick(commentView) + }, + onDownvoteClick = { + instantScores = + instantScores.update(VoteType.Downvote) + onDownvoteClick(commentView) + }, + onViewSourceClick = { + viewSource = !viewSource + }, + onEditCommentClick = onEditCommentClick, + onDeleteCommentClick = onDeleteCommentClick, + onReplyClick = onReplyClick, + onSaveClick = onSaveClick, + onReportClick = onReportClick, + onRemoveClick = onRemoveClick, + onBanPersonClick = onBanPersonClick, + onBanFromCommunityClick = onBanFromCommunityClick, + onCommentLinkClick = onCommentLinkClick, + onPersonClick = onPersonClick, + onViewVotesClick = onViewVotesClick, + onBlockCreatorClick = onBlockCreatorClick, + onClick = { + toggleExpanded(commentId) + }, + onLongClick = { + toggleActionBar(commentId) + }, + account = account, + enableDownVotes = enableDownVotes, + showScores = showScores, + viewSource = viewSource, + ) + } } } } @@ -373,6 +415,19 @@ fun LazyListScope.commentNodeItem( } } } + + if (swipeToActionPreset != SwipeToActionPreset.DISABLED) { + SwipeToAction( + swipeToActionPreset = swipeToActionPreset, + enableDownVotes = enableDownVotes, + swipeableContent = swipeableContent, + swipeState = swipeState, + ) + } else { + Row { + swipeableContent() + } + } } increaseLazyListIndexTracker() @@ -426,6 +481,7 @@ fun LazyListScope.commentNodeItem( showScores = showScores, admins = admins, moderators = moderators, + swipeToActionPreset = swipeToActionPreset, ) } @@ -469,6 +525,7 @@ fun LazyListScope.missingCommentNodeItem( showAvatar: Boolean, blurNSFW: Int, showScores: Boolean, + swipeToActionPreset: SwipeToActionPreset, ) { val commentId = node.missingCommentView.commentId @@ -574,6 +631,7 @@ fun LazyListScope.missingCommentNodeItem( showAvatar = showAvatar, blurNSFW = blurNSFW, showScores = showScores, + swipeToActionPreset = swipeToActionPreset, ) } @@ -873,6 +931,7 @@ fun CommentNodesPreview() { blurNSFW = 1, account = AnonAccount, showScores = true, + swipeToActionPreset = SwipeToActionPreset.DEFAULT, ) } diff --git a/app/src/main/java/com/jerboa/ui/components/comment/CommentNodes.kt b/app/src/main/java/com/jerboa/ui/components/comment/CommentNodes.kt index aee9b1260..086ad0e4a 100644 --- a/app/src/main/java/com/jerboa/ui/components/comment/CommentNodes.kt +++ b/app/src/main/java/com/jerboa/ui/components/comment/CommentNodes.kt @@ -13,6 +13,7 @@ import com.jerboa.CommentNodeData import com.jerboa.MissingCommentNode import com.jerboa.datatypes.BanFromCommunityData import com.jerboa.db.entity.Account +import com.jerboa.feat.SwipeToActionPreset import it.vercruysse.lemmyapi.v0x19.datatypes.CommentId import it.vercruysse.lemmyapi.v0x19.datatypes.CommentView import it.vercruysse.lemmyapi.v0x19.datatypes.Community @@ -64,6 +65,7 @@ fun CommentNodes( showAvatar: Boolean, blurNSFW: Int, showScores: Boolean, + swipeToActionPreset: SwipeToActionPreset, ) { LazyColumn(state = listState) { commentNodeItems( @@ -106,6 +108,7 @@ fun CommentNodes( showAvatar = showAvatar, blurNSFW = blurNSFW, showScores = showScores, + swipeToActionPreset = swipeToActionPreset, ) item { Spacer(modifier = Modifier.height(100.dp)) @@ -153,6 +156,7 @@ fun LazyListScope.commentNodeItems( showAvatar: Boolean, blurNSFW: Int, showScores: Boolean, + swipeToActionPreset: SwipeToActionPreset, ) { nodes.forEach { node -> when (node) { @@ -197,6 +201,7 @@ fun LazyListScope.commentNodeItems( showAvatar = showAvatar, blurNSFW = blurNSFW, showScores = showScores, + swipeToActionPreset = swipeToActionPreset, ) is MissingCommentNode -> @@ -240,6 +245,7 @@ fun LazyListScope.commentNodeItems( showAvatar = showAvatar, blurNSFW = blurNSFW, showScores = showScores, + swipeToActionPreset = swipeToActionPreset, ) } } diff --git a/app/src/main/java/com/jerboa/ui/components/common/SwipeToAction.kt b/app/src/main/java/com/jerboa/ui/components/common/SwipeToAction.kt new file mode 100644 index 000000000..990b8daef --- /dev/null +++ b/app/src/main/java/com/jerboa/ui/components/common/SwipeToAction.kt @@ -0,0 +1,210 @@ +package com.jerboa.ui.components.common + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxState +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.unit.dp +import com.jerboa.feat.SwipeToActionPreset +import com.jerboa.feat.SwipeToActionType +import com.jerboa.feat.SwipeToActionType.Companion.START_THRESHOLD + +@Composable +@ExperimentalMaterial3Api +fun SwipeToAction( + swipeToActionPreset: SwipeToActionPreset, + enableDownVotes: Boolean, + swipeableContent: @Composable RowScope.() -> Unit, + swipeState: SwipeToDismissBoxState, +) { + val haptic = LocalHapticFeedback.current + + val leftActionsRanges = + remember(swipeToActionPreset, enableDownVotes) { + SwipeToActionType.getActionToRangeList( + swipeToActionPreset.leftActions + .filter { !(it == SwipeToActionType.Downvote && !enableDownVotes) }, + ) + } + val rightActionsRanges = + remember(swipeToActionPreset, enableDownVotes) { + SwipeToActionType.getActionToRangeList( + swipeToActionPreset.rightActions + .filter { !(it == SwipeToActionType.Downvote && !enableDownVotes) }, + ) + } + + fun actionByState(state: SwipeToDismissBoxState): Pair, SwipeToActionType>? { + return when (state.targetValue) { + SwipeToDismissBoxValue.StartToEnd -> { + leftActionsRanges.findLast { swipeState.progress in it.first } + } + + SwipeToDismissBoxValue.EndToStart -> { + rightActionsRanges.findLast { swipeState.progress in it.first } + } + + else -> null + } + } + + val swipeAction = + remember(swipeState.progress, swipeState.targetValue) { actionByState(swipeState) } + + SwipeToDismissBox( + enableDismissFromStartToEnd = leftActionsRanges.isNotEmpty(), + enableDismissFromEndToStart = rightActionsRanges.isNotEmpty(), + state = swipeState, + backgroundContent = { + val lastSwipeAction = remember { mutableStateOf(null) } + val transition = updateTransition(swipeState, label = "swipe state") + val color by transition.animateColor( + transitionSpec = { + val currentAction = actionByState(this.targetState) + // vibrates when icon changes + if (lastSwipeAction.value != currentAction?.second) { + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + lastSwipeAction.value = currentAction?.second + } + spring(stiffness = 1800f) + }, + label = "swipe color animation", + targetValueByState = { state -> + val currentAction = actionByState(state) + currentAction?.second?.getActionColor() ?: Color.Transparent + }, + ) + Box( + modifier = + Modifier + .fillMaxSize(), + ) { + Box( + modifier = + Modifier + .fillMaxWidth(if (swipeState.progress != 1f) swipeState.progress else 0f) + .fillMaxHeight() + .background(color = color) + .align( + if (swipeState.targetValue == SwipeToDismissBoxValue.EndToStart) Alignment.TopEnd else Alignment.TopStart, + ), + contentAlignment = + if (swipeState.targetValue == SwipeToDismissBoxValue.EndToStart) { + Alignment.CenterStart + } else { + Alignment.CenterEnd + }, + ) { + val tint = Color.White + val modifier = + Modifier + .padding(10.dp) + .requiredWidth(35.dp) + .fillMaxHeight() + + swipeAction?.second?.getImageVector()?.let { icon -> + Icon( + imageVector = icon, + contentDescription = null, + tint = tint, + modifier = modifier, + ) + } + } + } + }, + content = { swipeableContent() }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun rememberSwipeActionState( + swipeToActionPreset: SwipeToActionPreset, + enableDownVotes: Boolean, + rememberKey: Any? = Unit, + onAction: (action: SwipeToActionType) -> Unit, +): SwipeToDismissBoxState { + /* + This hacky solution is required because confirmValueChange lambda doesn't pass progress state + They didn't fix it with new SwipeToDismissBoxState + */ + val density = LocalDensity.current + + val leftActionsRanges = + remember(swipeToActionPreset, enableDownVotes) { + SwipeToActionType.getActionToRangeList( + swipeToActionPreset.leftActions + .filter { !(it == SwipeToActionType.Downvote && !enableDownVotes) }, + ) + } + val rightActionsRanges = + remember(swipeToActionPreset, enableDownVotes) { + SwipeToActionType.getActionToRangeList( + swipeToActionPreset.rightActions + .filter { !(it == SwipeToActionType.Downvote && !enableDownVotes) }, + ) + } + + val progressState = remember { mutableFloatStateOf(1.0f) } + + val confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { dismissValue -> + val action = + when (dismissValue) { + SwipeToDismissBoxValue.StartToEnd -> { + leftActionsRanges.findLast { progressState.floatValue in it.first } + } + + SwipeToDismissBoxValue.EndToStart -> { + rightActionsRanges.findLast { progressState.floatValue in it.first } + } + + else -> { + null + } + } + action?.second?.let { actionType -> + onAction(actionType) + } + false // do not dismiss + } + + val positionalThreshold: (totalDistance: Float) -> Float = { totalDistance -> totalDistance * START_THRESHOLD } + + val dismissState = rememberSaveable( + saver = SwipeToDismissBoxState.Saver( + confirmValueChange = confirmValueChange, + density = density, + positionalThreshold = { totalDistance -> totalDistance * START_THRESHOLD }, + ), + inputs = arrayOf(rememberKey), + ) { + SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, confirmValueChange, positionalThreshold) + } + progressState.floatValue = dismissState.progress + return dismissState +} diff --git a/app/src/main/java/com/jerboa/ui/components/community/CommunityActivity.kt b/app/src/main/java/com/jerboa/ui/components/community/CommunityActivity.kt index 0529dfca6..54e20cfa2 100644 --- a/app/src/main/java/com/jerboa/ui/components/community/CommunityActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/community/CommunityActivity.kt @@ -35,6 +35,7 @@ import com.jerboa.api.ApiState import com.jerboa.datatypes.BanFromCommunityData import com.jerboa.db.entity.isAnon import com.jerboa.feat.BlurTypes +import com.jerboa.feat.SwipeToActionPreset import com.jerboa.feat.VoteType import com.jerboa.feat.doIfReadyElseDisplayInfo import com.jerboa.feat.newVote @@ -43,6 +44,7 @@ import com.jerboa.hostName import com.jerboa.model.AccountViewModel import com.jerboa.model.AppSettingsViewModel import com.jerboa.model.CommunityViewModel +import com.jerboa.model.ReplyItem import com.jerboa.model.SiteViewModel import com.jerboa.scrollToTop import com.jerboa.ui.components.ban.BanFromCommunityReturn @@ -88,6 +90,7 @@ fun CommunityActivity( showPostLinkPreviews: Boolean, markAsReadOnScroll: Boolean, postActionbarMode: Int, + swipeToActionPreset: SwipeToActionPreset, ) { Log.d("jerboa", "got to community activity") @@ -310,6 +313,11 @@ fun CommunityActivity( ) } }, + onReplyClick = { pv -> + appState.toCommentReply( + replyItem = ReplyItem.PostItem(pv), + ) + }, onEditPostClick = { postView -> appState.toPostEdit( postView = postView, @@ -419,6 +427,7 @@ fun CommunityActivity( showScores = siteViewModel.showScores(), postActionbarMode = postActionbarMode, showPostAppendRetry = communityViewModel.postsRes is ApiState.AppendingFailure, + swipeToActionPreset = swipeToActionPreset, ) } diff --git a/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt b/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt index da58ce013..971189baa 100644 --- a/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/home/BottomNavActivity.kt @@ -47,6 +47,7 @@ import com.jerboa.model.AccountViewModel import com.jerboa.model.AppSettingsViewModel import com.jerboa.model.HomeViewModel import com.jerboa.model.SiteViewModel +import com.jerboa.toEnum import com.jerboa.ui.components.common.BottomAppBarAll import com.jerboa.ui.components.common.JerboaSnackbarHost import com.jerboa.ui.components.common.getCurrentAccount @@ -216,6 +217,7 @@ fun BottomNavActivity( showPostLinkPreviews = appSettings.showPostLinkPreviews, markAsReadOnScroll = appSettings.markAsReadOnScroll, postActionbarMode = appSettings.postActionbarMode, + swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), ) } @@ -255,6 +257,7 @@ fun BottomNavActivity( drawerState = drawerState, markAsReadOnScroll = appSettings.markAsReadOnScroll, postActionbarMode = appSettings.postActionbarMode, + swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), ) } @@ -274,6 +277,7 @@ fun BottomNavActivity( drawerState = drawerState, markAsReadOnScroll = appSettings.markAsReadOnScroll, postActionbarMode = appSettings.postActionbarMode, + swipeToActionPreset = appSettings.swipeToActionPreset.toEnum(), ) } } diff --git a/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt b/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt index bc4ff5409..ba39d6f69 100644 --- a/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/home/HomeActivity.kt @@ -42,12 +42,14 @@ import com.jerboa.datatypes.BanFromCommunityData import com.jerboa.db.entity.Account import com.jerboa.db.entity.isAnon import com.jerboa.db.entity.isReady +import com.jerboa.feat.SwipeToActionPreset import com.jerboa.feat.VoteType import com.jerboa.feat.doIfReadyElseDisplayInfo import com.jerboa.feat.newVote import com.jerboa.model.AccountViewModel import com.jerboa.model.AppSettingsViewModel import com.jerboa.model.HomeViewModel +import com.jerboa.model.ReplyItem import com.jerboa.model.SiteViewModel import com.jerboa.scrollToTop import com.jerboa.ui.components.ban.BanFromCommunityReturn @@ -93,6 +95,7 @@ fun HomeActivity( showPostLinkPreviews: Boolean, markAsReadOnScroll: Boolean, postActionbarMode: Int, + swipeToActionPreset: SwipeToActionPreset, ) { Log.d("jerboa", "got to home activity") @@ -159,6 +162,7 @@ fun HomeActivity( markAsReadOnScroll = markAsReadOnScroll, snackbarHostState = snackbarHostState, postActionbarMode = postActionbarMode, + swipeToActionPreset = swipeToActionPreset, ) }, floatingActionButtonPosition = FabPosition.End, @@ -207,6 +211,7 @@ fun MainPostListingsContent( snackbarHostState: SnackbarHostState, markAsReadOnScroll: Boolean, postActionbarMode: Int, + swipeToActionPreset: SwipeToActionPreset, ) { val ctx = LocalContext.current val scope = rememberCoroutineScope() @@ -314,6 +319,11 @@ fun MainPostListingsContent( ) } }, + onReplyClick = { pv -> + appState.toCommentReply( + replyItem = ReplyItem.PostItem(pv), + ) + }, onEditPostClick = { postView -> appState.toPostEdit( postView = postView, @@ -417,6 +427,7 @@ fun MainPostListingsContent( showScores = siteViewModel.showScores(), postActionbarMode = postActionbarMode, showPostAppendRetry = homeViewModel.postsRes is ApiState.AppendingFailure, + swipeToActionPreset = swipeToActionPreset, ) } } diff --git a/app/src/main/java/com/jerboa/ui/components/person/PersonProfileActivity.kt b/app/src/main/java/com/jerboa/ui/components/person/PersonProfileActivity.kt index 49f31102d..8cbf2dfcc 100644 --- a/app/src/main/java/com/jerboa/ui/components/person/PersonProfileActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/person/PersonProfileActivity.kt @@ -51,6 +51,7 @@ import com.jerboa.datatypes.getDisplayName import com.jerboa.datatypes.getLocalizedStringForUserTab import com.jerboa.db.entity.Account import com.jerboa.db.entity.isAnon +import com.jerboa.feat.SwipeToActionPreset import com.jerboa.feat.VoteType import com.jerboa.feat.doIfReadyElseDisplayInfo import com.jerboa.feat.newVote @@ -121,6 +122,7 @@ fun PersonProfileActivity( markAsReadOnScroll: Boolean, postActionbarMode: Int, onBack: (() -> Unit)? = null, + swipeToActionPreset: SwipeToActionPreset, ) { Log.d("jerboa", "got to person activity") @@ -271,6 +273,7 @@ fun PersonProfileActivity( snackbarHostState = snackbarHostState, showScores = siteViewModel.showScores(), postActionbarMode = postActionbarMode, + swipeToActionPreset = swipeToActionPreset, ) }, ) @@ -306,6 +309,7 @@ fun UserTabs( snackbarHostState: SnackbarHostState, showScores: Boolean, postActionbarMode: Int, + swipeToActionPreset: SwipeToActionPreset, ) { val tabTitles = if (savedMode) { @@ -514,6 +518,11 @@ fun UserTabs( ) } }, + onReplyClick = { pv -> + appState.toCommentReply( + replyItem = ReplyItem.PostItem(pv), + ) + }, onEditPostClick = { pv -> appState.toPostEdit( postView = pv, @@ -618,6 +627,7 @@ fun UserTabs( showScores = showScores, postActionbarMode = postActionbarMode, showPostAppendRetry = personProfileViewModel.personDetailsRes is ApiState.AppendingFailure, + swipeToActionPreset = swipeToActionPreset, ) } else -> {} @@ -839,6 +849,7 @@ fun UserTabs( showAvatar = showAvatar, blurNSFW = blurNSFW, showScores = showScores, + swipeToActionPreset = swipeToActionPreset, ) } } diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostActivity.kt b/app/src/main/java/com/jerboa/ui/components/post/PostActivity.kt index 1a6908a75..eefd8fe09 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostActivity.kt @@ -59,6 +59,7 @@ import com.jerboa.buildCommentsTree import com.jerboa.datatypes.BanFromCommunityData import com.jerboa.datatypes.getLocalizedCommentSortTypeName import com.jerboa.db.entity.isAnon +import com.jerboa.feat.SwipeToActionPreset import com.jerboa.feat.VoteType import com.jerboa.feat.doIfReadyElseDisplayInfo import com.jerboa.feat.newVote @@ -133,6 +134,7 @@ fun PostActivity( blurNSFW: Int, showPostLinkPreview: Boolean, postActionbarMode: Int, + swipeToActionPreset: SwipeToActionPreset, ) { Log.d("jerboa", "got to post activity") @@ -461,6 +463,7 @@ fun PostActivity( showIfRead = false, showScores = siteViewModel.showScores(), postActionbarMode = postActionbarMode, + swipeToActionPreset = swipeToActionPreset, ) } @@ -691,6 +694,7 @@ fun PostActivity( }, blurNSFW = blurNSFW, showScores = siteViewModel.showScores(), + swipeToActionPreset = swipeToActionPreset, ) item { Spacer(modifier = Modifier.height(100.dp)) diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt index 217f017ab..d659d8555 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -47,6 +48,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -72,10 +74,13 @@ import com.jerboa.db.entity.AnonAccount import com.jerboa.feat.BlurTypes import com.jerboa.feat.InstantScores import com.jerboa.feat.PostActionbarMode +import com.jerboa.feat.SwipeToActionPreset +import com.jerboa.feat.SwipeToActionType import com.jerboa.feat.VoteType import com.jerboa.feat.amAdmin import com.jerboa.feat.amMod import com.jerboa.feat.canMod +import com.jerboa.feat.isReadyAndIfNotShowSimplifiedInfoToast import com.jerboa.feat.needBlur import com.jerboa.getPostType import com.jerboa.hostName @@ -97,8 +102,10 @@ import com.jerboa.ui.components.common.PictrsThumbnailImage import com.jerboa.ui.components.common.PictrsUrlImage import com.jerboa.ui.components.common.ScoreAndTime import com.jerboa.ui.components.common.SimpleTopAppBar +import com.jerboa.ui.components.common.SwipeToAction import com.jerboa.ui.components.common.TimeAgo import com.jerboa.ui.components.common.VoteGeneric +import com.jerboa.ui.components.common.rememberSwipeActionState import com.jerboa.ui.components.common.scoreColor import com.jerboa.ui.components.community.CommunityLink import com.jerboa.ui.components.community.CommunityName @@ -719,7 +726,14 @@ fun PostFooterLine( account = account, onClick = { showMoreOptions = !showMoreOptions }, requiresAccount = false, - modifier = if (postActionbar == PostActionbarMode.LeftHandShort) Modifier.weight(1F, true) else Modifier, + modifier = if (postActionbar == PostActionbarMode.LeftHandShort) { + Modifier.weight( + 1F, + true, + ) + } else { + Modifier + }, ) if (postActionbar == PostActionbarMode.LeftHandShort) { @@ -867,6 +881,7 @@ fun PreviewPostListingCard() { showIfRead = true, showScores = true, postActionbarMode = PostActionbarMode.Long.ordinal, + swipeToActionPreset = SwipeToActionPreset.DEFAULT, ) } @@ -907,6 +922,7 @@ fun PreviewLinkPostListing() { showIfRead = true, showScores = true, postActionbarMode = PostActionbarMode.Long.ordinal, + swipeToActionPreset = SwipeToActionPreset.DEFAULT, ) } @@ -947,6 +963,7 @@ fun PreviewImagePostListingCard() { showIfRead = true, showScores = true, postActionbarMode = PostActionbarMode.Long.ordinal, + swipeToActionPreset = SwipeToActionPreset.DEFAULT, ) } @@ -987,6 +1004,7 @@ fun PreviewImagePostListingSmallCard() { showIfRead = true, showScores = true, postActionbarMode = PostActionbarMode.Long.ordinal, + swipeToActionPreset = SwipeToActionPreset.DEFAULT, ) } @@ -1027,9 +1045,11 @@ fun PreviewLinkNoThumbnailPostListing() { showIfRead = true, showScores = true, postActionbarMode = PostActionbarMode.Long.ordinal, + swipeToActionPreset = SwipeToActionPreset.DEFAULT, ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun PostListing( postView: PostView, @@ -1067,7 +1087,9 @@ fun PostListing( showIfRead: Boolean, showScores: Boolean, postActionbarMode: Int, + swipeToActionPreset: SwipeToActionPreset, ) { + val ctx = LocalContext.current // This stores vote data var instantScores by remember { @@ -1083,129 +1105,181 @@ fun PostListing( var viewSource by remember { mutableStateOf(false) } - when (postViewMode) { - PostViewMode.Card -> - PostListingCard( - postView = postView, - admins = admins, - moderators = moderators, - instantScores = instantScores, - onUpvoteClick = { - instantScores = instantScores.update(VoteType.Upvote) - onUpvoteClick(postView) - }, - onDownvoteClick = { - instantScores = instantScores.update(VoteType.Downvote) - onDownvoteClick(postView) - }, - onReplyClick = onReplyClick, - onPostClick = onPostClick, - onSaveClick = onSaveClick, - onCommunityClick = onCommunityClick, - onEditPostClick = onEditPostClick, - onDeletePostClick = onDeletePostClick, - onReportClick = onReportClick, - onRemoveClick = onRemoveClick, - onBanPersonClick = onBanPersonClick, - onBanFromCommunityClick = onBanFromCommunityClick, - onLockPostClick = onLockPostClick, - onFeaturePostClick = onFeaturePostClick, - onViewVotesClick = onViewVotesClick, - onPersonClick = onPersonClick, - onViewSourceClick = { - viewSource = !viewSource - }, - viewSource = viewSource, - showReply = showReply, - showCommunityName = showCommunityName, - fullBody = fullBody, - account = account, - expandedImage = true, - enableDownVotes = enableDownVotes, - showAvatar = showAvatar, - useCustomTabs = useCustomTabs, - usePrivateTabs = usePrivateTabs, - blurNSFW = blurNSFW, - showPostLinkPreview = showPostLinkPreview, - appState = appState, - showIfRead = showIfRead, - showScores = showScores, - postActionbarMode = postActionbarMode, - ) + val swipeAction: (action: SwipeToActionType) -> Unit = remember(postView) { + { + if (account.isReadyAndIfNotShowSimplifiedInfoToast(ctx)) { + when (it) { + SwipeToActionType.Upvote -> { + instantScores = + instantScores.update(VoteType.Upvote) + onUpvoteClick(postView) + } - PostViewMode.SmallCard -> - PostListingCard( - postView = postView, - admins = admins, - moderators = moderators, - instantScores = instantScores, - onUpvoteClick = { - instantScores = instantScores.update(VoteType.Upvote) - onUpvoteClick(postView) - }, - onDownvoteClick = { - instantScores = instantScores.update(VoteType.Downvote) - onDownvoteClick(postView) - }, - onReplyClick = onReplyClick, - onPostClick = onPostClick, - onSaveClick = onSaveClick, - onCommunityClick = onCommunityClick, - onEditPostClick = onEditPostClick, - onDeletePostClick = onDeletePostClick, - onReportClick = onReportClick, - onRemoveClick = onRemoveClick, - onBanPersonClick = onBanPersonClick, - onBanFromCommunityClick = onBanFromCommunityClick, - onLockPostClick = onLockPostClick, - onFeaturePostClick = onFeaturePostClick, - onViewVotesClick = onViewVotesClick, - onPersonClick = onPersonClick, - onViewSourceClick = { - viewSource = !viewSource - }, - viewSource = viewSource, - showReply = showReply, - showCommunityName = showCommunityName, - fullBody = false, - account = account, - expandedImage = false, - enableDownVotes = enableDownVotes, - showAvatar = showAvatar, - useCustomTabs = useCustomTabs, - usePrivateTabs = usePrivateTabs, - blurNSFW = blurNSFW, - showPostLinkPreview = showPostLinkPreview, - appState = appState, - showScores = showScores, - postActionbarMode = postActionbarMode, - ) + SwipeToActionType.Downvote -> { + instantScores = + instantScores.update(VoteType.Downvote) + onDownvoteClick(postView) + } - PostViewMode.List -> - PostListingList( - postView = postView, - instantScores = instantScores, - onUpvoteClick = { - instantScores = instantScores.update(VoteType.Upvote) - onUpvoteClick(postView) - }, - onDownvoteClick = { - instantScores = instantScores.update(VoteType.Downvote) - onDownvoteClick(postView) - }, - onPostClick = onPostClick, - showCommunityName = showCommunityName, - account = account, - showVotingArrowsInListView = showVotingArrowsInListView, - showAvatar = showAvatar, - useCustomTabs = useCustomTabs, - usePrivateTabs = usePrivateTabs, - blurNSFW = blurNSFW, - appState = appState, - showIfRead = showIfRead, - enableDownVotes = enableDownVotes, - showScores = showScores, - ) + SwipeToActionType.Reply -> { + onReplyClick(postView) + } + + SwipeToActionType.Save -> { + onSaveClick(postView) + } + } + } + } + } + + val swipeState = rememberSwipeActionState( + swipeToActionPreset = swipeToActionPreset, + enableDownVotes = enableDownVotes, + onAction = swipeAction, + rememberKey = postView, + ) + + val swipeableContent: @Composable RowScope.() -> Unit = { + Row { + when (postViewMode) { + PostViewMode.Card -> + PostListingCard( + postView = postView, + admins = admins, + moderators = moderators, + instantScores = instantScores, + onUpvoteClick = { + instantScores = instantScores.update(VoteType.Upvote) + onUpvoteClick(postView) + }, + onDownvoteClick = { + instantScores = instantScores.update(VoteType.Downvote) + onDownvoteClick(postView) + }, + onReplyClick = onReplyClick, + onPostClick = onPostClick, + onSaveClick = onSaveClick, + onCommunityClick = onCommunityClick, + onEditPostClick = onEditPostClick, + onDeletePostClick = onDeletePostClick, + onReportClick = onReportClick, + onRemoveClick = onRemoveClick, + onBanPersonClick = onBanPersonClick, + onBanFromCommunityClick = onBanFromCommunityClick, + onLockPostClick = onLockPostClick, + onFeaturePostClick = onFeaturePostClick, + onViewVotesClick = onViewVotesClick, + onPersonClick = onPersonClick, + onViewSourceClick = { + viewSource = !viewSource + }, + viewSource = viewSource, + showReply = showReply, + showCommunityName = showCommunityName, + fullBody = fullBody, + account = account, + expandedImage = true, + enableDownVotes = enableDownVotes, + showAvatar = showAvatar, + useCustomTabs = useCustomTabs, + usePrivateTabs = usePrivateTabs, + blurNSFW = blurNSFW, + showPostLinkPreview = showPostLinkPreview, + appState = appState, + showIfRead = showIfRead, + showScores = showScores, + postActionbarMode = postActionbarMode, + ) + + PostViewMode.SmallCard -> + PostListingCard( + postView = postView, + admins = admins, + moderators = moderators, + instantScores = instantScores, + onUpvoteClick = { + instantScores = instantScores.update(VoteType.Upvote) + onUpvoteClick(postView) + }, + onDownvoteClick = { + instantScores = instantScores.update(VoteType.Downvote) + onDownvoteClick(postView) + }, + onReplyClick = onReplyClick, + onPostClick = onPostClick, + onSaveClick = onSaveClick, + onCommunityClick = onCommunityClick, + onEditPostClick = onEditPostClick, + onDeletePostClick = onDeletePostClick, + onReportClick = onReportClick, + onRemoveClick = onRemoveClick, + onBanPersonClick = onBanPersonClick, + onBanFromCommunityClick = onBanFromCommunityClick, + onLockPostClick = onLockPostClick, + onFeaturePostClick = onFeaturePostClick, + onViewVotesClick = onViewVotesClick, + onPersonClick = onPersonClick, + onViewSourceClick = { + viewSource = !viewSource + }, + viewSource = viewSource, + showReply = showReply, + showCommunityName = showCommunityName, + fullBody = false, + account = account, + expandedImage = false, + enableDownVotes = enableDownVotes, + showAvatar = showAvatar, + useCustomTabs = useCustomTabs, + usePrivateTabs = usePrivateTabs, + blurNSFW = blurNSFW, + showPostLinkPreview = showPostLinkPreview, + appState = appState, + showScores = showScores, + postActionbarMode = postActionbarMode, + ) + + PostViewMode.List -> + PostListingList( + postView = postView, + instantScores = instantScores, + onUpvoteClick = { + instantScores = instantScores.update(VoteType.Upvote) + onUpvoteClick(postView) + }, + onDownvoteClick = { + instantScores = instantScores.update(VoteType.Downvote) + onDownvoteClick(postView) + }, + onPostClick = onPostClick, + showCommunityName = showCommunityName, + account = account, + showVotingArrowsInListView = showVotingArrowsInListView, + showAvatar = showAvatar, + useCustomTabs = useCustomTabs, + usePrivateTabs = usePrivateTabs, + blurNSFW = blurNSFW, + appState = appState, + showIfRead = showIfRead, + enableDownVotes = enableDownVotes, + showScores = showScores, + ) + } + } + } + + if (swipeToActionPreset != SwipeToActionPreset.DISABLED) { + SwipeToAction( + swipeToActionPreset = swipeToActionPreset, + enableDownVotes = enableDownVotes, + swipeableContent = swipeableContent, + swipeState = swipeState, + ) + } else { + Row { + swipeableContent() + } } } diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt index 26b14549d..808bdc97c 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt @@ -26,6 +26,7 @@ import com.jerboa.datatypes.sampleLinkPostView import com.jerboa.datatypes.samplePostView import com.jerboa.db.entity.Account import com.jerboa.db.entity.AnonAccount +import com.jerboa.feat.SwipeToActionPreset import com.jerboa.isScrolledToEnd import com.jerboa.rememberJerboaAppState import com.jerboa.ui.components.common.RetryLoadingPosts @@ -49,6 +50,7 @@ fun PostListings( onDownvoteClick: (postView: PostView) -> Unit, onPostClick: (postView: PostView) -> Unit, onSaveClick: (postView: PostView) -> Unit, + onReplyClick: (postView: PostView) -> Unit, onEditPostClick: (postView: PostView) -> Unit, onDeletePostClick: (postView: PostView) -> Unit, onReportClick: (postView: PostView) -> Unit, @@ -80,6 +82,7 @@ fun PostListings( showScores: Boolean, postActionbarMode: Int, showPostAppendRetry: Boolean, + swipeToActionPreset: SwipeToActionPreset, ) { LazyColumn( state = listState, @@ -106,6 +109,7 @@ fun PostListings( usePrivateTabs = usePrivateTabs, onUpvoteClick = onUpvoteClick, onDownvoteClick = onDownvoteClick, + onReplyClick = onReplyClick, onPostClick = onPostClick, onSaveClick = onSaveClick, onCommunityClick = onCommunityClick, @@ -132,6 +136,7 @@ fun PostListings( showIfRead = showIfRead, showScores = showScores, postActionbarMode = postActionbarMode, + swipeToActionPreset = swipeToActionPreset, ).let { if (!postView.read && markAsReadOnScroll) { DisposableEffect(key1 = postView.post.id) { @@ -208,5 +213,7 @@ fun PreviewPostListings() { showScores = true, postActionbarMode = 0, showPostAppendRetry = false, + swipeToActionPreset = SwipeToActionPreset.DEFAULT, + onReplyClick = {}, ) } diff --git a/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt b/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt index 28321fcb3..f843be712 100644 --- a/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/settings/lookandfeel/LookAndFeelActivity.kt @@ -46,6 +46,7 @@ import com.jerboa.feat.BackConfirmationMode import com.jerboa.feat.BlurTypes import com.jerboa.feat.PostActionbarMode import com.jerboa.feat.PostNavigationGestureMode +import com.jerboa.feat.SwipeToActionPreset import com.jerboa.getLangPreferenceDropdownEntries import com.jerboa.matchLocale import com.jerboa.model.AppSettingsViewModel @@ -109,6 +110,8 @@ fun LookAndFeelActivity( val markAsReadOnScroll = rememberBooleanSettingState(settings.markAsReadOnScroll) val autoPlayGifs = rememberBooleanSettingState(settings.autoPlayGifs) + val swipeToActionPreset = rememberIntSettingState(settings.swipeToActionPreset) + fun updateAppSettings() { appSettingsViewModel.update( AppSettings( @@ -135,6 +138,7 @@ fun LookAndFeelActivity( postActionbarMode = postActionbarMode.value, autoPlayGifs = autoPlayGifs.value, postNavigationGestureMode = postNavigationGestureModeState.value, + swipeToActionPreset = swipeToActionPreset.value, ), ) } @@ -312,6 +316,18 @@ fun LookAndFeelActivity( items = BlurTypes.entries.map { stringResource(it.resId) }, onItemSelected = { _, _ -> updateAppSettings() }, ) + SettingsListDropdown( + state = swipeToActionPreset, + icon = { + Icon( + imageVector = Icons.Outlined.Swipe, + contentDescription = null, + ) + }, + title = { Text(stringResource(id = R.string.swipe_to_action_presets)) }, + items = SwipeToActionPreset.entries.map { stringResource(it.resId) }, + onItemSelected = { _, _ -> updateAppSettings() }, + ) SettingsCheckbox( modifier = Modifier.height(SETTINGS_MENU_LINK_HEIGHT), state = showBottomNavState, diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 7f070d43d..72c558322 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -158,6 +158,7 @@ Пожаловаться на пользователя Загрузить изображение Удалено + Удалено Выделено в сообществе Выделено локально Перейти в сообщество @@ -203,7 +204,7 @@ Присоединяйтесь к c/jerboa Открытый исходный код Исходный код - Jerboa — это свободное программное обеспечение с открытым исходным кодом, распространяемое под лицензией + "Jerboa — это свободное программное обеспечение с открытым исходным кодом, распространяемое под лицензией " GNU Affero General Public License v3.0 Поддержка Версия %1$s @@ -235,8 +236,8 @@ Сначала авторизуйтесь Поддержать Опустить - Вы повысили пост - Вы понизили пост + Вы поддержали пост + Вы снизили оценку поста Сохраненное изображение Сохранение изображения… Доступ запрещен @@ -368,6 +369,10 @@ Поделится Включить 2FA Запустить ссылку 2FA + Длинная панель + Короткая панель для левшей + Короткая панель для правшей + Режим панели действий поста Автоматическое воспроизведение GIF Разблокировать сообщество %1s заблокирован @@ -396,5 +401,23 @@ Не удалось разобрать дату Запись об этом комментарии отсутствует Версия (%s) не поддерживается + Комментарий удалён + Комментарий восстановлен + Пост удалён + Пост восстановлен + Удалить комментарий + Вернуть комментарий + Удалить пост + Вернуть пост + Заморозить пост + Разморозить бост + Введите причину + Преднастройка для смахивающего жеста + Отключить + С двух сторон + Отриц. - слева, положит. - справа + Только справа + Только слева + Только оценки \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c16d27f9d..0e16dacca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -415,6 +415,11 @@ Lock Post Unlock Post Type your reason + Disabled + Two sides + Left-right votes + Only right swipe + Only left swipe Banned %1$s Banned %1$s for %2$d days Banned %1$s from %2$s @@ -432,6 +437,8 @@ Unfeature in Community Feature in Local Unfeature in Local + Preset for swipe gesture + Only votes View votes Post votes Comment votes