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
This commit is contained in:
Ilyas Hallak 2025-08-11 21:10:03 +02:00
parent 3981f086f9
commit b71fc0a4e0
7 changed files with 94 additions and 59 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

@ -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
@ -25,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
@ -196,10 +198,16 @@ struct BookmarkDetailView: View {
.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(

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

@ -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
}
}
}
}