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:
parent
3981f086f9
commit
b71fc0a4e0
@ -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" : {
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: [
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user