Compare commits

...

6 Commits

Author SHA1 Message Date
b71fc0a4e0 feat: Enhance search UX and improve localization
- Replace German strings with English translations in Localizable.xcstrings
- Add smooth zoom transitions between bookmark list and detail views using matchedGeometryEffect
- Improve search interface with better styling, focus management, and loading states
- Enhance bookmark card interactions and visual consistency
- Refactor search functionality for cleaner code structure
2025-08-11 21:12:16 +02:00
3981f086f9 bump build version 2025-08-07 20:42:40 +02:00
1ffafc3b35 fix: Improve tag chip alignment in TagManagementView
- Update grid container to align chips to top when fewer than 3 rows
- Adjust top padding to prevent label overlap with chips
2025-08-07 20:41:45 +02:00
bbcb7bd81f feat: Add tappable hero image with zoom functionality
- Add ImageViewerView for full-screen image viewing with zoom and pan
- Make hero image in BookmarkDetailView tappable with zoom icon
- Implement drag-to-dismiss functionality for image viewer
- Extract ImageViewerView to separate file for better code organization
- Add zoom icon (arrow symbol) to indicate tappable hero image
- Support pinch-to-zoom (1x-4x), double-tap zoom, and pan gestures
2025-08-06 22:19:00 +02:00
f3f94f1cfe refactor: Clean up tag management UI and update project version
- Remove duplicate search functionality from BookmarkLabelsView
- Update TagManagementView font sizes for better readability
- Bump URLShare extension version to 13
2025-08-06 21:49:12 +02:00
4915a773d6 fix: Improve keyboard behavior in ShareBookmarkView
- Add focus state management for proper text field scrolling
- Fix keyboard padding to prevent content overlap
- Smooth animations and remove visual artifacts
2025-08-06 21:44:57 +02:00
12 changed files with 278 additions and 144 deletions

View File

