Merge pull request #910 from omnivore-app/macos-read-now

macOS Share Extension Saving
This commit is contained in:
Satindar Dhillon 2022-07-12 22:41:07 -07:00 committed by GitHub
commit d69a43d00f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 848 additions and 757 deletions

View file

@ -153,6 +153,15 @@
"version" : "2.1.0"
}
},
{
"identity" : "pspdfkit-sp",
"kind" : "remoteSourceControl",
"location" : "https://github.com/PSPDFKit/PSPDFKit-SP",
"state" : {
"branch" : "master",
"revision" : "e9757beadad1b30de84073d3c33c1cd0f7a94b80"
}
},
{
"identity" : "sovran-swift",
"kind" : "remoteSourceControl",

View file

@ -55,9 +55,9 @@ let package = Package(
var appPackageDependencies: [Target.Dependency] {
var deps: [Target.Dependency] = ["Views", "Services", "Models", "Utils"]
#if canImport(UIKit)
deps.append(.product(name: "PSPDFKit", package: "PSPDFKit-SP"))
#endif
// #if canImport(UIKit)
deps.append(.product(name: "PSPDFKit", package: "PSPDFKit-SP"))
// #endif
return deps
}
@ -69,8 +69,8 @@ var dependencies: [Package.Dependency] {
.package(url: "git@github.com:segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2")
]
#if canImport(UIKit)
deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", branch: "master"))
#endif
// #if canImport(UIKit)
deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", branch: "master"))
// #endif
return deps
}

View file

@ -1,198 +0,0 @@
import Foundation
import Models
import Services
import Utils
import Views
class ExtensionSaveService {
let queue: OperationQueue
init() {
self.queue = OperationQueue()
}
#if os(iOS)
private func queueSaveOperation(
_ pageScrape: PageScrapePayload,
shareExtensionViewModel: ShareExtensionChildViewModel
) {
ProcessInfo().performExpiringActivity(withReason: "app.omnivore.SaveActivity") { [self] expiring in
guard !expiring else {
self.queue.cancelAllOperations()
self.queue.waitUntilAllOperationsAreFinished()
return
}
let operation = SaveOperation(pageScrapePayload: pageScrape, shareExtensionViewModel: shareExtensionViewModel)
self.queue.addOperation(operation)
self.queue.waitUntilAllOperationsAreFinished()
}
}
#endif
public func save(_ extensionContext: NSExtensionContext, shareExtensionViewModel: ShareExtensionChildViewModel) {
PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in
guard let self = self else { return }
switch result {
case let .success(payload):
DispatchQueue.main.async {
shareExtensionViewModel.status = .saved
let url = URLComponents(string: payload.url)
let hostname = URL(string: payload.url)?.host ?? ""
switch payload.contentType {
case let .html(html: _, title: title, iconURL: iconURL):
shareExtensionViewModel.title = title
shareExtensionViewModel.iconURL = iconURL
shareExtensionViewModel.url = hostname
case .none:
shareExtensionViewModel.url = hostname
shareExtensionViewModel.title = payload.url
if var url = url {
url.path = "/favicon.ico"
shareExtensionViewModel.iconURL = url.url?.absoluteString
}
case let .pdf(localUrl: localUrl):
shareExtensionViewModel.url = hostname
shareExtensionViewModel.title = PDFUtils.titleFromPdfFile(localUrl.absoluteString)
Task {
let localThumbnail = try await PDFUtils.createThumbnailFor(inputUrl: localUrl)
DispatchQueue.main.async {
shareExtensionViewModel.iconURL = localThumbnail?.absoluteString
}
}
}
}
#if os(iOS)
// TODO: need alternative call for macos
self.queueSaveOperation(payload, shareExtensionViewModel: shareExtensionViewModel)
#endif
case .failure:
DispatchQueue.main.async {
shareExtensionViewModel.status = .failed(error: .unknown(description: "Could not retrieve content"))
}
}
}
}
class SaveOperation: Operation, URLSessionDelegate {
let services: Services
let pageScrapePayload: PageScrapePayload
let shareExtensionViewModel: ShareExtensionChildViewModel
var queue: OperationQueue?
var uploadTask: URLSessionTask?
// swiftlint:disable:next nesting
enum State: Int {
case created
case started
case finished
}
init(pageScrapePayload: PageScrapePayload, shareExtensionViewModel: ShareExtensionChildViewModel) {
self.pageScrapePayload = pageScrapePayload
self.shareExtensionViewModel = shareExtensionViewModel
self.state = .created
self.services = Services()
}
open var state: State = .created {
willSet {
willChangeValue(forKey: "isReady")
willChangeValue(forKey: "isExecuting")
willChangeValue(forKey: "isFinished")
willChangeValue(forKey: "isCancelled")
}
didSet {
didChangeValue(forKey: "isCancelled")
didChangeValue(forKey: "isFinished")
didChangeValue(forKey: "isExecuting")
didChangeValue(forKey: "isReady")
}
}
override var isAsynchronous: Bool {
true
}
override var isReady: Bool {
true
}
override var isExecuting: Bool {
self.state == .started
}
override var isFinished: Bool {
self.state == .finished
}
override func start() {
guard !isCancelled else { return }
state = .started
queue = OperationQueue()
Task {
await persist(services: self.services, pageScrapePayload: self.pageScrapePayload)
}
}
override func cancel() {
super.cancel()
}
private func updateStatus(_ requestId: String?, newStatus: ShareExtensionStatus) {
DispatchQueue.main.async {
self.shareExtensionViewModel.status = newStatus
if let requestId = requestId {
self.shareExtensionViewModel.requestId = requestId
}
}
}
private func persist(services: Services, pageScrapePayload: PageScrapePayload) async {
var requestId = shareExtensionViewModel.requestId
do {
try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId)
} catch {
updateStatus(nil, newStatus: .failed(error: SaveArticleError.unknown(description: "Unable to access content")))
return
}
do {
updateStatus(requestId, newStatus: .saved)
switch pageScrapePayload.contentType {
case .none:
requestId = try await services.dataService.createPageFromUrl(id: requestId, url: pageScrapePayload.url)
case let .pdf(localUrl):
try await services.dataService.createPageFromPdf(
id: requestId,
localPdfURL: localUrl,
url: pageScrapePayload.url
)
case let .html(html, title, _):
requestId = try await services.dataService.createPage(
id: requestId,
originalHtml: html,
title: title,
url: pageScrapePayload.url
)
}
} catch {
updateStatus(nil, newStatus: .syncFailed(error: SaveArticleError.unknown(description: "Unknown Error")))
return
}
updateStatus(requestId, newStatus: .synced)
state = .finished
}
}
}

