Add a "search posts" feature to the post list (#245)

This commit is contained in:
Angelo Stavrow 2023-06-10 06:49:45 -04:00 committed by GitHub
parent 579db0c889
commit 6bb8be4d46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 131 additions and 55 deletions

View File

@ -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] 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 a context-menu item to delete local posts from the post list.
- [Mac] Added methods to fetch device logs. - [Mac] Added methods to fetch device logs.
- [iOS, Mac] Added a way to search for text across all posts.
### Changed ### Changed

View File

@ -4,6 +4,7 @@ struct PostListFilteredView: View {
@EnvironmentObject var model: WriteFreelyModel @EnvironmentObject var model: WriteFreelyModel
@Binding var postCount: Int @Binding var postCount: Int
@FetchRequest(entity: WFACollection.entity(), sortDescriptors: []) var collections: FetchedResults<WFACollection> @FetchRequest(entity: WFACollection.entity(), sortDescriptors: []) var collections: FetchedResults<WFACollection>
var fetchRequest: FetchRequest<WFAPost> var fetchRequest: FetchRequest<WFAPost>
init(collection: WFACollection?, showAllPosts: Bool, postCount: Binding<Int>) { init(collection: WFACollection?, showAllPosts: Bool, postCount: Binding<Int>) {
@ -32,67 +33,64 @@ struct PostListFilteredView: View {
var body: some View { var body: some View {
#if os(iOS) #if os(iOS)
List(selection: $model.selectedPost) { if #available(iOS 15, *) {
ForEach(fetchRequest.wrappedValue, id: \.self) { post in SearchablePostListFilteredView(
NavigationLink( postCount: $postCount,
destination: PostEditorView(post: post), collections: collections,
tag: post, fetchRequest: fetchRequest,
selection: $model.selectedPost, onDelete: delete(_:)
label: { )
if model.showAllPosts { .environmentObject(model)
if let collection = collections.filter { $0.alias == post.collectionAlias }.first { .onAppear(perform: {
PostCellView(post: post, collectionName: collection.title) 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 { } else {
let collectionName = model.account.server == "https://write.as" ? "Anonymous" : "Drafts" PostCellView(post: post)
PostCellView(post: post, collectionName: collectionName)
} }
} else { })
PostCellView(post: post)
}
})
.deleteDisabled(post.status != PostStatus.local.rawValue) .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 #else
List(selection: $model.selectedPost) { SearchablePostListFilteredView(
ForEach(fetchRequest.wrappedValue, id: \.self) { post in postCount: $postCount,
NavigationLink( collections: collections,
destination: PostEditorView(post: post), fetchRequest: fetchRequest,
tag: post, onDelete: delete(_:)
selection: $model.selectedPost, )
label: { .environmentObject(model)
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)
}
})
}
.alert(isPresented: $model.isPresentingDeleteAlert) { .alert(isPresented: $model.isPresentingDeleteAlert) {
Alert( Alert(
title: Text("Delete Post?"), title: Text("Delete Post?"),

View File

@ -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<WFACollection>
var fetchRequest: FetchRequest<WFAPost>
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)
}
}

View File

@ -137,7 +137,11 @@
17DFDE8C251D309400A25F31 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE86251D309400A25F31 /* OpenSans-License.txt */; }; 17DFDE8C251D309400A25F31 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE86251D309400A25F31 /* OpenSans-License.txt */; };
17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */; }; 17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */; };
375A67E828FC555C007A1AC0 /* MultilineTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375A67E728FC555C007A1AC0 /* MultilineTextView.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 */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -269,7 +273,9 @@
17DFDE86251D309400A25F31 /* OpenSans-License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "OpenSans-License.txt"; sourceTree = "<group>"; }; 17DFDE86251D309400A25F31 /* OpenSans-License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "OpenSans-License.txt"; sourceTree = "<group>"; };
17E5DF892543610700DCDC9B /* PostTextEditingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTextEditingView.swift; sourceTree = "<group>"; }; 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTextEditingView.swift; sourceTree = "<group>"; };
375A67E728FC555C007A1AC0 /* MultilineTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextView.swift; sourceTree = "<group>"; }; 375A67E728FC555C007A1AC0 /* MultilineTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextView.swift; sourceTree = "<group>"; };
37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePostListFilteredView.swift; sourceTree = "<group>"; };
3779389629EC0C880032D6C1 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = "<group>"; }; 3779389629EC0C880032D6C1 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = "<group>"; };
37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePostListFilteredView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -606,6 +612,7 @@
1756AE7924CB65DF00FD7257 /* PostListView.swift */, 1756AE7924CB65DF00FD7257 /* PostListView.swift */,
17DF32D424C8CA3400BCE2E3 /* PostStatusBadgeView.swift */, 17DF32D424C8CA3400BCE2E3 /* PostStatusBadgeView.swift */,
17C42E642509237800072984 /* PostListFilteredView.swift */, 17C42E642509237800072984 /* PostListFilteredView.swift */,
37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */,
); );
path = PostList; path = PostList;
sourceTree = "<group>"; sourceTree = "<group>";
@ -937,6 +944,7 @@
17B996DA2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */, 17B996DA2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */,
1756AE7724CB2EDD00FD7257 /* PostEditorView.swift in Sources */, 1756AE7724CB2EDD00FD7257 /* PostEditorView.swift in Sources */,
17DF32D524C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */, 17DF32D524C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */,
37F749D129B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */,
17D435E824E3128F0036B539 /* PreferencesModel.swift in Sources */, 17D435E824E3128F0036B539 /* PreferencesModel.swift in Sources */,
1756AE7A24CB65DF00FD7257 /* PostListView.swift in Sources */, 1756AE7A24CB65DF00FD7257 /* PostListView.swift in Sources */,
17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */, 17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */,
@ -981,6 +989,7 @@
17027E26286741B90062EB29 /* Logging.swift in Sources */, 17027E26286741B90062EB29 /* Logging.swift in Sources */,
1727526B2809991A003D0A6A /* ErrorHandling.swift in Sources */, 1727526B2809991A003D0A6A /* ErrorHandling.swift in Sources */,
17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */, 17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */,
37F749D229B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */,
17C42E71250AAFD500072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 17C42E71250AAFD500072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */,
1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */, 1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */,
1753F6AC24E431CC00309365 /* MacPreferencesView.swift in Sources */, 1753F6AC24E431CC00309365 /* MacPreferencesView.swift in Sources */,
@ -1483,7 +1492,7 @@
repositoryURL = "https://github.com/writefreely/writefreely-swift"; repositoryURL = "https://github.com/writefreely/writefreely-swift";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 0.3.6; minimumVersion = 0.3.7;
}; };
}; };
17D4926327947B4D0035BD7E /* XCRemoteSwiftPackageReference "Sparkle" */ = { 17D4926327947B4D0035BD7E /* XCRemoteSwiftPackageReference "Sparkle" */ = {

View File

@ -5,6 +5,16 @@ struct PostCommands: Commands {
var body: some Commands { var body: some Commands {
CommandMenu("Post") { 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 { Group {
Button(action: sendPostUrlToPasteboard, label: { Text("Copy Link To Published Post") }) Button(action: sendPostUrlToPasteboard, label: { Text("Copy Link To Published Post") })
.disabled(model.selectedPost?.status == PostStatus.local.rawValue) .disabled(model.selectedPost?.status == PostStatus.local.rawValue)