diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 4edf2a2..4ffeeb1 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -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" : { diff --git a/readeck/Data/API/API.swift b/readeck/Data/API/API.swift index ccb37ba..06de15c 100644 --- a/readeck/Data/API/API.swift +++ b/readeck/Data/API/API.swift @@ -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, diff --git a/readeck/Data/Repository/BookmarksRepository.swift b/readeck/Data/Repository/BookmarksRepository.swift index 7705118..56e7208 100644 --- a/readeck/Data/Repository/BookmarksRepository.swift +++ b/readeck/Data/Repository/BookmarksRepository.swift @@ -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() } } diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift index 4de602e..5ba8cf2 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift @@ -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 @@ -190,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: [ diff --git a/readeck/UI/Bookmarks/BookmarkCardView.swift b/readeck/UI/Bookmarks/BookmarkCardView.swift index e098191..b74d85e 100644 --- a/readeck/UI/Bookmarks/BookmarkCardView.swift +++ b/readeck/UI/Bookmarks/BookmarkCardView.swift @@ -1,6 +1,16 @@ import SwiftUI import SafariServices +extension View { + @ViewBuilder func `if`(_ 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 - - } -} diff --git a/readeck/UI/Bookmarks/BookmarksView.swift b/readeck/UI/Bookmarks/BookmarksView.swift index d13a01d..d58e11a 100644 --- a/readeck/UI/Bookmarks/BookmarksView.swift +++ b/readeck/UI/Bookmarks/BookmarksView.swift @@ -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) diff --git a/readeck/UI/Search/SearchBookmarksView.swift b/readeck/UI/Search/SearchBookmarksView.swift index 3cc2eba..d35996f 100644 --- a/readeck/UI/Search/SearchBookmarksView.swift +++ b/readeck/UI/Search/SearchBookmarksView.swift @@ -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( + 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 + } + } } }