View file

@ -0,0 +1,76 @@
import Foundation
import Models
import Services
import Utils
import Views
final class ShareExtensionSaveOperation: Operation, URLSessionDelegate {
let pageScrapePayload: PageScrapePayload
let shareExtensionViewModel: ShareExtensionViewModel
var queue: OperationQueue?
var uploadTask: URLSessionTask?
enum State: Int {
case created
case started
case finished
}
init(pageScrapePayload: PageScrapePayload, shareExtensionViewModel: ShareExtensionViewModel) {
self.pageScrapePayload = pageScrapePayload
self.shareExtensionViewModel = shareExtensionViewModel
self.state = .created
}
public var state: State = .created {
willSet {
willChangeValue(forKey: "isReady")
willChangeValue(forKey: "isExecuting")
willChangeValue(forKey: "isFinished")
willChangeValue(forKey: "isCancelled")
}
didSet {
didChangeValue(forKey: "isCancelled")
didChangeValue(forKey: "isFinished")
didChangeValue(forKey: "isExecuting")
didChangeValue(forKey: "isReady")
}
}
override var isAsynchronous: Bool {
true
}
override var isReady: Bool {
true
}
override var isExecuting: Bool {
self.state == .started
}
override var isFinished: Bool {
self.state == .finished
}
override func start() {
guard !isCancelled else { return }
state = .started
queue = OperationQueue()
Task {
let pageCreated = await shareExtensionViewModel.createPage(
pageScrapePayload: pageScrapePayload
)
if pageCreated {
state = .finished
}
}
}
override func cancel() {
super.cancel()
}
}

View file

@ -1,9 +1,5 @@
import Foundation
import Models
import Services
import SwiftUI
import Utils
import Views
public extension PlatformViewController {
static func makeShareExtensionController(extensionContext: NSExtensionContext?) -> PlatformViewController {
@ -18,52 +14,3 @@ public extension PlatformViewController {
return hostingController
}
}
public class ShareExtensionViewModel: ObservableObject {
@Published var title: String?
@Published var debugText: String?
let saveService = ExtensionSaveService()
func handleReadNowAction(requestId: String, extensionContext: NSExtensionContext?) {
#if os(iOS)
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)")
application.perform(NSSelectorFromString("openURL:"), with: deepLinkUrl)
}
#endif
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
func savePage(extensionContext: NSExtensionContext?, shareExtensionViewModel: ShareExtensionChildViewModel) {
if let extensionContext = extensionContext {
saveService.save(extensionContext, shareExtensionViewModel: shareExtensionViewModel)
} else {
DispatchQueue.main.async {
shareExtensionViewModel.status = .failed(error: .unknown(description: "Internal Error"))
}
}
}
}
struct ShareExtensionView: View {
let extensionContext: NSExtensionContext?
@StateObject private var viewModel = ShareExtensionViewModel()
@StateObject private var childViewModel = ShareExtensionChildViewModel()
var body: some View {
ShareExtensionChildView(
viewModel: childViewModel,
onAppearAction: {
viewModel.savePage(
extensionContext: extensionContext,
shareExtensionViewModel: childViewModel
)
},
readNowButtonAction: { viewModel.handleReadNowAction(requestId: $0, extensionContext: extensionContext) },
dismissButtonTappedAction: { _, _ in
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
)
}
}

View file

@ -0,0 +1,194 @@
import Models
import SwiftUI
import Utils
import Views
public class ShareExtensionViewModel: ObservableObject {
@Published public var status: ShareExtensionStatus = .processing
@Published public var title: String?
@Published public var url: String?
@Published public var iconURL: String?
@Published public var linkedItem: LinkedItem?
@Published public var requestId = UUID().uuidString.lowercased()
@Published var debugText: String?
let services = Services()
let queue = OperationQueue()
func handleReadNowAction(extensionContext: NSExtensionContext?) {
#if os(iOS)
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {
let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)")
application.perform(NSSelectorFromString("openURL:"), with: deepLinkUrl)
}
#endif
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
func savePage(extensionContext: NSExtensionContext?) {
if let extensionContext = extensionContext {
save(extensionContext)
} else {
DispatchQueue.main.async {
self.status = .failed(error: .unknown(description: "Internal Error"))
}
}
}
#if os(iOS)
func queueSaveOperation(_ payload: PageScrapePayload) {
ProcessInfo().performExpiringActivity(withReason: "app.omnivore.SaveActivity") { [self] expiring in
guard !expiring else {
self.queue.cancelAllOperations()
self.queue.waitUntilAllOperationsAreFinished()
return
}
let operation = ShareExtensionSaveOperation(pageScrapePayload: payload, shareExtensionViewModel: self)
self.queue.addOperation(operation)
self.queue.waitUntilAllOperationsAreFinished()
}
}
#endif
public func save(_ extensionContext: NSExtensionContext) {
PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in
guard let self = self else { return }
switch result {
case let .success(payload):
DispatchQueue.main.async {
self.status = .saved
let url = URLComponents(string: payload.url)
let hostname = URL(string: payload.url)?.host ?? ""
switch payload.contentType {
case let .html(html: _, title: title, iconURL: iconURL):
self.title = title
self.iconURL = iconURL
self.url = hostname
case .none:
self.url = hostname
self.title = payload.url
if var url = url {
url.path = "/favicon.ico"
self.iconURL = url.url?.absoluteString
}
case let .pdf(localUrl: localUrl):
self.url = hostname
self.title = PDFUtils.titleFromPdfFile(localUrl.absoluteString)
Task {
let localThumbnail = try await PDFUtils.createThumbnailFor(inputUrl: localUrl)
DispatchQueue.main.async {
self.iconURL = localThumbnail?.absoluteString
}
}
}
}
#if os(iOS)
self.queueSaveOperation(payload)
#else
Task {
await createPage(services: self.services, pageScrapePayload: payload)
}
#endif
case .failure:
DispatchQueue.main.async {
self.status = .failed(error: .unknown(description: "Could not retrieve content"))
}
}
}
}
func createPage(pageScrapePayload: PageScrapePayload) async -> Bool {
var newRequestID: String?
do {
try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId)
} catch {
updateStatusOnMain(
requestId: nil,
newStatus: .failed(error: SaveArticleError.unknown(description: "Unable to access content"))
)
return false
}
do {
updateStatusOnMain(requestId: requestId, newStatus: .saved)
switch pageScrapePayload.contentType {
case .none:
newRequestID = try await services.dataService.createPageFromUrl(id: requestId, url: pageScrapePayload.url)
case let .pdf(localUrl):
try await services.dataService.createPageFromPdf(
id: requestId,
localPdfURL: localUrl,
url: pageScrapePayload.url
)
case let .html(html, title, _):
newRequestID = try await services.dataService.createPage(
id: requestId,
originalHtml: html,
title: title,
url: pageScrapePayload.url
)
}
} catch {
updateStatusOnMain(
requestId: nil,
newStatus: .syncFailed(error: SaveArticleError.unknown(description: "Unknown Error"))
)
return false
}
updateStatusOnMain(requestId: newRequestID, newStatus: .synced)
return true
}
func updateStatusOnMain(requestId: String?, newStatus: ShareExtensionStatus) {
DispatchQueue.main.async {
self.status = newStatus
if let requestId = requestId {
self.requestId = requestId
}
}
}
}
public enum ShareExtensionStatus {
case processing
case saved
case synced
case failed(error: SaveArticleError)
case syncFailed(error: SaveArticleError)
var displayMessage: String {
switch self {
case .processing:
return LocalText.saveArticleProcessingState
case .saved:
return LocalText.saveArticleSavedState
case .synced:
return "Synced"
case let .failed(error: error):
return "Save failed \(error.displayMessage)"
case let .syncFailed(error: error):
return "Sync failed \(error.displayMessage)"
}
}
}
private extension SaveArticleError {
var displayMessage: String {
switch self {
case .unauthorized:
return LocalText.extensionAppUnauthorized
case .network:
return LocalText.networkError
case .badData, .unknown:
return LocalText.genericError
}
}
}

