mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 01:07:46 +00:00
feat(desktop): full-width search expansion with Cmd+F via CompositionLocal
- FeedTabsHeader expands to full-width search card when active - Feed tabs hidden, search input fills entire header width - History section (recent + saved) shown below input - "Open full search" link at bottom - Scrim overlay dims feed content when search is active - Cmd+F toggles search via LocalFeedSearchActive CompositionLocal - MutableState<Boolean> provided at Window level - FeedScreen reads it directly, no param threading needed - SearchPill simplified back to clickable-only (no inline expansion) - Escape or click scrim dismisses search Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c69c4462b9
commit
ec8fbd3980
@@ -263,6 +263,7 @@ fun main() {
|
||||
var showAppDrawer by remember { mutableStateOf(false) }
|
||||
var showAddColumnDialog by remember { mutableStateOf(false) }
|
||||
var showImportFollowListDialog by remember { mutableStateOf(false) }
|
||||
val feedSearchActiveState = remember { mutableStateOf(false) }
|
||||
|
||||
// Tor state at Window level — survives key() app rebuild
|
||||
var torSettings by remember {
|
||||
@@ -451,7 +452,7 @@ fun main() {
|
||||
} else {
|
||||
KeyShortcut(Key.F, ctrl = true)
|
||||
},
|
||||
onClick = { navigateToScreen?.invoke(DeckColumnType.Search) },
|
||||
onClick = { feedSearchActiveState.value = !feedSearchActiveState.value },
|
||||
)
|
||||
Item(
|
||||
"App Drawer",
|
||||
@@ -617,39 +618,43 @@ fun main() {
|
||||
LocalIsImmersiveFullscreen provides immersiveFullscreenState,
|
||||
) {
|
||||
key(appRestartKey) {
|
||||
App(
|
||||
layoutMode = layoutMode,
|
||||
onLayoutModeChange = { newMode ->
|
||||
layoutMode = newMode
|
||||
DesktopPreferences.layoutMode = newMode.name
|
||||
},
|
||||
deckState = deckState,
|
||||
workspaceManager = workspaceManager,
|
||||
accountManager = accountManager,
|
||||
showComposeDialog = showComposeDialog,
|
||||
showAppDrawer = showAppDrawer,
|
||||
onShowComposeDialog = { showComposeDialog = true },
|
||||
onShowReplyDialog = { event ->
|
||||
replyToNote = event
|
||||
showComposeDialog = true
|
||||
},
|
||||
onDismissComposeDialog = {
|
||||
showComposeDialog = false
|
||||
replyToNote = null
|
||||
},
|
||||
onDismissAppDrawer = { showAppDrawer = false },
|
||||
onShowAppDrawer = { showAppDrawer = true },
|
||||
replyToNote = replyToNote,
|
||||
showImportFollowListDialog = showImportFollowListDialog,
|
||||
onShowImportFollowListDialog = { showImportFollowListDialog = true },
|
||||
onDismissImportFollowListDialog = { showImportFollowListDialog = false },
|
||||
onRestartApp = { appRestartKey++ },
|
||||
torManager = torManager,
|
||||
torTypeFlow = torTypeFlow,
|
||||
externalPortFlow = externalPortFlow,
|
||||
initialTorSettings = torSettings,
|
||||
onNavigateToScreen = { navigateToScreen = it },
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
com.vitorpamplona.amethyst.desktop.ui.theme.LocalFeedSearchActive provides feedSearchActiveState,
|
||||
) {
|
||||
App(
|
||||
layoutMode = layoutMode,
|
||||
onLayoutModeChange = { newMode ->
|
||||
layoutMode = newMode
|
||||
DesktopPreferences.layoutMode = newMode.name
|
||||
},
|
||||
deckState = deckState,
|
||||
workspaceManager = workspaceManager,
|
||||
accountManager = accountManager,
|
||||
showComposeDialog = showComposeDialog,
|
||||
showAppDrawer = showAppDrawer,
|
||||
onShowComposeDialog = { showComposeDialog = true },
|
||||
onShowReplyDialog = { event ->
|
||||
replyToNote = event
|
||||
showComposeDialog = true
|
||||
},
|
||||
onDismissComposeDialog = {
|
||||
showComposeDialog = false
|
||||
replyToNote = null
|
||||
},
|
||||
onDismissAppDrawer = { showAppDrawer = false },
|
||||
onShowAppDrawer = { showAppDrawer = true },
|
||||
replyToNote = replyToNote,
|
||||
showImportFollowListDialog = showImportFollowListDialog,
|
||||
onShowImportFollowListDialog = { showImportFollowListDialog = true },
|
||||
onDismissImportFollowListDialog = { showImportFollowListDialog = false },
|
||||
onRestartApp = { appRestartKey++ },
|
||||
torManager = torManager,
|
||||
torTypeFlow = torTypeFlow,
|
||||
externalPortFlow = externalPortFlow,
|
||||
initialTorSettings = torSettings,
|
||||
onNavigateToScreen = { navigateToScreen = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+291
-42
@@ -23,6 +23,9 @@ package com.vitorpamplona.amethyst.desktop.ui
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.TooltipArea
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -34,10 +37,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -54,7 +60,18 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
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.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.vitorpamplona.amethyst.commons.compose.elements.BoostedMark
|
||||
import com.vitorpamplona.amethyst.commons.compose.layouts.GenericRepostLayout
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
@@ -66,12 +83,14 @@ import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
|
||||
import com.vitorpamplona.amethyst.desktop.SearchHistoryStore
|
||||
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.feeds.DesktopCustomFeedFilter
|
||||
import com.vitorpamplona.amethyst.desktop.feeds.DesktopFollowingFeedFilter
|
||||
import com.vitorpamplona.amethyst.desktop.feeds.DesktopGlobalFeedFilter
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.platform.PlatformInfo
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.FeedMode
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.FilterBuilders
|
||||
@@ -300,6 +319,9 @@ fun FeedScreen(
|
||||
onNavigateToRelays: () -> Unit = {},
|
||||
onSearchClick: () -> Unit = {},
|
||||
) {
|
||||
val feedSearchActiveState = com.vitorpamplona.amethyst.desktop.ui.theme.LocalFeedSearchActive.current
|
||||
var searchActive by feedSearchActiveState
|
||||
val onSearchActiveChange: (Boolean) -> Unit = { searchActive = it }
|
||||
val relayStatuses by relayManager.relayStatuses.collectAsState()
|
||||
val connectedRelays by relayManager.connectedRelays.collectAsState()
|
||||
val followedUsers by localCache.followedUsers.collectAsState()
|
||||
@@ -560,10 +582,12 @@ fun FeedScreen(
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ReadingColumn {
|
||||
// Header: pinned feed tabs + "Show More +"
|
||||
// Header: pinned feed tabs + search pill (or expanded search card)
|
||||
FeedTabsHeader(
|
||||
feedMode = feedMode,
|
||||
activeFeedId = activeFeedId,
|
||||
searchExpanded = searchActive,
|
||||
onSearchExpandedChange = onSearchActiveChange,
|
||||
onFeedModeChange = { mode ->
|
||||
feedMode = mode
|
||||
activeFeedId = null
|
||||
@@ -724,6 +748,22 @@ fun FeedScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Search scrim — dims feed content when search is expanded
|
||||
if (searchActive) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.3f))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
) {
|
||||
onSearchActiveChange(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Lightbox overlay
|
||||
lightboxState?.let { state ->
|
||||
LightboxOverlay(
|
||||
@@ -750,6 +790,8 @@ fun FeedScreen(
|
||||
private fun FeedTabsHeader(
|
||||
feedMode: FeedMode,
|
||||
activeFeedId: String? = null,
|
||||
searchExpanded: Boolean = false,
|
||||
onSearchExpandedChange: (Boolean) -> Unit = {},
|
||||
onFeedModeChange: (FeedMode) -> Unit,
|
||||
onNavigateToFeed: (com.vitorpamplona.amethyst.commons.feeds.custom.FeedDefinition) -> Unit = {},
|
||||
onOpenFeedsDrawer: () -> Unit,
|
||||
@@ -764,58 +806,265 @@ private fun FeedTabsHeader(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding + 8.dp, vertical = 8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = sidePadding + 8.dp, vertical = 8.dp)
|
||||
.zIndex(if (searchExpanded) 10f else 0f),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
// Compact feed tabs
|
||||
pinnedFeeds.forEach { feed ->
|
||||
val isSelected =
|
||||
when (feed.source) {
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Following ->
|
||||
feedMode == FeedMode.FOLLOWING
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global ->
|
||||
feedMode == FeedMode.GLOBAL
|
||||
else -> activeFeedId == feed.id
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
if (!searchExpanded) {
|
||||
// Compact feed tabs
|
||||
pinnedFeeds.forEach { feed ->
|
||||
val isSelected =
|
||||
when (feed.source) {
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Following ->
|
||||
feedMode == FeedMode.FOLLOWING
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global ->
|
||||
feedMode == FeedMode.GLOBAL
|
||||
else -> activeFeedId == feed.id
|
||||
}
|
||||
FilterChip(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
when (feed.source) {
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Following ->
|
||||
onFeedModeChange(FeedMode.FOLLOWING)
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global ->
|
||||
onFeedModeChange(FeedMode.GLOBAL)
|
||||
else -> onNavigateToFeed(feed)
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
"${feed.emoji} ${feed.name}",
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
FilterChip(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
when (feed.source) {
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Following ->
|
||||
onFeedModeChange(FeedMode.FOLLOWING)
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global ->
|
||||
onFeedModeChange(FeedMode.GLOBAL)
|
||||
else -> onNavigateToFeed(feed)
|
||||
}
|
||||
|
||||
// Search pill / expanded input — takes remaining center space
|
||||
if (searchExpanded) {
|
||||
SearchInput(
|
||||
onDismiss = { onSearchExpandedChange(false) },
|
||||
onOpenFullSearch = onSearchClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
} else {
|
||||
com.vitorpamplona.amethyst.desktop.ui.search.SearchPill(
|
||||
onClick = { onSearchExpandedChange(true) },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
|
||||
if (!searchExpanded) {
|
||||
// Compose button
|
||||
IconButton(onClick = onCompose, modifier = Modifier.size(32.dp)) {
|
||||
Icon(
|
||||
MaterialSymbols.Edit,
|
||||
contentDescription = "Compose",
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expanded search: history section below the input
|
||||
if (searchExpanded) {
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
SearchHistorySection(
|
||||
onOpenFullSearch = {
|
||||
onSearchExpandedChange(false)
|
||||
onSearchClick()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchInput(
|
||||
onDismiss: () -> Unit,
|
||||
onOpenFullSearch: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
var textFieldValue by remember { mutableStateOf(TextFieldValue("")) }
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.Search,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { textFieldValue = it },
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(focusRequester)
|
||||
.onPreviewKeyEvent { event ->
|
||||
if (event.type == KeyEventType.KeyDown) {
|
||||
when (event.key) {
|
||||
Key.Escape -> {
|
||||
onDismiss()
|
||||
focusManager.clearFocus()
|
||||
true
|
||||
}
|
||||
Key.Enter -> {
|
||||
onOpenFullSearch()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
label = {
|
||||
textStyle =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
singleLine = true,
|
||||
decorationBox = { innerTextField ->
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
if (textFieldValue.text.isEmpty()) {
|
||||
Text(
|
||||
"${feed.emoji} ${feed.name}",
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
"Search notes, profiles, hashtags...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
if (PlatformInfo.isMacOS) "\u2318F" else "Ctrl+F",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
}
|
||||
|
||||
// Search pill — takes remaining center space
|
||||
com.vitorpamplona.amethyst.desktop.ui.search.SearchPill(
|
||||
onClick = onSearchClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchHistorySection(onOpenFullSearch: () -> Unit) {
|
||||
val history by SearchHistoryStore.history.collectAsState()
|
||||
val savedSearches by SearchHistoryStore.savedSearches.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
) {
|
||||
if (history.isNotEmpty()) {
|
||||
Text(
|
||||
"Recent",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
)
|
||||
history.take(5).forEach { query ->
|
||||
val text =
|
||||
com.vitorpamplona.amethyst.commons.search.QuerySerializer
|
||||
.serialize(query)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onOpenFullSearch() }
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.History,
|
||||
null,
|
||||
Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(text, style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose button
|
||||
IconButton(onClick = onCompose, modifier = Modifier.size(32.dp)) {
|
||||
Icon(
|
||||
MaterialSymbols.Edit,
|
||||
contentDescription = "Compose",
|
||||
modifier = Modifier.size(18.dp),
|
||||
if (savedSearches.isNotEmpty()) {
|
||||
if (history.isNotEmpty()) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
"Saved",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
)
|
||||
savedSearches.take(5).forEach { saved ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onOpenFullSearch() }
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.Bookmark,
|
||||
null,
|
||||
Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(saved.label, style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onOpenFullSearch() }
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.AutoMirrored.OpenInNew,
|
||||
null,
|
||||
Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(
|
||||
"Open full search",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-171
@@ -20,46 +20,22 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.ui.search
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.search.QuerySerializer
|
||||
import com.vitorpamplona.amethyst.desktop.SearchHistoryStore
|
||||
import com.vitorpamplona.amethyst.desktop.platform.PlatformInfo
|
||||
import com.vitorpamplona.amethyst.desktop.ui.theme.hoverHighlight
|
||||
|
||||
@@ -69,16 +45,8 @@ fun SearchPill(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val shortcutHint = if (PlatformInfo.isMacOS) "\u2318F" else "Ctrl+F"
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var textFieldValue by remember { mutableStateOf(TextFieldValue("")) }
|
||||
|
||||
val history by SearchHistoryStore.history.collectAsState()
|
||||
val savedSearches by SearchHistoryStore.savedSearches.collectAsState()
|
||||
|
||||
Surface(
|
||||
onClick = { expanded = true },
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
modifier = modifier.height(36.dp).hoverHighlight(),
|
||||
@@ -94,52 +62,12 @@ fun SearchPill(
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
if (expanded) {
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { textFieldValue = it },
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(focusRequester)
|
||||
.onPreviewKeyEvent { event ->
|
||||
if (event.type == KeyEventType.KeyDown && event.key == Key.Escape) {
|
||||
expanded = false
|
||||
textFieldValue = TextFieldValue("")
|
||||
focusManager.clearFocus()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
textStyle =
|
||||
MaterialTheme.typography.labelMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
singleLine = true,
|
||||
decorationBox = { innerTextField ->
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
if (textFieldValue.text.isEmpty()) {
|
||||
Text(
|
||||
"Search notes, profiles...",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
"Search...",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
"Search...",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
shortcutHint,
|
||||
@@ -148,96 +76,4 @@ fun SearchPill(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown below the pill when expanded
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
textFieldValue = TextFieldValue("")
|
||||
},
|
||||
) {
|
||||
if (history.isNotEmpty()) {
|
||||
Text(
|
||||
"Recent",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
)
|
||||
history.take(5).forEach { query ->
|
||||
val text = QuerySerializer.serialize(query)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
textFieldValue = TextFieldValue(text, TextRange(text.length))
|
||||
}.hoverHighlight()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(MaterialSymbols.History, null, Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(text, style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (savedSearches.isNotEmpty()) {
|
||||
if (history.isNotEmpty()) {
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
Text(
|
||||
"Saved",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
)
|
||||
savedSearches.take(5).forEach { saved ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.hoverHighlight()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(MaterialSymbols.Bookmark, null, Modifier.size(16.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(saved.label, style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (history.isEmpty() && savedSearches.isEmpty()) {
|
||||
Text(
|
||||
"Type to search",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(12.dp),
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.hoverHighlight()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(MaterialSymbols.AutoMirrored.OpenInNew, null, Modifier.size(16.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Open full search", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -24,6 +24,8 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -48,6 +50,9 @@ val LocalSpacing = staticCompositionLocalOf { AmethystSpacing() }
|
||||
|
||||
val LocalIsDarkTheme = staticCompositionLocalOf { false }
|
||||
|
||||
/** Mutable state for Cmd+F feed search. Provided at Main.kt level. */
|
||||
val LocalFeedSearchActive = compositionLocalOf { mutableStateOf(false) }
|
||||
|
||||
val MaterialTheme.spacing: AmethystSpacing
|
||||
@Composable @ReadOnlyComposable
|
||||
get() = LocalSpacing.current
|
||||
|
||||
Reference in New Issue
Block a user