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%%)" : {
|
"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" : {
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import SwiftUI
|
|||||||
struct ShareBookmarkView: View {
|
struct ShareBookmarkView: View {
|
||||||
@ObservedObject var viewModel: ShareBookmarkViewModel
|
@ObservedObject var viewModel: ShareBookmarkViewModel
|
||||||
@State private var keyboardHeight: CGFloat = 0
|
@State private var keyboardHeight: CGFloat = 0
|
||||||
@State private var shouldScrollToTitle = false
|
@FocusState private var focusedField: AddBookmarkFieldFocus?
|
||||||
|
|
||||||
private func dismissKeyboard() {
|
private func dismissKeyboard() {
|
||||||
NotificationCenter.default.post(name: NSNotification.Name("DismissKeyboard"), object: nil)
|
NotificationCenter.default.post(name: NSNotification.Name("DismissKeyboard"), object: nil)
|
||||||
@ -17,19 +17,20 @@ struct ShareBookmarkView: View {
|
|||||||
logoSection
|
logoSection
|
||||||
urlSection
|
urlSection
|
||||||
tagManagementSection
|
tagManagementSection
|
||||||
|
.id(AddBookmarkFieldFocus.labels)
|
||||||
titleSection
|
titleSection
|
||||||
.id("titleField")
|
.id(AddBookmarkFieldFocus.title)
|
||||||
statusSection
|
statusSection
|
||||||
Spacer(minLength: 100) // Space for button
|
Spacer(minLength: 100) // Space for button
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.bottom, keyboardHeight / 2)
|
.padding(.bottom, max(0, keyboardHeight - 120))
|
||||||
.onChange(of: shouldScrollToTitle) { shouldScroll, _ in
|
.onChange(of: focusedField) { newField, _ in
|
||||||
if shouldScroll {
|
guard let field = newField else { return }
|
||||||
withAnimation(.easeInOut(duration: 0.3)) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
proxy.scrollTo("titleField", anchor: .center)
|
withAnimation(.easeInOut(duration: 0.25)) {
|
||||||
|
proxy.scrollTo(field, anchor: .center)
|
||||||
}
|
}
|
||||||
shouldScrollToTitle = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,25 +40,21 @@ struct ShareBookmarkView: View {
|
|||||||
.background(Color(.systemGroupedBackground))
|
.background(Color(.systemGroupedBackground))
|
||||||
.onAppear { viewModel.onAppear() }
|
.onAppear { viewModel.onAppear() }
|
||||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||||
.background(
|
.contentShape(Rectangle())
|
||||||
Color.clear
|
.onTapGesture {
|
||||||
.contentShape(Rectangle())
|
dismissKeyboard()
|
||||||
.onTapGesture {
|
}
|
||||||
// Fallback for extensions: tap anywhere to dismiss keyboard
|
|
||||||
dismissKeyboard()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
|
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
|
||||||
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
||||||
keyboardHeight = keyboardFrame.height
|
withAnimation(.easeInOut(duration: 0.3)) {
|
||||||
// Scroll to title field when keyboard appears
|
keyboardHeight = keyboardFrame.height
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
||||||
shouldScrollToTitle = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
|
.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)
|
.padding(.horizontal, 4)
|
||||||
.frame(maxWidth: 420)
|
.frame(maxWidth: 420)
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.focused($focusedField, equals: .title)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .keyboard) {
|
ToolbarItemGroup(placement: .keyboard) {
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -123,6 +121,7 @@ struct ShareBookmarkView: View {
|
|||||||
isLabelsLoading: false,
|
isLabelsLoading: false,
|
||||||
availableLabelPages: convertToBookmarkLabelPages(viewModel.availableLabelPages),
|
availableLabelPages: convertToBookmarkLabelPages(viewModel.availableLabelPages),
|
||||||
filteredLabels: convertToBookmarkLabels(viewModel.filteredLabels),
|
filteredLabels: convertToBookmarkLabels(viewModel.filteredLabels),
|
||||||
|
searchFieldFocus: $focusedField,
|
||||||
onAddCustomTag: {
|
onAddCustomTag: {
|
||||||
addCustomTag()
|
addCustomTag()
|
||||||
},
|
},
|
||||||
@ -176,7 +175,6 @@ struct ShareBookmarkView: View {
|
|||||||
.padding(.top, 16)
|
.padding(.top, 16)
|
||||||
.padding(.bottom, 32)
|
.padding(.bottom, 32)
|
||||||
.disabled(viewModel.isSaving)
|
.disabled(viewModel.isSaving)
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helper Functions
|
// MARK: - Helper Functions
|
||||||
|
|||||||
@ -435,7 +435,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
|
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_TEAM = 8J69P655GN;
|
DEVELOPMENT_TEAM = 8J69P655GN;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = URLShare/Info.plist;
|
INFOPLIST_FILE = URLShare/Info.plist;
|
||||||
@ -468,7 +468,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
|
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_TEAM = 8J69P655GN;
|
DEVELOPMENT_TEAM = 8J69P655GN;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = URLShare/Info.plist;
|
INFOPLIST_FILE = URLShare/Info.plist;
|
||||||
@ -623,7 +623,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
|
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 13;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 8J69P655GN;
|
DEVELOPMENT_TEAM = 8J69P655GN;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@ -667,7 +667,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
|
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 13;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 8J69P655GN;
|
DEVELOPMENT_TEAM = 8J69P655GN;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ struct BookmarkDetailView: View {
|
|||||||
@State private var scrollViewHeight: CGFloat = 1
|
@State private var scrollViewHeight: CGFloat = 1
|
||||||
@State private var showJumpToProgressButton: Bool = false
|
@State private var showJumpToProgressButton: Bool = false
|
||||||
@State private var scrollPosition = ScrollPosition(edge: .top)
|
@State private var scrollPosition = ScrollPosition(edge: .top)
|
||||||
|
@State private var showingImageViewer = false
|
||||||
|
|
||||||
// MARK: - Envs
|
// MARK: - Envs
|
||||||
|
|
||||||
@ -24,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
|
||||||
@ -152,6 +155,9 @@ struct BookmarkDetailView: View {
|
|||||||
.sheet(isPresented: $showingLabelsSheet) {
|
.sheet(isPresented: $showingLabelsSheet) {
|
||||||
BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels)
|
BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showingImageViewer) {
|
||||||
|
ImageViewerView(imageUrl: viewModel.bookmarkDetail.imageUrl)
|
||||||
|
}
|
||||||
.onChange(of: showingFontSettings) { _, isShowing in
|
.onChange(of: showingFontSettings) { _, isShowing in
|
||||||
if !isShowing {
|
if !isShowing {
|
||||||
// Reload settings when sheet is dismissed
|
// Reload settings when sheet is dismissed
|
||||||
@ -186,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: [
|
||||||
@ -213,10 +225,41 @@ struct BookmarkDetailView: View {
|
|||||||
.frame(height: 240)
|
.frame(height: 240)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.offset(y: (offset > 0 ? -offset : 0))
|
.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)
|
.frame(height: headerHeight)
|
||||||
.ignoresSafeArea(edges: .top)
|
.ignoresSafeArea(edges: .top)
|
||||||
|
.onTapGesture {
|
||||||
|
showingImageViewer = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,6 @@ struct BookmarkLabelsView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
searchSection
|
|
||||||
availableLabelsSection
|
availableLabelsSection
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
@ -53,59 +52,7 @@ struct BookmarkLabelsView: View {
|
|||||||
|
|
||||||
// MARK: - View Components
|
// 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
|
@ViewBuilder
|
||||||
private var availableLabelsSection: some View {
|
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 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)
|
||||||
|
|||||||
@ -93,18 +93,18 @@ struct TagManagementView: View {
|
|||||||
!selectedLabelsSet.contains(searchText.wrappedValue) {
|
!selectedLabelsSet.contains(searchText.wrappedValue) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Add new tag:")
|
Text("Add new tag:")
|
||||||
.font(.caption)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
Text(searchText.wrappedValue)
|
Text(searchText.wrappedValue)
|
||||||
.font(.caption)
|
.font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: onAddCustomTag) {
|
Button(action: onAddCustomTag) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Image(systemName: "plus.circle.fill")
|
Image(systemName: "plus.circle.fill")
|
||||||
.font(.caption)
|
.font(.subheadline)
|
||||||
Text("Add")
|
Text("Add")
|
||||||
.font(.caption)
|
.font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,13 +173,13 @@ struct TagManagementView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tabViewStyle(.page(indexDisplayMode: availableLabelPages.count > 1 ? .automatic : .never))
|
.tabViewStyle(.page(indexDisplayMode: availableLabelPages.count > 1 ? .automatic : .never))
|
||||||
.frame(height: 180)
|
.frame(height: 180)
|
||||||
.padding(.top, -20)
|
.padding(.top, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
@ -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