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:
parent
bdd7d234a9
commit
15ce5a223b
@ -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" : {
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user