move share extension view from Views to App

This commit is contained in:
Satindar Dhillon 2022-07-06 13:27:43 -07:00
parent 4747b685ba
commit d6ccc5b318
7 changed files with 339 additions and 335 deletions

View file

@ -4,33 +4,6 @@ import SwiftUI
import Utils
import Views
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()
@ -52,3 +25,198 @@ struct ShareExtensionView: View {
)
}
}
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
hideUntilReminded = false
} else {
reminderTime = selectedTime
hideUntilReminded = true
}
}
private var titleText: String {
switch viewModel.status {
case .saved, .synced, .syncFailed(error: _):
return "Saved to Omnivore"
case .processing:
return "Saving to Omnivore"
case .failed(error: _):
return "Error saving to Omnivore"
}
}
private var cloudIconName: String {
switch viewModel.status {
case .synced:
return "checkmark.icloud"
case .saved, .processing:
return "icloud"
case .failed(error: _), .syncFailed(error: _):
return "exclamationmark.icloud"
}
}
private var cloudIconColor: Color {
switch viewModel.status {
case .saved:
return .appGrayText
case .processing:
return .clear
case .failed(error: _), .syncFailed(error: _):
return .red
case .synced:
return .blue
}
}
private func localImage(from url: URL) -> Image? {
#if os(iOS)
if let data = try? Data(contentsOf: url), let img = UIImage(data: data) {
return Image(uiImage: img)
}
#else
if let data = try? Data(contentsOf: url), let img = NSImage(data: data) {
return Image(nsImage: img)
}
#endif
return nil
}
public var previewCard: some View {
HStack {
if let iconURLStr = viewModel.iconURL, let iconURL = URL(string: iconURLStr) {
if !iconURL.isFileURL {
AsyncLoadingImage(url: iconURL) { imageStatus in
if case let AsyncImageStatus.loaded(image) = imageStatus {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 61, height: 61)
.clipped()
} else {
Color.appButtonBackground
.aspectRatio(contentMode: .fill)
.frame(width: 61, height: 61)
}
}
} else {
if let localImage = localImage(from: iconURL) {
localImage
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 61, height: 61)
.clipped()
} else {
Color.appButtonBackground
.aspectRatio(contentMode: .fill)
.frame(width: 61, height: 61)
}
}
} else {
Color.appButtonBackground
.aspectRatio(contentMode: .fill)
.frame(width: 61, height: 61)
}
VStack(alignment: .leading) {
Text(viewModel.title ?? "")
.lineLimit(1)
.foregroundColor(.appGrayText)
.font(Font.system(size: 15, weight: .semibold))
Text(viewModel.url ?? "")
.lineLimit(1)
.foregroundColor(.appGrayText)
.font(Font.system(size: 12, weight: .regular))
}
Spacer()
VStack {
Spacer()
Image(systemName: cloudIconName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 12, height: 12, alignment: .trailing)
.foregroundColor(cloudIconColor)
// .padding(.trailing, 6)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 8))
}
}
.background(Color.appButtonBackground)
.frame(maxWidth: .infinity, maxHeight: 61)
.cornerRadius(8)
}
public var body: some View {
VStack(alignment: .leading) {
Text(titleText)
.foregroundColor(.appGrayText)
.font(Font.system(size: 17, weight: .semibold))
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 23)
.padding(.bottom, 12)
Rectangle()
.foregroundColor(.appGrayText)
.frame(maxWidth: .infinity, maxHeight: 1)
.opacity(0.06)
.padding(.top, 0)
.padding(.bottom, 18)
previewCard
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
Spacer()
HStack {
Button(
action: { readNowButtonAction(self.viewModel.requestId) },
label: { Text("Read Now").frame(maxWidth: .infinity) }
)
.buttonStyle(RoundedRectButtonStyle())
Button(
action: {
dismissButtonTappedAction(reminderTime, hideUntilReminded)
},
label: {
Text("Dismiss")
.frame(maxWidth: .infinity)
}
)
.buttonStyle(RoundedRectButtonStyle())
}
.padding(.horizontal)
.padding(.bottom)
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.onAppear {
onAppearAction()
}
}
}

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,79 @@
import Models
import SwiftUI
import Utils
import Views
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"))
}
}
}
}
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
}
}
}

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

