mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
merge ExtensionSaveService into ExtensionViewModel
This commit is contained in:
parent
d6ccc5b318
commit
d7370a579d
3 changed files with 121 additions and 178 deletions
|
|
@ -4,165 +4,58 @@ import Services
|
|||
import Utils
|
||||
import Views
|
||||
|
||||
final class ExtensionSaveService {
|
||||
#if os(macOS)
|
||||
let services = Services()
|
||||
#endif
|
||||
|
||||
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) {
|
||||
extension ShareExtensionViewModel {
|
||||
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 {
|
||||
shareExtensionViewModel.status = .saved
|
||||
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):
|
||||
shareExtensionViewModel.title = title
|
||||
shareExtensionViewModel.iconURL = iconURL
|
||||
shareExtensionViewModel.url = hostname
|
||||
self.title = title
|
||||
self.iconURL = iconURL
|
||||
self.url = hostname
|
||||
case .none:
|
||||
shareExtensionViewModel.url = hostname
|
||||
shareExtensionViewModel.title = payload.url
|
||||
self.url = hostname
|
||||
self.title = payload.url
|
||||
if var url = url {
|
||||
url.path = "/favicon.ico"
|
||||
shareExtensionViewModel.iconURL = url.url?.absoluteString
|
||||
self.iconURL = url.url?.absoluteString
|
||||
}
|
||||
case let .pdf(localUrl: localUrl):
|
||||
shareExtensionViewModel.url = hostname
|
||||
shareExtensionViewModel.title = PDFUtils.titleFromPdfFile(localUrl.absoluteString)
|
||||
self.url = hostname
|
||||
self.title = PDFUtils.titleFromPdfFile(localUrl.absoluteString)
|
||||
Task {
|
||||
let localThumbnail = try await PDFUtils.createThumbnailFor(inputUrl: localUrl)
|
||||
DispatchQueue.main.async {
|
||||
shareExtensionViewModel.iconURL = localThumbnail?.absoluteString
|
||||
self.iconURL = localThumbnail?.absoluteString
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
self.queueSaveOperation(payload, shareExtensionViewModel: shareExtensionViewModel)
|
||||
self.queueSaveOperation(payload)
|
||||
#else
|
||||
Task {
|
||||
await shareExtensionViewModel.createPage(services: self.services, pageScrapePayload: payload)
|
||||
await createPage(services: self.services, pageScrapePayload: payload)
|
||||
}
|
||||
#endif
|
||||
case .failure:
|
||||
DispatchQueue.main.async {
|
||||
shareExtensionViewModel.status = .failed(error: .unknown(description: "Could not retrieve content"))
|
||||
self.status = .failed(error: .unknown(description: "Could not retrieve content"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final 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()
|
||||
}
|
||||
|
||||
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(
|
||||
services: services,
|
||||
pageScrapePayload: pageScrapePayload
|
||||
)
|
||||
if pageCreated {
|
||||
state = .finished
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
super.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ShareExtensionChildViewModel {
|
||||
func createPage(services: Services, pageScrapePayload: PageScrapePayload) async -> Bool {
|
||||
var newRequestID: String?
|
||||
|
||||
|
|
@ -217,3 +110,77 @@ extension ShareExtensionChildViewModel {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class SaveOperation: Operation, URLSessionDelegate {
|
||||
let services: Services
|
||||
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
|
||||
self.services = Services()
|
||||
}
|
||||
|
||||
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(
|
||||
services: services,
|
||||
pageScrapePayload: pageScrapePayload
|
||||
)
|
||||
if pageCreated {
|
||||
state = .finished
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
super.cancel()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,49 +4,13 @@ import SwiftUI
|
|||
import Utils
|
||||
import Views
|
||||
|
||||
struct ShareExtensionView: View {
|
||||
public 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ShareExtensionChildView: View {
|
||||
let viewModel: ShareExtensionChildViewModel
|
||||
let onAppearAction: () -> Void
|
||||
let readNowButtonAction: (String) -> Void
|
||||
let dismissButtonTappedAction: (ReminderTime?, Bool) -> Void
|
||||
|
||||
@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
|
||||
|
|
@ -191,14 +155,14 @@ public struct ShareExtensionChildView: View {
|
|||
|
||||
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")
|
||||
|
|
@ -216,7 +180,7 @@ public struct ShareExtensionChildView: View {
|
|||
alignment: .topLeading
|
||||
)
|
||||
.onAppear {
|
||||
onAppearAction()
|
||||
viewModel.savePage(extensionContext: extensionContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,20 @@ import Utils
|
|||
import Views
|
||||
|
||||
public class ShareExtensionViewModel: ObservableObject {
|
||||
@Published var title: String?
|
||||
@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 = UUID().uuidString.lowercased()
|
||||
@Published var debugText: String?
|
||||
|
||||
let saveService = ExtensionSaveService()
|
||||
#if os(macOS)
|
||||
let services = Services()
|
||||
#endif
|
||||
|
||||
func handleReadNowAction(requestId: String, extensionContext: NSExtensionContext?) {
|
||||
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)")
|
||||
|
|
@ -19,27 +27,31 @@ public class ShareExtensionViewModel: ObservableObject {
|
|||
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
|
||||
func savePage(extensionContext: NSExtensionContext?, shareExtensionViewModel: ShareExtensionChildViewModel) {
|
||||
func savePage(extensionContext: NSExtensionContext?) {
|
||||
if let extensionContext = extensionContext {
|
||||
saveService.save(extensionContext, shareExtensionViewModel: shareExtensionViewModel)
|
||||
save(extensionContext)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
shareExtensionViewModel.status = .failed(error: .unknown(description: "Internal Error"))
|
||||
self.status = .failed(error: .unknown(description: "Internal Error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
#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
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.requestId = UUID().uuidString.lowercased()
|
||||
}
|
||||
let operation = SaveOperation(pageScrapePayload: payload, shareExtensionViewModel: self)
|
||||
self.queue.addOperation(operation)
|
||||
self.queue.waitUntilAllOperationsAreFinished()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public enum ShareExtensionStatus {
|
||||
|
|
|
|||
Loading…
Reference in a new issue