UI/UX: Bookmark Detail and CardView improvements
- Progress indicator as a compact circle at the bottom right of the CardView, with percent display - Jump-to-progress button in detail view, using ScrollPosition logic (removed iOS 17 mention) - Archive/Unarchive button with flexible parameter and label - Various bugfixes and refactoring (progress, mock, WebView, strings) - Improved reading progress logic and display - Code cleanup: removed debug prints, mutated properties directly
This commit is contained in:
parent
15ce5a223b
commit
8e8e67bfe1
@ -3,6 +3,9 @@
|
|||||||
"strings" : {
|
"strings" : {
|
||||||
"" : {
|
"" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"%" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"%@ (%lld)" : {
|
"%@ (%lld)" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@ -67,9 +70,6 @@
|
|||||||
},
|
},
|
||||||
"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." : {
|
||||||
|
|
||||||
@ -148,15 +148,15 @@
|
|||||||
},
|
},
|
||||||
"General" : {
|
"General" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Go Back" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"https://example.com" : {
|
"https://example.com" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"https://readeck.example.com" : {
|
"https://readeck.example.com" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Jump to last read position (%lld%%)" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Keine Bookmarks gefunden." : {
|
"Keine Bookmarks gefunden." : {
|
||||||
|
|
||||||
@ -320,6 +320,9 @@
|
|||||||
},
|
},
|
||||||
"Title" : {
|
"Title" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Unarchive Bookmark" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"URL" : {
|
"URL" : {
|
||||||
|
|
||||||
|
|||||||
38
readeck/Assets.xcassets/splashBg.colorset/Contents.json
Normal file
38
readeck/Assets.xcassets/splashBg.colorset/Contents.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x5A",
|
||||||
|
"green" : "0x4A",
|
||||||
|
"red" : "0x1F"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x5A",
|
||||||
|
"green" : "0x4A",
|
||||||
|
"red" : "0x1F"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -31,7 +31,8 @@ class BookmarksRepository: PBookmarksRepository {
|
|||||||
labels: bookmarkDetailDto.labels,
|
labels: bookmarkDetailDto.labels,
|
||||||
thumbnailUrl: bookmarkDetailDto.resources.thumbnail?.src ?? "",
|
thumbnailUrl: bookmarkDetailDto.resources.thumbnail?.src ?? "",
|
||||||
imageUrl: bookmarkDetailDto.resources.image?.src ?? "",
|
imageUrl: bookmarkDetailDto.resources.image?.src ?? "",
|
||||||
lang: bookmarkDetailDto.lang ?? ""
|
lang: bookmarkDetailDto.lang ?? "",
|
||||||
|
readProgress: bookmarkDetailDto.readProgress
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ struct BookmarkDetail {
|
|||||||
let imageUrl: String
|
let imageUrl: String
|
||||||
let lang: String
|
let lang: String
|
||||||
var content: String?
|
var content: String?
|
||||||
|
let readProgress: Int?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BookmarkDetail {
|
extension BookmarkDetail {
|
||||||
@ -39,6 +40,7 @@ extension BookmarkDetail {
|
|||||||
labels: [],
|
labels: [],
|
||||||
thumbnailUrl: "",
|
thumbnailUrl: "",
|
||||||
imageUrl: "",
|
imageUrl: "",
|
||||||
lang: ""
|
lang: "",
|
||||||
|
readProgress: 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ struct BookmarkUpdateRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience Initializers für häufige Aktionen
|
|
||||||
extension BookmarkUpdateRequest {
|
extension BookmarkUpdateRequest {
|
||||||
static func archive(_ isArchived: Bool) -> BookmarkUpdateRequest {
|
static func archive(_ isArchived: Bool) -> BookmarkUpdateRequest {
|
||||||
return BookmarkUpdateRequest(isArchived: isArchived)
|
return BookmarkUpdateRequest(isArchived: isArchived)
|
||||||
@ -67,4 +67,4 @@ extension BookmarkUpdateRequest {
|
|||||||
static func removeLabels(_ labels: [String]) -> BookmarkUpdateRequest {
|
static func removeLabels(_ labels: [String]) -> BookmarkUpdateRequest {
|
||||||
return BookmarkUpdateRequest(removeLabels: labels)
|
return BookmarkUpdateRequest(removeLabels: labels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,4 +63,4 @@ class UpdateBookmarkUseCase: PUpdateBookmarkUseCase {
|
|||||||
let request = BookmarkUpdateRequest.removeLabels(labels)
|
let request = BookmarkUpdateRequest.removeLabels(labels)
|
||||||
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
<key>UILaunchScreen</key>
|
<key>UILaunchScreen</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIColorName</key>
|
<key>UIColorName</key>
|
||||||
<string>green2</string>
|
<string>splashBg</string>
|
||||||
<key>UIImageName</key>
|
<key>UIImageName</key>
|
||||||
<string>readeck</string>
|
<string>readeck</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@ -2,25 +2,22 @@ import SwiftUI
|
|||||||
import SafariServices
|
import SafariServices
|
||||||
import Combine
|
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
|
||||||
|
|
||||||
|
// MARK: - States
|
||||||
|
|
||||||
@State private var viewModel: BookmarkDetailViewModel
|
@State private var viewModel: BookmarkDetailViewModel
|
||||||
@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
|
@State private var readingProgress: Double = 0.0
|
||||||
// contentHeight entfernt, webViewHeight wird verwendet
|
|
||||||
@State private var scrollViewHeight: CGFloat = 1
|
@State private var scrollViewHeight: CGFloat = 1
|
||||||
|
@State private var showJumpToProgressButton: Bool = false
|
||||||
|
@State private var scrollPosition = ScrollPosition(edge: .top)
|
||||||
|
|
||||||
|
// MARK: - Envs
|
||||||
|
|
||||||
@EnvironmentObject var playerUIState: PlayerUIState
|
@EnvironmentObject var playerUIState: PlayerUIState
|
||||||
@EnvironmentObject var appSettings: AppSettings
|
@EnvironmentObject var appSettings: AppSettings
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@ -43,10 +40,10 @@ struct BookmarkDetailView: View {
|
|||||||
GeometryReader { outerGeo in
|
GeometryReader { outerGeo in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Track scroll offset at the top
|
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
Color.clear
|
Color.clear
|
||||||
.preference(key: ScrollOffsetPreferenceKey.self, value: geo.frame(in: .named("scroll")).minY)
|
.preference(key: ScrollOffsetPreferenceKey.self,
|
||||||
|
value: geo.frame(in: .named("scroll")).minY)
|
||||||
}
|
}
|
||||||
.frame(height: 0)
|
.frame(height: 0)
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
@ -55,6 +52,9 @@ struct BookmarkDetailView: View {
|
|||||||
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)
|
||||||
|
if showJumpToProgressButton {
|
||||||
|
JumpButton()
|
||||||
|
}
|
||||||
if let settings = viewModel.settings, !viewModel.articleContent.isEmpty {
|
if let settings = viewModel.settings, !viewModel.articleContent.isEmpty {
|
||||||
WebView(htmlContent: viewModel.articleContent, settings: settings, onHeightChange: { height in
|
WebView(htmlContent: viewModel.articleContent, settings: settings, onHeightChange: { height in
|
||||||
webViewHeight = height
|
webViewHeight = height
|
||||||
@ -83,32 +83,26 @@ struct BookmarkDetailView: View {
|
|||||||
.padding(.top, 0)
|
.padding(.top, 0)
|
||||||
}
|
}
|
||||||
Spacer(minLength: 40)
|
Spacer(minLength: 40)
|
||||||
if viewModel.isLoadingArticle == false {
|
if viewModel.isLoadingArticle == false && viewModel.isLoading == false {
|
||||||
archiveSection
|
archiveSection
|
||||||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
||||||
.animation(.easeInOut, value: viewModel.articleContent)
|
.animation(.easeInOut, value: viewModel.articleContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Kein GeometryReader am Ende nötig
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.coordinateSpace(name: "scroll")
|
.coordinateSpace(name: "scroll")
|
||||||
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
|
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
|
||||||
scrollViewHeight = outerGeo.size.height
|
scrollViewHeight = outerGeo.size.height
|
||||||
print("offset:", offset)
|
|
||||||
print("webViewHeight:", webViewHeight)
|
|
||||||
print("scrollViewHeight:", scrollViewHeight)
|
|
||||||
let maxOffset = webViewHeight - scrollViewHeight
|
let maxOffset = webViewHeight - scrollViewHeight
|
||||||
print("maxOffset:", maxOffset)
|
|
||||||
// Am Anfang: offset = 0, am Ende: offset = -maxOffset
|
|
||||||
let rawProgress = -offset / (maxOffset != 0 ? maxOffset : 1)
|
let rawProgress = -offset / (maxOffset != 0 ? maxOffset : 1)
|
||||||
print("rawProgress:", rawProgress)
|
|
||||||
let progress = min(max(rawProgress, 0), 1)
|
let progress = min(max(rawProgress, 0), 1)
|
||||||
print("progress:", progress)
|
readingProgress = progress
|
||||||
readingProgress = progress
|
viewModel.debouncedUpdateReadProgress(id: bookmarkId, progress: progress, anchor: nil)
|
||||||
}
|
}
|
||||||
.ignoresSafeArea(edges: .top)
|
.ignoresSafeArea(edges: .top)
|
||||||
|
.scrollPosition($scrollPosition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
@ -169,6 +163,9 @@ struct BookmarkDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: viewModel.readProgress) { _, progress in
|
||||||
|
showJumpToProgressButton = progress > 0 && progress < 100
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
||||||
await viewModel.loadArticleContent(id: bookmarkId)
|
await viewModel.loadArticleContent(id: bookmarkId)
|
||||||
@ -393,33 +390,19 @@ struct BookmarkDetailView: View {
|
|||||||
// Archive button
|
// Archive button
|
||||||
Button(action: {
|
Button(action: {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.archiveBookmark(id: bookmarkId)
|
await viewModel.archiveBookmark(id: bookmarkId, isArchive: !viewModel.bookmarkDetail.isArchived)
|
||||||
if viewModel.bookmarkDetail.isArchived {
|
|
||||||
showDismissButton = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: viewModel.bookmarkDetail.isArchived ? "checkmark.circle.fill" : "archivebox")
|
Image(systemName: viewModel.bookmarkDetail.isArchived ? "checkmark.circle" : "archivebox")
|
||||||
.foregroundColor(viewModel.bookmarkDetail.isArchived ? .green : .primary)
|
Text(viewModel.bookmarkDetail.isArchived ? "Unarchive Bookmark" : "Archive bookmark")
|
||||||
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 || viewModel.bookmarkDetail.isArchived)
|
.disabled(viewModel.isLoading)
|
||||||
}
|
|
||||||
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)
|
||||||
@ -430,6 +413,36 @@ struct BookmarkDetailView: View {
|
|||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.bottom, 32)
|
.padding(.bottom, 32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func JumpButton() -> some View {
|
||||||
|
Button(action: {
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
|
let maxOffset = webViewHeight - scrollViewHeight
|
||||||
|
let offset = maxOffset * (Double(viewModel.readProgress) / 100.0)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
|
scrollPosition = ScrollPosition(y: offset)
|
||||||
|
showJumpToProgressButton = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("Jump to last read position (\(viewModel.readProgress)%)")
|
||||||
|
.font(.subheadline)
|
||||||
|
.padding(8)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.background(Color.accentColor.opacity(0.15))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.padding([.top, .horizontal])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScrollOffsetPreferenceKey: PreferenceKey {
|
||||||
|
typealias Value = CGFloat
|
||||||
|
static var defaultValue = CGFloat.zero
|
||||||
|
static func reduce(value: inout Value, nextValue: () -> Value) {
|
||||||
|
value += nextValue()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
class BookmarkDetailViewModel {
|
class BookmarkDetailViewModel {
|
||||||
@ -16,15 +17,28 @@ class BookmarkDetailViewModel {
|
|||||||
var isLoadingArticle = true
|
var isLoadingArticle = true
|
||||||
var errorMessage: String?
|
var errorMessage: String?
|
||||||
var settings: Settings?
|
var settings: Settings?
|
||||||
|
var readProgress: Int = 0
|
||||||
|
|
||||||
private var factory: UseCaseFactory?
|
private var factory: UseCaseFactory?
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private let readProgressSubject = PassthroughSubject<(id: String, progress: Double, anchor: String?), Never>()
|
||||||
|
|
||||||
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) {
|
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) {
|
||||||
self.getBookmarkUseCase = factory.makeGetBookmarkUseCase()
|
self.getBookmarkUseCase = factory.makeGetBookmarkUseCase()
|
||||||
self.getBookmarkArticleUseCase = factory.makeGetBookmarkArticleUseCase()
|
self.getBookmarkArticleUseCase = factory.makeGetBookmarkArticleUseCase()
|
||||||
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
|
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
|
||||||
self.updateBookmarkUseCase = factory.makeUpdateBookmarkUseCase()
|
self.updateBookmarkUseCase = factory.makeUpdateBookmarkUseCase()
|
||||||
self.factory = factory
|
self.factory = factory
|
||||||
|
|
||||||
|
readProgressSubject
|
||||||
|
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
|
||||||
|
.sink { [weak self] (id, progress, anchor) in
|
||||||
|
let progressInt = Int(progress * 100)
|
||||||
|
Task {
|
||||||
|
await self?.updateReadProgress(id: id, progress: progressInt, anchor: anchor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -35,6 +49,8 @@ class BookmarkDetailViewModel {
|
|||||||
do {
|
do {
|
||||||
settings = try await loadSettingsUseCase.execute()
|
settings = try await loadSettingsUseCase.execute()
|
||||||
bookmarkDetail = try await getBookmarkUseCase.execute(id: id)
|
bookmarkDetail = try await getBookmarkUseCase.execute(id: id)
|
||||||
|
readProgress = bookmarkDetail.readProgress ?? 0
|
||||||
|
|
||||||
if settings?.enableTTS == true {
|
if settings?.enableTTS == true {
|
||||||
self.addTextToSpeechQueueUseCase = factory?.makeAddTextToSpeechQueueUseCase()
|
self.addTextToSpeechQueueUseCase = factory?.makeAddTextToSpeechQueueUseCase()
|
||||||
}
|
}
|
||||||
@ -68,11 +84,11 @@ class BookmarkDetailViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func archiveBookmark(id: String) async {
|
func archiveBookmark(id: String, isArchive: Bool = true) async {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
do {
|
do {
|
||||||
try await updateBookmarkUseCase.toggleArchive(bookmarkId: id, isArchived: true)
|
try await updateBookmarkUseCase.toggleArchive(bookmarkId: id, isArchived: isArchive)
|
||||||
bookmarkDetail.isArchived = true
|
bookmarkDetail.isArchived = true
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Error archiving bookmark"
|
errorMessage = "Error archiving bookmark"
|
||||||
@ -103,4 +119,16 @@ class BookmarkDetailViewModel {
|
|||||||
}
|
}
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateReadProgress(id: String, progress: Int, anchor: String?) async {
|
||||||
|
do {
|
||||||
|
try await updateBookmarkUseCase.updateReadProgress(bookmarkId: id, progress: progress, anchor: anchor)
|
||||||
|
} catch {
|
||||||
|
// ignore error in this case
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func debouncedUpdateReadProgress(id: String, progress: Double, anchor: String?) {
|
||||||
|
readProgressSubject.send((id, progress, anchor))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,19 +13,46 @@ struct BookmarkCardView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
AsyncImage(url: imageURL) { image in
|
ZStack(alignment: .bottomTrailing) {
|
||||||
image
|
AsyncImage(url: imageURL) { image in
|
||||||
.resizable()
|
image
|
||||||
.aspectRatio(contentMode: .fill)
|
.resizable()
|
||||||
.frame(height: 120)
|
.aspectRatio(contentMode: .fill)
|
||||||
} placeholder: {
|
.frame(height: 120)
|
||||||
|
} placeholder: {
|
||||||
|
|
||||||
|
Image(R.image.placeholder.name)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(height: 120)
|
||||||
|
}
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
|
||||||
Image(R.image.placeholder.name)
|
if bookmark.readProgress > 0 {
|
||||||
.resizable()
|
ZStack {
|
||||||
.aspectRatio(contentMode: .fill)
|
Circle()
|
||||||
.frame(height: 120)
|
.fill(Color(.systemBackground))
|
||||||
|
.frame(width: 36, height: 36)
|
||||||
|
Circle()
|
||||||
|
.stroke(Color.gray.opacity(0.2), lineWidth: 4)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
Circle()
|
||||||
|
.trim(from: 0, to: CGFloat(bookmark.readProgress) / 100)
|
||||||
|
.stroke(Color.accentColor, style: StrokeStyle(lineWidth: 4, lineCap: .round))
|
||||||
|
.rotationEffect(.degrees(-90))
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 0) {
|
||||||
|
Text("\(bookmark.readProgress)")
|
||||||
|
.font(.caption2)
|
||||||
|
.bold()
|
||||||
|
Text("%")
|
||||||
|
.font(.system(size: 8))
|
||||||
|
.baselineOffset(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(8)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(bookmark.title)
|
Text(bookmark.title)
|
||||||
@ -58,7 +85,6 @@ struct BookmarkCardView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Site") + " open", systemImage: "safari")
|
Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Site") + " open", systemImage: "safari")
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
SafariUtil.openInSafari(url: bookmark.url)
|
SafariUtil.openInSafari(url: bookmark.url)
|
||||||
@ -68,12 +94,6 @@ struct BookmarkCardView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
// Progress Bar for reading progress
|
|
||||||
if bookmark.readProgress > 0 {
|
|
||||||
ProgressView(value: Double(bookmark.readProgress), total: 100)
|
|
||||||
.progressViewStyle(LinearProgressViewStyle())
|
|
||||||
.frame(height: 4)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.bottom, 12)
|
.padding(.bottom, 12)
|
||||||
@ -203,3 +223,12 @@ struct IconBadge: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
BookmarkCardView(bookmark: .mock, currentState: .all) { _ in
|
||||||
|
|
||||||
|
} onDelete: { _ in
|
||||||
|
|
||||||
|
} onToggleFavorite: { _ in
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -289,20 +289,14 @@ 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 {
|
if message.name == "scrollProgress", let progress = message.body as? Double {
|
||||||
print("[WebView] scrollProgress received: \(progress)")
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.onScroll?(progress)
|
self.onScroll?(progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
|
||||||
// Der Message Handler wird automatisch mit der WebView entfernt
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -141,7 +141,7 @@ class MockSaveSettingsUseCase: PSaveSettingsUseCase {
|
|||||||
|
|
||||||
class MockGetBookmarkUseCase: PGetBookmarkUseCase {
|
class MockGetBookmarkUseCase: PGetBookmarkUseCase {
|
||||||
func execute(id: String) async throws -> BookmarkDetail {
|
func execute(id: String) async throws -> BookmarkDetail {
|
||||||
BookmarkDetail(id: "123", title: "Test", url: "https://www.google.com", description: "Test", siteName: "Test", authors: ["Test"], created: "2021-01-01", updated: "2021-01-01", wordCount: 100, readingTime: 100, hasArticle: true, isMarked: false, isArchived: false, labels: ["Test"], thumbnailUrl: "https://picsum.photos/30/30", imageUrl: "https://picsum.photos/400/400", lang: "en")
|
BookmarkDetail(id: "123", title: "Test", url: "https://www.google.com", description: "Test", siteName: "Test", authors: ["Test"], created: "2021-01-01", updated: "2021-01-01", wordCount: 100, readingTime: 100, hasArticle: true, isMarked: false, isArchived: false, labels: ["Test"], thumbnailUrl: "https://picsum.photos/30/30", imageUrl: "https://picsum.photos/400/400", lang: "en", readProgress: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +180,7 @@ class MockAddTextToSpeechQueueUseCase: PAddTextToSpeechQueueUseCase {
|
|||||||
func execute(bookmarkDetail: BookmarkDetail) {}
|
func execute(bookmarkDetail: BookmarkDetail) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension Bookmark {
|
extension Bookmark {
|
||||||
static let mock: Bookmark = .init(
|
static let mock: Bookmark = .init(
|
||||||
id: "123", title: "title", url: "https://example.com", href: "https://example.com", description: "description", authors: ["Tom"], created: "", published: "", updated: "", siteName: "example.com", site: "https://example.com", readingTime: 2, wordCount: 20, hasArticle: true, isArchived: false, isDeleted: false, isMarked: true, labels: ["Test"], lang: "EN", loaded: false, readProgress: 0, documentType: "", state: 0, textDirection: "ltr", type: "", resources: .init(article: nil, icon: nil, image: nil, log: nil, props: nil, thumbnail: nil)
|
id: "123", title: "title", url: "https://example.com", href: "https://example.com", description: "description", authors: ["Tom"], created: "", published: "", updated: "", siteName: "example.com", site: "https://example.com", readingTime: 2, wordCount: 20, hasArticle: true, isArchived: false, isDeleted: false, isMarked: true, labels: ["Test"], lang: "EN", loaded: false, readProgress: 0, documentType: "", state: 0, textDirection: "ltr", type: "", resources: .init(article: nil, icon: nil, image: nil, log: nil, props: nil, thumbnail: nil)
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user