New library item cards, fix issue with read position

This commit is contained in:
Jackson Harper 2023-02-15 13:56:11 +08:00
parent 557bb2ba7b
commit 4fd4d5a4db
31 changed files with 1935 additions and 78 deletions

View file

@ -58,7 +58,7 @@ struct FeedCardNavigationLink: View {
.onAppear {
Task { await viewModel.itemAppeared(item: item, dataService: dataService) }
}
FeedCard(item: item, viewer: dataService.currentViewer)
LibraryItemCard(item: item, viewer: dataService.currentViewer)
}
}
}

View file

@ -13,6 +13,8 @@ import Views
struct HomeFeedContainerView: View {
@State var hasHighlightMutations = false
@State var searchPresented = false
@State var addLinkPresented = false
@State var settingsPresented = false
@EnvironmentObject var dataService: DataService
@EnvironmentObject var audioController: AudioController
@ -129,16 +131,18 @@ import Views
}
ToolbarItem(placement: .barTrailing) {
if UIDevice.isIPhone {
NavigationLink(
destination: { ProfileView() },
label: {
Image(systemName: "person.circle")
.resizable()
.frame(width: 22, height: 22)
.padding(.vertical, 16)
.foregroundColor(.appGrayTextContrast)
}
)
Menu(content: {
Button(action: { settingsPresented = true }, label: {
Label(LocalText.genericProfile, systemImage: "person.circle")
})
Button(action: { addLinkPresented = true }, label: {
Label("Add Link", systemImage: "plus.square")
})
}, label: {
Image(systemName: "ellipsis")
.foregroundColor(.appGrayTextContrast)
.frame(width: 24, height: 24)
})
} else {
EmptyView()
}
@ -189,6 +193,16 @@ import Views
.fullScreenCover(isPresented: $searchPresented) {
LibrarySearchView(homeFeedViewModel: self.viewModel)
}
.sheet(isPresented: $addLinkPresented) {
NavigationView {
LibraryAddLinkView()
}
}
.sheet(isPresented: $settingsPresented) {
NavigationView {
ProfileView()
}
}
.task {
if viewModel.items.isEmpty {
loadItems(isRefresh: false)
@ -364,6 +378,7 @@ import Views
item: item,
viewModel: viewModel
)
.listRowInsets(.init(top: 0, leading: 8, bottom: 8, trailing: 8))
.contextMenu {
menuItems(for: item)
}
@ -407,8 +422,9 @@ import Views
}
}
}
.padding(.top, 0)
.padding(0)
.listStyle(PlainListStyle())
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
.alert("Are you sure you want to delete this item? All associated notes and highlights will be deleted.",
isPresented: $confirmationShown) {
Button("Remove Item", role: .destructive) {

View file

@ -0,0 +1,110 @@
import Introspect
import Models
import Services
import SwiftUI
import UIKit
import Views
@MainActor final class LibraryAddLinkViewModel: NSObject, ObservableObject {
@Published var isLoading = false
@Published var errorMessage: String = ""
@Published var showErrorMessage: Bool = false
func addLink(dataService: DataService, newLinkURL: String, dismiss: DismissAction) {
isLoading = true
Task {
if URL(string: newLinkURL) == nil {
error("Invalid link")
} else {
let result = try? await dataService.saveURL(id: UUID().uuidString, url: newLinkURL)
if result == nil {
error("Error adding link")
} else {
dismiss()
}
}
isLoading = false
}
}
func error(_ msg: String) {
errorMessage = msg
showErrorMessage = true
isLoading = false
}
}
struct LibraryAddLinkView: View {
@StateObject var viewModel = LibraryAddLinkViewModel()
@State var newLinkURL: String = ""
@EnvironmentObject var dataService: DataService
@Environment(\.dismiss) private var dismiss
enum FocusField: Hashable {
case addLinkEditor
}
@FocusState private var focusedField: FocusField?
var body: some View {
innerBody
.navigationTitle("Add Link")
.navigationBarTitleDisplayMode(.inline)
.onAppear {
focusedField = .addLinkEditor
}
}
var innerBody: some View {
Form {
TextField("Add Link", text: $newLinkURL)
.keyboardType(.URL)
.textFieldStyle(StandardTextFieldStyle())
.focused($focusedField, equals: .addLinkEditor)
Button(action: {
if let url = UIPasteboard.general.url {
newLinkURL = url.absoluteString
} else {
viewModel.error("No URL on pasteboard")
}
}, label: {
Text("Get from pasteboard")
})
}
.navigationTitle("Add Link")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
dismissButton
}
ToolbarItem(placement: .navigationBarTrailing) {
viewModel.isLoading ? AnyView(ProgressView()) : AnyView(addButton)
}
}
.alert(viewModel.errorMessage,
isPresented: $viewModel.showErrorMessage) {
Button(LocalText.genericOk, role: .cancel) { viewModel.showErrorMessage = false }
}
}
var addButton: some View {
Button(
action: {
viewModel.addLink(dataService: dataService, newLinkURL: newLinkURL, dismiss: dismiss)
},
label: { Text("Add").bold() }
)
.disabled(viewModel.isLoading)
}
var dismissButton: some View {
Button(
action: { dismiss() },
label: { Text(LocalText.genericClose) }
)
.disabled(viewModel.isLoading)
}
}

View file

@ -59,6 +59,7 @@ struct ProfileView: View {
@EnvironmentObject var authenticator: Authenticator
@EnvironmentObject var dataService: DataService
@Environment(\.openURL) var openURL
@Environment(\.dismiss) var dismiss
@StateObject private var viewModel = ProfileContainerViewModel()
@ -69,6 +70,13 @@ struct ProfileView: View {
Form {
innerBody
}
.navigationTitle(LocalText.genericProfile)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
dismissButton
}
}
#elseif os(macOS)
List {
innerBody
@ -77,6 +85,13 @@ struct ProfileView: View {
#endif
}
var dismissButton: some View {
Button(
action: { dismiss() },
label: { Text(LocalText.genericClose) }
)
}
private var accountSection: some View {
Section {
NavigationLink(destination: LabelsView()) {
@ -171,7 +186,6 @@ struct ProfileView: View {
}
}
}
.navigationTitle(LocalText.genericProfile)
}
}

View file

@ -46,44 +46,48 @@ struct SubscriptionsView: View {
@State private var progressViewOpacity = 0.0
var body: some View {
if viewModel.isLoading {
ProgressView()
.opacity(progressViewOpacity)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) {
progressViewOpacity = 1
Group {
if viewModel.isLoading {
ProgressView()
.opacity(progressViewOpacity)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) {
progressViewOpacity = 1
}
}
.task { await viewModel.loadSubscriptions(dataService: dataService) }
} else if viewModel.hasNetworkError {
VStack {
Text(LocalText.subscriptionsErrorRetrieving).multilineTextAlignment(.center)
Button(
action: { Task { await viewModel.loadSubscriptions(dataService: dataService) } },
label: { Text(LocalText.genericRetry) }
)
.buttonStyle(RoundedRectButtonStyle())
}
} else if viewModel.subscriptions.isEmpty {
VStack(alignment: .center) {
Spacer()
Text(LocalText.subscriptionsNone)
Spacer()
}
} else {
Group {
#if os(iOS)
Form {
innerBody
}
#elseif os(macOS)
List {
innerBody
}
.listStyle(InsetListStyle())
#endif
}
.task { await viewModel.loadSubscriptions(dataService: dataService) }
} else if viewModel.hasNetworkError {
VStack {
Text(LocalText.subscriptionsErrorRetrieving).multilineTextAlignment(.center)
Button(
action: { Task { await viewModel.loadSubscriptions(dataService: dataService) } },
label: { Text(LocalText.genericRetry) }
)
.buttonStyle(RoundedRectButtonStyle())
}
} else if viewModel.subscriptions.isEmpty {
VStack(alignment: .center) {
Spacer()
Text(LocalText.subscriptionsNone)
Spacer()
}
} else {
Group {
#if os(iOS)
Form {
innerBody
}
#elseif os(macOS)
List {
innerBody
}
.listStyle(InsetListStyle())
#endif
}
}
.navigationTitle("Subscriptions")
.navigationBarTitleDisplayMode(.inline)
}
private var innerBody: some View {

View file

@ -9,6 +9,7 @@ struct WebReader: PlatformViewRepresentable {
let articleContent: ArticleContent
let openLinkAction: (URL) -> Void
let tapHandler: () -> Void
let scrollPercentHandler: (Int) -> Void
let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void
let navBarVisibilityRatioUpdater: (Double) -> Void
@ -52,6 +53,7 @@ struct WebReader: PlatformViewRepresentable {
webView.configuration.userContentController = contentController
webView.configuration.userContentController.removeAllScriptMessageHandlers()
webView.uiDelegate
#if os(iOS)
webView.isOpaque = false
webView.backgroundColor = .clear
@ -74,6 +76,7 @@ struct WebReader: PlatformViewRepresentable {
context.coordinator.linkHandler = openLinkAction
context.coordinator.webViewActionHandler = webViewActionHandler
context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater
context.coordinator.scrollPercentHandler = scrollPercentHandler
context.coordinator.updateShowBottomBar = { newValue in
self.showBottomBar = newValue
}

View file

@ -30,6 +30,7 @@ struct WebReaderContainerView: View {
@State private var errorAlertMessage: String?
@State private var showErrorAlertMessage = false
@State private var showRecommendSheet = false
@State private var lastScrollPercentage: Int?
@State var safariWebLink: SafariWebLink?
@State var displayLinkSheet = false
@ -56,6 +57,10 @@ struct WebReaderContainerView: View {
}
}
func scrollPercentHandler(percent: Int) {
lastScrollPercentage = percent
}
func onHighlightListViewDismissal() {
// Reload the web view if mutation happened in highlights list modal
guard hasPerformedHighlightMutations else { return }
@ -369,6 +374,7 @@ struct WebReaderContainerView: View {
#endif
},
tapHandler: tapHandler,
scrollPercentHandler: scrollPercentHandler,
webViewActionHandler: webViewActionHandler,
navBarVisibilityRatioUpdater: {
navBarVisibilityRatio = $0
@ -381,6 +387,20 @@ struct WebReaderContainerView: View {
showBottomBar: $showBottomBar,
showHighlightAnnotationModal: $showHighlightAnnotationModal
)
.onAppear {
if item.isUnread {
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0)
}
}
.onDisappear {
// if let lastScrollPercentage = self.lastScrollPercentage {
// dataService.updateLinkReadingProgress(
// itemID: item.unwrappedID,
// readingProgress: Double(lastScrollPercentage),
// anchorIndex: 0
// )
// }
}
.confirmationDialog(linkToOpen?.absoluteString ?? "", isPresented: $displayLinkSheet) {
Button(action: {
if let linkToOpen = linkToOpen {
@ -495,9 +515,13 @@ struct WebReaderContainerView: View {
readerSettingsChangedTransactionID = UUID()
}
#endif
.onAppear {
try? WebViewManager.shared().dispatchEvent(.saveReadPosition)
}
.onDisappear {
try? WebViewManager.shared().dispatchEvent(.saveReadPosition)
// Clear the shared webview content when exiting
WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
// WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
}
}

View file

@ -13,6 +13,7 @@ typealias WKScriptMessageReplyHandler = (Any?, String?) -> Void
final class WebReaderCoordinator: NSObject {
var webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void = { _, _ in }
var linkHandler: (URL) -> Void = { _ in }
var scrollPercentHandler: ((Int) -> Void) = { _ in }
var needsReload = false
var lastSavedAnnotationID: UUID?
var previousReaderSettingsChangedUUID: UUID?
@ -128,6 +129,9 @@ extension WebReaderCoordinator: WKNavigationDelegate {
} else {
updateShowBottomBar(false)
}
let percent = Int(((yOffset + scrollView.visibleSize.height) / scrollView.contentSize.height) * 100)
scrollPercentHandler(max(0, min(percent, 100)))
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {

View file

@ -56,6 +56,7 @@
<attribute name="tempPDFURL" optional="YES" attributeType="URI"/>
<attribute name="title" attributeType="String"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="wordsCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="highlights" toMany="YES" deletionRule="Cascade" destinationEntity="Highlight" inverseName="linkedItem" inverseEntity="Highlight"/>
<relationship name="labels" toMany="YES" deletionRule="Nullify" destinationEntity="LinkedItemLabel" inverseName="linkedItems" inverseEntity="LinkedItemLabel"/>
<relationship name="recommendations" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Recommendation" inverseName="linkeditem" inverseEntity="Recommendation"/>

View file

@ -55,6 +55,7 @@ public struct JSONArticle: Decodable {
public let url: String
public let isArchived: Bool
public let language: String?
public let wordsCount: Int?
}
public extension LinkedItem {
@ -69,6 +70,10 @@ public extension LinkedItem {
(labels?.count ?? 0) > 0
}
var isUnread: Bool {
readingProgress <= 0
}
var isRead: Bool {
readingProgress >= 0.98
}

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@ extension DataService {
guard let self = self else { return }
guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return }
print("updateLinkReadingProgress", readingProgress, anchorIndex)
linkedItem.update(
inContext: self.backgroundContext,
newReadingProgress: readingProgress,

View file

@ -44,6 +44,7 @@ extension DataService {
contentReader: try $0.contentReader().rawValue,
originalHtml: nil,
language: try $0.language(),
wordsCount: try $0.wordsCount(),
recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
),

View file

@ -275,6 +275,7 @@ private let libraryArticleSelection = Selection.Article {
contentReader: try $0.contentReader().rawValue,
originalHtml: nil,
language: try $0.language(),
wordsCount: try $0.wordsCount(),
recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
)
@ -313,6 +314,7 @@ private let searchItemSelection = Selection.SearchItem {
contentReader: try $0.contentReader().rawValue,
originalHtml: nil,
language: try $0.language(),
wordsCount: try $0.wordsCount(),
recommendations: try $0.recommendations(selection: recommendationSelection.list.nullable) ?? [],
labels: try $0.labels(selection: feedItemLabelSelection.list.nullable) ?? []
)

View file

@ -26,6 +26,7 @@ struct InternalLinkedItem {
let contentReader: String?
let originalHtml: String?
let language: String?
let wordsCount: Int?
let recommendations: [InternalRecommendation]
var labels: [InternalLinkedItemLabel]
@ -63,6 +64,7 @@ struct InternalLinkedItem {
linkedItem.contentReader = contentReader
linkedItem.originalHtml = originalHtml
linkedItem.language = language
linkedItem.wordsCount = Int64(wordsCount ?? 0)
// Remove existing labels in case a label had been deleted
if let existingLabels = linkedItem.labels {
@ -142,6 +144,7 @@ extension JSONArticle {
contentReader: contentReader,
originalHtml: nil,
language: language,
wordsCount: wordsCount,
recommendations: [], // TODO:
labels: []
)

View file

@ -28,4 +28,5 @@ public enum UserDefaultKey: String {
case notificationsEnabled
case deviceTokenID
case shouldPromptCommunityModal
case userWordsPerMinute
}

View file

@ -415,6 +415,7 @@ public enum WebViewDispatchEvent {
case speakingSection(anchorIdx: String)
case updateLabels(labels: String)
case updateTitle(title: String)
case saveReadPosition
var script: String {
get throws {
@ -463,6 +464,8 @@ public enum WebViewDispatchEvent {
return "updateTitle"
case .handleAutoHighlightModeChange:
return "handleAutoHighlightModeChange"
case .saveReadPosition:
return "saveReadPosition"
}
}
@ -503,10 +506,17 @@ public enum WebViewDispatchEvent {
}
case let .speakingSection(anchorIdx: anchorIdx):
return "event.anchorIdx = '\(anchorIdx)';"
case .annotate, .highlight, .setHighlightLabels, .share, .remove, .copyHighlight, .dismissHighlight:
return ""
case let .handleAutoHighlightModeChange(isEnabled: isEnabled):
return "event.enableHighlightOnRelease = '\(isEnabled ? "on" : "off")';"
case .annotate,
.highlight,
.setHighlightLabels,
.share,
.remove,
.copyHighlight,
.dismissHighlight,
.saveReadPosition:
return ""
}
}
}

View file

@ -19,7 +19,7 @@ public extension Color {
static var appButtonBackground: Color { Color("_buttonBackground", bundle: .module) }
static var appTextDefault: Color { Color("_utilityTextDefault", bundle: .module) }
static var appPrimaryBackground: Color { Color("_appPrimaryBackground", bundle: .module) }
static var checkmarkBlue: Color { Color("_checkmarkBlue", bundle: .module) }
static var indicatorBlue: Color { Color("_indicatorBlue", bundle: .module) }
static var webControlButtonBackground: Color { Color("_webControlButtonBackground", bundle: .module) }
// Apple system UIColor equivalents

View file

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.294",
"green" : "0.843",
"red" : "0.196"
"blue" : "0.220",
"green" : "0.725",
"red" : "0.333"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.294",
"green" : "0.843",
"red" : "0.196"
"blue" : "0.220",
"green" : "0.725",
"red" : "0.333"
}
},
"idiom" : "universal"

View file

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0x84",
"red" : "0x0A"
"blue" : "0xF7",
"green" : "0x82",
"red" : "0x3A"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0x84",
"red" : "0x0A"
"blue" : "0xF7",
"green" : "0x82",
"red" : "0x3A"
}
},
"idiom" : "universal"

View file

@ -0,0 +1,221 @@
import Models
import SwiftUI
import Utils
public struct LibraryItemCard: View {
let viewer: Viewer?
let tapHandler: () -> Void
@ObservedObject var item: LinkedItem
public init(item: LinkedItem, viewer: Viewer?, tapHandler: @escaping () -> Void = {}) {
self.item = item
self.viewer = viewer
self.tapHandler = tapHandler
}
public var body: some View {
VStack {
HStack(alignment: .top, spacing: 0) {
readIndicator
articleInfo
imageBox
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
if item.hasLabels {
labels
}
}
.padding(.bottom, 8)
}
var isFullyRead: Bool {
item.readingProgress > 95
}
var isPartiallyRead: Bool {
Int(item.readingProgress) > 0
}
var readIndicator: some View {
HStack {
Circle()
.foregroundColor(item.readingProgress > 0 ? .clear : .indicatorBlue)
.frame(width: 9, height: 9, alignment: .topLeading)
.padding(.top, 22)
.padding(.leading, 0)
.padding(.trailing, 8)
}
.padding(0)
.frame(width: 20)
}
var readingSpeed: Int64 {
var result = UserDefaults.standard.integer(forKey: UserDefaultKey.userWordsPerMinute.rawValue)
if result <= 0 {
result = 235
}
return Int64(result)
}
var estimatedReadingTime: String {
if item.wordsCount > 0 {
let readLen = max(1, item.wordsCount / readingSpeed)
return "\(readLen) MIN READ • "
}
return ""
}
var readingProgress: String {
// If there is no wordsCount don't show progress because it will make no sense
if item.wordsCount > 0 {
return "\(String(format: "%d", Int(item.readingProgress)))%"
}
return ""
}
var highlightsText: String {
if let highlights = item.highlights, highlights.count > 0 {
let fmted = LocalText.pluralizedText(key: "number_of_highlights", count: highlights.count)
if item.wordsCount > 0 {
return "\(fmted)"
}
return fmted
}
return ""
}
var notesText: String {
let notes = item.highlights?.filter { item in
if let highlight = item as? Highlight {
return !(highlight.annotation ?? "").isEmpty
}
return false
}
if let notes = notes, notes.count > 0 {
let fmted = LocalText.pluralizedText(key: "number_of_notes", count: notes.count)
if item.wordsCount > 0 {
return "\(fmted)"
}
return fmted
}
return ""
}
var readInfo: some View {
AnyView(HStack {
Text("\(estimatedReadingTime)")
.font(Font.system(size: 11, weight: .medium))
.foregroundColor(Color(hex: "#898989"))
+
Text("\(readingProgress)")
.font(Font.system(size: 11, weight: .medium))
.foregroundColor(isPartiallyRead ? Color.appGreenSuccess : Color(hex: "#898989"))
+
Text("\(highlightsText)")
.font(Font.system(size: 11, weight: .medium))
.foregroundColor(Color(hex: "#898989"))
+
Text("\(notesText)")
.font(Font.system(size: 11, weight: .medium))
.foregroundColor(Color(hex: "#898989"))
}
.frame(maxWidth: .infinity, alignment: .leading))
}
var imageBox: some View {
Group {
if let imageURL = item.imageURL {
AsyncImage(url: imageURL) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 55, height: 73)
.cornerRadius(4)
.padding(.top, 2)
} else {
Color.systemBackground
.frame(width: 55, height: 73)
.cornerRadius(4)
.padding(.top, 2)
}
}
}
}.opacity(isFullyRead ? 0.4 : 1.0)
}
var bylineStr: String {
// It seems like it could be cleaner just having author, instead of
// concating, maybe we fall back
if let author = item.author {
return author
} else if let publisherDisplayName = item.publisherDisplayName {
return publisherDisplayName
}
return ""
// var str = ""
// if let author = item.author {
// str += author
// }
//
// if item.author != nil, item.publisherDisplayName != nil {
// str += ", "
// }
//
// if let publisherDisplayName = item.publisherDisplayName {
// str += publisherDisplayName
// }
//
// return str
}
var byLine: some View {
Text(bylineStr)
.font(Font.system(size: 15, weight: .regular))
.foregroundColor(Color(hex: "#898989"))
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
}
public var articleInfo: some View {
VStack(alignment: .leading, spacing: 5) {
readInfo
Text(item.unwrappedTitle)
.font(Font.system(size: 18, weight: .semibold))
.lineSpacing(1.25)
.foregroundColor(isFullyRead ? Color(hex: "#898989") : .appGrayTextContrast)
.fixedSize(horizontal: false, vertical: true)
byLine
}
.padding(0)
.padding(.trailing, 8)
}
var labels: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(item.sortedLabels, id: \.self) {
TextChip(feedItemLabel: $0)
}
Spacer()
}
}.introspectScrollView { scrollView in
scrollView.bounces = false
}
.padding(.top, 0)
.padding(.leading, 20)
#if os(macOS)
.onTapGesture {
tapHandler()
}
#endif
}
}

View file

@ -5,6 +5,12 @@ public enum LocalText {
NSLocalizedString(key, bundle: .module, comment: comment ?? "no comment provided by developer")
}
public static func pluralizedText(key: String, count: Int) -> String {
let format = NSLocalizedString(key, bundle: .module, comment: "")
print("key", key, "format", format)
return String.localizedStringWithFormat(format, count)
}
// Share extension
public static let saveArticleSavedState = localText(key: "saveArticleSavedState")
public static let saveArticleProcessingState = localText(key: "saveArticleProcessingState")

File diff suppressed because one or more lines are too long

View file

@ -138,7 +138,6 @@
//"library.by.author.suffix" = "by \(author)" // unused
//"Recommended by \(byStr) in \(inStr)" // unused
// Generic
"genericSnooze" = "Snooze";
"genericClose" = "Close";

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>number_of_highlights</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@value@</string>
<key>value</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d highlight</string>
<key>other</key>
<string>%d highlights</string>
</dict>
</dict>
<key>number_of_notes</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@value@</string>
<key>value</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d note</string>
<key>other</key>
<string>%d notes</string>
</dict>
</dict>
</dict>
</plist>

View file

@ -49,7 +49,6 @@ public struct TextChip: View {
return .white
}
print(" Luminance: ", luminance, "for color", color.hex)
return luminance > 0.35 ? .black : .white
}

View file

@ -14,7 +14,7 @@ const mutation = async (name, input) => {
actionID: name,
...input,
})
console.log('action result', result, result.result)
console.log('action result', name, result, result.result)
return result.result
} else {
// Send android a message

View file

@ -17,23 +17,18 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
const g = (bigint >> 8) & 255
const b = bigint & 255
console.log(' -- ', r, g, b, 'for', hex)
return [r, g, b]
}
const parsed = parseToRgba(props.color)
console.log(' -- parsed: ', parsed, 'for', props.color)
function f(x: number) {
const channel = x / 255
return channel <= 0.03928
? channel / 12.92
: Math.pow((channel + 0.055) / 1.055, 2.4)
}
console.log(' -- parts: ', f(parsed[0]), f(parsed[1]), f(parsed[2]))
const luminance = getLuminance(props.color)
const backgroundColor = hexToRgb(props.color)
console.log('luminance', luminance, 'for color: ', props.color)
const textColor = luminance > 0.5 ? '#000000' : '#ffffff'
return (

View file

@ -90,7 +90,8 @@ export function Article(props: ArticleProps): JSX.Element {
useScrollWatcher((changeset: ScrollOffsetChangeset) => {
if (window && window.document.scrollingElement) {
const newReadingProgress =
window.scrollY / window.document.scrollingElement.scrollHeight
(window.scrollY + window.innerHeight) /
window.document.scrollingElement.scrollHeight
const adjustedReadingProgress =
newReadingProgress > 0.92 ? 1 : newReadingProgress
debouncedSetReadingProgress(adjustedReadingProgress * 100)
@ -119,6 +120,22 @@ export function Article(props: ArticleProps): JSX.Element {
[]
)
useEffect(() => {
const saveReadPosition = () => {
console.log(
'saving read position from article: ',
readingProgress,
readingAnchorIndex
)
}
document.addEventListener('saveReadPosition', saveReadPosition)
return () => {
document.removeEventListener('saveReadPosition', saveReadPosition)
}
}, [readingProgress, readingAnchorIndex])
// Scroll to initial anchor position
useEffect(() => {
if (typeof window === 'undefined') {

View file

@ -249,6 +249,12 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
}
}
const saveReadPosition = () => {
console.log('saving read position')
}
document.addEventListener('saveReadPosition', saveReadPosition)
document.addEventListener('updateFontFamily', updateFontFamily)
document.addEventListener('updateLineHeight', updateLineHeight)
document.addEventListener(
@ -292,6 +298,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
'handleAutoHighlightModeChange',
updateHighlightMode
)
document.removeEventListener('saveReadPosition', saveReadPosition)
}
})

View file

@ -131,7 +131,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
grayProgressBackground: '#FFFFFF',
// Semantic Colors
highlightBackground: '255, 255, 0',
highlightBackground: '255, 210, 52',
recommendedHighlightBackground: '#E5FFE5',
highlight: '#FFD234',
highlightText: '#3D3D3D',