mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1380 from omnivore-app/fix/ios-note-saving
Improve note saving reliability on iOS
This commit is contained in:
commit
1ad5fdb838
19 changed files with 437 additions and 163 deletions
|
|
@ -83,8 +83,11 @@ final class AppStoreScreenshots: XCTestCase {
|
|||
func testScreenshotSubscriptions() throws {
|
||||
let app = XCUIApplication()
|
||||
setupSnapshot(app)
|
||||
app.navigationBars["Home"]/*@START_MENU_TOKEN@*/ .buttons["_profile"]/*[[".otherElements[\"_profile\"].buttons[\"_profile\"]",".buttons[\"_profile\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/ .tap()
|
||||
app.collectionViews.buttons["Subscriptions"].tap()
|
||||
// app.navigationBars.firstMatch.buttons["person.circle"].tap()
|
||||
// app.collectionViews.buttons["Subscriptions"].tap()
|
||||
//
|
||||
// XCUIApplication().navigationBars["_TtGC7SwiftUI19UIHosting"]/*@START_MENU_TOKEN@*/.buttons["ToggleSidebar"]/*[[".buttons[\"Show Sidebar\"]",".buttons[\"ToggleSidebar\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
|
||||
//
|
||||
|
||||
snapshot("Newsletters")
|
||||
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ import Views
|
|||
func handleArchiveAction(dataService: DataService) {
|
||||
guard let objectID = item?.objectID ?? pdfItem?.objectID else { return }
|
||||
dataService.archiveLink(objectID: objectID, archived: !isItemArchived)
|
||||
Snackbar.show(message: !isItemArchived ? "Link archived" : "Link moved to Inbox")
|
||||
showInSnackbar(!isItemArchived ? "Link archived" : "Link moved to Inbox")
|
||||
}
|
||||
|
||||
func handleDeleteAction(dataService: DataService) {
|
||||
guard let objectID = item?.objectID ?? pdfItem?.objectID else { return }
|
||||
Snackbar.show(message: "Link removed")
|
||||
showInSnackbar("Link removed")
|
||||
dataService.removeLink(objectID: objectID)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ struct WebReader: PlatformViewRepresentable {
|
|||
@Binding var shareActionID: UUID?
|
||||
@Binding var annotation: String
|
||||
@Binding var showBottomBar: Bool
|
||||
@Binding var showHighlightAnnotationModal: Bool
|
||||
|
||||
func makeCoordinator() -> WebReaderCoordinator {
|
||||
WebReaderCoordinator()
|
||||
|
|
@ -84,11 +85,19 @@ struct WebReader: PlatformViewRepresentable {
|
|||
private func updatePlatformView(_ webView: WKWebView, context: Context) {
|
||||
if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID {
|
||||
context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID
|
||||
(webView as? OmnivoreWebView)?.dispatchEvent(.saveAnnotation(annotation: annotation))
|
||||
do {
|
||||
try (webView as? OmnivoreWebView)?.dispatchEvent(.saveAnnotation(annotation: annotation))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
||||
showHighlightAnnotationModal = false
|
||||
}
|
||||
} catch {
|
||||
showInSnackbar("Error saving note.")
|
||||
}
|
||||
}
|
||||
|
||||
if readerSettingsChangedTransactionID != context.coordinator.previousReaderSettingsChangedUUID {
|
||||
context.coordinator.previousReaderSettingsChangedUUID = readerSettingsChangedTransactionID
|
||||
(webView as? OmnivoreWebView)?.updateTheme()
|
||||
(webView as? OmnivoreWebView)?.updateFontFamily()
|
||||
(webView as? OmnivoreWebView)?.updateFontSize()
|
||||
(webView as? OmnivoreWebView)?.updateTextContrast()
|
||||
|
|
|
|||
|
|
@ -299,7 +299,8 @@ struct WebReaderContainerView: View {
|
|||
showNavBarActionID: $showNavBarActionID,
|
||||
shareActionID: $shareActionID,
|
||||
annotation: $annotation,
|
||||
showBottomBar: $showBottomBar
|
||||
showBottomBar: $showBottomBar,
|
||||
showHighlightAnnotationModal: $showHighlightAnnotationModal
|
||||
)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
|
|
@ -317,7 +318,6 @@ struct WebReaderContainerView: View {
|
|||
annotation: $annotation,
|
||||
onSave: {
|
||||
annotationSaveTransactionID = UUID()
|
||||
showHighlightAnnotationModal = false
|
||||
},
|
||||
onCancel: {
|
||||
showHighlightAnnotationModal = false
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ struct SpeechSynthesizer {
|
|||
func createPlayerItems(from: Int) -> [SpeechItem] {
|
||||
var result: [SpeechItem] = []
|
||||
|
||||
for idx in from ..< document.utterances.count {
|
||||
for idx in from ..< min(7, document.utterances.count) {
|
||||
let utterance = document.utterances[idx]
|
||||
let voiceStr = utterance.voice ?? document.defaultVoice
|
||||
let segmentStr = String(format: "%04d", arguments: [idx])
|
||||
|
|
|
|||
18
apple/OmnivoreKit/Sources/Utils/ShowInSnackbar.swift
Normal file
18
apple/OmnivoreKit/Sources/Utils/ShowInSnackbar.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// ShowInSnackbar.swift
|
||||
//
|
||||
//
|
||||
// Created by Jackson Harper on 11/1/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public func showInSnackbar(_ message: String) {
|
||||
let nname = Notification.Name("OperationSuccess")
|
||||
NotificationCenter.default.post(name: nname, object: nil, userInfo: ["message": message])
|
||||
}
|
||||
|
||||
public func showErrorInSnackbar(_ message: String) {
|
||||
let nname = Notification.Name("OperationFailure")
|
||||
NotificationCenter.default.post(name: nname, object: nil, userInfo: ["message": message])
|
||||
}
|
||||
|
|
@ -19,4 +19,5 @@ public enum UserDefaultKey: String {
|
|||
case textToSpeechPreloadEnabled
|
||||
case recentSearchTerms
|
||||
case audioPlayerExpanded
|
||||
case themeName
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Models
|
||||
import Utils
|
||||
import WebKit
|
||||
|
||||
|
|
@ -31,12 +32,6 @@ public final class OmnivoreWebView: WKWebView {
|
|||
self.isFindInteractionEnabled = true
|
||||
}
|
||||
#endif
|
||||
|
||||
NotificationCenter.default.addObserver(forName: NSNotification.Name("SpeakingReaderItem"), object: nil, queue: OperationQueue.main, using: { notification in
|
||||
if let pageID = notification.userInfo?["pageID"] as? String, let anchorIdx = notification.userInfo?["anchorIdx"] as? String {
|
||||
self.dispatchEvent(.speakingSection(anchorIdx: anchorIdx))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
|
|
@ -44,15 +39,33 @@ public final class OmnivoreWebView: WKWebView {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func updateTheme() {
|
||||
do {
|
||||
if let themeName = UserDefaults.standard.value(forKey: UserDefaultKey.themeName.rawValue) as? String {
|
||||
try dispatchEvent(.updateTheme(themeName: "Gray" /* themeName */ ))
|
||||
}
|
||||
} catch {
|
||||
showErrorInSnackbar("Error updating theme")
|
||||
}
|
||||
}
|
||||
|
||||
public func updateFontFamily() {
|
||||
if let fontFamily = UserDefaults.standard.value(forKey: UserDefaultKey.preferredWebFont.rawValue) as? String {
|
||||
dispatchEvent(.updateFontFamily(family: fontFamily))
|
||||
do {
|
||||
if let fontFamily = UserDefaults.standard.value(forKey: UserDefaultKey.preferredWebFont.rawValue) as? String {
|
||||
try dispatchEvent(.updateFontFamily(family: fontFamily))
|
||||
}
|
||||
} catch {
|
||||
showErrorInSnackbar("Error updating font")
|
||||
}
|
||||
}
|
||||
|
||||
public func updateFontSize() {
|
||||
if let fontSize = UserDefaults.standard.value(forKey: UserDefaultKey.preferredWebFontSize.rawValue) as? Int {
|
||||
dispatchEvent(.updateFontSize(size: fontSize))
|
||||
do {
|
||||
if let fontSize = UserDefaults.standard.value(forKey: UserDefaultKey.preferredWebFontSize.rawValue) as? Int {
|
||||
try dispatchEvent(.updateFontSize(size: fontSize))
|
||||
}
|
||||
} catch {
|
||||
showErrorInSnackbar("Error updating font")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,13 +73,21 @@ public final class OmnivoreWebView: WKWebView {
|
|||
if let maxWidthPercentage = UserDefaults.standard.value(
|
||||
forKey: UserDefaultKey.preferredWebMaxWidthPercentage.rawValue
|
||||
) as? Int {
|
||||
dispatchEvent(.updateMaxWidthPercentage(maxWidthPercentage: maxWidthPercentage))
|
||||
do {
|
||||
try dispatchEvent(.updateMaxWidthPercentage(maxWidthPercentage: maxWidthPercentage))
|
||||
} catch {
|
||||
showErrorInSnackbar("Error updating max width")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLineHeight() {
|
||||
if let height = UserDefaults.standard.value(forKey: UserDefaultKey.preferredWebLineSpacing.rawValue) as? Int {
|
||||
dispatchEvent(.updateLineHeight(height: height))
|
||||
do {
|
||||
try dispatchEvent(.updateLineHeight(height: height))
|
||||
} catch {
|
||||
showErrorInSnackbar("Error updating line height")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,17 +97,35 @@ public final class OmnivoreWebView: WKWebView {
|
|||
) as? Bool
|
||||
|
||||
if let isHighContrast = isHighContrast {
|
||||
dispatchEvent(.handleFontContrastChange(isHighContrast: isHighContrast))
|
||||
do {
|
||||
try dispatchEvent(.handleFontContrastChange(isHighContrast: isHighContrast))
|
||||
} catch {
|
||||
showErrorInSnackbar("Error updating text contrast")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func shareOriginalItem() {
|
||||
dispatchEvent(.share)
|
||||
do {
|
||||
try dispatchEvent(.share)
|
||||
} catch {
|
||||
showErrorInSnackbar("Error updating line height")
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatchEvent(_ event: WebViewDispatchEvent) {
|
||||
evaluateJavaScript(event.script) { _, err in
|
||||
if let err = err { print("evaluateJavaScript error", err) }
|
||||
public func dispatchEvent(_ event: WebViewDispatchEvent) throws {
|
||||
let script = try event.script
|
||||
var errResult: Error?
|
||||
|
||||
evaluateJavaScript(script) { _, err in
|
||||
if let err = err {
|
||||
print("evaluateJavaScript error", err)
|
||||
errResult = err
|
||||
}
|
||||
}
|
||||
|
||||
if let errResult = errResult {
|
||||
throw errResult
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +133,11 @@ public final class OmnivoreWebView: WKWebView {
|
|||
override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
guard previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle else { return }
|
||||
dispatchEvent(.updateColorMode(isDark: traitCollection.userInterfaceStyle == .dark))
|
||||
do {
|
||||
try dispatchEvent(.updateColorMode(isDark: traitCollection.userInterfaceStyle == .dark))
|
||||
} catch {
|
||||
showErrorInSnackbar("Error updating theme")
|
||||
}
|
||||
}
|
||||
|
||||
#elseif os(macOS)
|
||||
|
|
@ -204,28 +247,48 @@ public final class OmnivoreWebView: WKWebView {
|
|||
}
|
||||
|
||||
@objc private func annotateSelection() {
|
||||
dispatchEvent(.annotate)
|
||||
do {
|
||||
try dispatchEvent(.annotate)
|
||||
} catch {
|
||||
showErrorInSnackbar("Error creating highlight")
|
||||
}
|
||||
hideMenu()
|
||||
}
|
||||
|
||||
@objc private func highlightSelection() {
|
||||
dispatchEvent(.highlight)
|
||||
do {
|
||||
try dispatchEvent(.highlight)
|
||||
} catch {
|
||||
showErrorInSnackbar("Error creating highlight")
|
||||
}
|
||||
hideMenu()
|
||||
}
|
||||
|
||||
@objc private func shareSelection() {
|
||||
dispatchEvent(.share)
|
||||
do {
|
||||
try dispatchEvent(.share)
|
||||
} catch {
|
||||
showErrorInSnackbar("Error sharing highlight")
|
||||
}
|
||||
hideMenu()
|
||||
}
|
||||
|
||||
@objc private func removeSelection() {
|
||||
dispatchEvent(.remove)
|
||||
do {
|
||||
try dispatchEvent(.remove)
|
||||
} catch {
|
||||
showErrorInSnackbar("Error deleting highlight")
|
||||
}
|
||||
hideMenu()
|
||||
}
|
||||
|
||||
@objc override public func copy(_ sender: Any?) {
|
||||
super.copy(sender)
|
||||
dispatchEvent(.copyHighlight)
|
||||
do {
|
||||
try dispatchEvent(.copyHighlight)
|
||||
} catch {
|
||||
showErrorInSnackbar("Error copying highlight")
|
||||
}
|
||||
hideMenu()
|
||||
}
|
||||
|
||||
|
|
@ -259,7 +322,7 @@ public final class OmnivoreWebView: WKWebView {
|
|||
|
||||
private func hideMenuAndDismissHighlight() {
|
||||
hideMenu()
|
||||
dispatchEvent(.dismissHighlight)
|
||||
try? dispatchEvent(.dismissHighlight)
|
||||
}
|
||||
|
||||
private func showHighlightMenu(_ rect: CGRect) {
|
||||
|
|
@ -293,6 +356,7 @@ public enum WebViewDispatchEvent {
|
|||
case updateFontSize(size: Int)
|
||||
case updateColorMode(isDark: Bool)
|
||||
case updateFontFamily(family: String)
|
||||
case updateTheme(themeName: String)
|
||||
case saveAnnotation(annotation: String)
|
||||
case annotate
|
||||
case highlight
|
||||
|
|
@ -303,7 +367,10 @@ public enum WebViewDispatchEvent {
|
|||
case speakingSection(anchorIdx: String)
|
||||
|
||||
var script: String {
|
||||
"var event = new Event('\(eventName)');\(scriptPropertyLine)document.dispatchEvent(event);"
|
||||
get throws {
|
||||
let propertyLine = try scriptPropertyLine
|
||||
return "var event = new Event('\(eventName)');\(propertyLine)document.dispatchEvent(event);"
|
||||
}
|
||||
}
|
||||
|
||||
private var eventName: String {
|
||||
|
|
@ -320,6 +387,8 @@ public enum WebViewDispatchEvent {
|
|||
return "updateColorMode"
|
||||
case .updateFontFamily:
|
||||
return "updateFontFamily"
|
||||
case .updateTheme:
|
||||
return "updateTheme"
|
||||
case .saveAnnotation:
|
||||
return "saveAnnotation"
|
||||
case .annotate:
|
||||
|
|
@ -340,25 +409,35 @@ public enum WebViewDispatchEvent {
|
|||
}
|
||||
|
||||
private var scriptPropertyLine: String {
|
||||
switch self {
|
||||
case let .handleFontContrastChange(isHighContrast: isHighContrast):
|
||||
return "event.fontContrast = '\(isHighContrast ? "high" : "normal")';"
|
||||
case let .updateLineHeight(height: height):
|
||||
return "event.lineHeight = '\(height)';"
|
||||
case let .updateMaxWidthPercentage(maxWidthPercentage: maxWidthPercentage):
|
||||
return "event.maxWidthPercentage = '\(maxWidthPercentage)';"
|
||||
case let .updateFontSize(size: size):
|
||||
return "event.fontSize = '\(size)';"
|
||||
case let .updateColorMode(isDark: isDark):
|
||||
return "event.isDark = '\(isDark)';"
|
||||
case let .updateFontFamily(family: family):
|
||||
return "event.fontFamily = '\(family)';"
|
||||
case let .saveAnnotation(annotation: annotation):
|
||||
return "event.annotation = '\(annotation)';"
|
||||
case let .speakingSection(anchorIdx: anchorIdx):
|
||||
return "event.anchorIdx = '\(anchorIdx)';"
|
||||
case .annotate, .highlight, .share, .remove, .copyHighlight, .dismissHighlight:
|
||||
return ""
|
||||
get throws {
|
||||
switch self {
|
||||
case let .handleFontContrastChange(isHighContrast: isHighContrast):
|
||||
return "event.fontContrast = '\(isHighContrast ? "high" : "normal")';"
|
||||
case let .updateLineHeight(height: height):
|
||||
return "event.lineHeight = '\(height)';"
|
||||
case let .updateMaxWidthPercentage(maxWidthPercentage: maxWidthPercentage):
|
||||
return "event.maxWidthPercentage = '\(maxWidthPercentage)';"
|
||||
case let .updateTheme(themeName: themeName):
|
||||
return "event.themeName = '\(themeName)';"
|
||||
case let .updateFontSize(size: size):
|
||||
return "event.fontSize = '\(size)';"
|
||||
case let .updateColorMode(isDark: isDark):
|
||||
return "event.isDark = '\(isDark)';"
|
||||
case let .updateFontFamily(family: family):
|
||||
return "event.fontFamily = '\(family)';"
|
||||
case let .saveAnnotation(annotation: annotation):
|
||||
let encoder = JSONEncoder()
|
||||
if let encoded = try? encoder.encode(annotation) {
|
||||
let str = String(decoding: encoded, as: UTF8.self)
|
||||
return "event.annotation = '\(str)';"
|
||||
} else {
|
||||
throw BasicError.message(messageText: "Unable to serialize highlight note.")
|
||||
}
|
||||
case let .speakingSection(anchorIdx: anchorIdx):
|
||||
return "event.anchorIdx = '\(anchorIdx)';"
|
||||
case .annotate, .highlight, .share, .remove, .copyHighlight, .dismissHighlight:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,81 +69,112 @@ public enum WebFont: String, CaseIterable {
|
|||
.navigationTitle("Reader Font")
|
||||
}
|
||||
|
||||
var themePicker: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 16) {
|
||||
ForEach(Theme.allCases, id: \.self) { theme in
|
||||
VStack {
|
||||
ZStack {
|
||||
Circle()
|
||||
.foregroundColor(theme.bgColor)
|
||||
.frame(minWidth: 32, minHeight: 32)
|
||||
.padding(8)
|
||||
}
|
||||
|
||||
Text(theme.rawValue).font(.appCaption)
|
||||
}
|
||||
.padding(8)
|
||||
.background(Color(red: 248 / 255.0, green: 248 / 255.0, blue: 248 / 255.0))
|
||||
.onTapGesture {
|
||||
ThemeManager.currentThemeName = theme.rawValue
|
||||
updateReaderPreferences()
|
||||
}
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(ThemeManager.currentThemeName == theme.rawValue ? Color.appCtaYellow : .clear, lineWidth: 2)
|
||||
)
|
||||
.padding(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
NavigationView {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .center) {
|
||||
VStack {
|
||||
LabelledStepper(
|
||||
labelText: "Font Size:",
|
||||
onIncrement: {
|
||||
storedFontSize = min(storedFontSize + 2, 28)
|
||||
updateReaderPreferences()
|
||||
},
|
||||
onDecrement: {
|
||||
storedFontSize = max(storedFontSize - 2, 10)
|
||||
updateReaderPreferences()
|
||||
}
|
||||
)
|
||||
VStack(alignment: .center) {
|
||||
// themePicker
|
||||
// .padding(.bottom, 16)
|
||||
|
||||
LabelledStepper(
|
||||
labelText: "Margin:",
|
||||
onIncrement: {
|
||||
storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40)
|
||||
updateReaderPreferences()
|
||||
},
|
||||
onDecrement: {
|
||||
storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100)
|
||||
updateReaderPreferences()
|
||||
}
|
||||
)
|
||||
|
||||
LabelledStepper(
|
||||
labelText: "Line Spacing:",
|
||||
onIncrement: {
|
||||
storedLineSpacing = min(storedLineSpacing + 25, 300)
|
||||
updateReaderPreferences()
|
||||
},
|
||||
onDecrement: {
|
||||
storedLineSpacing = max(storedLineSpacing - 25, 100)
|
||||
updateReaderPreferences()
|
||||
}
|
||||
)
|
||||
|
||||
Toggle("High Contrast Text:", isOn: $prefersHighContrastText)
|
||||
.frame(height: 40)
|
||||
.padding(.trailing, 6)
|
||||
.onChange(of: prefersHighContrastText) { _ in
|
||||
updateReaderPreferences()
|
||||
}
|
||||
|
||||
HStack {
|
||||
NavigationLink(destination: fontList) {
|
||||
Text("Change Reader Font")
|
||||
}
|
||||
Image(systemName: "chevron.right")
|
||||
Spacer()
|
||||
}
|
||||
.frame(height: 40)
|
||||
|
||||
Spacer()
|
||||
LabelledStepper(
|
||||
labelText: "Font Size",
|
||||
onIncrement: {
|
||||
storedFontSize = min(storedFontSize + 2, 28)
|
||||
updateReaderPreferences()
|
||||
},
|
||||
onDecrement: {
|
||||
storedFontSize = max(storedFontSize - 2, 10)
|
||||
updateReaderPreferences()
|
||||
}
|
||||
)
|
||||
|
||||
LabelledStepper(
|
||||
labelText: "Margin",
|
||||
onIncrement: {
|
||||
storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40)
|
||||
updateReaderPreferences()
|
||||
},
|
||||
onDecrement: {
|
||||
storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100)
|
||||
updateReaderPreferences()
|
||||
}
|
||||
)
|
||||
|
||||
LabelledStepper(
|
||||
labelText: "Line Spacing",
|
||||
onIncrement: {
|
||||
storedLineSpacing = min(storedLineSpacing + 25, 300)
|
||||
updateReaderPreferences()
|
||||
},
|
||||
onDecrement: {
|
||||
storedLineSpacing = max(storedLineSpacing - 25, 100)
|
||||
updateReaderPreferences()
|
||||
}
|
||||
)
|
||||
|
||||
HStack {
|
||||
NavigationLink(destination: fontList) {
|
||||
Text("Font")
|
||||
}
|
||||
Spacer()
|
||||
Button(action: {}, label: { Text("Crimson Text").frame(width: 91) })
|
||||
.buttonStyle(RoundedRectButtonStyle())
|
||||
}
|
||||
.frame(height: 40)
|
||||
|
||||
Toggle("High Contrast Text:", isOn: $prefersHighContrastText)
|
||||
.frame(height: 40)
|
||||
.padding(.trailing, 6)
|
||||
.onChange(of: prefersHighContrastText) { _ in
|
||||
updateReaderPreferences()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.navigationTitle("Reader Preferences")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
Button(
|
||||
action: dismissAction,
|
||||
label: { Text("Done").foregroundColor(.appGrayTextContrast).padding() }
|
||||
)
|
||||
}
|
||||
}
|
||||
// .toolbar {
|
||||
// ToolbarItem(placement: .barTrailing) {
|
||||
// Button(
|
||||
// action: dismissAction,
|
||||
// label: { Text("Done").foregroundColor(.appGrayTextContrast).padding() }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
.accentColor(.appGrayTextContrast)
|
||||
// .navigationViewStyle(.stack)
|
||||
// .accentColor(.appGrayTextContrast)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
54
apple/OmnivoreKit/Sources/Views/Theme.swift
Normal file
54
apple/OmnivoreKit/Sources/Views/Theme.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Jackson Harper on 10/27/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Utils
|
||||
|
||||
public enum Theme: String, CaseIterable {
|
||||
case system = "System"
|
||||
case sepia = "Sepia"
|
||||
case charcoal = "Charcoal"
|
||||
case mint = "Mint"
|
||||
|
||||
case solarized = "Solarized"
|
||||
|
||||
case light = "Light"
|
||||
case dark = "Dark"
|
||||
|
||||
public var bgColor: Color {
|
||||
switch self {
|
||||
case .system:
|
||||
return Color.systemBackground
|
||||
case .charcoal:
|
||||
return Color(red: 48 / 255.0, green: 48 / 255.0, blue: 48 / 255.0)
|
||||
case .sepia:
|
||||
return Color(red: 249 / 255.0, green: 241 / 255.0, blue: 220 / 255.0)
|
||||
case .mint:
|
||||
return Color(red: 202 / 255.0, green: 230 / 255.0, blue: 208 / 255.0)
|
||||
case .solarized:
|
||||
return Color(red: 13 / 255.0, green: 39 / 255.0, blue: 50 / 255.0)
|
||||
case .light:
|
||||
return Color.white
|
||||
case .dark:
|
||||
return Color.black
|
||||
}
|
||||
}
|
||||
|
||||
public static func fromName(themeName: String) -> Theme? {
|
||||
for theme in Theme.allCases {
|
||||
if theme.rawValue == themeName {
|
||||
return theme
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public enum ThemeManager {
|
||||
@AppStorage(UserDefaultKey.themeName.rawValue) public static var currentThemeName = "System"
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import WebKit
|
|||
webView.isOpaque = false
|
||||
webView.backgroundColor = UIColor.clear
|
||||
if let url = request.url {
|
||||
// let themeID = Color.isDarkMode ? "Gray" /* "Sepia" */ : "Charcoal"
|
||||
let themeID = Color.isDarkMode ? "Gray" : "LightGray"
|
||||
webView.injectCookie(cookieString: "theme=\(themeID); Max-Age=31536000;", url: url)
|
||||
}
|
||||
|
|
@ -51,7 +52,7 @@ import WebKit
|
|||
if let url = request.url {
|
||||
// Dark mode is still rendering a white background on mac for some reason.
|
||||
// Forcing light mode for now until we figure out a fix
|
||||
let themeID = "LightGray" // NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua ? "Gray" : "LightGray"
|
||||
let themeID = "Charcoal" // NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua ? "Gray" : "LightGray"
|
||||
webView.injectCookie(cookieString: "theme=\(themeID); Max-Age=31536000;", url: url)
|
||||
}
|
||||
return webView
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { ReportIssuesModal } from './ReportIssuesModal'
|
|||
import { reportIssueMutation } from '../../../lib/networking/mutations/reportIssueMutation'
|
||||
import { ArticleHeaderToolbar } from './ArticleHeaderToolbar'
|
||||
import { userPersonalizationMutation } from '../../../lib/networking/mutations/userPersonalizationMutation'
|
||||
import { updateThemeLocally } from '../../../lib/themeUpdater'
|
||||
import { updateTheme, updateThemeLocally } from '../../../lib/themeUpdater'
|
||||
import { ArticleMutations } from '../../../lib/articleActions'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
|
|
@ -121,6 +121,17 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
}
|
||||
}
|
||||
|
||||
interface UpdateThemeEvent extends Event {
|
||||
themeName?: string
|
||||
}
|
||||
|
||||
const handleThemeChange = async (event: UpdateThemeEvent) => {
|
||||
const newTheme = event.themeName
|
||||
if (newTheme) {
|
||||
updateTheme(newTheme)
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateColorModeEvent extends Event {
|
||||
isDark?: string
|
||||
}
|
||||
|
|
@ -145,6 +156,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
'updateMaxWidthPercentage',
|
||||
updateMaxWidthPercentage
|
||||
)
|
||||
document.addEventListener('updateTheme', handleThemeChange)
|
||||
document.addEventListener('updateFontSize', handleFontSizeChange)
|
||||
document.addEventListener('updateColorMode', updateColorMode)
|
||||
document.addEventListener(
|
||||
|
|
@ -160,6 +172,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
'updateMaxWidthPercentage',
|
||||
updateMaxWidthPercentage
|
||||
)
|
||||
document.removeEventListener('updateTheme', handleThemeChange)
|
||||
document.removeEventListener('updateFontSize', handleFontSizeChange)
|
||||
document.removeEventListener('updateColorMode', updateColorMode)
|
||||
document.removeEventListener(
|
||||
|
|
@ -179,7 +192,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
readerFontColor: highContrastFont
|
||||
? theme.colors.readerFontHighContrast.toString()
|
||||
: theme.colors.readerFont.toString(),
|
||||
readerFontColorTransparent: theme.colors.readerFontTransparent.toString(),
|
||||
readerTableHeaderColor: theme.colors.readerTableHeader.toString(),
|
||||
readerHeadersColor: theme.colors.readerHeader.toString(),
|
||||
}
|
||||
|
|
@ -193,7 +205,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
maxWidth: `${styles.maxWidthPercentage ?? 100}%`,
|
||||
background: props.isAppleAppEmbed
|
||||
? 'unset'
|
||||
: theme.colors.grayBg.toString(),
|
||||
: theme.colors.readerBg.toString(),
|
||||
'--text-font-family': styles.fontFamily,
|
||||
'--text-font-size': `${styles.fontSize}px`,
|
||||
'--line-height': `${styles.lineHeight}%`,
|
||||
|
|
@ -202,7 +214,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
'--figure-margin': '1.6rem auto',
|
||||
'--hr-margin': '1em',
|
||||
'--font-color': styles.readerFontColor,
|
||||
'--font-color-transparent': styles.readerFontColorTransparent,
|
||||
'--table-header-color': styles.readerTableHeaderColor,
|
||||
'--headers-color': styles.readerHeadersColor,
|
||||
'@sm': {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export function ReaderSettingsControl(props: ReaderSettingsProps): JSX.Element {
|
|||
}}
|
||||
>
|
||||
<StyledText
|
||||
color={theme.colors.readerFontTransparent.toString()}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
css={{ pl: '12px', m: '0px', pt: '14px' }}
|
||||
>
|
||||
Margin:
|
||||
|
|
@ -193,7 +193,7 @@ export function ReaderSettingsControl(props: ReaderSettingsProps): JSX.Element {
|
|||
}}
|
||||
>
|
||||
<StyledText
|
||||
color={theme.colors.readerFontTransparent.toString()}
|
||||
color={theme.colors.readerFont.toString()}
|
||||
css={{ pl: '12px', m: '0px', pt: '14px' }}
|
||||
>
|
||||
Line Spacing:
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export function SkeletonArticleContainer(props: SkeletonArticleContainerProps):
|
|||
lineHeight: props.lineHeight ?? 150,
|
||||
fontFamily: props.fontFamily ?? 'inter',
|
||||
readerFontColor: theme.colors.readerFont.toString(),
|
||||
readerFontColorTransparent: theme.colors.readerFontTransparent.toString(),
|
||||
readerTableHeaderColor: theme.colors.readerTableHeader.toString(),
|
||||
readerHeadersColor: theme.colors.readerHeader.toString(),
|
||||
}
|
||||
|
|
@ -40,7 +39,6 @@ export function SkeletonArticleContainer(props: SkeletonArticleContainerProps):
|
|||
'--figure-margin': '1.6rem auto',
|
||||
'--hr-margin': '1em',
|
||||
'--font-color': styles.readerFontColor,
|
||||
'--font-color-transparent': styles.readerFontColorTransparent,
|
||||
'--table-header-color': styles.readerTableHeaderColor,
|
||||
'--headers-color': styles.readerHeadersColor,
|
||||
'@sm': {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ export enum ThemeId {
|
|||
Light = 'LightGray',
|
||||
Dark = 'Gray',
|
||||
Darker = 'Dark',
|
||||
Sepia = 'Sepia',
|
||||
Charcoal = 'Charcoal'
|
||||
}
|
||||
|
||||
export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
|
||||
|
|
@ -211,7 +213,6 @@ const darkThemeSpec = {
|
|||
readerBg: '#303030',
|
||||
readerFont: '#b9b9b9',
|
||||
readerFontHighContrast: 'white',
|
||||
readerFontTransparent: 'rgba(185,185,185,0.65)',
|
||||
readerHeader: '#b9b9b9',
|
||||
readerTableHeader: '#FFFFFF',
|
||||
tooltipIcons: '#5F5E58',
|
||||
|
|
@ -236,12 +237,35 @@ const darkThemeSpec = {
|
|||
},
|
||||
}
|
||||
|
||||
// Avatar Fallback color
|
||||
const sepiaThemeSpec = {
|
||||
colors: {
|
||||
// Reader Colors
|
||||
readerBg: '#F9F1DC',
|
||||
readerFont: '#554A34',
|
||||
readerFontHighContrast: 'black',
|
||||
readerHeader: '554A34',
|
||||
readerTableHeader: '#FFFFFF',
|
||||
}
|
||||
}
|
||||
|
||||
const charcoalThemeSpec = {
|
||||
colors: {
|
||||
// Reader Colors
|
||||
readerBg: '#303030',
|
||||
readerFont: '#b9b9b9',
|
||||
readerFontHighContrast: 'white',
|
||||
readerHeader: '#b9b9b9',
|
||||
readerTableHeader: '#FFFFFF',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Dark and Darker theme now match each other.
|
||||
// Use the darkThemeSpec object to make updates.
|
||||
export const darkTheme = createTheme(ThemeId.Dark, darkThemeSpec)
|
||||
export const darkerTheme = createTheme(ThemeId.Darker, darkThemeSpec)
|
||||
export const sepiaTheme = createTheme(ThemeId.Sepia, {...darkThemeSpec, ...sepiaThemeSpec})
|
||||
export const charcoalTheme = createTheme(ThemeId.Charcoal, {...darkThemeSpec, ...charcoalThemeSpec})
|
||||
|
||||
// Lighter theme now matches the default theme.
|
||||
// This only exists for users that might still have a lighter theme set
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import {
|
|||
lighterTheme,
|
||||
darkTheme,
|
||||
darkerTheme,
|
||||
sepiaTheme,
|
||||
charcoalTheme,
|
||||
} from '../components/tokens/stitches.config'
|
||||
import { userPersonalizationMutation } from './networking/mutations/userPersonalizationMutation'
|
||||
|
||||
|
|
@ -24,9 +26,14 @@ export function updateThemeLocally(themeId: string): void {
|
|||
|
||||
document.body.classList.remove(
|
||||
lighterTheme,
|
||||
ThemeId.Light,
|
||||
darkTheme,
|
||||
darkerTheme
|
||||
darkerTheme,
|
||||
ThemeId.Light,
|
||||
ThemeId.Dark,
|
||||
ThemeId.Darker,
|
||||
ThemeId.Lighter,
|
||||
ThemeId.Sepia,
|
||||
ThemeId.Charcoal,
|
||||
)
|
||||
document.body.classList.add(themeId)
|
||||
}
|
||||
|
|
@ -41,6 +48,10 @@ export function currentThemeName(): string {
|
|||
return 'Darker'
|
||||
case ThemeId.Lighter:
|
||||
return 'Lighter'
|
||||
case ThemeId.Sepia:
|
||||
return 'Sepia'
|
||||
case ThemeId.Charcoal:
|
||||
return 'Charcoal'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable functional/no-class */
|
||||
import { useEffect } from 'react'
|
||||
import NextDocument, { Html, Head, Main, NextScript } from 'next/document'
|
||||
import { getCssText, globalStyles } from '../components/tokens/stitches.config'
|
||||
|
||||
|
|
@ -35,7 +34,7 @@ export default class Document extends NextDocument {
|
|||
var themeId = window.localStorage.getItem('theme')
|
||||
|
||||
if (themeId) {
|
||||
document.body.classList.remove('theme-default', 'White', 'Gray', 'LightGray', 'Dark')
|
||||
document.body.classList.remove('theme-default', 'White', 'Gray', 'LightGray', 'Dark', 'Sepia', 'Charcoal')
|
||||
document.body.classList.add(themeId)
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -362,8 +362,9 @@ on smaller screens we display the note icon
|
|||
width: 20%;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet {
|
||||
background: #ffffff;
|
||||
.article-inner-css ._omnivore-static-tweet {
|
||||
color: #0F1419 !important;
|
||||
background: #ffffff !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
|
@ -377,74 +378,108 @@ on smaller screens we display the note icon
|
|||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
._omnivore-static-tweet ._omnivore-static-tweet-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
margin: unset;
|
||||
justify-content: flex-start;
|
||||
color: #0F1419 !important;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-link-top {
|
||||
|
||||
._omnivore-static-tweet-header ._omnivore-static-tweet-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #0F1419 !important;
|
||||
color: #496F72;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-header-text .tweet-author-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-header-text .tweet-author-handle {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
|
||||
._omnivore-static-tweet-header .tweet-fake-link {
|
||||
color: #1da1f2;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet ._omnivore-static-tweet-text {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #0F1419 !important;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet ._omnivore-static-tweet-link-top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-header-avatar {
|
||||
._omnivore-static-tweet ._omnivore-static-tweet-header-avatar {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: none !important;
|
||||
border-radius: 50%;
|
||||
float: left;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
margin: 0;
|
||||
margin: unset !important;
|
||||
margin-right: 12px;
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
._omnivore-static-tweet ._omnivore-static-tweet-link-bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-decoration: none;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-link-bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-decoration: none;
|
||||
white-space: pre-wrap;
|
||||
._omnivore-static-tweet ._omnivore-static-tweet-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
color: #808080;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-footer hr {
|
||||
._omnivore-static-tweet ._omnivore-static-tweet-footer hr {
|
||||
background: #e0e0e0;
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 12px 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
color: #496F72;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-author-handle {
|
||||
._omnivore-static-tweet ._omnivore-static-tweet-author-handle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-ufi {
|
||||
._omnivore-static-tweet ._omnivore-static-tweet-ufi {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
._omnivore-static-tweet-ufi .likes, .retweets {
|
||||
._omnivore-static-tweet ._omnivore-static-tweet-ufi .likes, .retweets {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
text-decoration: none;
|
||||
color: #808080;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
Loading…
Reference in a new issue