Add reading progress bar for article view, optimize archive button UX, and improve WebView scroll tracking

- Add a reading progress bar at the top of the article detail view, based on WebView height and ScrollView height
- Remove unused contentHeight logic, use webViewHeight as the single source of truth
- Optimize archive button: show checkmark and 'Archived' after archiving, disable button, and show 'Go Back' button for dismiss
- Enable scrolling in WebView and add JavaScript for scroll progress reporting and debug logs
- Add new localization keys for 'Archived' and 'Go Back'
- Bump project version
This commit is contained in:
Ilyas Hallak 2025-07-22 23:27:52 +02:00
parent bdd7d234a9
commit 15ce5a223b
4 changed files with 134 additions and 22 deletions

View File

@ -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" : {

View File

@ -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;

View File

@ -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,15 +36,52 @@ struct BookmarkDetailView: View {
}
var body: some View {
GeometryReader { geometry in
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: geometry)
headerView(geometry: outerGeo)
VStack(alignment: .center, spacing: 16) {
Color.clear.frame(height: viewModel.bookmarkDetail.imageUrl.isEmpty ? 84 : headerHeight)
titleSection
Divider().padding(.horizontal)
contentSection
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
@ -38,9 +90,27 @@ struct BookmarkDetailView: View {
}
}
}
// 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)
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
@ -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)

View File

@ -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 {
<body>
\(htmlContent)
<script>
console.log('Script loaded!');
alert('Script loaded!');
function updateHeight() {
const height = document.body.scrollHeight;
window.webkit.messageHandlers.heightUpdate.postMessage(height);
@ -224,6 +230,14 @@ struct WebView: UIViewRepresentable {
document.querySelectorAll('img').forEach(img => {
img.addEventListener('load', updateHeight);
});
// Scroll progress reporting
window.addEventListener('scroll', function() {
var scrollTop = window.scrollY || document.documentElement.scrollTop;
var docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
var progress = docHeight > 0 ? scrollTop / docHeight : 0;
window.webkit.messageHandlers.scrollProgress.postMessage(progress);
console.log('Scroll event fired, progress:', progress);
});
</script>
</body>
</html>
@ -260,6 +274,7 @@ struct WebView: UIViewRepresentable {
class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
var onHeightChange: ((CGFloat) -> Void)?
var onScroll: ((Double) -> Void)?
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated {
@ -274,10 +289,17 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "heightUpdate", let height = message.body as? CGFloat {
print("[WebView] heightUpdate received: \(height)")
DispatchQueue.main.async {
self.onHeightChange?(height)
}
}
if message.name == "scrollProgress", let progress = message.body as? Double {
print("[WebView] scrollProgress received: \(progress)")
DispatchQueue.main.async {
self.onScroll?(progress)
}
}
}
deinit {