Compare commits
6 Commits
e4f055a6af
...
b71fc0a4e0
| Author | SHA1 | Date | |
|---|---|---|---|
| b71fc0a4e0 | |||
| 3981f086f9 | |||
| 1ffafc3b35 | |||
| bbcb7bd81f | |||
| f3f94f1cfe | |||
| 4915a773d6 |
@ -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" : {
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
118
readeck/UI/BookmarkDetail/ImageViewerView.swift
Normal file
118
readeck/UI/BookmarkDetail/ImageViewerView.swift
Normal 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")
|
||||
}
|
||||
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user