View file

@ -1,126 +1,16 @@
import Models
import Services
import SwiftUI
import Utils
import Views
public class ShareExtensionChildViewModel: ObservableObject {
@Published public var status: ShareExtensionStatus = .processing
@Published public var title: String?
@Published public var url: String?
@Published public var iconURL: String?
@Published public var requestId: String
public init() {
self.requestId = UUID().uuidString.lowercased()
}
}
public enum ShareExtensionStatus {
case processing
case saved
case synced
case failed(error: SaveArticleError)
case syncFailed(error: SaveArticleError)
var displayMessage: String {
switch self {
case .processing:
return LocalText.saveArticleProcessingState
case .saved:
return LocalText.saveArticleSavedState
case .synced:
return "Synced"
case let .failed(error: error):
return "Save failed \(error.displayMessage)"
case let .syncFailed(error: error):
return "Sync failed \(error.displayMessage)"
}
}
}
private extension SaveArticleError {
var displayMessage: String {
switch self {
case .unauthorized:
return LocalText.extensionAppUnauthorized
case .network:
return LocalText.networkError
case .badData, .unknown:
return LocalText.genericError
}
}
}
struct IconButtonView: View {
let title: String
let systemIconName: String
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(alignment: .center, spacing: 8) {
Image(systemName: systemIconName)
.font(.appTitle)
.foregroundColor(.appYellow48)
Text(title)
.font(.appBody)
.foregroundColor(.appGrayText)
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity
)
.background(Color.appButtonBackground)
.cornerRadius(8)
}
.frame(height: 100)
}
}
struct CheckmarkButtonView: View {
let titleText: String
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(
action: action,
label: {
HStack {
Text(titleText)
Spacer()
if isSelected {
Image(systemName: "checkmark")
.foregroundColor(.appYellow48)
}
}
.padding(.vertical, 8)
}
)
.buttonStyle(RectButtonStyle())
}
}
public struct ShareExtensionChildView: View {
let viewModel: ShareExtensionChildViewModel
let onAppearAction: () -> Void
let readNowButtonAction: (String) -> Void
let dismissButtonTappedAction: (ReminderTime?, Bool) -> Void
public struct ShareExtensionView: View {
let extensionContext: NSExtensionContext?
@StateObject private var viewModel = ShareExtensionViewModel()
@State var reminderTime: ReminderTime?
@State var hideUntilReminded = false
public init(
viewModel: ShareExtensionChildViewModel,
onAppearAction: @escaping () -> Void,
readNowButtonAction: @escaping (String) -> Void,
dismissButtonTappedAction: @escaping (ReminderTime?, Bool) -> Void
) {
self.viewModel = viewModel
self.onAppearAction = onAppearAction
self.readNowButtonAction = readNowButtonAction
self.dismissButtonTappedAction = dismissButtonTappedAction
}
private func handleReminderTimeSelection(_ selectedTime: ReminderTime) {
if selectedTime == reminderTime {
reminderTime = nil
@ -263,16 +153,20 @@ public struct ShareExtensionChildView: View {
Spacer()
if let item = viewModel.linkedItem {
ApplyLabelsView(mode: .item(item), onSave: nil)
}
HStack {
Button(
action: { readNowButtonAction(self.viewModel.requestId) },
action: { viewModel.handleReadNowAction(extensionContext: extensionContext) },
label: { Text("Read Now").frame(maxWidth: .infinity) }
)
.buttonStyle(RoundedRectButtonStyle())
Button(
action: {
dismissButtonTappedAction(reminderTime, hideUntilReminded)
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
},
label: {
Text("Dismiss")
@ -290,7 +184,8 @@ public struct ShareExtensionChildView: View {
alignment: .topLeading
)
.onAppear {
onAppearAction()
viewModel.savePage(extensionContext: extensionContext)
}
.environmentObject(viewModel.services.dataService)
}
}

View file

@ -0,0 +1,53 @@
import SwiftUI
import Views
// TODO: maybe move this into Views package?
struct IconButtonView: View {
let title: String
let systemIconName: String
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(alignment: .center, spacing: 8) {
Image(systemName: systemIconName)
.font(.appTitle)
.foregroundColor(.appYellow48)
Text(title)
.font(.appBody)
.foregroundColor(.appGrayText)
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity
)
.background(Color.appButtonBackground)
.cornerRadius(8)
}
.frame(height: 100)
}
}
struct CheckmarkButtonView: View {
let titleText: String
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(
action: action,
label: {
HStack {
Text(titleText)
Spacer()
if isSelected {
Image(systemName: "checkmark")
.foregroundColor(.appYellow48)
}
}
.padding(.vertical, 8)
}
)
.buttonStyle(RectButtonStyle())
}
}

View file

@ -0,0 +1,120 @@
import SwiftUI
import Utils
import Views
#if os(macOS)
public struct MacMenuCommands: Commands {
@AppStorage(UserDefaultKey.preferredWebFontSize.rawValue) var storedFontSize = Int(
NSFont.userFont(ofSize: 16)?.pointSize ?? 16
)
@AppStorage(UserDefaultKey.preferredWebLineSpacing.rawValue) var storedLineSpacing = 150
@AppStorage(UserDefaultKey.preferredWebMaxWidthPercentage.rawValue) var storedMaxWidthPercentage = 100
@Binding var preferredFont: String
@Binding var prefersHighContrastText: Bool
public var fontSizeButtons: some View {
Group {
Button(
action: {
storedFontSize = max(storedFontSize - 2, 10)
NSNotification.readerSettingsChanged()
},
label: { Text("Decrease Font Size")
}
)
.keyboardShortcut("-")
Button(
action: {
storedFontSize = min(storedFontSize + 2, 28)
NSNotification.readerSettingsChanged()
},
label: { Text("Increase Font Size") }
)
.keyboardShortcut("+")
}
}
public var marginSizeButtons: some View {
Group {
Button(
action: {
storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100)
NSNotification.readerSettingsChanged()
},
label: { Text("Decrease Margin")
}
)
.keyboardShortcut("[")
Button(
action: {
storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40)
NSNotification.readerSettingsChanged()
},
label: { Text("Increase Margin")
}
)
.keyboardShortcut("]")
}
}
public var lineSpacingButtons: some View {
Group {
Button(
action: {
storedLineSpacing = max(storedLineSpacing - 25, 100)
NSNotification.readerSettingsChanged()
},
label: { Text("Decrease Line Spacing") }
)
.keyboardShortcut("k")
Button(
action: {
storedLineSpacing = min(storedLineSpacing + 25, 300)
NSNotification.readerSettingsChanged()
},
label: { Text("Increase Line Spacing") }
)
.keyboardShortcut("l")
}
}
public init(
preferredFont: Binding<String>,
prefersHighContrastText: Binding<Bool>
) {
self._preferredFont = preferredFont
self._prefersHighContrastText = prefersHighContrastText
}
public var body: some Commands {
CommandMenu("Reader Display") {
fontSizeButtons
Divider()
marginSizeButtons
Divider()
lineSpacingButtons
Divider()
Picker(selection: $preferredFont, label: Text("Font Family")) {
ForEach(WebFont.allCases, id: \.self) { font in
Text(font.displayValue).tag(font.rawValue)
}
}
Toggle(
isOn: $prefersHighContrastText,
label: { Text("High Contrast Text") }
)
}
}
}
#endif

View file

@ -22,7 +22,6 @@ import Views
ZStack {
if let linkRequest = viewModel.linkRequest {
NavigationLink(
// TODO: add alt for macOS
destination: WebReaderLoadingContainer(requestID: linkRequest.serverID),
tag: linkRequest,
selection: $viewModel.linkRequest

View file

@ -18,93 +18,104 @@ import Views
}
var body: some View {
List {
Section {
ForEach(viewModel.items) { item in
FeedCardNavigationLink(
item: item,
viewModel: viewModel
)
.contextMenu {
Button(
action: { viewModel.itemUnderTitleEdit = item },
label: { Label("Edit Title/Description", systemImage: "textbox") }
ZStack {
if let linkRequest = viewModel.linkRequest {
NavigationLink(
destination: WebReaderLoadingContainer(requestID: linkRequest.serverID),
tag: linkRequest,
selection: $viewModel.linkRequest
) {
EmptyView()
}
}
List {
Section {
ForEach(viewModel.items) { item in
FeedCardNavigationLink(
item: item,
viewModel: viewModel
)
Button(
action: { viewModel.itemUnderLabelEdit = item },
label: { Label("Edit Labels", systemImage: "tag") }
)
Button(action: {
withAnimation(.linear(duration: 0.4)) {
viewModel.setLinkArchived(
dataService: dataService,
objectID: item.objectID,
archived: !item.isArchived
)
}
}, label: {
Label(
item.isArchived ? "Unarchive" : "Archive",
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
.contextMenu {
Button(
action: { viewModel.itemUnderTitleEdit = item },
label: { Label("Edit Title/Description", systemImage: "textbox") }
)
})
Button(
action: {
itemToRemove = item
confirmationShown = true
},
label: { Label("Delete", systemImage: "trash") }
)
if FeatureFlag.enableSnooze {
Button {
viewModel.itemToSnoozeID = item.id
viewModel.snoozePresented = true
} label: {
Label { Text("Snooze") } icon: { Image.moon }
Button(
action: { viewModel.itemUnderLabelEdit = item },
label: { Label("Edit Labels", systemImage: "tag") }
)
Button(action: {
withAnimation(.linear(duration: 0.4)) {
viewModel.setLinkArchived(
dataService: dataService,
objectID: item.objectID,
archived: !item.isArchived
)
}
}, label: {
Label(
item.isArchived ? "Unarchive" : "Archive",
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
)
})
Button(
action: {
itemToRemove = item
confirmationShown = true
},
label: { Label("Delete", systemImage: "trash") }
)
if FeatureFlag.enableSnooze {
Button {
viewModel.itemToSnoozeID = item.id
viewModel.snoozePresented = true
} label: {
Label { Text("Snooze") } icon: { Image.moon }
}
}
}
}
}
}
if viewModel.isLoading {
LoadingSection()
if viewModel.isLoading {
LoadingSection()
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Home")
.searchable(
text: $viewModel.searchTerm,
placement: .toolbar
) {
if viewModel.searchTerm.isEmpty {
Text("Inbox").searchCompletion("in:inbox ")
Text("All").searchCompletion("in:all ")
Text("Archived").searchCompletion("in:archive ")
Text("Files").searchCompletion("type:file ")
.listStyle(PlainListStyle())
.navigationTitle("Home")
.searchable(
text: $viewModel.searchTerm,
placement: .toolbar
) {
if viewModel.searchTerm.isEmpty {
Text("Inbox").searchCompletion("in:inbox ")
Text("All").searchCompletion("in:all ")
Text("Archived").searchCompletion("in:archive ")
Text("Files").searchCompletion("type:file ")
}
}
}
.onChange(of: viewModel.searchTerm) { _ in
// Maybe we should debounce this, but
// it feels like it works ok without
loadItems(isRefresh: true)
}
.onSubmit(of: .search) {
loadItems(isRefresh: true)
}
.toolbar {
ToolbarItem {
Button(
action: {
loadItems(isRefresh: true)
},
label: { Label("Refresh Feed", systemImage: "arrow.clockwise") }
)
.disabled(viewModel.isLoading)
.opacity(viewModel.isLoading ? 0 : 1)
.overlay {
if viewModel.isLoading {
ProgressView()
.onChange(of: viewModel.searchTerm) { _ in
// Maybe we should debounce this, but
// it feels like it works ok without
loadItems(isRefresh: true)
}
.onSubmit(of: .search) {
loadItems(isRefresh: true)
}
.toolbar {
ToolbarItem {
Button(
action: {
loadItems(isRefresh: true)
},
label: { Label("Refresh Feed", systemImage: "arrow.clockwise") }
)
.disabled(viewModel.isLoading)
.opacity(viewModel.isLoading ? 0 : 1)
.overlay {
if viewModel.isLoading {
ProgressView()
}
}
}
}
@ -131,6 +142,15 @@ import Views
loadItems(isRefresh: true)
}
}
.handlesExternalEvents(preferring: Set(["shareExtensionRequestID"]), allowing: Set(["*"]))
.onOpenURL { url in
viewModel.linkRequest = nil
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
if let linkRequestID = DeepLink.make(from: url)?.linkRequestID {
viewModel.linkRequest = LinkRequest(id: UUID(), serverID: linkRequestID)
}
}
}
}
}

View file

@ -11,11 +11,7 @@ struct WebReader: PlatformViewRepresentable {
let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void
let navBarVisibilityRatioUpdater: (Double) -> Void
@Binding var updateFontFamilyActionID: UUID?
@Binding var updateFontActionID: UUID?
@Binding var updateTextContrastActionID: UUID?
@Binding var updateMaxWidthActionID: UUID?
@Binding var updateLineHeightActionID: UUID?
@Binding var readerSettingsChangedTransactionID: UUID?
@Binding var annotationSaveTransactionID: UUID?
@Binding var showNavBarActionID: UUID?
@Binding var shareActionID: UUID?
@ -79,35 +75,18 @@ struct WebReader: PlatformViewRepresentable {
return webView
}
// swiftlint:disable:next cyclomatic_complexity
private func updatePlatformView(_ webView: WKWebView, context: Context) {
if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID {
context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID
(webView as? OmnivoreWebView)?.dispatchEvent(.saveAnnotation(annotation: annotation))
}
if updateFontFamilyActionID != context.coordinator.previousUpdateFontFamilyActionID {
context.coordinator.previousUpdateFontFamilyActionID = updateFontFamilyActionID
if readerSettingsChangedTransactionID != context.coordinator.previousReaderSettingsChangedUUID {
context.coordinator.previousReaderSettingsChangedUUID = readerSettingsChangedTransactionID
(webView as? OmnivoreWebView)?.updateFontFamily()
}
if updateFontActionID != context.coordinator.previousUpdateFontActionID {
context.coordinator.previousUpdateFontActionID = updateFontActionID
(webView as? OmnivoreWebView)?.updateFontSize()
}
if updateTextContrastActionID != context.coordinator.previousUpdateTextContrastActionID {
context.coordinator.previousUpdateTextContrastActionID = updateTextContrastActionID
(webView as? OmnivoreWebView)?.updateTextContrast()
}
if updateMaxWidthActionID != context.coordinator.previousUpdateMaxWidthActionID {
context.coordinator.previousUpdateMaxWidthActionID = updateMaxWidthActionID
(webView as? OmnivoreWebView)?.updateMaxWidthPercentage()
}
if updateLineHeightActionID != context.coordinator.previousUpdateLineHeightActionID {
context.coordinator.previousUpdateLineHeightActionID = updateLineHeightActionID
(webView as? OmnivoreWebView)?.updateLineHeight()
}

View file

@ -15,11 +15,7 @@ struct WebReaderContainerView: View {
@State private var navBarVisibilityRatio = 1.0
@State private var showDeleteConfirmation = false
@State private var progressViewOpacity = 0.0
@State var updateFontFamilyActionID: UUID?
@State var updateFontActionID: UUID?
@State var updateTextContrastActionID: UUID?
@State var updateMaxWidthActionID: UUID?
@State var updateLineHeightActionID: UUID?
@State var readerSettingsChangedTransactionID: UUID?
@State var annotationSaveTransactionID: UUID?
@State var showNavBarActionID: UUID?
@State var shareActionID: UUID?
@ -159,16 +155,14 @@ struct WebReaderContainerView: View {
#endif
}
var webPreferencesPopoverView: some View {
WebPreferencesPopoverView(
updateFontFamilyAction: { updateFontFamilyActionID = UUID() },
updateFontAction: { updateFontActionID = UUID() },
updateTextContrastAction: { updateTextContrastActionID = UUID() },
updateMaxWidthAction: { updateMaxWidthActionID = UUID() },
updateLineHeightAction: { updateLineHeightActionID = UUID() },
dismissAction: { showPreferencesPopover = false }
)
}
#if os(iOS)
var webPreferencesPopoverView: some View {
WebPreferencesPopoverView(
updateReaderPreferences: { readerSettingsChangedTransactionID = UUID() },
dismissAction: { showPreferencesPopover = false }
)
}
#endif
var body: some View {
ZStack {
@ -187,11 +181,7 @@ struct WebReaderContainerView: View {
navBarVisibilityRatioUpdater: {
navBarVisibilityRatio = $0
},
updateFontFamilyActionID: $updateFontFamilyActionID,
updateFontActionID: $updateFontActionID,
updateTextContrastActionID: $updateTextContrastActionID,
updateMaxWidthActionID: $updateMaxWidthActionID,
updateLineHeightActionID: $updateLineHeightActionID,
readerSettingsChangedTransactionID: $readerSettingsChangedTransactionID,
annotationSaveTransactionID: $annotationSaveTransactionID,
showNavBarActionID: $showNavBarActionID,
shareActionID: $shareActionID,
@ -234,19 +224,20 @@ struct WebReaderContainerView: View {
await viewModel.loadContent(dataService: dataService, itemID: item.unwrappedID)
}
}
VStack(spacing: 0) {
navBar
Spacer()
}
#if os(iOS)
VStack(spacing: 0) {
navBar
Spacer()
}
#endif
}
#if os(iOS)
.formSheet(isPresented: $showPreferencesPopover, useSmallDetent: false) {
webPreferencesPopoverView
}
#else
.sheet(isPresented: $showPreferencesPopover) {
webPreferencesPopoverView
.frame(minWidth: 400, minHeight: 400)
.onReceive(NSNotification.readerSettingsChangedPublisher) { _ in
readerSettingsChangedTransactionID = UUID()
}
#endif
.onDisappear {

View file

@ -15,11 +15,7 @@ final class WebReaderCoordinator: NSObject {
var linkHandler: (URL) -> Void = { _ in }
var needsReload = false
var lastSavedAnnotationID: UUID?
var previousUpdateFontFamilyActionID: UUID?
var previousUpdateFontActionID: UUID?
var previousUpdateTextContrastActionID: UUID?
var previousUpdateMaxWidthActionID: UUID?
var previousUpdateLineHeightActionID: UUID?
var previousReaderSettingsChangedUUID: UUID?
var previousShowNavBarActionID: UUID?
var previousShareActionID: UUID?
var updateNavBarVisibilityRatio: (Double) -> Void = { _ in }

View file

@ -52,6 +52,9 @@ struct WelcomeView: View {
}
)
.foregroundColor(.appGrayTextContrast)
#if os(macOS)
.buttonStyle(PlainButtonStyle())
#endif
}
}

View file

@ -28,6 +28,11 @@ public struct PageScrapePayload {
self.contentType = .pdf(localUrl: localUrl)
}
init(localUrl: URL) {
self.url = localUrl.absoluteString
self.contentType = .pdf(localUrl: localUrl)
}
init(url: String, title: String?, html: String, iconURL: String? = nil) {
self.url = url
self.contentType = .html(html: html, title: title, iconURL: iconURL)
@ -238,15 +243,38 @@ private extension PageScrapePayload {
if let url = item as? NSURL {
return makeFromURL(url as URL)
}
if let data = item as? NSData {
return makeFromData(data)
}
return nil
}
static func sharedContainerURL() -> URL {
FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.app.omnivoreapp"
#if os(iOS)
let appGroupID = "group.app.omnivoreapp"
#else
let appGroupID = "QJF2XZ86HB.app.omnivore.app"
#endif
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: appGroupID
)!
}
static func makeFromData(_ data: NSData) -> PageScrapePayload? {
// Copy PDFs into a temporary file where they are staged for processing.
var fileURL = sharedContainerURL()
let filePath = UUID().uuidString.lowercased() + ".pdf"
fileURL.appendPathComponent(filePath)
do {
try data.write(to: fileURL)
return PageScrapePayload(localUrl: fileURL)
} catch {
print("error copying file locally", error)
return nil
}
}
static func makeFromURL(_ url: URL) -> PageScrapePayload? {
if url.isFileURL {
let type = try? url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier

View file

@ -6,6 +6,10 @@ public enum PublicValet {
public static var storedAppEnvironment: AppEnvironment? {
ValetKey.appEnvironmentString.value().flatMap { AppEnvironment(rawValue: $0) }
}
public static var authToken: String? {
ValetKey.authToken.value()
}
}
enum ValetKey: String {

View file

@ -1,10 +1,3 @@
//
// NSNotification+Operation.swift
//
//
// Created by Jackson Harper on 1/31/22.
//
import Foundation
import Models
@ -12,6 +5,7 @@ public extension NSNotification {
static let PushJSONArticle = Notification.Name("PushJSONArticle")
static let OperationSuccess = Notification.Name("OperationSuccess")
static let OperationFailure = Notification.Name("OperationFailure")
static let ReaderSettingsChanged = Notification.Name("ReaderSettingsChanged")
static var pushFeedItemPublisher: NotificationCenter.Publisher {
NotificationCenter.default.publisher(for: PushJSONArticle)
@ -25,6 +19,10 @@ public extension NSNotification {
NotificationCenter.default.publisher(for: OperationFailure)
}
static var readerSettingsChangedPublisher: NotificationCenter.Publisher {
NotificationCenter.default.publisher(for: ReaderSettingsChanged)
}
internal var operationMessage: String? {
if let message = userInfo?["message"] as? String {
return message
@ -47,4 +45,8 @@ public extension NSNotification {
static func operationFailed(message: String) {
NotificationCenter.default.post(name: NSNotification.OperationFailure, object: nil, userInfo: ["message": message])
}
static func readerSettingsChanged() {
NotificationCenter.default.post(name: NSNotification.ReaderSettingsChanged, object: nil)
}
}

View file

@ -3,23 +3,23 @@ import Models
import SwiftUI
import Utils
enum AsyncImageStatus {
public enum AsyncImageStatus {
case loading
case loaded(image: Image)
case error
}
struct AsyncLoadingImage<Content: View>: View {
public struct AsyncLoadingImage<Content: View>: View {
let viewBuilder: (AsyncImageStatus) -> Content
let url: URL
@StateObject private var imageLoader = ImageLoader()
init(url: URL, @ViewBuilder viewBuilder: @escaping (AsyncImageStatus) -> Content) {
public init(url: URL, @ViewBuilder viewBuilder: @escaping (AsyncImageStatus) -> Content) {
self.url = url
self.viewBuilder = viewBuilder
}
var body: some View {
public var body: some View {
viewBuilder(imageLoader.status)
.task { await imageLoader.load(fromUrl: url) }
}

View file

@ -42,16 +42,16 @@ public struct RoundedRectButtonStyle: ButtonStyle {
}
}
struct RectButtonStyle: ButtonStyle {
public struct RectButtonStyle: ButtonStyle {
let backgroundColor: Color
let textColor: Color
init(color: Color = .appButtonBackground, textColor: Color = .appGrayText) {
public init(color: Color = .appButtonBackground, textColor: Color = .appGrayText) {
self.backgroundColor = color
self.textColor = textColor
}
func makeBody(configuration: Configuration) -> some View {
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.appBody)
.foregroundColor(textColor)

View file

@ -12,7 +12,7 @@ public enum WebFont: String, CaseIterable {
case sourceserifpro = "Source Serif Pro"
case openDyslexic = "OpenDyslexic"
var displayValue: String {
public var displayValue: String {
switch self {
case .inter, .merriweather, .lora, .opensans, .roboto, .crimsontext, .sourceserifpro:
return rawValue
@ -24,169 +24,193 @@ public enum WebFont: String, CaseIterable {
}
}
public struct WebPreferencesPopoverView: View {
let updateFontFamilyAction: () -> Void
let updateFontAction: () -> Void
let updateTextContrastAction: () -> Void
let updateMaxWidthAction: () -> Void
let updateLineHeightAction: () -> Void
let dismissAction: () -> Void
#if os(iOS)
public struct WebPreferencesPopoverView: View {
let updateReaderPreferences: () -> Void
let dismissAction: () -> Void
static let preferredWebFontSizeKey = UserDefaultKey.preferredWebFontSize.rawValue
#if os(macOS)
@AppStorage(preferredWebFontSizeKey) var storedFontSize = Int(NSFont.userFont(ofSize: 16)?.pointSize ?? 16)
#else
@AppStorage(preferredWebFontSizeKey) var storedFontSize: Int = UITraitCollection.current.preferredWebFontSize
#endif
@AppStorage(UserDefaultKey.preferredWebFontSize.rawValue) var storedFontSize: Int =
UITraitCollection.current.preferredWebFontSize
@AppStorage(UserDefaultKey.preferredWebLineSpacing.rawValue) var storedLineSpacing = 150
@AppStorage(UserDefaultKey.preferredWebMaxWidthPercentage.rawValue) var storedMaxWidthPercentage = 100
@AppStorage(UserDefaultKey.preferredWebFont.rawValue) var preferredFont = WebFont.inter.rawValue
@AppStorage(UserDefaultKey.prefersHighContrastWebFont.rawValue) var prefersHighContrastText = true
@AppStorage(UserDefaultKey.preferredWebLineSpacing.rawValue) var storedLineSpacing = 150
@AppStorage(UserDefaultKey.preferredWebMaxWidthPercentage.rawValue) var storedMaxWidthPercentage = 100
@AppStorage(UserDefaultKey.preferredWebFont.rawValue) var preferredFont = WebFont.inter.rawValue
@AppStorage(UserDefaultKey.prefersHighContrastWebFont.rawValue) var prefersHighContrastText = true
public init(
updateFontFamilyAction: @escaping () -> Void,
updateFontAction: @escaping () -> Void,
updateTextContrastAction: @escaping () -> Void,
updateMaxWidthAction: @escaping () -> Void,
updateLineHeightAction: @escaping () -> Void,
dismissAction: @escaping () -> Void
) {
self.updateFontFamilyAction = updateFontFamilyAction
self.updateFontAction = updateFontAction
self.updateTextContrastAction = updateTextContrastAction
self.updateMaxWidthAction = updateMaxWidthAction
self.updateLineHeightAction = updateLineHeightAction
self.dismissAction = dismissAction
}
var fontList: some View {
List {
ForEach(WebFont.allCases, id: \.self) { font in
Button(
action: {
preferredFont = font.rawValue
updateFontFamilyAction()
},
label: {
HStack {
Text(font.displayValue).foregroundColor(.appGrayTextContrast)
Spacer()
if font.rawValue == preferredFont {
Image(systemName: "checkmark").foregroundColor(.appGrayTextContrast)
}
}
}
)
}
public init(
updateReaderPreferences: @escaping () -> Void,
dismissAction: @escaping () -> Void
) {
self.updateReaderPreferences = updateReaderPreferences
self.dismissAction = dismissAction
}
.listStyle(.plain)
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.navigationTitle("Reader Font")
}
public var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(alignment: .center) {
VStack {
LabelledStepper(
labelText: "Font Size:",
onIncrement: {
storedFontSize = min(storedFontSize + 2, 28)
updateFontAction()
},
onDecrement: {
storedFontSize = max(storedFontSize - 2, 10)
updateFontAction()
}
)
LabelledStepper(
labelText: "Margin:",
onIncrement: {
storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40)
updateMaxWidthAction()
},
onDecrement: {
storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100)
updateMaxWidthAction()
}
)
LabelledStepper(
labelText: "Line Spacing:",
onIncrement: {
storedLineSpacing = min(storedLineSpacing + 25, 300)
updateLineHeightAction()
},
onDecrement: {
storedLineSpacing = max(storedLineSpacing - 25, 100)
updateLineHeightAction()
}
)
Toggle("High Contrast Text:", isOn: $prefersHighContrastText)
.frame(height: 40)
.padding(.trailing, 6)
.onChange(of: prefersHighContrastText) { _ in
updateTextContrastAction()
}
HStack {
NavigationLink(destination: fontList) {
Text("Change Reader Font")
}
Image(systemName: "chevron.right")
Spacer()
}
.frame(height: 40)
Spacer()
}
}
}
.padding()
.navigationTitle("Reader Preferences")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
ToolbarItem(placement: .barTrailing) {
var fontList: some View {
List {
ForEach(WebFont.allCases, id: \.self) { font in
Button(
action: dismissAction,
label: { Text("Done").foregroundColor(.appGrayTextContrast).padding() }
action: {
preferredFont = font.rawValue
updateReaderPreferences()
},
label: {
HStack {
Text(font.displayValue).foregroundColor(.appGrayTextContrast)
Spacer()
if font.rawValue == preferredFont {
Image(systemName: "checkmark").foregroundColor(.appGrayTextContrast)
}
}
}
)
}
}
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Reader Font")
}
#if os(iOS)
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()
}
)
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()
}
}
}
.padding()
.navigationTitle("Reader Preferences")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .barTrailing) {
Button(
action: dismissAction,
label: { Text("Done").foregroundColor(.appGrayTextContrast).padding() }
)
}
}
}
.navigationViewStyle(.stack)
#endif
.accentColor(.appGrayTextContrast)
.accentColor(.appGrayTextContrast)
}
}
}
struct LabelledStepper: View {
let labelText: String
let onIncrement: () -> Void
let onDecrement: () -> Void
struct LabelledStepper: View {
let labelText: String
let onIncrement: () -> Void
let onDecrement: () -> Void
var body: some View {
HStack(alignment: .center, spacing: 0) {
Text(labelText)
Spacer()
HStack(spacing: 0) {
var body: some View {
HStack(alignment: .center, spacing: 0) {
Text(labelText)
Spacer()
HStack(spacing: 0) {
Button(
action: onDecrement,
label: {
Image(systemName: "minus")
.foregroundColor(.systemLabel)
.padding()
}
)
.frame(width: 55, height: 40, alignment: .center)
Divider()
.frame(height: 30)
.background(Color.systemLabel)
Button(
action: onIncrement,
label: {
Image(systemName: "plus")
.foregroundColor(.systemLabel)
.padding()
}
)
.frame(width: 55, height: 40, alignment: .center)
}
.background(Color.appButtonBackground)
.cornerRadius(8)
}
}
}
public struct FontSizeAdjustmentPopoverView: View {
let increaseFontAction: () -> Void
let decreaseFontAction: () -> Void
public init(
increaseFontAction: @escaping () -> Void,
decreaseFontAction: @escaping () -> Void
) {
self.increaseFontAction = increaseFontAction
self.decreaseFontAction = decreaseFontAction
}
@AppStorage(UserDefaultKey.preferredWebFontSize.rawValue) var storedFontSize: Int =
UITraitCollection.current.preferredWebFontSize
public var body: some View {
HStack(alignment: .center, spacing: 0) {
Button(
action: onDecrement,
action: {
storedFontSize = max(storedFontSize - 2, 10)
decreaseFontAction()
},
label: {
Image(systemName: "minus")
#if os(iOS)
.foregroundColor(.systemLabel)
.padding()
#endif
}
)
.frame(width: 55, height: 40, alignment: .center)
@ -194,75 +218,18 @@ struct LabelledStepper: View {
.frame(height: 30)
.background(Color.systemLabel)
Button(
action: onIncrement,
action: {
storedFontSize = min(storedFontSize + 2, 28)
increaseFontAction()
},
label: {
Image(systemName: "plus")
#if os(iOS)
.foregroundColor(.systemLabel)
.padding()
#endif
}
)
.frame(width: 55, height: 40, alignment: .center)
}
.background(Color.appButtonBackground)
.cornerRadius(8)
}
}
}
public struct FontSizeAdjustmentPopoverView: View {
let increaseFontAction: () -> Void
let decreaseFontAction: () -> Void
public init(
increaseFontAction: @escaping () -> Void,
decreaseFontAction: @escaping () -> Void
) {
self.increaseFontAction = increaseFontAction
self.decreaseFontAction = decreaseFontAction
}
static let preferredWebFontSizeKey = UserDefaultKey.preferredWebFontSize.rawValue
#if os(macOS)
@AppStorage(preferredWebFontSizeKey) var storedFontSize = Int(NSFont.userFont(ofSize: 16)?.pointSize ?? 16)
#else
@AppStorage(preferredWebFontSizeKey) var storedFontSize: Int = UITraitCollection.current.preferredWebFontSize
#endif
public var body: some View {
HStack(alignment: .center, spacing: 0) {
Button(
action: {
storedFontSize = max(storedFontSize - 2, 10)
decreaseFontAction()
},
label: {
Image(systemName: "minus")
#if os(iOS)
.foregroundColor(.systemLabel)
.padding()
#endif
}
)
.frame(width: 55, height: 40, alignment: .center)
Divider()
.frame(height: 30)
.background(Color.systemLabel)
Button(
action: {
storedFontSize = min(storedFontSize + 2, 28)
increaseFontAction()
},
label: {
Image(systemName: "plus")
#if os(iOS)
.foregroundColor(.systemLabel)
.padding()
#endif
}
)
.frame(width: 55, height: 40, alignment: .center)
}
}
}
#endif

View file

@ -10,12 +10,12 @@ public enum LocalText {
static let registrationViewSignInHeadline = localText(key: "registrationViewSignInHeadline")
static let registrationViewSignUpHeadline = localText(key: "registrationViewSignUpHeadline")
public static let registrationViewHeadline = localText(key: "registrationViewHeadline")
static let networkError = localText(key: "error.network")
static let genericError = localText(key: "error.generic")
public static let networkError = localText(key: "error.network")
public static let genericError = localText(key: "error.generic")
static let invalidCredsLoginError = localText(key: "loginError.invalidCreds")
static let saveArticleSavedState = localText(key: "saveArticleSavedState")
static let saveArticleProcessingState = localText(key: "saveArticleProcessingState")
static let extensionAppUnauthorized = localText(key: "extensionAppUnauthorized")
public static let saveArticleSavedState = localText(key: "saveArticleSavedState")
public static let saveArticleProcessingState = localText(key: "saveArticleProcessingState")
public static let extensionAppUnauthorized = localText(key: "extensionAppUnauthorized")
static let dismissButton = localText(key: "dismissButton")
static let usernameValidationErrorInvalid = localText(key: "username.validation.error.invalidPattern")
static let usernameValidationErrorTooShort = localText(key: "username.validation.error.tooshort")

View file

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x9F",
"green" : "0xEA",
"red" : "0xFF"
"blue" : "0xA8",
"green" : "0xEB",
"red" : "0xFB"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x9F",
"green" : "0xEA",
"red" : "0xFF"
"blue" : "0x4C",
"green" : "0x4D",
"red" : "0x4F"
}
},
"idiom" : "universal"

View file

@ -1,19 +1,22 @@
// swiftlint:disable weak_delegate
import App
import SwiftUI
import Utils
#if os(macOS)
import AppKit
import Views
#elseif os(iOS)
import Intercom
import UIKit
import Utils
#endif
@main
struct MainApp: App {
#if os(macOS)
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@AppStorage(UserDefaultKey.preferredWebFont.rawValue) var preferredFont = WebFont.inter.rawValue
@AppStorage(UserDefaultKey.prefersHighContrastWebFont.rawValue) var prefersHighContrastText = true
#elseif os(iOS)
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#endif
@ -33,6 +36,18 @@ struct MainApp: App {
WindowGroup {
RootView(intercomProvider: nil)
}
.commands {
MacMenuCommands(
preferredFont: $preferredFont,
prefersHighContrastText: $prefersHighContrastText
)
}
.onChange(of: preferredFont) { _ in
NSNotification.readerSettingsChanged()
}
.onChange(of: prefersHighContrastText) { _ in
NSNotification.readerSettingsChanged()
}
#endif
}
}

View file

@ -1,22 +1,13 @@
//
// SafariWebExtensionHandler.swift
// Shared (Extension)
//
// Created by JacksonH on 10/8/21.
//
import App
import os.log
import SafariServices
import Services
let SFExtensionMessageKey = "message"
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
let services = Services()
func beginRequest(with context: NSExtensionContext) {
let response = NSExtensionItem()
let authToken = services.authenticator.authToken
let authToken = PublicValet.authToken
response.userInfo = [SFExtensionMessageKey: ["authToken": authToken]]
context.completeRequest(returningItems: [response], completionHandler: nil)
}