Update view models

This commit is contained in:
Jackson Harper 2023-10-30 10:33:56 +08:00
parent 92b2f9112b
commit fa82917f55
10 changed files with 279 additions and 290 deletions

View file

@ -3,11 +3,17 @@ import Utils
import Views
public extension PlatformViewController {
static func makeShareExtensionController(extensionContext: NSExtensionContext?) -> PlatformViewController {
static func makeShareExtensionController(
viewModel: ShareExtensionViewModel,
labelsViewModel: LabelsViewModel,
extensionContext: NSExtensionContext?
) -> PlatformViewController {
registerFonts()
let hostingController = PlatformHostingController(
rootView: ShareExtensionView(extensionContext: extensionContext)
rootView: ShareExtensionView(viewModel: viewModel,
labelsViewModel: labelsViewModel,
extensionContext: extensionContext)
)
#if os(iOS)
hostingController.view.layer.cornerRadius = 12

View file

@ -14,10 +14,13 @@ public class ShareExtensionViewModel: ObservableObject {
@Published public var linkedItem: LinkedItem?
@Published public var requestId = UUID().uuidString.lowercased()
@Published var debugText: String?
@Published var noteText: String = ""
let services = Services()
let queue = OperationQueue()
public init() {}
func handleReadNowAction(extensionContext: NSExtensionContext?) {
#if os(iOS)
if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication {

View file

@ -12,7 +12,11 @@ import Utils
import Views
public struct AddNoteSheet: View {
@State var text = ""
@Environment(\.dismiss) private var dismiss
@StateObject var viewModel: ShareExtensionViewModel
let highlightId = UUID().uuidString.lowercased()
let shortId = NanoID.generate(alphabet: NanoID.Alphabet.urlSafe.rawValue, size: 8)
enum FocusField: Hashable {
case noteEditor
@ -20,13 +24,25 @@ public struct AddNoteSheet: View {
@FocusState private var focusedField: FocusField?
public init() {
UITextView.appearance().textContainerInset = UIEdgeInsets(top: 5, left: 2, bottom: 5, right: 2)
public init(viewModel: ShareExtensionViewModel) {
_viewModel = StateObject(wrappedValue: viewModel)
UITextView.appearance().textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 10, right: 4)
}
func saveNote() {
if let linkedItem = viewModel.linkedItem {
_ = viewModel.services.dataService.createNote(shortId: shortId,
highlightID: highlightId,
articleId: linkedItem.unwrappedID,
annotation: viewModel.noteText)
} else {
// Maybe we shouldn't even allow this UI without linkeditem existing
}
}
public var body: some View {
NavigationView {
TextEditor(text: $text)
TextEditor(text: $viewModel.noteText)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.focused($focusedField, equals: .noteEditor)
.task {
@ -35,10 +51,15 @@ public struct AddNoteSheet: View {
.background(Color.extensionPanelBackground)
.navigationTitle("Add Note")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: Button(action: {}, label: {
.navigationBarItems(leading: Button(action: {
dismiss()
}, label: {
Text("Cancel")
}))
.navigationBarItems(trailing: Button(action: {}, label: {
.navigationBarItems(trailing: Button(action: {
saveNote()
dismiss()
}, label: {
Text("Save").bold()
}))
}

View file

@ -0,0 +1,98 @@
//
// EditLabelsSheet.swift
//
//
// Created by Jackson Harper on 10/27/23.
//
import Models
import Services
import SwiftUI
import Utils
import Views
@MainActor
public struct EditLabelsSheet: View {
@State var text = ""
@Environment(\.dismiss) private var dismiss
@EnvironmentObject var dataService: DataService
@StateObject var labelsViewModel: LabelsViewModel
@StateObject var viewModel: ShareExtensionViewModel
enum FocusField: Hashable {
case noteEditor
}
@FocusState private var focusedField: FocusField?
public init(viewModel: ShareExtensionViewModel, labelsViewModel: LabelsViewModel) {
_viewModel = StateObject(wrappedValue: viewModel)
_labelsViewModel = StateObject(wrappedValue: labelsViewModel)
UITextView.appearance().textContainerInset = UIEdgeInsets(top: 5, left: 2, bottom: 5, right: 2)
}
func onLabelTap(label: LinkedItemLabel, textChip _: TextChip) {
if labelsViewModel.selectedLabels.contains(label) {
labelsViewModel.selectedLabels.remove(label)
} else {
labelsViewModel.selectedLabels.insert(label)
}
if let linkedItem = viewModel.linkedItem {
labelsViewModel.saveItemLabelChanges(itemID: linkedItem.unwrappedID, dataService: viewModel.services.dataService)
}
}
var content: some View {
VStack(spacing: 15) {
SearchBar(searchTerm: $labelsViewModel.labelSearchFilter)
// swiftlint:disable line_length
ScrollView {
LabelsMasonaryView(labels: labelsViewModel.labels.applySearchFilter(labelsViewModel.labelSearchFilter),
selectedLabels: labelsViewModel.selectedLabels.applySearchFilter(labelsViewModel.labelSearchFilter),
onLabelTap: onLabelTap)
Button(
action: { labelsViewModel.showCreateLabelModal = true },
label: {
HStack {
let trimmedLabelName = labelsViewModel.labelSearchFilter.trimmingCharacters(in: .whitespacesAndNewlines)
Image(systemName: "tag").foregroundColor(.blue)
Text(
labelsViewModel.labelSearchFilter.count > 0 ?
"Create: \"\(trimmedLabelName)\" label" :
LocalText.createLabelMessage
).foregroundColor(.blue)
.font(Font.system(size: 14))
Spacer()
}
}
)
.buttonStyle(PlainButtonStyle())
.padding(10)
}
}
.background(Color.clear)
.padding(20)
}
public var body: some View {
NavigationView {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.extensionBackground)
.navigationTitle("Set Labels")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: Button(action: {
dismiss()
}, label: {
Text("Done").bold()
}))
}
.task {
await labelsViewModel.loadLabelsFromStore(dataService: viewModel.services.dataService)
}
}
}

View file

@ -7,8 +7,8 @@ import Views
// swiftlint:disable file_length type_body_length
public struct ShareExtensionView: View {
let extensionContext: NSExtensionContext?
@StateObject var labelsViewModel = LabelsViewModel()
@StateObject private var viewModel = ShareExtensionViewModel()
@StateObject var viewModel: ShareExtensionViewModel
@StateObject var labelsViewModel: LabelsViewModel
@State var previousLabels: [LinkedItemLabel]?
@State var messageText: String?
@ -32,6 +32,15 @@ public struct ShareExtensionView: View {
@FocusState private var focusedField: FocusField?
public init(viewModel: ShareExtensionViewModel,
labelsViewModel: LabelsViewModel,
extensionContext: NSExtensionContext?)
{
_viewModel = StateObject(wrappedValue: viewModel)
_labelsViewModel = StateObject(wrappedValue: labelsViewModel)
self.extensionContext = extensionContext
}
private var titleText: String {
switch viewModel.status {
case .saved, .synced, .syncFailed(error: _):
@ -132,76 +141,76 @@ public struct ShareExtensionView: View {
}
}
var labelsSection: some View {
HStack {
if viewState != .editingLabels {
ZStack {
Circle()
.foregroundColor(Color.blue)
.frame(width: 34, height: 34)
Image(systemName: "tag")
.font(.appCallout)
.frame(width: 34, height: 34)
}
.padding(.trailing, 8)
VStack {
Text(LocalText.labelsGeneric)
.font(.appSubheadline)
.foregroundColor(Color.appGrayTextContrast)
.frame(maxWidth: .infinity, alignment: .leading)
let labelCount = labelsViewModel.selectedLabels.count
Text(labelCount > 0 ?
"\(labelCount) label\(labelCount > 1 ? "s" : "") selected"
: "Add labels to your saved link")
.font(.appFootnote)
.foregroundColor(Color.appGrayText)
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer()
Image(systemName: "chevron.right")
.font(.appCallout)
} else {
VStack(spacing: 15) {
SearchBar(searchTerm: $labelsViewModel.labelSearchFilter)
// swiftlint:disable line_length
ScrollView {
LabelsMasonaryView(labels: labelsViewModel.labels.applySearchFilter(labelsViewModel.labelSearchFilter),
selectedLabels: labelsViewModel.selectedLabels.applySearchFilter(labelsViewModel.labelSearchFilter),
onLabelTap: onLabelTap)
Button(
action: { labelsViewModel.showCreateLabelModal = true },
label: {
HStack {
let trimmedLabelName = labelsViewModel.labelSearchFilter.trimmingCharacters(in: .whitespacesAndNewlines)
Image(systemName: "tag").foregroundColor(.blue)
Text(
labelsViewModel.labelSearchFilter.count > 0 ?
"Create: \"\(trimmedLabelName)\" label" :
LocalText.createLabelMessage
).foregroundColor(.blue)
.font(Font.system(size: 14))
Spacer()
}
}
)
.buttonStyle(PlainButtonStyle())
.padding(10)
}.background(Color.appButtonBackground)
// swiftlint:enable line_length
}
}
}
.padding(viewState == .editingLabels ? 0 : 16)
.background(Color.extensionBackground)
.frame(maxWidth: .infinity, maxHeight: viewState == .editingLabels ? .infinity : 60)
.cornerRadius(8)
}
// var labelsSection: some View {
// HStack {
// if viewState != .editingLabels {
// ZStack {
// Circle()
// .foregroundColor(Color.blue)
// .frame(width: 34, height: 34)
//
// Image(systemName: "tag")
// .font(.appCallout)
// .frame(width: 34, height: 34)
// }
// .padding(.trailing, 8)
//
// VStack {
// Text(LocalText.labelsGeneric)
// .font(.appSubheadline)
// .foregroundColor(Color.appGrayTextContrast)
// .frame(maxWidth: .infinity, alignment: .leading)
//
// let labelCount = labelsViewModel.selectedLabels.count
// Text(labelCount > 0 ?
// "\(labelCount) label\(labelCount > 1 ? "s" : "") selected"
// : "Add labels to your saved link")
// .font(.appFootnote)
// .foregroundColor(Color.appGrayText)
// .frame(maxWidth: .infinity, alignment: .leading)
// }
//
// Spacer()
//
// Image(systemName: "chevron.right")
// .font(.appCallout)
// } else {
// VStack(spacing: 15) {
// SearchBar(searchTerm: $labelsViewModel.labelSearchFilter)
//
// // swiftlint:disable line_length
// ScrollView {
// LabelsMasonaryView(labels: labelsViewModel.labels.applySearchFilter(labelsViewModel.labelSearchFilter),
// selectedLabels: labelsViewModel.selectedLabels.applySearchFilter(labelsViewModel.labelSearchFilter),
// onLabelTap: onLabelTap)
// Button(
// action: { labelsViewModel.showCreateLabelModal = true },
// label: {
// HStack {
// let trimmedLabelName = labelsViewModel.labelSearchFilter.trimmingCharacters(in: .whitespacesAndNewlines)
// Image(systemName: "tag").foregroundColor(.blue)
// Text(
// labelsViewModel.labelSearchFilter.count > 0 ?
// "Create: \"\(trimmedLabelName)\" label" :
// LocalText.createLabelMessage
// ).foregroundColor(.blue)
// .font(Font.system(size: 14))
// Spacer()
// }
// }
// )
// .buttonStyle(PlainButtonStyle())
// .padding(10)
// }.background(Color.appButtonBackground)
// // swiftlint:enable line_length
// }
// }
// }
// .padding(viewState == .editingLabels ? 0 : 16)
// .background(Color.extensionBackground)
// .frame(maxWidth: .infinity, maxHeight: viewState == .editingLabels ? .infinity : 60)
// .cornerRadius(8)
// }
var highlightSection: some View {
HStack {
@ -249,52 +258,6 @@ public struct ShareExtensionView: View {
.cornerRadius(8)
}
func onLabelTap(label: LinkedItemLabel, textChip _: TextChip) {
if labelsViewModel.selectedLabels.contains(label) {
labelsViewModel.selectedLabels.remove(label)
} else {
labelsViewModel.selectedLabels.insert(label)
}
if let linkedItem = viewModel.linkedItem {
labelsViewModel.saveItemLabelChanges(itemID: linkedItem.unwrappedID, dataService: viewModel.services.dataService)
}
}
var primaryButtons: some View {
HStack {
Button(
action: { viewModel.handleReadNowAction(extensionContext: extensionContext) },
label: {
Label("Read Now", systemImage: "book")
.padding(16)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
)
.foregroundColor(.appGrayTextContrast)
.background(Color.appButtonBackground)
.frame(height: 52)
.cornerRadius(8)
Spacer(minLength: 8)
Button(
action: {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
},
label: {
Label(LocalText.readLaterGeneric, systemImage: "text.book.closed.fill")
.padding(16)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
)
.foregroundColor(.black)
.background(Color.appBackground)
.frame(height: 52)
.cornerRadius(8)
}
}
var moreActionsMenu: some View {
Menu {
Button(action: {}, label: {
@ -370,11 +333,21 @@ public struct ShareExtensionView: View {
var articleInfoBox: some View {
HStack(alignment: .top, spacing: 15) {
AsyncImage(url: self.viewModel.iconURL)
.frame(width: 56, height: 56).overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(.white, lineWidth: 1)
).cornerRadius(14)
AsyncImage(url: self.viewModel.iconURL) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 56, height: 56)
} else {
Color.appButtonBackground
.frame(width: 56, height: 56)
}
}
.frame(width: 56, height: 56).overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(.white, lineWidth: 1)
).cornerRadius(14)
VStack(alignment: .leading) {
Text(self.viewModel.url ?? "")
.font(Font.system(size: 12))
@ -399,19 +372,31 @@ public struct ShareExtensionView: View {
}
}
var hasNoteText: Bool {
!viewModel.noteText.isEmpty
}
var noteBox: some View {
Button(action: {
NotificationCenter.default.post(name: Notification.Name("ExpandForm"), object: nil)
// showAddNoteModal = true
}, label: { Text("Add note...") })
.foregroundColor(Color.extensionTextSubtle)
NotificationCenter.default.post(name: Notification.Name("ShowAddNoteSheet"), object: nil)
}, label: {
Text(hasNoteText ? viewModel.noteText : "Add note...")
.frame(height: 50, alignment: .top)
.frame(maxWidth: .infinity, alignment: .leading)
})
.foregroundColor(hasNoteText ?
Color.appGrayTextContrast : Color.extensionTextSubtle
)
.font(Font.system(size: 13, weight: .semibold))
.frame(height: 50, alignment: .top)
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
var labelsBox: some View {
Button(action: {}, label: {
Button(action: {
NotificationCenter.default.post(name: Notification.Name("ShowEditLabelsSheet"), object: nil)
}, label: {
Label {
Text("Add Labels").font(Font.system(size: 12, weight: .medium)).tint(Color.white)
} icon: {
@ -546,7 +531,7 @@ public struct ShareExtensionView: View {
.frame(height: 50)
.background(Color.blue)
.cornerRadius(24)
.padding(15)
.padding(.bottom, 15)
}.frame(maxWidth: .infinity)
}.padding(.horizontal, 15)
.background(Color.extensionBackground)
@ -554,128 +539,4 @@ public struct ShareExtensionView: View {
viewModel.savePage(extensionContext: extensionContext)
}
}
public var oldbody: some View {
VStack(alignment: .center) {
Capsule()
.fill(.gray)
.frame(width: 60, height: 4)
.padding(.top, 10)
if viewState == .mainView {
titleBar
.padding(.top, 10)
.padding(.bottom, 12)
} else {
ZStack {
Text(editingViewTitle).bold()
.frame(maxWidth: .infinity, alignment: .center)
Button(action: {
withAnimation {
submitEditTitle()
}
}, label: { Text(LocalText.doneGeneric).bold() })
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(8)
.padding(.bottom, 4)
}
if viewState == .mainView {
titleBox
}
if viewState == .editingTitle {
ScrollView(showsIndicators: false) {
VStack(alignment: .center, spacing: 16) {
VStack(alignment: .leading, spacing: 6) {
TextEditor(text: $viewModel.title)
.textFieldStyle(.roundedBorder)
.lineSpacing(6)
.submitLabel(.done)
.accentColor(.appGraySolid)
.foregroundColor(.appGrayTextContrast)
.font(.appSubheadline)
.padding(8)
.background(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(Color.appGrayBorder, lineWidth: 1)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.systemBackground))
)
.frame(height: 100)
.focused($focusedField, equals: .titleEditor)
.task {
self.focusedField = .titleEditor
}
.onChange(of: viewModel.title) { text in
if text.last?.isNewline == .some(true) {
viewModel.title.removeLast()
submitEditTitle()
}
}
}
}
.padding(8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Spacer()
}
if viewState != .editingTitle {
if viewState != .viewingHighlight {
labelsSection
.onTapGesture {
withAnimation {
previousLabels = Array(self.labelsViewModel.selectedLabels)
viewState = .editingLabels
}
}
}
if viewState != .editingLabels {
highlightSection
.onTapGesture {
withAnimation {
if viewModel.highlightData != nil {
viewState = .viewingHighlight
} else {
showHighlightInstructionAlert = true
}
}
}
}
}
Spacer()
if viewState == .mainView {
Divider()
.padding(.bottom, 20)
primaryButtons
moreActionsMenu
}
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(.horizontal, 16)
.onAppear {
viewModel.savePage(extensionContext: extensionContext)
}
.sheet(isPresented: $labelsViewModel.showCreateLabelModal) {
CreateLabelView(viewModel: labelsViewModel, newLabelName: labelsViewModel.labelSearchFilter)
}
.alert("Before saving an article select text in Safari to create a highlight on save.",
isPresented: $showHighlightInstructionAlert) {
Button(LocalText.genericOk, role: .cancel) { showHighlightInstructionAlert = false }
}
.task {
await labelsViewModel.loadLabelsFromStore(dataService: viewModel.services.dataService)
}.environmentObject(viewModel.services.dataService)
}
}

View file

@ -11,6 +11,7 @@ import SwiftUI
import Models
import Views
@MainActor
struct LabelsMasonaryView: View {
var onLabelTap: (LinkedItemLabel, TextChip) -> Void

View file

@ -4,7 +4,7 @@ import Services
import SwiftUI
import Views
@MainActor final class LabelsViewModel: ObservableObject {
@MainActor public final class LabelsViewModel: ObservableObject {
let labelNameMaxLength = 64
@Published var isLoading = false
@ -14,6 +14,8 @@ import Views
@Published var showCreateLabelModal = false
@Published var labelSearchFilter = ""
public init() {}
func setLabels(_ labels: [LinkedItemLabel]) {
self.labels = labels.sorted { left, right in
let aTrimmed = left.unwrappedName.trimmingCharacters(in: .whitespaces)

View file

@ -48,23 +48,6 @@
child.didMove(toParent: self)
}
//
// @objc func keyboardWillShow(notification: Notification) {
// if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
// if self.view.frame.origin.y == 0{
// self.view.frame.origin.y -= keyboardSize.height
// }
// }
//
// }
//
// @objc func keyboardWillHide(notification: Notification) {
// if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
// if self.view.frame.origin.y != 0 {
// self.view.frame.origin.y += keyboardSize.height
// }
// }
// }
}
#endif

View file

@ -12,7 +12,7 @@ public struct SearchBar: View {
public var body: some View {
HStack(spacing: 0) {
TextField("Search", text: $searchTerm)
TextField("Add Labels", text: $searchTerm)
.frame(height: 36)
.frame(maxWidth: .infinity)
.padding(.leading, 28)

View file

@ -1,4 +1,5 @@
import App
import Services
import SwiftUI
import Utils
import Views
@ -6,32 +7,45 @@ import Views
#if os(iOS)
import UIKit
final class SheetViewController: UIViewController {}
@objc(ShareExtensionViewController)
final class ShareExtensionViewController: UIViewController {
let labelsViewModel = LabelsViewModel()
let viewModel = ShareExtensionViewModel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
NotificationCenter.default.addObserver(forName: Notification.Name("ExpandForm"), object: nil, queue: OperationQueue.main) { _ in
NotificationCenter.default.addObserver(
forName: Notification.Name("ShowAddNoteSheet"),
object: nil,
queue: OperationQueue.main
) { _ in
self.openSheet(AnyView(AddNoteSheet(viewModel: self.viewModel)))
}
self.openSheet()
NotificationCenter.default.addObserver(
forName: Notification.Name("ShowEditLabelsSheet"),
object: nil,
queue: OperationQueue.main
) { _ in
self.openSheet(AnyView(EditLabelsSheet(viewModel: self.viewModel, labelsViewModel: self.labelsViewModel)))
}
embed(
childViewController: UIViewController.makeShareExtensionController(extensionContext: extensionContext),
childViewController: UIViewController.makeShareExtensionController(
viewModel: viewModel,
labelsViewModel: labelsViewModel,
extensionContext: extensionContext
),
heightRatio: 0.60
)
}
@IBAction func openSheet() {
let hostingController = UIHostingController(rootView: AddNoteSheet())
func openSheet(_ rootView: AnyView) {
let hostingController = UIHostingController(rootView: rootView)
present(hostingController, animated: true, completion: nil)
// Present it w/o any adjustments so it uses the default sheet presentation.
// present(sheetViewController., animated: true, completion: nil)
}
}