@ -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

@ -1,296 +0,0 @@
import Models
import SwiftUI
import Utils
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
@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
hideUntilReminded = false
} else {
reminderTime = selectedTime
hideUntilReminded = true
}
}
private var titleText: String {
switch viewModel.status {
case .saved, .synced, .syncFailed(error: _):
return "Saved to Omnivore"
case .processing:
return "Saving to Omnivore"
case .failed(error: _):
return "Error saving to Omnivore"
}
}
private var cloudIconName: String {
switch viewModel.status {
case .synced:
return "checkmark.icloud"
case .saved, .processing:
return "icloud"
case .failed(error: _), .syncFailed(error: _):
return "exclamationmark.icloud"
}
}
private var cloudIconColor: Color {
switch viewModel.status {
case .saved:
return .appGrayText
case .processing:
return .clear
case .failed(error: _), .syncFailed(error: _):
return .red
case .synced:
return .blue
}
}
private func localImage(from url: URL) -> Image? {
#if os(iOS)
if let data = try? Data(contentsOf: url), let img = UIImage(data: data) {
return Image(uiImage: img)
}
#else
if let data = try? Data(contentsOf: url), let img = NSImage(data: data) {
return Image(nsImage: img)
}
#endif
return nil
}
public var previewCard: some View {
HStack {
if let iconURLStr = viewModel.iconURL, let iconURL = URL(string: iconURLStr) {
if !iconURL.isFileURL {
AsyncLoadingImage(url: iconURL) { imageStatus in
if case let AsyncImageStatus.loaded(image) = imageStatus {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 61, height: 61)
.clipped()
} else {
Color.appButtonBackground
.aspectRatio(contentMode: .fill)
.frame(width: 61, height: 61)
}
}
} else {
if let localImage = localImage(from: iconURL) {
localImage
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 61, height: 61)
.clipped()
} else {
Color.appButtonBackground
.aspectRatio(contentMode: .fill)
.frame(width: 61, height: 61)
}
}
} else {
Color.appButtonBackground
.aspectRatio(contentMode: .fill)
.frame(width: 61, height: 61)
}
VStack(alignment: .leading) {
Text(viewModel.title ?? "")
.lineLimit(1)
.foregroundColor(.appGrayText)
.font(Font.system(size: 15, weight: .semibold))
Text(viewModel.url ?? "")
.lineLimit(1)
.foregroundColor(.appGrayText)
.font(Font.system(size: 12, weight: .regular))
}
Spacer()
VStack {
Spacer()
Image(systemName: cloudIconName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 12, height: 12, alignment: .trailing)
.foregroundColor(cloudIconColor)
// .padding(.trailing, 6)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 8))
}
}
.background(Color.appButtonBackground)
.frame(maxWidth: .infinity, maxHeight: 61)
.cornerRadius(8)
}
public var body: some View {
VStack(alignment: .leading) {
Text(titleText)
.foregroundColor(.appGrayText)
.font(Font.system(size: 17, weight: .semibold))
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 23)
.padding(.bottom, 12)
Rectangle()
.foregroundColor(.appGrayText)
.frame(maxWidth: .infinity, maxHeight: 1)
.opacity(0.06)
.padding(.top, 0)
.padding(.bottom, 18)
previewCard
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
Spacer()
HStack {
Button(
action: { readNowButtonAction(self.viewModel.requestId) },
label: { Text("Read Now").frame(maxWidth: .infinity) }
)
.buttonStyle(RoundedRectButtonStyle())
Button(
action: {
dismissButtonTappedAction(reminderTime, hideUntilReminded)
},
label: {
Text("Dismiss")
.frame(maxWidth: .infinity)
}
)
.buttonStyle(RoundedRectButtonStyle())
}
.padding(.horizontal)
.padding(.bottom)
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.onAppear {
onAppearAction()
}
}
}