@ -160,12 +160,6 @@
},
"Jump to last read position (%lld%%)" : {
},
"Keine Bookmarks gefunden." : {
},
"Keine Ergebnisse" : {
},
"Key" : {
"extractionState" : "manual"
@ -202,6 +196,12 @@
},
"No bookmarks found in %@." : {
},
"No bookmarks found." : {
},
"No results" : {
},
"OK" : {
@ -278,12 +278,21 @@
},
"Saving..." : {
},
"Search" : {
},
"Search or add new tag..." : {
},
"Search results" : {
},
"Search..." : {
},
"Searching..." : {
},
"Select a bookmark or tag" : {
@ -302,15 +311,6 @@
},
"Successfully logged in" : {
},
"Suchbegriff eingeben..." : {
},
"Suche" : {
},
"Suche..." : {
},
"Sync interval" : {

View File

@ -3,7 +3,7 @@ import SwiftUI
struct ShareBookmarkView: View {
@ObservedObject var viewModel: ShareBookmarkViewModel
@State private var keyboardHeight: CGFloat = 0
@State private var shouldScrollToTitle = false
@FocusState private var focusedField: AddBookmarkFieldFocus?
private func dismissKeyboard() {
NotificationCenter.default.post(name: NSNotification.Name("DismissKeyboard"), object: nil)
@ -17,19 +17,20 @@ struct ShareBookmarkView: View {
logoSection
urlSection
tagManagementSection
.id(AddBookmarkFieldFocus.labels)
titleSection
.id("titleField")
.id(AddBookmarkFieldFocus.title)
statusSection
Spacer(minLength: 100) // Space for button
}
}
.padding(.bottom, keyboardHeight / 2)
.onChange(of: shouldScrollToTitle) { shouldScroll, _ in
if shouldScroll {
withAnimation(.easeInOut(duration: 0.3)) {
proxy.scrollTo("titleField", anchor: .center)
.padding(.bottom, max(0, keyboardHeight - 120))
.onChange(of: focusedField) { newField, _ in
guard let field = newField else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation(.easeInOut(duration: 0.25)) {
proxy.scrollTo(field, anchor: .center)
}
shouldScrollToTitle = false
}
}
}
@ -39,25 +40,21 @@ struct ShareBookmarkView: View {
.background(Color(.systemGroupedBackground))
.onAppear { viewModel.onAppear() }
.ignoresSafeArea(.keyboard, edges: .bottom)
.background(
Color.clear
.contentShape(Rectangle())
.onTapGesture {
// Fallback for extensions: tap anywhere to dismiss keyboard
dismissKeyboard()
}
)
.contentShape(Rectangle())
.onTapGesture {
dismissKeyboard()
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
keyboardHeight = keyboardFrame.height
// Scroll to title field when keyboard appears
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
shouldScrollToTitle = true
withAnimation(.easeInOut(duration: 0.3)) {
keyboardHeight = keyboardFrame.height
}
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
keyboardHeight = 0
withAnimation(.easeInOut(duration: 0.3)) {
keyboardHeight = 0
}
}
}
@ -103,6 +100,7 @@ struct ShareBookmarkView: View {
.padding(.horizontal, 4)
.frame(maxWidth: 420)
.frame(maxWidth: .infinity, alignment: .center)
.focused($focusedField, equals: .title)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
@ -123,6 +121,7 @@ struct ShareBookmarkView: View {
isLabelsLoading: false,
availableLabelPages: convertToBookmarkLabelPages(viewModel.availableLabelPages),
filteredLabels: convertToBookmarkLabels(viewModel.filteredLabels),
searchFieldFocus: $focusedField,
onAddCustomTag: {
addCustomTag()
},
@ -176,7 +175,6 @@ struct ShareBookmarkView: View {
.padding(.top, 16)
.padding(.bottom, 32)
.disabled(viewModel.isSaving)
.background(Color(.systemGroupedBackground))
}
// MARK: - Helper Functions

View File

@ -435,7 +435,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_TEAM = 8J69P655GN;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist;
@ -468,7 +468,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_TEAM = 8J69P655GN;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist;
@ -623,7 +623,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES;
@ -667,7 +667,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES;

View File

@ -342,11 +342,13 @@ class API: PAPI {
endpoint: endpoint,
responseType: [BookmarkDto].self
)
let currentPage = response.value(forHTTPHeaderField: "Current-Page").flatMap { Int($0) }
let totalCount = response.value(forHTTPHeaderField: "Total-Count").flatMap { Int($0) }
let totalPages = response.value(forHTTPHeaderField: "Total-Pages").flatMap { Int($0) }
let linksHeader = response.value(forHTTPHeaderField: "Link")
let links = linksHeader?.components(separatedBy: ",")
return BookmarksPageDto(
bookmarks: bookmarks,
currentPage: currentPage,

View File

@ -78,7 +78,6 @@ class BookmarksRepository: PBookmarksRepository {
}
func searchBookmarks(search: String) async throws -> BookmarksPage {
let bookmarkDtos = try await api.searchBookmarks(search: search)
return bookmarkDtos.toDomain()
try await api.searchBookmarks(search: search).toDomain()
}
}

View File

@ -4,6 +4,7 @@ import Combine
struct BookmarkDetailView: View {
let bookmarkId: String
let namespace: Namespace.ID?
// MARK: - States
@ -15,6 +16,7 @@ struct BookmarkDetailView: View {
@State private var scrollViewHeight: CGFloat = 1
@State private var showJumpToProgressButton: Bool = false
@State private var scrollPosition = ScrollPosition(edge: .top)
@State private var showingImageViewer = false
// MARK: - Envs
@ -24,8 +26,9 @@ struct BookmarkDetailView: View {
private let headerHeight: CGFloat = 320
init(bookmarkId: String, viewModel: BookmarkDetailViewModel = BookmarkDetailViewModel(), webViewHeight: CGFloat = 300, showingFontSettings: Bool = false, showingLabelsSheet: Bool = false, playerUIState: PlayerUIState = .init()) {
init(bookmarkId: String, namespace: Namespace.ID? = nil, viewModel: BookmarkDetailViewModel = BookmarkDetailViewModel(), webViewHeight: CGFloat = 300, showingFontSettings: Bool = false, showingLabelsSheet: Bool = false, playerUIState: PlayerUIState = .init()) {
self.bookmarkId = bookmarkId
self.namespace = namespace
self.viewModel = viewModel
self.webViewHeight = webViewHeight
self.showingFontSettings = showingFontSettings
@ -152,6 +155,9 @@ struct BookmarkDetailView: View {
.sheet(isPresented: $showingLabelsSheet) {
BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels)
}
.sheet(isPresented: $showingImageViewer) {
ImageViewerView(imageUrl: viewModel.bookmarkDetail.imageUrl)
}
.onChange(of: showingFontSettings) { _, isShowing in
if !isShowing {
// Reload settings when sheet is dismissed
@ -186,17 +192,23 @@ struct BookmarkDetailView: View {
let offset = geo.frame(in: .global).minY
ZStack(alignment: .top) {
AsyncImage(url: URL(string: viewModel.bookmarkDetail.imageUrl)) { image in
image
.resizable()
.scaledToFill()
.frame(width: geometry.size.width, height: headerHeight + (offset > 0 ? offset : 0))
.clipped()
.offset(y: (offset > 0 ? -offset : 0))
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.4))
.frame(width: geometry.size.width, height: headerHeight)
}
image
.resizable()
.scaledToFill()
.frame(width: geometry.size.width, height: headerHeight + (offset > 0 ? offset : 0))
.clipped()
.offset(y: (offset > 0 ? -offset : 0))
.if(namespace != nil) { view in
view.matchedGeometryEffect(id: "image-\(viewModel.bookmarkDetail.id)", in: namespace!)
}
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.4))
.frame(width: geometry.size.width, height: headerHeight)
.if(namespace != nil) { view in
view.matchedGeometryEffect(id: "image-\(viewModel.bookmarkDetail.id)", in: namespace!)
}
}
// Gradient overlay für bessere Button-Sichtbarkeit
LinearGradient(
gradient: Gradient(colors: [
@ -213,10 +225,41 @@ struct BookmarkDetailView: View {
.frame(height: 240)
.frame(maxWidth: .infinity)
.offset(y: (offset > 0 ? -offset : 0))
// Tap area and zoom icon
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
showingImageViewer = true
}) {
Image(systemName: "arrow.up.left.and.arrow.down.right")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
.padding(8)
.background(
Circle()
.fill(Color.black.opacity(0.6))
.overlay(
Circle()
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
)
}
.padding(.trailing, 16)
.padding(.bottom, 16)
}
}
.frame(height: headerHeight + (offset > 0 ? offset : 0))
.offset(y: (offset > 0 ? -offset : 0))
}
}
.frame(height: headerHeight)
.ignoresSafeArea(edges: .top)
.onTapGesture {
showingImageViewer = true
}
}
}

View File

@ -16,7 +16,6 @@ struct BookmarkLabelsView: View {
var body: some View {
NavigationView {
VStack(spacing: 12) {
searchSection
availableLabelsSection
Spacer()
}
@ -53,59 +52,7 @@ struct BookmarkLabelsView: View {
// MARK: - View Components
@ViewBuilder
private var searchSection: some View {
VStack(spacing: 8) {
searchField
customTagSuggestion
}
.padding(.horizontal)
}
@ViewBuilder
private var searchField: some View {
TextField("Search or add new tag...", text: $viewModel.searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onSubmit {
Task {
await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText)
}
}
}
@ViewBuilder
private var customTagSuggestion: some View {
if !viewModel.searchText.isEmpty &&
!viewModel.filteredLabels.contains(where: { $0.name.lowercased() == viewModel.searchText.lowercased() }) {
HStack {
Text("Add new tag:")
.font(.caption)
.foregroundColor(.secondary)
Text(viewModel.searchText)
.font(.caption)
.fontWeight(.medium)
Spacer()
Button(action: {
Task {
await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText)
}
}) {
HStack(spacing: 6) {
Image(systemName: "plus.circle.fill")
.font(.caption)
Text("Add")
.font(.caption)
.fontWeight(.medium)
}
}
.foregroundColor(.accentColor)
}
.padding(.horizontal, 12)
.padding(.vertical, 12)
.background(Color.accentColor.opacity(0.1))
.cornerRadius(10)
}
}
@ViewBuilder
private var availableLabelsSection: some View {
@ -132,6 +79,7 @@ struct BookmarkLabelsView: View {
}
}
)
.padding(.horizontal)
}
}

View File

@ -0,0 +1,118 @@
import SwiftUI
struct ImageViewerView: View {
let imageUrl: String
@Environment(\.dismiss) private var dismiss
@State private var scale: CGFloat = 1.0
@State private var lastScale: CGFloat = 1.0
@State private var offset: CGSize = .zero
@State private var lastOffset: CGSize = .zero
@State private var dragOffset: CGSize = .zero
@State private var isDraggingToDismiss = false
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
Color.black
.ignoresSafeArea()
AsyncImage(url: URL(string: imageUrl)) { image in
image
.resizable()
.scaledToFit()
.scaleEffect(scale)
.offset(offset)
.offset(dragOffset)
.opacity(isDraggingToDismiss ? 0.8 : 1.0)
.gesture(
SimultaneousGesture(
MagnificationGesture()
.onChanged { value in
let delta = value / lastScale
lastScale = value
scale = min(max(scale * delta, 1), 4)
}
.onEnded { _ in
lastScale = 1.0
if scale < 1 {
withAnimation(.spring()) {
scale = 1
offset = .zero
}
}
if scale > 4 {
scale = 4
}
},
DragGesture()
.onChanged { value in
if scale > 1 {
let newOffset = CGSize(
width: lastOffset.width + value.translation.width,
height: lastOffset.height + value.translation.height
)
offset = newOffset
} else {
// Dismiss gesture when not zoomed
dragOffset = value.translation
let dragDistance = sqrt(pow(value.translation.width, 2) + pow(value.translation.height, 2))
if dragDistance > 50 {
isDraggingToDismiss = true
}
}
}
.onEnded { value in
if scale <= 1 {
lastOffset = offset
let dragDistance = sqrt(pow(value.translation.width, 2) + pow(value.translation.height, 2))
let velocity = sqrt(pow(value.velocity.width, 2) + pow(value.velocity.height, 2))
if dragDistance > 100 || velocity > 500 {
dismiss()
} else {
withAnimation(.spring()) {
dragOffset = .zero
isDraggingToDismiss = false
}
}
} else {
lastOffset = offset
}
}
)
)
.onTapGesture(count: 2) {
withAnimation(.spring()) {
if scale > 1 {
scale = 1
offset = .zero
lastOffset = .zero
} else {
scale = 2
}
}
}
} placeholder: {
ProgressView()
.scaleEffect(1.5)
.foregroundColor(.white)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Close") {
dismiss()
}
.foregroundColor(.white)
}
}
}
}
}
#Preview {
ImageViewerView(imageUrl: "https://example.com/image.jpg")
}

View File

@ -1,6 +1,16 @@
import SwiftUI
import SafariServices
extension View {
@ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
struct BookmarkCardView: View {
@Environment(\.colorScheme) var colorScheme
@ -10,6 +20,7 @@ struct BookmarkCardView: View {
let onArchive: (Bookmark) -> Void
let onDelete: (Bookmark) -> Void
let onToggleFavorite: (Bookmark) -> Void
let namespace: Namespace.ID?
var body: some View {
VStack(alignment: .leading, spacing: 8) {
@ -27,6 +38,9 @@ struct BookmarkCardView: View {
.frame(height: 120)
}
.clipShape(RoundedRectangle(cornerRadius: 8))
.if(namespace != nil) { view in
view.matchedGeometryEffect(id: "image-\(bookmark.id)", in: namespace!)
}
if bookmark.readProgress > 0 && bookmark.isArchived == false && bookmark.isMarked == false {
ZStack {
@ -223,12 +237,3 @@ struct IconBadge: View {
}
}
#Preview {
BookmarkCardView(bookmark: .mock, currentState: .all) { _ in
} onDelete: { _ in
} onToggleFavorite: { _ in
}
}

View File

@ -4,6 +4,8 @@ import SwiftUI
struct BookmarksView: View {
@Namespace private var namespace
// MARK: States
@State private var viewModel: BookmarksViewModel
@ -95,7 +97,8 @@ struct BookmarksView: View {
Task {
await viewModel.toggleFavorite(bookmark: bookmark)
}
}
},
namespace: namespace
)
.onAppear {
if bookmark.id == viewModel.bookmarks?.bookmarks.last?.id {
@ -109,6 +112,7 @@ struct BookmarksView: View {
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.listRowSeparator(.hidden)
.listRowBackground(Color(R.color.bookmark_list_bg))
.matchedTransitionSource(id: bookmark.id, in: namespace)
}
}
.listStyle(.plain)
@ -161,7 +165,8 @@ struct BookmarksView: View {
set: { selectedBookmarkId = $0 }
)
) { bookmarkId in
BookmarkDetailView(bookmarkId: bookmarkId)
BookmarkDetailView(bookmarkId: bookmarkId, namespace: namespace)
.navigationTransition(.zoom(sourceID: bookmarkId, in: namespace))
}
.sheet(isPresented: $showingAddBookmark) {
AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle)

View File

@ -93,18 +93,18 @@ struct TagManagementView: View {
!selectedLabelsSet.contains(searchText.wrappedValue) {
HStack {
Text("Add new tag:")
.font(.caption)
.font(.subheadline)
.foregroundColor(.secondary)
Text(searchText.wrappedValue)
.font(.caption)
.font(.subheadline)
.fontWeight(.medium)
Spacer()
Button(action: onAddCustomTag) {
HStack(spacing: 6) {
Image(systemName: "plus.circle.fill")
.font(.caption)
.font(.subheadline)
Text("Add")
.font(.caption)
.font(.subheadline)
.fontWeight(.medium)
}
}
@ -173,13 +173,13 @@ struct TagManagementView: View {
)
}
}
.frame(maxWidth: .infinity, alignment: .top)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding(.horizontal)
}
}
.tabViewStyle(.page(indexDisplayMode: availableLabelPages.count > 1 ? .automatic : .never))
.frame(height: 180)
.padding(.top, -20)
.padding(.top, 10)
}
@ViewBuilder

View File

@ -5,13 +5,15 @@ struct SearchBookmarksView: View {
@FocusState private var searchFieldIsFocused: Bool
@State private var selectedBookmarkId: String?
@Binding var selectedBookmark: Bookmark?
@Namespace private var namespace
@State private var isFirstAppearance = true
var body: some View {
VStack(spacing: 0) {
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
TextField("Suchbegriff eingeben...", text: $viewModel.searchQuery)
TextField("Search...", text: $viewModel.searchQuery)
.focused($searchFieldIsFocused)
.textFieldStyle(PlainTextFieldStyle())
.autocapitalization(.none)
@ -33,7 +35,7 @@ struct SearchBookmarksView: View {
.padding([.horizontal, .top])
if viewModel.isLoading {
ProgressView("Suche...")
ProgressView("Searching...")
.padding()
}
@ -45,16 +47,7 @@ struct SearchBookmarksView: View {
if let bookmarks = viewModel.bookmarks?.bookmarks, !bookmarks.isEmpty {
List(bookmarks) { bookmark in
NavigationLink {
BookmarkDetailView(bookmarkId: bookmark.id)
} label: {
BookmarkCardView(bookmark: bookmark, currentState: .all, onArchive: {_ in }, onDelete: {_ in }, onToggleFavorite: {_ in })
.listRowBackground(Color(.systemBackground))
.padding(.vertical, 4)
}
/*Button(action: {
Button(action: {
if UIDevice.isPhone {
selectedBookmarkId = bookmark.id
} else {
@ -68,21 +61,44 @@ struct SearchBookmarksView: View {
}
}
}) {
BookmarkCardView(bookmark: bookmark, currentState: .all, onArchive: {_ in }, onDelete: {_ in }, onToggleFavorite: {_ in })
.listRowBackground(Color(.systemBackground))
.padding(.vertical, 4)
BookmarkCardView(bookmark: bookmark, currentState: .all, onArchive: {_ in }, onDelete: {_ in }, onToggleFavorite: {_ in }, namespace: namespace)
}
.buttonStyle(.plain)
.buttonStyle(PlainButtonStyle())
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.listRowSeparator(.hidden)
*/
.listRowBackground(Color(R.color.bookmark_list_bg))
}
.listStyle(.plain)
.background(Color(R.color.bookmark_list_bg))
.scrollContentBackground(.hidden)
.simultaneousGesture(
DragGesture()
.onChanged { _ in
searchFieldIsFocused = false
}
)
} else if !viewModel.isLoading && viewModel.bookmarks != nil {
ContentUnavailableView("Keine Ergebnisse", systemImage: "magnifyingglass", description: Text("Keine Bookmarks gefunden."))
ContentUnavailableView("No results", systemImage: "magnifyingglass", description: Text("No bookmarks found."))
.padding()
}
Spacer()
}
.navigationTitle("Suche")
.background(Color(R.color.bookmark_list_bg))
.navigationTitle("Search")
.navigationDestination(
item: Binding<String?>(
get: { selectedBookmarkId },
set: { selectedBookmarkId = $0 }
)
) { bookmarkId in
BookmarkDetailView(bookmarkId: bookmarkId, namespace: namespace)
.navigationTransition(.zoom(sourceID: bookmarkId, in: namespace))
}
.onAppear {
if isFirstAppearance {
searchFieldIsFocused = true
isFirstAppearance = false
}
}
}
}