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" : {
|
"Archive bookmark" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Archived" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Are you sure you want to log out? This will delete all your login credentials and return you to setup." : {
|
"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" : {
|
"General" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Go Back" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"https://example.com" : {
|
"https://example.com" : {
|
||||||
|
|
||||||
|
|||||||
@ -609,7 +609,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 = 8;
|
CURRENT_PROJECT_VERSION = 9;
|
||||||
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;
|
||||||
@ -653,7 +653,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 = 8;
|
CURRENT_PROJECT_VERSION = 9;
|
||||||
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;
|
||||||
|
|||||||
@ -1,5 +1,15 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SafariServices
|
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 {
|
struct BookmarkDetailView: View {
|
||||||
let bookmarkId: String
|
let bookmarkId: String
|
||||||
@ -7,8 +17,13 @@ struct BookmarkDetailView: View {
|
|||||||
@State private var webViewHeight: CGFloat = 300
|
@State private var webViewHeight: CGFloat = 300
|
||||||
@State private var showingFontSettings = false
|
@State private var showingFontSettings = false
|
||||||
@State private var showingLabelsSheet = 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 playerUIState: PlayerUIState
|
||||||
@EnvironmentObject var appSettings: AppSettings
|
@EnvironmentObject var appSettings: AppSettings
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
private let headerHeight: CGFloat = 320
|
private let headerHeight: CGFloat = 320
|
||||||
|
|
||||||
@ -21,15 +36,52 @@ struct BookmarkDetailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
VStack(spacing: 0) {
|
||||||
|
ProgressView(value: readingProgress)
|
||||||
|
.progressViewStyle(LinearProgressViewStyle())
|
||||||
|
.frame(height: 3)
|
||||||
|
GeometryReader { outerGeo in
|
||||||
ScrollView {
|
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) {
|
ZStack(alignment: .top) {
|
||||||
headerView(geometry: geometry)
|
headerView(geometry: outerGeo)
|
||||||
VStack(alignment: .center, spacing: 16) {
|
VStack(alignment: .center, spacing: 16) {
|
||||||
Color.clear.frame(height: viewModel.bookmarkDetail.imageUrl.isEmpty ? 84 : headerHeight)
|
Color.clear.frame(height: viewModel.bookmarkDetail.imageUrl.isEmpty ? 84 : headerHeight)
|
||||||
titleSection
|
titleSection
|
||||||
Divider().padding(.horizontal)
|
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)
|
Spacer(minLength: 40)
|
||||||
if viewModel.isLoadingArticle == false {
|
if viewModel.isLoadingArticle == false {
|
||||||
archiveSection
|
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)
|
.ignoresSafeArea(edges: .top)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
@ -320,22 +390,36 @@ struct BookmarkDetailView: View {
|
|||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.disabled(viewModel.isLoading)
|
.disabled(viewModel.isLoading)
|
||||||
|
|
||||||
// Archivieren-Button
|
// Archive button
|
||||||
Button(action: {
|
Button(action: {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.archiveBookmark(id: bookmarkId)
|
await viewModel.archiveBookmark(id: bookmarkId)
|
||||||
|
if viewModel.bookmarkDetail.isArchived {
|
||||||
|
showDismissButton = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "archivebox")
|
Image(systemName: viewModel.bookmarkDetail.isArchived ? "checkmark.circle.fill" : "archivebox")
|
||||||
Text("Archive bookmark")
|
.foregroundColor(viewModel.bookmarkDetail.isArchived ? .green : .primary)
|
||||||
|
Text(viewModel.bookmarkDetail.isArchived ? "Archived" : "Archive bookmark")
|
||||||
}
|
}
|
||||||
.font(.title3.bold())
|
.font(.title3.bold())
|
||||||
.frame(maxHeight: 60)
|
.frame(maxHeight: 60)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.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 {
|
if let error = viewModel.errorMessage {
|
||||||
Text(error)
|
Text(error)
|
||||||
|
|||||||
@ -5,18 +5,21 @@ struct WebView: UIViewRepresentable {
|
|||||||
let htmlContent: String
|
let htmlContent: String
|
||||||
let settings: Settings
|
let settings: Settings
|
||||||
let onHeightChange: (CGFloat) -> Void
|
let onHeightChange: (CGFloat) -> Void
|
||||||
|
var onScroll: ((Double) -> Void)? = nil
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
func makeUIView(context: Context) -> WKWebView {
|
func makeUIView(context: Context) -> WKWebView {
|
||||||
let webView = WKWebView()
|
let webView = WKWebView()
|
||||||
webView.navigationDelegate = context.coordinator
|
webView.navigationDelegate = context.coordinator
|
||||||
webView.scrollView.isScrollEnabled = false
|
webView.scrollView.isScrollEnabled = true
|
||||||
webView.isOpaque = false
|
webView.isOpaque = false
|
||||||
webView.backgroundColor = UIColor.clear
|
webView.backgroundColor = UIColor.clear
|
||||||
|
|
||||||
// Message Handler hier einmalig hinzufügen
|
// Message Handler hier einmalig hinzufügen
|
||||||
webView.configuration.userContentController.add(context.coordinator, name: "heightUpdate")
|
webView.configuration.userContentController.add(context.coordinator, name: "heightUpdate")
|
||||||
|
webView.configuration.userContentController.add(context.coordinator, name: "scrollProgress")
|
||||||
context.coordinator.onHeightChange = onHeightChange
|
context.coordinator.onHeightChange = onHeightChange
|
||||||
|
context.coordinator.onScroll = onScroll
|
||||||
|
|
||||||
return webView
|
return webView
|
||||||
}
|
}
|
||||||
@ -24,6 +27,7 @@ struct WebView: UIViewRepresentable {
|
|||||||
func updateUIView(_ webView: WKWebView, context: Context) {
|
func updateUIView(_ webView: WKWebView, context: Context) {
|
||||||
// Nur den HTML-Inhalt laden, keine Handler-Konfiguration
|
// Nur den HTML-Inhalt laden, keine Handler-Konfiguration
|
||||||
context.coordinator.onHeightChange = onHeightChange
|
context.coordinator.onHeightChange = onHeightChange
|
||||||
|
context.coordinator.onScroll = onScroll
|
||||||
|
|
||||||
let isDarkMode = colorScheme == .dark
|
let isDarkMode = colorScheme == .dark
|
||||||
|
|
||||||
@ -210,6 +214,8 @@ struct WebView: UIViewRepresentable {
|
|||||||
<body>
|
<body>
|
||||||
\(htmlContent)
|
\(htmlContent)
|
||||||
<script>
|
<script>
|
||||||
|
console.log('Script loaded!');
|
||||||
|
alert('Script loaded!');
|
||||||
function updateHeight() {
|
function updateHeight() {
|
||||||
const height = document.body.scrollHeight;
|
const height = document.body.scrollHeight;
|
||||||
window.webkit.messageHandlers.heightUpdate.postMessage(height);
|
window.webkit.messageHandlers.heightUpdate.postMessage(height);
|
||||||
@ -224,6 +230,14 @@ struct WebView: UIViewRepresentable {
|
|||||||
document.querySelectorAll('img').forEach(img => {
|
document.querySelectorAll('img').forEach(img => {
|
||||||
img.addEventListener('load', updateHeight);
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -260,6 +274,7 @@ struct WebView: UIViewRepresentable {
|
|||||||
|
|
||||||
class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
|
class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
|
||||||
var onHeightChange: ((CGFloat) -> Void)?
|
var onHeightChange: ((CGFloat) -> Void)?
|
||||||
|
var onScroll: ((Double) -> Void)?
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||||
if navigationAction.navigationType == .linkActivated {
|
if navigationAction.navigationType == .linkActivated {
|
||||||
@ -274,10 +289,17 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
|
|||||||
|
|
||||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||||
if message.name == "heightUpdate", let height = message.body as? CGFloat {
|
if message.name == "heightUpdate", let height = message.body as? CGFloat {
|
||||||
|
print("[WebView] heightUpdate received: \(height)")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.onHeightChange?(height)
|
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 {
|
deinit {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user