Compare commits

..

No commits in common. "b71fc0a4e07eca5625431b726bff37e1aff5b086" and "e4f055a6af7a36b6cc8fd366890e452518fe80fe" have entirely different histories.

12 changed files with 144 additions and 278 deletions

View File

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

View File

@ -3,7 +3,7 @@ import SwiftUI
struct ShareBookmarkView: View {
@ObservedObject var viewModel: ShareBookmarkViewModel
@State private var keyboardHeight: CGFloat = 0
@FocusState private var focusedField: AddBookmarkFieldFocus?
@State private var shouldScrollToTitle = false
private func dismissKeyboard() {
NotificationCenter.default.post(name: NSNotification.Name("DismissKeyboard"), object: nil)
@ -17,20 +17,19 @@ struct ShareBookmarkView: View {
logoSection
urlSection
tagManagementSection
.id(AddBookmarkFieldFocus.labels)
titleSection
.id(AddBookmarkFieldFocus.title)
.id("titleField")
statusSection
Spacer(minLength: 100) // Space for button
}
}
.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)
.padding(.bottom, keyboardHeight / 2)
.onChange(of: shouldScrollToTitle) { shouldScroll, _ in
if shouldScroll {
withAnimation(.easeInOut(duration: 0.3)) {
proxy.scrollTo("titleField", anchor: .center)
}
shouldScrollToTitle = false
}
}
}
@ -40,21 +39,25 @@ struct ShareBookmarkView: View {
.background(Color(.systemGroupedBackground))
.onAppear { viewModel.onAppear() }
.ignoresSafeArea(.keyboard, edges: .bottom)
.contentShape(Rectangle())
.onTapGesture {
dismissKeyboard()
}
.background(
Color.clear
.contentShape(Rectangle())
.onTapGesture {
// Fallback for extensions: tap anywhere to dismiss keyboard
dismissKeyboard()
}
)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
withAnimation(.easeInOut(duration: 0.3)) {
keyboardHeight = keyboardFrame.height
keyboardHeight = keyboardFrame.height
// Scroll to title field when keyboard appears
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
shouldScrollToTitle = true
}
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
withAnimation(.easeInOut(duration: 0.3)) {
keyboardHeight = 0
}
keyboardHeight = 0
}
}
@ -100,7 +103,6 @@ struct ShareBookmarkView: View {
.padding(.horizontal, 4)
.frame(maxWidth: 420)
.frame(maxWidth: .infinity, alignment: .center)
.focused($focusedField, equals: .title)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
@ -121,7 +123,6 @@ struct ShareBookmarkView: View {
isLabelsLoading: false,
availableLabelPages: convertToBookmarkLabelPages(viewModel.availableLabelPages),
filteredLabels: convertToBookmarkLabels(viewModel.filteredLabels),
searchFieldFocus: $focusedField,
onAddCustomTag: {
addCustomTag()
},
@ -175,6 +176,7 @@ struct ShareBookmarkView: View {
.padding(.top, 16)
.padding(.bottom, 32)
.disabled(viewModel.isSaving)
.background(Color(.systemGroupedBackground))
}
// MARK: - Helper Functions

View File

@ -435,7 +435,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 14;
CURRENT_PROJECT_VERSION = 1;
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 = 14;
CURRENT_PROJECT_VERSION = 1;
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 = 14;
CURRENT_PROJECT_VERSION = 13;
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 = 14;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES;

View File

@ -342,13 +342,11 @@ 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,

View File

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

View File

@ -4,7 +4,6 @@ import Combine
struct BookmarkDetailView: View {
let bookmarkId: String
let namespace: Namespace.ID?
// MARK: - States
@ -16,7 +15,6 @@ 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
@ -26,9 +24,8 @@ struct BookmarkDetailView: View {
private let headerHeight: CGFloat = 320
init(bookmarkId: String, namespace: Namespace.ID? = nil, viewModel: BookmarkDetailViewModel = BookmarkDetailViewModel(), webViewHeight: CGFloat = 300, showingFontSettings: Bool = false, showingLabelsSheet: Bool = false, playerUIState: PlayerUIState = .init()) {
init(bookmarkId: String, 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
@ -155,9 +152,6 @@ 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
@ -192,23 +186,17 @@ 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))
.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!)
}
}
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)
}
// Gradient overlay für bessere Button-Sichtbarkeit
LinearGradient(
gradient: Gradient(colors: [
@ -225,41 +213,10 @@ 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
}
}
}

View File

