From 6bb8be4d46b61a909e65123df35075a137cdd98b Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Sat, 10 Jun 2023 06:49:45 -0400 Subject: [PATCH] Add a "search posts" feature to the post list (#245) --- CHANGELOG.md | 1 + Shared/PostList/PostListFilteredView.swift | 106 +++++++++--------- .../SearchablePostListFilteredView.swift | 58 ++++++++++ .../project.pbxproj | 11 +- macOS/Navigation/PostCommands.swift | 10 ++ 5 files changed, 131 insertions(+), 55 deletions(-) create mode 100644 Shared/PostList/SearchablePostListFilteredView.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a1650a4..9540fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [Mac] In a post with unpublished changes (i.e., with "local" or "edited" status), the post is autosaved after a one-second pause in typing. - [Mac] Added a context-menu item to delete local posts from the post list. - [Mac] Added methods to fetch device logs. +- [iOS, Mac] Added a way to search for text across all posts. ### Changed diff --git a/Shared/PostList/PostListFilteredView.swift b/Shared/PostList/PostListFilteredView.swift index cb52c3d..24b2453 100644 --- a/Shared/PostList/PostListFilteredView.swift +++ b/Shared/PostList/PostListFilteredView.swift @@ -4,6 +4,7 @@ struct PostListFilteredView: View { @EnvironmentObject var model: WriteFreelyModel @Binding var postCount: Int @FetchRequest(entity: WFACollection.entity(), sortDescriptors: []) var collections: FetchedResults + var fetchRequest: FetchRequest init(collection: WFACollection?, showAllPosts: Bool, postCount: Binding) { @@ -32,67 +33,64 @@ struct PostListFilteredView: View { var body: some View { #if os(iOS) - List(selection: $model.selectedPost) { - ForEach(fetchRequest.wrappedValue, id: \.self) { post in - NavigationLink( - destination: PostEditorView(post: post), - tag: post, - selection: $model.selectedPost, - label: { - if model.showAllPosts { - if let collection = collections.filter { $0.alias == post.collectionAlias }.first { - PostCellView(post: post, collectionName: collection.title) + if #available(iOS 15, *) { + SearchablePostListFilteredView( + postCount: $postCount, + collections: collections, + fetchRequest: fetchRequest, + onDelete: delete(_:) + ) + .environmentObject(model) + .onAppear(perform: { + self.postCount = fetchRequest.wrappedValue.count + }) + .onChange(of: fetchRequest.wrappedValue.count, perform: { value in + self.postCount = value + }) + } else { + List(selection: $model.selectedPost) { + ForEach(fetchRequest.wrappedValue, id: \.self) { post in + NavigationLink( + destination: PostEditorView(post: post), + tag: post, + selection: $model.selectedPost, + label: { + if model.showAllPosts { + if let collection = collections.filter({ $0.alias == post.collectionAlias }).first { + PostCellView(post: post, collectionName: collection.title) + } else { + // swiftlint:disable:next line_length + let collectionName = model.account.server == "https://write.as" ? "Anonymous" : "Drafts" + PostCellView(post: post, collectionName: collectionName) + } } else { - let collectionName = model.account.server == "https://write.as" ? "Anonymous" : "Drafts" - PostCellView(post: post, collectionName: collectionName) + PostCellView(post: post) } - } else { - PostCellView(post: post) - } - }) + }) .deleteDisabled(post.status != PostStatus.local.rawValue) - } - .onDelete(perform: { indexSet in - for index in indexSet { - let post = fetchRequest.wrappedValue[index] - delete(post) } + .onDelete(perform: { indexSet in + for index in indexSet { + let post = fetchRequest.wrappedValue[index] + delete(post) + } + }) + } + .onAppear(perform: { + self.postCount = fetchRequest.wrappedValue.count + }) + .onChange(of: fetchRequest.wrappedValue.count, perform: { value in + self.postCount = value }) } - .onAppear(perform: { - self.postCount = fetchRequest.wrappedValue.count - }) - .onChange(of: fetchRequest.wrappedValue.count, perform: { value in - self.postCount = value - }) #else - List(selection: $model.selectedPost) { - ForEach(fetchRequest.wrappedValue, id: \.self) { post in - NavigationLink( - destination: PostEditorView(post: post), - tag: post, - selection: $model.selectedPost, - label: { - if model.showAllPosts { - if let collection = collections.filter { $0.alias == post.collectionAlias }.first { - PostCellView(post: post, collectionName: collection.title) - } else { - let collectionName = model.account.server == "https://write.as" ? "Anonymous" : "Drafts" - PostCellView(post: post, collectionName: collectionName) - } - } else { - PostCellView(post: post) - } - }) - .deleteDisabled(post.status != PostStatus.local.rawValue) - } - .onDelete(perform: { indexSet in - for index in indexSet { - let post = fetchRequest.wrappedValue[index] - delete(post) - } - }) - } + SearchablePostListFilteredView( + postCount: $postCount, + collections: collections, + fetchRequest: fetchRequest, + onDelete: delete(_:) + ) + .environmentObject(model) .alert(isPresented: $model.isPresentingDeleteAlert) { Alert( title: Text("Delete Post?"), diff --git a/Shared/PostList/SearchablePostListFilteredView.swift b/Shared/PostList/SearchablePostListFilteredView.swift new file mode 100644 index 0000000..d4b2701 --- /dev/null +++ b/Shared/PostList/SearchablePostListFilteredView.swift @@ -0,0 +1,58 @@ +import SwiftUI + +@available(iOS 15, macOS 12.0, *) +struct SearchablePostListFilteredView: View { + @EnvironmentObject var model: WriteFreelyModel + @Binding var postCount: Int + @State private var searchString = "" + + var collections: FetchedResults + var fetchRequest: FetchRequest + var onDelete: (WFAPost) -> Void + + var body: some View { + List(selection: $model.selectedPost) { + ForEach(fetchRequest.wrappedValue, id: \.self) { post in + if !searchString.isEmpty && + !post.title.localizedCaseInsensitiveContains(searchString) && + !post.body.localizedCaseInsensitiveContains(searchString) { + EmptyView() + } else { + NavigationLink( + destination: PostEditorView(post: post), + tag: post, + selection: $model.selectedPost, + label: { + if model.showAllPosts { + if let collection = collections.filter({ $0.alias == post.collectionAlias }).first { + PostCellView(post: post, collectionName: collection.title) + } else { + // swiftlint:disable:next line_length + let collectionName = model.account.server == "https://write.as" ? "Anonymous" : "Drafts" + PostCellView(post: post, collectionName: collectionName) + } + } else { + PostCellView(post: post) + } + }) + .deleteDisabled(post.status != PostStatus.local.rawValue) + } + } + .onDelete(perform: { indexSet in + for index in indexSet { + let post = fetchRequest.wrappedValue[index] + delete(post) + } + }) + } + #if os(iOS) + .searchable(text: $searchString, prompt: "Search across posts") + #else + .searchable(text: $searchString, placement: .toolbar, prompt: "Search across posts") + #endif + } + + func delete(_ post: WFAPost) { + onDelete(post) + } +} diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index 2b127fa..4d04e3a 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -137,7 +137,11 @@ 17DFDE8C251D309400A25F31 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE86251D309400A25F31 /* OpenSans-License.txt */; }; 17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */; }; 375A67E828FC555C007A1AC0 /* MultilineTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375A67E728FC555C007A1AC0 /* MultilineTextView.swift */; }; + 37F749D129B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */; }; + 37F749D229B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */; }; 3779389729EC0C880032D6C1 /* HelpCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3779389629EC0C880032D6C1 /* HelpCommands.swift */; }; + 37F749D129B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */; }; + 37F749D229B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -269,7 +273,9 @@ 17DFDE86251D309400A25F31 /* OpenSans-License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "OpenSans-License.txt"; sourceTree = ""; }; 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTextEditingView.swift; sourceTree = ""; }; 375A67E728FC555C007A1AC0 /* MultilineTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextView.swift; sourceTree = ""; }; + 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePostListFilteredView.swift; sourceTree = ""; }; 3779389629EC0C880032D6C1 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = ""; }; + 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePostListFilteredView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -606,6 +612,7 @@ 1756AE7924CB65DF00FD7257 /* PostListView.swift */, 17DF32D424C8CA3400BCE2E3 /* PostStatusBadgeView.swift */, 17C42E642509237800072984 /* PostListFilteredView.swift */, + 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */, ); path = PostList; sourceTree = ""; @@ -937,6 +944,7 @@ 17B996DA2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */, 1756AE7724CB2EDD00FD7257 /* PostEditorView.swift in Sources */, 17DF32D524C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */, + 37F749D129B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */, 17D435E824E3128F0036B539 /* PreferencesModel.swift in Sources */, 1756AE7A24CB65DF00FD7257 /* PostListView.swift in Sources */, 17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */, @@ -981,6 +989,7 @@ 17027E26286741B90062EB29 /* Logging.swift in Sources */, 1727526B2809991A003D0A6A /* ErrorHandling.swift in Sources */, 17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */, + 37F749D229B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */, 17C42E71250AAFD500072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */, 1753F6AC24E431CC00309365 /* MacPreferencesView.swift in Sources */, @@ -1483,7 +1492,7 @@ repositoryURL = "https://github.com/writefreely/writefreely-swift"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.3.6; + minimumVersion = 0.3.7; }; }; 17D4926327947B4D0035BD7E /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/macOS/Navigation/PostCommands.swift b/macOS/Navigation/PostCommands.swift index 2b2d45a..1a2b7b6 100644 --- a/macOS/Navigation/PostCommands.swift +++ b/macOS/Navigation/PostCommands.swift @@ -5,6 +5,16 @@ struct PostCommands: Commands { var body: some Commands { CommandMenu("Post") { + Button("Find In Posts") { + if let toolbar = NSApp.keyWindow?.toolbar, + let search = toolbar.items.first(where: { + $0.itemIdentifier.rawValue == "com.apple.SwiftUI.search" + }) as? NSSearchToolbarItem { + search.beginSearchInteraction() + } + } + .keyboardShortcut("f", modifiers: [.command, .shift]) + Group { Button(action: sendPostUrlToPasteboard, label: { Text("Copy Link To Published Post") }) .disabled(model.selectedPost?.status == PostStatus.local.rawValue)