diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 085232a..04f2b62 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -67,6 +67,9 @@ }, "Archive bookmark" : { + }, + "Archived" : { + }, "Are you sure you want to log out? This will delete all your login credentials and return you to setup." : { @@ -145,6 +148,9 @@ }, "General" : { + }, + "Go Back" : { + }, "https://example.com" : { diff --git a/readeck.xcodeproj/project.pbxproj b/readeck.xcodeproj/project.pbxproj index 765a2ee..def67d1 100644 --- a/readeck.xcodeproj/project.pbxproj +++ b/readeck.xcodeproj/project.pbxproj @@ -609,7 +609,7 @@ CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\""; DEVELOPMENT_TEAM = 8J69P655GN; ENABLE_HARDENED_RUNTIME = YES; @@ -653,7 +653,7 @@ CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\""; DEVELOPMENT_TEAM = 8J69P655GN; ENABLE_HARDENED_RUNTIME = YES; diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift index 6b76313..600626f 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift @@ -1,5 +1,15 @@ import SwiftUI import SafariServices +import Combine + +// PreferenceKey for logging scroll offset +struct ScrollOffsetPreferenceKey: PreferenceKey { + typealias Value = CGFloat + static var defaultValue = CGFloat.zero + static func reduce(value: inout Value, nextValue: () -> Value) { + value += nextValue() + } +} struct BookmarkDetailView: View { let bookmarkId: String @@ -7,8 +17,13 @@ struct BookmarkDetailView: View { @State private var webViewHeight: CGFloat = 300 @State private var showingFontSettings = false @State private var showingLabelsSheet = false + @State private var showDismissButton = false + @State private var readingProgress: Double = 0.0 + // contentHeight entfernt, webViewHeight wird verwendet + @State private var scrollViewHeight: CGFloat = 1 @EnvironmentObject var playerUIState: PlayerUIState @EnvironmentObject var appSettings: AppSettings + @Environment(\.dismiss) private var dismiss private let headerHeight: CGFloat = 320 @@ -21,25 +36,80 @@ struct BookmarkDetailView: View { } var body: some View { - GeometryReader { geometry in - ScrollView { - ZStack(alignment: .top) { - headerView(geometry: geometry) - VStack(alignment: .center, spacing: 16) { - Color.clear.frame(height: viewModel.bookmarkDetail.imageUrl.isEmpty ? 84 : headerHeight) - titleSection - Divider().padding(.horizontal) - contentSection - Spacer(minLength: 40) - if viewModel.isLoadingArticle == false { - archiveSection - .transition(.opacity.combined(with: .move(edge: .bottom))) - .animation(.easeInOut, value: viewModel.articleContent) + VStack(spacing: 0) { + ProgressView(value: readingProgress) + .progressViewStyle(LinearProgressViewStyle()) + .frame(height: 3) + GeometryReader { outerGeo in + ScrollView { + VStack(spacing: 0) { + // Track scroll offset at the top + GeometryReader { geo in + Color.clear + .preference(key: ScrollOffsetPreferenceKey.self, value: geo.frame(in: .named("scroll")).minY) } + .frame(height: 0) + ZStack(alignment: .top) { + headerView(geometry: outerGeo) + VStack(alignment: .center, spacing: 16) { + Color.clear.frame(height: viewModel.bookmarkDetail.imageUrl.isEmpty ? 84 : headerHeight) + titleSection + Divider().padding(.horizontal) + if let settings = viewModel.settings, !viewModel.articleContent.isEmpty { + WebView(htmlContent: viewModel.articleContent, settings: settings, onHeightChange: { height in + webViewHeight = height + }) + .frame(height: webViewHeight) + .cornerRadius(14) + .padding(.horizontal) + .animation(.easeInOut, value: webViewHeight) + } else if viewModel.isLoadingArticle { + ProgressView("Loading article...") + .frame(maxWidth: .infinity, alignment: .center) + .padding() + } else { + Button(action: { + SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url) + }) { + HStack { + Image(systemName: "safari") + Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Open original page") + " open") + } + .font(.title3.bold()) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .padding(.horizontal) + .padding(.top, 0) + } + Spacer(minLength: 40) + if viewModel.isLoadingArticle == false { + archiveSection + .transition(.opacity.combined(with: .move(edge: .bottom))) + .animation(.easeInOut, value: viewModel.articleContent) + } + } + } + // Kein GeometryReader am Ende nötig } } + .coordinateSpace(name: "scroll") + .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in + scrollViewHeight = outerGeo.size.height + print("offset:", offset) + print("webViewHeight:", webViewHeight) + print("scrollViewHeight:", scrollViewHeight) + let maxOffset = webViewHeight - scrollViewHeight + print("maxOffset:", maxOffset) + // Am Anfang: offset = 0, am Ende: offset = -maxOffset + let rawProgress = -offset / (maxOffset != 0 ? maxOffset : 1) + print("rawProgress:", rawProgress) + let progress = min(max(rawProgress, 0), 1) + print("progress:", progress) + readingProgress = progress + } + .ignoresSafeArea(edges: .top) } - .ignoresSafeArea(edges: .top) } .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -320,22 +390,36 @@ struct BookmarkDetailView: View { .buttonStyle(.bordered) .disabled(viewModel.isLoading) - // Archivieren-Button + // Archive button Button(action: { Task { await viewModel.archiveBookmark(id: bookmarkId) + if viewModel.bookmarkDetail.isArchived { + showDismissButton = true + } } }) { HStack { - Image(systemName: "archivebox") - Text("Archive bookmark") + Image(systemName: viewModel.bookmarkDetail.isArchived ? "checkmark.circle.fill" : "archivebox") + .foregroundColor(viewModel.bookmarkDetail.isArchived ? .green : .primary) + Text(viewModel.bookmarkDetail.isArchived ? "Archived" : "Archive bookmark") } .font(.title3.bold()) .frame(maxHeight: 60) .padding(10) } .buttonStyle(.borderedProminent) - .disabled(viewModel.isLoading) + .disabled(viewModel.isLoading || viewModel.bookmarkDetail.isArchived) + } + if showDismissButton { + Button(action: { + dismiss() + }) { + Label("Go Back", systemImage: "arrow.backward.circle") + .font(.title3.bold()) + .padding(.top, 8) + } + .id("goBackButton") } if let error = viewModel.errorMessage { Text(error) diff --git a/readeck/UI/Components/WebView.swift b/readeck/UI/Components/WebView.swift index 78726cd..0923247 100644 --- a/readeck/UI/Components/WebView.swift +++ b/readeck/UI/Components/WebView.swift @@ -5,18 +5,21 @@ struct WebView: UIViewRepresentable { let htmlContent: String let settings: Settings let onHeightChange: (CGFloat) -> Void + var onScroll: ((Double) -> Void)? = nil @Environment(\.colorScheme) private var colorScheme func makeUIView(context: Context) -> WKWebView { let webView = WKWebView() webView.navigationDelegate = context.coordinator - webView.scrollView.isScrollEnabled = false + webView.scrollView.isScrollEnabled = true webView.isOpaque = false webView.backgroundColor = UIColor.clear // Message Handler hier einmalig hinzufügen webView.configuration.userContentController.add(context.coordinator, name: "heightUpdate") + webView.configuration.userContentController.add(context.coordinator, name: "scrollProgress") context.coordinator.onHeightChange = onHeightChange + context.coordinator.onScroll = onScroll return webView } @@ -24,6 +27,7 @@ struct WebView: UIViewRepresentable { func updateUIView(_ webView: WKWebView, context: Context) { // Nur den HTML-Inhalt laden, keine Handler-Konfiguration context.coordinator.onHeightChange = onHeightChange + context.coordinator.onScroll = onScroll let isDarkMode = colorScheme == .dark @@ -210,6 +214,8 @@ struct WebView: UIViewRepresentable {
\(htmlContent)