@ -16,6 +16,7 @@ struct BookmarkLabelsView: View {
var body: some View {
NavigationView {
VStack(spacing: 12) {
searchSection
availableLabelsSection
Spacer()
}
@ -52,7 +53,59 @@ 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 {
@ -79,7 +132,6 @@ struct BookmarkLabelsView: View {
}
}
)
.padding(.horizontal)
}
}

View File

@ -1,118 +0,0 @@
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")
}

View File

@ -1,16 +1,6 @@
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
@ -20,7 +10,6 @@ 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) {
@ -38,9 +27,6 @@ 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 {
@ -237,3 +223,12 @@ struct IconBadge: View {
}
}
#Preview {
BookmarkCardView(bookmark: .mock, currentState: .all) { _ in
} onDelete: { _ in
} onToggleFavorite: { _ in
}
}

View File

@ -4,8 +4,6 @@ import SwiftUI
struct BookmarksView: View {
@Namespace private var namespace
// MARK: States
@State private var viewModel: BookmarksViewModel
@ -97,8 +95,7 @@ struct BookmarksView: View {
Task {
await viewModel.toggleFavorite(bookmark: bookmark)
}
},
namespace: namespace
}
)
.onAppear {
if bookmark.id == viewModel.bookmarks?.bookmarks.last?.id {
@ -112,7 +109,6 @@ 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)
@ -165,8 +161,7 @@ struct BookmarksView: View {
set: { selectedBookmarkId = $0 }
)
) { bookmarkId in
BookmarkDetailView(bookmarkId: bookmarkId, namespace: namespace)
.navigationTransition(.zoom(sourceID: bookmarkId, in: namespace))
BookmarkDetailView(bookmarkId: bookmarkId)
}
.sheet(isPresented: $showingAddBookmark) {
AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle)

View File

@ -93,18 +93,18 @@ struct TagManagementView: View {
!selectedLabelsSet.contains(searchText.wrappedValue) {
HStack {
Text("Add new tag:")
.font(.subheadline)
.font(.caption)
.foregroundColor(.secondary)
Text(searchText.wrappedValue)
.font(.subheadline)
.font(.caption)
.fontWeight(.medium)
Spacer()
Button(action: onAddCustomTag) {
HStack(spacing: 6) {
Image(systemName: "plus.circle.fill")
.font(.subheadline)
.font(.caption)
Text("Add")
.font(.subheadline)
.font(.caption)
.fontWeight(.medium)
}
}
@ -173,13 +173,13 @@ struct TagManagementView: View {
)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.frame(maxWidth: .infinity, alignment: .top)
.padding(.horizontal)
}
}
.tabViewStyle(.page(indexDisplayMode: availableLabelPages.count > 1 ? .automatic : .never))
.frame(height: 180)
.padding(.top, 10)
.padding(.top, -20)
}
@ViewBuilder

View File

@ -5,15 +5,13 @@ 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("Search...", text: $viewModel.searchQuery)
TextField("Suchbegriff eingeben...", text: $viewModel.searchQuery)
.focused($searchFieldIsFocused)
.textFieldStyle(PlainTextFieldStyle())
.autocapitalization(.none)
@ -35,7 +33,7 @@ struct SearchBookmarksView: View {
.padding([.horizontal, .top])
if viewModel.isLoading {
ProgressView("Searching...")
ProgressView("Suche...")
.padding()
}
@ -47,7 +45,16 @@ struct SearchBookmarksView: View {
if let bookmarks = viewModel.bookmarks?.bookmarks, !bookmarks.isEmpty {
List(bookmarks) { bookmark in
Button(action: {
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: {
if UIDevice.isPhone {
selectedBookmarkId = bookmark.id
} else {
@ -61,44 +68,21 @@ struct SearchBookmarksView: View {
}
}
}) {
BookmarkCardView(bookmark: bookmark, currentState: .all, onArchive: {_ in }, onDelete: {_ in }, onToggleFavorite: {_ in }, namespace: namespace)
BookmarkCardView(bookmark: bookmark, currentState: .all, onArchive: {_ in }, onDelete: {_ in }, onToggleFavorite: {_ in })
.listRowBackground(Color(.systemBackground))
.padding(.vertical, 4)
}
.buttonStyle(PlainButtonStyle())
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.buttonStyle(.plain)
.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("No results", systemImage: "magnifyingglass", description: Text("No bookmarks found."))
ContentUnavailableView("Keine Ergebnisse", systemImage: "magnifyingglass", description: Text("Keine Bookmarks gefunden."))
.padding()
}
Spacer()
}
.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
}
}
.navigationTitle("Suche")
}
}