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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import Combine
struct BookmarkDetailView: View { struct BookmarkDetailView: View {
let bookmarkId: String let bookmarkId: String
let namespace: Namespace.ID?
// MARK: - States // MARK: - States
@ -25,8 +26,9 @@ struct BookmarkDetailView: View {
private let headerHeight: CGFloat = 320 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.bookmarkId = bookmarkId
self.namespace = namespace
self.viewModel = viewModel self.viewModel = viewModel
self.webViewHeight = webViewHeight self.webViewHeight = webViewHeight
self.showingFontSettings = showingFontSettings self.showingFontSettings = showingFontSettings
@ -190,17 +192,23 @@ struct BookmarkDetailView: View {
let offset = geo.frame(in: .global).minY let offset = geo.frame(in: .global).minY
ZStack(alignment: .top) { ZStack(alignment: .top) {
AsyncImage(url: URL(string: viewModel.bookmarkDetail.imageUrl)) { image in AsyncImage(url: URL(string: viewModel.bookmarkDetail.imageUrl)) { image in
image image
.resizable() .resizable()
.scaledToFill() .scaledToFill()
.frame(width: geometry.size.width, height: headerHeight + (offset > 0 ? offset : 0)) .frame(width: geometry.size.width, height: headerHeight + (offset > 0 ? offset : 0))
.clipped() .clipped()
.offset(y: (offset > 0 ? -offset : 0)) .offset(y: (offset > 0 ? -offset : 0))
} placeholder: { .if(namespace != nil) { view in
Rectangle() view.matchedGeometryEffect(id: "image-\(viewModel.bookmarkDetail.id)", in: namespace!)
.fill(Color.gray.opacity(0.4)) }
.frame(width: geometry.size.width, height: headerHeight) } 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 // Gradient overlay für bessere Button-Sichtbarkeit
LinearGradient( LinearGradient(
gradient: Gradient(colors: [ gradient: Gradient(colors: [

View File

@ -1,6 +1,16 @@
import SwiftUI import SwiftUI
import SafariServices 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 { struct BookmarkCardView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@ -10,6 +20,7 @@ struct BookmarkCardView: View {
let onArchive: (Bookmark) -> Void let onArchive: (Bookmark) -> Void
let onDelete: (Bookmark) -> Void let onDelete: (Bookmark) -> Void
let onToggleFavorite: (Bookmark) -> Void let onToggleFavorite: (Bookmark) -> Void
let namespace: Namespace.ID?
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
@ -27,6 +38,9 @@ struct BookmarkCardView: View {
.frame(height: 120) .frame(height: 120)
} }
.clipShape(RoundedRectangle(cornerRadius: 8)) .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 { if bookmark.readProgress > 0 && bookmark.isArchived == false && bookmark.isMarked == false {
ZStack { 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 { struct BookmarksView: View {
@Namespace private var namespace
// MARK: States // MARK: States
@State private var viewModel: BookmarksViewModel @State private var viewModel: BookmarksViewModel
@ -95,7 +97,8 @@ struct BookmarksView: View {
Task { Task {
await viewModel.toggleFavorite(bookmark: bookmark) await viewModel.toggleFavorite(bookmark: bookmark)
} }
} },
namespace: namespace
) )
.onAppear { .onAppear {
if bookmark.id == viewModel.bookmarks?.bookmarks.last?.id { 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)) .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.listRowBackground(Color(R.color.bookmark_list_bg)) .listRowBackground(Color(R.color.bookmark_list_bg))
.matchedTransitionSource(id: bookmark.id, in: namespace)
} }
} }
.listStyle(.plain) .listStyle(.plain)
@ -161,7 +165,8 @@ struct BookmarksView: View {
set: { selectedBookmarkId = $0 } set: { selectedBookmarkId = $0 }
) )
) { bookmarkId in ) { bookmarkId in
BookmarkDetailView(bookmarkId: bookmarkId) BookmarkDetailView(bookmarkId: bookmarkId, namespace: namespace)
.navigationTransition(.zoom(sourceID: bookmarkId, in: namespace))
} }
.sheet(isPresented: $showingAddBookmark) { .sheet(isPresented: $showingAddBookmark) {
AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle) AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle)

View File

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