Fixes for the splitview and app intents

This commit is contained in:
Jackson Harper 2023-12-06 09:57:28 +08:00
parent d6b30b593d
commit f48a36de79
16 changed files with 415 additions and 156 deletions

View file

@ -38,6 +38,7 @@
42321E892714E6B00056429F /* views in Resources */ = {isa = PBXBuildFile; fileRef = 42321E842714E6B00056429F /* views */; };
42321E8A2714E6B00056429F /* views in Resources */ = {isa = PBXBuildFile; fileRef = 42321E842714E6B00056429F /* views */; };
42704E7328E6BDB000C8C73E /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42704E7228E6BDAF00C8C73E /* SnapshotHelper.swift */; };
42B4483A2B1DD8AC00CEC5A0 /* AppIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B448392B1DD8AC00CEC5A0 /* AppIntents.swift */; };
42E2BFB428E458E0007F29B2 /* AppStoreScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E2BFB328E458E0007F29B2 /* AppStoreScreenshots.swift */; };
42FF1B33271154A700B38C38 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42FF1AEB271154A600B38C38 /* SafariWebExtensionHandler.swift */; };
42FF1B34271154A700B38C38 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42FF1AEB271154A600B38C38 /* SafariWebExtensionHandler.swift */; };
@ -212,6 +213,7 @@
42321E822714E6B00056429F /* scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = scripts; sourceTree = "<group>"; };
42321E842714E6B00056429F /* views */ = {isa = PBXFileReference; lastKnownFileType = folder; path = views; sourceTree = "<group>"; };
42704E7228E6BDAF00C8C73E /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = "<group>"; };
42B448392B1DD8AC00CEC5A0 /* AppIntents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppIntents.swift; sourceTree = "<group>"; };
42E2BFB128E458E0007F29B2 /* AppStoreScreenshots.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppStoreScreenshots.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
42E2BFB328E458E0007F29B2 /* AppStoreScreenshots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreScreenshots.swift; sourceTree = "<group>"; };
42FF1AEB271154A600B38C38 /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = "<group>"; };
@ -489,6 +491,7 @@
7C54279684D6252C9CD18562 /* Sources */ = {
isa = PBXGroup;
children = (
42B448392B1DD8AC00CEC5A0 /* AppIntents.swift */,
0480E71B26D95096006CAE2F /* AppDelegate.swift */,
D81BE98F0CB588F5FC577A13 /* MainApp.swift */,
42FF1AEA271154A600B38C38 /* SafariExtension */,
@ -1073,6 +1076,7 @@
buildActionMask = 2147483647;
files = (
0480E71C26D95096006CAE2F /* AppDelegate.swift in Sources */,
42B4483A2B1DD8AC00CEC5A0 /* AppIntents.swift in Sources */,
04920CC6279671EF003EC1B6 /* PushNotificationConfig.swift in Sources */,
CA7EE773095F267516D7AC98 /* MainApp.swift in Sources */,
);

View file

@ -1,32 +0,0 @@
#if os(iOS)
import AppIntents
import Services
import SwiftUI
@available(iOS 16.0, *)
struct SaveLinkToOmnivoreIntent: AppIntent {
static var title: LocalizedStringResource = "Show Sport Progress"
static var parameterSummary: some ParameterSummary {
Summary("Save \(\.$link) to your Omnivore library.")
}
@Parameter(title: "link")
var link: URL
@MainActor
func perform() async throws -> some IntentResult {
do {
let services = Services()
let requestId = UUID().uuidString.lowercased()
_ = try await services.dataService.saveURL(id: requestId, url: link.absoluteString)
return .result(dialog: "Link saved to Omnivore")
} catch {
print("error saving URL: ", error)
}
return .result(dialog: "Error saving link")
}
}
#endif

View file

@ -51,8 +51,8 @@ public class ShareExtensionViewModel: ObservableObject {
dataService.archiveLink(objectID: objectID, archived: archived)
}
func removeLink(dataService: DataService, objectID: NSManagedObjectID) {
dataService.removeLink(objectID: objectID)
func removeLibraryItem(dataService: DataService, objectID: NSManagedObjectID) {
dataService.removeLibraryItem(objectID: objectID)
}
func submitTitleEdit(dataService: DataService, itemID: String, title: String, description: String) {

View file

@ -220,7 +220,7 @@ public struct ShareExtensionView: View {
Button(
action: {
if let linkedItem = self.viewModel.linkedItem {
self.viewModel.removeLink(dataService: self.viewModel.services.dataService, objectID: linkedItem.objectID)
self.viewModel.removeLibraryItem(dataService: self.viewModel.services.dataService, objectID: linkedItem.objectID)
messageText = "Link Removed"
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)

View file

@ -0,0 +1,49 @@
#if os(iOS)
import AppIntents
import Services
import SwiftUI
@available(iOS 16.0, *)
public struct OmnivoreAppShorcuts: AppShortcutsProvider {
@AppShortcutsBuilder public static var appShortcuts: [AppShortcut] {
AppShortcut(intent: SaveToOmnivoreIntent(), phrases: ["Save URL to \(.applicationName)"])
}
}
//
// @available(iOS 16.0, *)
// struct ExportAllTransactionsIntent: AppIntent {
// static var title: LocalizedStringResource = "Export all transactions"
//
// static var description =
// IntentDescription("Exports your transaction history as CSV data.")
// }
@available(iOS 16.0, *)
struct SaveToOmnivoreIntent: AppIntent {
static var title: LocalizedStringResource = "Save to Omnivore"
static var description: LocalizedStringResource = "Save a URL to your Omnivore library"
static var parameterSummary: some ParameterSummary {
Summary("Save \(\.$link) to your Omnivore library.")
}
@Parameter(title: "link")
var link: URL
@MainActor
func perform() async throws -> some IntentResult & ReturnsValue {
do {
let services = Services()
let requestId = UUID().uuidString.lowercased()
_ = try await services.dataService.saveURL(id: requestId, url: link.absoluteString)
return .result(dialog: "Link saved to Omnivore")
} catch {
print("error saving URL: ", error)
}
return .result(dialog: "Error saving link")
}
}
#endif

View file

@ -51,11 +51,10 @@ struct FeedCardNavigationLink: View {
linkedItemObjectID: item.objectID,
isPDF: item.isPDF
)
.background(ThemeManager.currentBgColor)
}, label: {
EmptyView()
}
).opacity(0)
)
}
.onAppear {
Task { await viewModel.itemAppeared(item: item, dataService: dataService) }

View file

@ -188,14 +188,13 @@ struct AnimatingCellHeight: AnimatableModifier {
}
.frame(maxWidth: .infinity, alignment: .bottomLeading)
}
ToolbarItem(placement: .barTrailing) {
Button("", action: {})
.disabled(true)
.overlay {
if viewModel.isLoading, !prefersListLayout, enableGrid {
ProgressView()
}
}
if UIDevice.isIPad, viewModel.folder == "inbox" {
Button(action: { addLinkPresented = true }, label: {
Label("Add Link", systemImage: "plus")
})
}
}
ToolbarItem(placement: UIDevice.isIPhone ? .barLeading : .barTrailing) {
if enableGrid {

View file

@ -79,7 +79,7 @@ import Views
}
func loadFilters(dataService: DataService) async {
switch filterState.folder {
switch folder {
case "following":
updateFilters(newFilters: InternalFilter.DefaultFollowingFilters, defaultName: "rss")
default:

View file

@ -0,0 +1,205 @@
import Foundation
import Services
import SwiftUI
@MainActor struct LibrarySidebar: View {
@ObservedObject var inboxViewModel: HomeFeedViewModel
@ObservedObject var followingViewModel: HomeFeedViewModel
@EnvironmentObject var dataService: DataService
@State private var addLinkPresented = false
@State private var showProfile = false
@State private var selection: String?
@State private var selectedFilter: InternalFilter?
@AppStorage("inboxActive") private var inboxActive = true
@AppStorage("followingActive") private var followingActive = false
@AppStorage("inboxMenuState") var inboxMenuState = "open"
@AppStorage("followingMenuState") var followingMenuState = "open"
func createInboxViewModel(_ filter: InternalFilter) -> HomeFeedViewModel {
let result = HomeFeedViewModel(
folder: "inbox",
fetcher: LibraryItemFetcher(),
listConfig: LibraryListConfig(
hasFeatureCards: true,
leadingSwipeActions: [.pin],
trailingSwipeActions: [.archive, .delete],
cardStyle: .library
)
)
result.appliedFilter = filter
return result
}
func createFollowingViewModel(_ filter: InternalFilter) -> HomeFeedViewModel {
let result = HomeFeedViewModel(
folder: "following",
fetcher: LibraryItemFetcher(),
listConfig: LibraryListConfig(
hasFeatureCards: false,
leadingSwipeActions: [.moveToInbox],
trailingSwipeActions: [.archive, .delete],
cardStyle: .library
)
)
result.appliedFilter = filter
return result
}
var innerBody: some View {
ZStack {
NavigationLink("", destination: HomeView(viewModel: inboxViewModel), isActive: $inboxActive)
NavigationLink("", destination: HomeView(viewModel: followingViewModel), isActive: $followingActive)
List {
Section {
Button(action: { inboxMenuState = inboxMenuState == "open" ? "closed" : "open" }, label: {
HStack {
Image.tabLibrary
Text("Library")
Spacer()
if inboxMenuState == "open" {
Image(systemName: "chevron.down")
} else {
Image(systemName: "chevron.right")
}
}
})
if inboxMenuState == "open" {
ForEach(inboxViewModel.filters, id: \.self) { filter in
Button(action: {
inboxViewModel.appliedFilter = filter
selectedFilter = filter
followingActive = false
inboxActive = true
}, label: {
HStack {
Spacer().frame(width: 35)
Text(filter.name)
.lineLimit(1)
}
})
.listRowBackground(
selectedFilter == filter && inboxActive
? Color.systemBackground.cornerRadius(8) : Color.clear.cornerRadius(8)
)
}
}
}
Section {
Button(action: { followingMenuState = followingMenuState == "open" ? "closed" : "open" }, label: {
HStack {
Image.tabLibrary
Text("Following")
Spacer()
if followingMenuState == "open" {
Image(systemName: "chevron.down")
} else {
Image(systemName: "chevron.right")
}
}
})
if followingMenuState == "open" {
ForEach(followingViewModel.filters, id: \.self) { filter in
Button(action: {
followingViewModel.appliedFilter = filter
selectedFilter = filter
inboxActive = false
followingActive = true
}, label: {
HStack {
Spacer().frame(width: 35)
Text(filter.name)
.lineLimit(1)
}
})
.listRowBackground(
selectedFilter == filter && followingActive
? Color.systemBackground.cornerRadius(8) : Color.clear.cornerRadius(8)
)
}
}
}
}
.listStyle(.sidebar)
.dynamicTypeSize(.small ... .large)
.sheet(isPresented: $addLinkPresented) {
NavigationView {
LibraryAddLinkView()
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
}
}
.sheet(isPresented: $showProfile) {
NavigationView {
ProfileView()
.toolbar {
ToolbarItem(placement: .barTrailing) {
Button(action: { showProfile = false }, label: {
Text("Close")
.bold()
})
}
}
}
}.onAppear {
Task {
await inboxViewModel.loadFilters(dataService: dataService)
await followingViewModel.loadFilters(dataService: dataService)
if inboxActive {
selectedFilter = inboxViewModel.appliedFilter
} else {
selectedFilter = followingViewModel.appliedFilter
}
}
}.onChange(of: inboxViewModel.appliedFilter) { filter in
// When the user uses the dropdown menu to change filter we need to update in the sidebar
if inboxActive, filter != selectedFilter {
selectedFilter = filter
}
}.onChange(of: followingViewModel.appliedFilter) { filter in
if followingActive, filter != selectedFilter {
selectedFilter = filter
}
}
}
var body: some View {
#if os(iOS)
innerBody
.toolbar {
ToolbarItem(placement: .barTrailing) {
Button(action: { showProfile = true }, label: { Image.tabProfile })
}
}
#elseif os(macOS)
innerBody
.frame(minWidth: 200)
.toolbar {
ToolbarItem {
Button(
action: {
NSApp.keyWindow?.firstResponder?.tryToPerform(
#selector(NSSplitViewController.toggleSidebar(_:)), with: nil
)
},
label: { Label(LocalText.navigationSelectSidebarToggle, systemImage: "sidebar.left") }
)
}
}
#endif
}
}

View file

@ -0,0 +1,56 @@
import Foundation
import SwiftUI
@MainActor
public struct LibrarySplitView: View {
@StateObject private var inboxViewModel = HomeFeedViewModel(
folder: "inbox",
fetcher: LibraryItemFetcher(),
listConfig: LibraryListConfig(
hasFeatureCards: true,
leadingSwipeActions: [.pin],
trailingSwipeActions: [.archive, .delete],
cardStyle: .library
)
)
@StateObject private var followingViewModel = HomeFeedViewModel(
folder: "following",
fetcher: LibraryItemFetcher(),
listConfig: LibraryListConfig(
hasFeatureCards: false,
leadingSwipeActions: [.moveToInbox],
trailingSwipeActions: [.archive, .delete],
cardStyle: .library
)
)
#if os(iOS)
public var body: some View {
NavigationView {
LibrarySidebar(inboxViewModel: inboxViewModel, followingViewModel: followingViewModel)
.navigationBarTitleDisplayMode(.inline)
.tag("inbox")
HomeFeedContainerView(viewModel: inboxViewModel)
.navigationViewStyle(.stack)
.tag("following")
}
.navigationBarTitleDisplayMode(.inline)
.accentColor(.appGrayTextContrast)
.introspectSplitViewController {
$0.preferredPrimaryColumnWidth = 230
$0.displayModeButtonVisibility = .always
}
}
#endif
#if os(macOS)
public var body: some View {
NavigationView {
LibraryListView()
Text(LocalText.navigationSelectLink)
}
}
#endif
}

View file

@ -91,8 +91,10 @@ struct LinkItemDetailView: View {
pdfContainerView
.navigationBarBackButtonHidden(false)
}
.navigationViewStyle(.stack)
} else if let item = viewModel.item {
WebReaderContainerView(item: item, pop: { dismiss() })
.background(ThemeManager.currentBgColor)
}
}
.ignoresSafeArea(.all, edges: .bottom)

View file

@ -4,11 +4,6 @@ import SwiftUI
import Views
@MainActor public struct PrimaryContentView: View {
let categories = [
PrimaryContentCategory.feed,
PrimaryContentCategory.profile
]
@State var searchTerm: String = ""
public var body: some View {
@ -18,8 +13,9 @@ import Views
public var innerBody: some View {
#if os(iOS)
if UIDevice.isIPad {
return AnyView(LibraryTabView())
// return AnyView(splitView)
return AnyView(
LibrarySplitView()
)
} else {
return AnyView(
LibraryTabView()
@ -29,99 +25,4 @@ import Views
return AnyView(splitView)
#endif
}
#if os(macOS)
private var splitView: some View {
NavigationView {
PrimaryContentCategory.feed.destinationView
Text(LocalText.navigationSelectLink)
}
}
#endif
#if os(iOS)
private var splitView: some View {
NavigationView {
// The first column is the sidebar.
PrimaryContentSidebar(categories: categories)
.navigationBarTitleDisplayMode(.inline)
// Second column is the Primary Nav Stack
PrimaryContentCategory.feed.destinationView
.navigationBarTitleDisplayMode(.inline)
}
.navigationBarTitleDisplayMode(.inline)
.accentColor(.appGrayTextContrast)
.introspectSplitViewController {
$0.preferredPrimaryColumnWidth = 160
$0.displayModeButtonVisibility = .always
}
}
#endif
}
@MainActor struct PrimaryContentSidebar: View {
@State private var addLinkPresented = false
@State private var showProfile = false
@State private var selectedCategory: PrimaryContentCategory?
let categories: [PrimaryContentCategory]
var innerBody: some View {
List {
NavigationLink(
destination: PrimaryContentCategory.feed.destinationView,
tag: PrimaryContentCategory.feed,
selection: $selectedCategory,
label: { PrimaryContentCategory.feed.listLabel }
)
.listRowBackground(Color.systemBackground.cornerRadius(8))
Button(action: { showProfile = true }, label: {
PrimaryContentCategory.profile.listLabel
})
Button(action: { addLinkPresented = true }, label: {
Label("Add Link", systemImage: "plus.circle")
})
}
.dynamicTypeSize(.small ... .large)
.listStyle(.sidebar)
.sheet(isPresented: $addLinkPresented) {
NavigationView {
LibraryAddLinkView()
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
}
.sheet(isPresented: $showProfile) {
NavigationView {
PrimaryContentCategory.profile.destinationView
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
}
}
var body: some View {
#if os(iOS)
innerBody
#elseif os(macOS)
innerBody
.frame(minWidth: 200)
.toolbar {
ToolbarItem {
Button(
action: {
NSApp.keyWindow?.firstResponder?.tryToPerform(
#selector(NSSplitViewController.toggleSidebar(_:)), with: nil
)
},
label: { Label(LocalText.navigationSelectSidebarToggle, systemImage: "sidebar.left") }
)
}
}
#endif
}
}

View file

@ -25,7 +25,7 @@ func removeLibraryItemAction(dataService: DataService, objectID: NSManagedObject
let canceled = Task.isCancelled
if !canceled {
print("syncing link deletion")
dataService.removeLink(objectID: objectID, sync: true)
dataService.removeLibraryItem(objectID: objectID, sync: true)
}
} catch {
print("error running task: ", error)

View file

@ -48,7 +48,7 @@ public struct WebReaderLoadingContainer: View {
.navigationBarHidden(true)
.navigationViewStyle(.stack)
.accentColor(.appGrayTextContrast)
.task { viewModel.trackReadEvent() }
.onAppear { viewModel.trackReadEvent() }
#else
if let pdfURL = pdfItem.pdfURL {
PDFWrapperView(pdfURL: pdfURL)
@ -60,17 +60,19 @@ public struct WebReaderLoadingContainer: View {
.navigationViewStyle(.stack)
#endif
.accentColor(.appGrayTextContrast)
.task { viewModel.trackReadEvent() }
.onAppear { viewModel.trackReadEvent() }
}
} else if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
} else {
ProgressView()
.task {
if let username = dataService.currentViewer?.username {
await viewModel.loadItem(dataService: dataService, username: username, requestID: requestID)
} else {
viewModel.errorMessage = "You are not logged in."
.onAppear {
Task {
if let username = dataService.currentViewer?.username {
await viewModel.loadItem(dataService: dataService, username: username, requestID: requestID)
} else {
viewModel.errorMessage = "You are not logged in."
}
}
}
}

View file

@ -4,7 +4,7 @@ import Models
import SwiftGraphQL
public extension DataService {
func removeLink(objectID: NSManagedObjectID, sync: Bool = true) {
func removeLibraryItem(objectID: NSManagedObjectID, sync: Bool = true) {
// First try to get the item synchronously, this is used later to delete files
// Then we can async update core data and make the API call to sync the deletion

View file

@ -0,0 +1,74 @@
#if os(iOS)
import App
import AppIntents
import Firebase
import FirebaseMessaging
import Foundation
import Models
import Services
import UIKit
import Utils
@available(iOS 16.0, *)
public struct OmnivoreAppShorcuts: AppShortcutsProvider {
@AppShortcutsBuilder public static var appShortcuts: [AppShortcut] {
AppShortcut(intent: SaveToOmnivoreIntent(), phrases: ["Save URL to \(.applicationName)"])
}
}
//
// @available(iOS 16.0, *)
// struct ExportAllTransactionsIntent: AppIntent {
// static var title: LocalizedStringResource = "Export all transactions"
//
// static var description =
// IntentDescription("Exports your transaction history as CSV data.")
// }
@available(iOS 16.0, *)
struct SaveToOmnivoreIntent: AppIntent {
static var title: LocalizedStringResource = "Save to Omnivore"
static var description: LocalizedStringResource = "Save a URL to your Omnivore library"
static var parameterSummary: some ParameterSummary {
Summary("Save \(\.$link) to your Omnivore library.")
}
@Parameter(title: "link")
var link: URL
@MainActor
func perform() async throws -> some IntentResult & ProvidesDialog {
do {
let requestId = UUID().uuidString.lowercased()
_ = try? await Services().dataService.saveURL(id: requestId, url: link.absoluteString)
return .result(dialog: "Link saved to Omnivore")
} catch {
print("error saving URL: ", error)
}
return .result(dialog: "Error saving link")
}
}
@available(iOS 16.4, *)
struct ReadInOmnivoreIntent: ForegroundContinuableIntent {
static var title: LocalizedStringResource = "Save and read a URL in Omnivore"
static var openAppWhenRun: Bool = false
@Parameter(title: "link")
var link: URL
@MainActor
func perform() async throws -> some IntentResult & ProvidesDialog {
let requestId = UUID().uuidString.lowercased()
_ = try? await Services().dataService.saveURL(id: requestId, url: link.absoluteString)
throw needsToContinueInForegroundError("Please continue to open the app.") {
UIApplication.shared.open(URL(string: "omnivore://read/\(requestId)")!)
}
return .result(dialog: "I opened the app.")
}
}
#endif