mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #910 from omnivore-app/macos-read-now
macOS Share Extension Saving
This commit is contained in:
commit
d69a43d00f
25 changed files with 848 additions and 757 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
120
apple/OmnivoreKit/Sources/App/MacMenuCommands.swift
Normal file
120
apple/OmnivoreKit/Sources/App/MacMenuCommands.swift
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ struct WelcomeView: View {
|
|||
}
|
||||
)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
#if os(macOS)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue