diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 1b599c0..2f79f4c 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -1,108 +1,110 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var model: WriteFreelyModel var body: some View { NavigationView { #if os(macOS) CollectionListView() .toolbar { Button( action: { NSApp.keyWindow?.contentViewController?.tryToPerform( #selector(NSSplitViewController.toggleSidebar(_:)), with: nil ) }, label: { Image(systemName: "sidebar.left") } ) .help("Toggle the sidebar's visibility.") Spacer() Button(action: { withAnimation { self.model.selectedPost = nil } let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext) managedPost.createdDate = Date() managedPost.title = "" managedPost.body = "" managedPost.status = PostStatus.local.rawValue managedPost.collectionAlias = nil switch model.preferences.font { case 1: managedPost.appearance = "sans" case 2: managedPost.appearance = "wrap" default: managedPost.appearance = "serif" } if let languageCode = Locale.current.languageCode { managedPost.language = languageCode managedPost.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft } withAnimation { DispatchQueue.main.async { + self.model.showAllPosts = false + self.model.selectedCollection = nil self.model.selectedPost = managedPost } } }, label: { Image(systemName: "square.and.pencil") }) .help("Create a new local draft.") } #else CollectionListView() #endif #if os(macOS) ZStack { - PostListView(selectedCollection: nil, showAllPosts: false) //model.account.isLoggedIn) + PostListView() if model.isProcessingRequest { ZStack { Color(NSColor.controlBackgroundColor).opacity(0.75) ProgressView() } } } #else - PostListView(selectedCollection: nil, showAllPosts: model.account.isLoggedIn) + PostListView() #endif Text("Select a post, or create a new local draft.") .foregroundColor(.secondary) } .environmentObject(model) #if os(iOS) EmptyView() .sheet( isPresented: $model.isPresentingSettingsView, onDismiss: { model.isPresentingSettingsView = false }, content: { SettingsView() .environmentObject(model) } ) .alert(isPresented: $model.isPresentingNetworkErrorAlert, content: { Alert( title: Text("Connection Error"), message: Text(""" There is no internet connection at the moment. Please reconnect or try again later. """), dismissButton: .default(Text("OK"), action: { model.isPresentingNetworkErrorAlert = false }) ) }) #endif } } struct ContentView_Previews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let model = WriteFreelyModel() return ContentView() .environment(\.managedObjectContext, context) .environmentObject(model) } } diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift index 23a4aa5..a084f82 100644 --- a/Shared/PostCollection/CollectionListView.swift +++ b/Shared/PostCollection/CollectionListView.swift @@ -1,51 +1,78 @@ import SwiftUI struct CollectionListView: View { @EnvironmentObject var model: WriteFreelyModel @FetchRequest( entity: WFACollection.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)] ) var collections: FetchedResults var body: some View { - List { + List(selection: $model.selectedCollection) { if model.account.isLoggedIn { - NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: true)) { + NavigationLink( + destination: PostListView(), + isActive: Binding( + get: { () -> Bool in + model.selectedCollection == nil && model.showAllPosts + }, set: { newValue in + if newValue { + self.model.selectedCollection = nil + self.model.showAllPosts = true + } else { + // No-op + } + } + )) { Text("All Posts") } - NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) { + NavigationLink( + destination: PostListView(), + isActive: Binding( + get: { () -> Bool in + model.selectedCollection == nil && !model.showAllPosts + }, set: { newValue in + if newValue { + self.model.selectedCollection = nil + self.model.showAllPosts = false + } else { + // No-op + } + } + )) { Text(model.account.server == "https://write.as" ? "Anonymous" : "Drafts") } Section(header: Text("Your Blogs")) { ForEach(collections, id: \.alias) { collection in NavigationLink( - destination: PostListView(selectedCollection: collection, showAllPosts: false) - ) { - Text(collection.title) - } + collection.title, + destination: PostListView(), + tag: collection, + selection: $model.selectedCollection + ) } } } else { NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) { Text("Drafts") } } } .navigationTitle( model.account.isLoggedIn ? "\(URL(string: model.account.server)?.host ?? "WriteFreely")" : "WriteFreely" ) .listStyle(SidebarListStyle()) } } struct CollectionListView_LoggedOutPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let model = WriteFreelyModel() return CollectionListView() .environment(\.managedObjectContext, context) .environmentObject(model) } } diff --git a/Shared/PostList/PostListFilteredView.swift b/Shared/PostList/PostListFilteredView.swift index 3460033..29601d6 100644 --- a/Shared/PostList/PostListFilteredView.swift +++ b/Shared/PostList/PostListFilteredView.swift @@ -1,154 +1,140 @@ import SwiftUI struct PostListFilteredView: View { @EnvironmentObject var model: WriteFreelyModel @Binding var postCount: Int @FetchRequest(entity: WFACollection.entity(), sortDescriptors: []) var collections: FetchedResults var fetchRequest: FetchRequest - var showAllPosts: Bool { - didSet { - model.showAllPosts = showAllPosts - } - } - - var selectedCollection: WFACollection? { - didSet { - model.selectedCollection = selectedCollection - } - } - init(collection: WFACollection?, showAllPosts: Bool, postCount: Binding) { - self.showAllPosts = showAllPosts - self.selectedCollection = collection if showAllPosts { fetchRequest = FetchRequest( entity: WFAPost.entity(), sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)] ) } else { if let collectionAlias = collection?.alias { fetchRequest = FetchRequest( entity: WFAPost.entity(), sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)], predicate: NSPredicate(format: "collectionAlias == %@", collectionAlias) ) } else { fetchRequest = FetchRequest( entity: WFAPost.entity(), sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)], predicate: NSPredicate(format: "collectionAlias == nil") ) } } _postCount = postCount } var body: some View { #if os(iOS) List { ForEach(fetchRequest.wrappedValue, id: \.self) { post in NavigationLink( destination: PostEditorView(post: post), tag: post, - selection: $model.selectedPost - ) { - if showAllPosts { - if let collection = collections.filter { $0.alias == post.collectionAlias }.first { - PostCellView(post: post, collectionName: collection.title) + 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 { - 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) + }) + .deleteDisabled(post.status != PostStatus.local.rawValue) } .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 }) #else List { ForEach(fetchRequest.wrappedValue, id: \.self) { post in NavigationLink( destination: PostEditorView(post: post), tag: post, - selection: $model.selectedPost - ) { - if showAllPosts { - if let collection = collections.filter { $0.alias == post.collectionAlias }.first { - PostCellView(post: post, collectionName: collection.title) + 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 { - 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) + }) + .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( title: Text("Delete Post?"), message: Text("This action cannot be undone."), primaryButton: .cancel() { model.postToDelete = nil }, secondaryButton: .destructive(Text("Delete"), action: { if let postToDelete = model.postToDelete { model.selectedPost = nil DispatchQueue.main.async { model.editor.clearLastDraft() model.posts.remove(postToDelete) } model.postToDelete = nil } }) ) } .onDeleteCommand(perform: { guard let selectedPost = model.selectedPost else { return } if selectedPost.status == PostStatus.local.rawValue { model.postToDelete = selectedPost model.isPresentingDeleteAlert = true } }) #endif } func delete(_ post: WFAPost) { DispatchQueue.main.async { if post == model.selectedPost { model.selectedPost = nil model.editor.clearLastDraft() } model.posts.remove(post) } } } struct PostListFilteredView_Previews: PreviewProvider { static var previews: some View { return PostListFilteredView(collection: nil, showAllPosts: false, postCount: .constant(999)) } } diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index 1d16962..271e4e8 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -1,173 +1,168 @@ import SwiftUI import Combine struct PostListView: View { @EnvironmentObject var model: WriteFreelyModel @Environment(\.managedObjectContext) var managedObjectContext - @State var selectedCollection: WFACollection? - @State var showAllPosts: Bool = false @State private var postCount: Int = 0 #if os(iOS) private var frameHeight: CGFloat { var height: CGFloat = 50 let bottom = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0 height += bottom return height } #endif var body: some View { #if os(iOS) ZStack(alignment: .bottom) { - PostListFilteredView(collection: selectedCollection, showAllPosts: showAllPosts, postCount: $postCount) + PostListFilteredView( + collection: model.selectedCollection, + showAllPosts: model.showAllPosts, + postCount: $postCount + ) .navigationTitle( - showAllPosts ? "All Posts" : selectedCollection?.title ?? ( + model.showAllPosts ? "All Posts" : model.selectedCollection?.title ?? ( model.account.server == "https://write.as" ? "Anonymous" : "Drafts" ) ) .toolbar { ToolbarItem(placement: .primaryAction) { // We have to add a Spacer as a sibling view to the Button in some kind of Stack, so that any // a11y modifiers are applied as expected: bug report filed as FB8956392. ZStack { Spacer() Button(action: { let managedPost = WFAPost(context: self.managedObjectContext) managedPost.createdDate = Date() managedPost.title = "" managedPost.body = "" managedPost.status = PostStatus.local.rawValue managedPost.collectionAlias = nil switch model.preferences.font { case 1: managedPost.appearance = "sans" case 2: managedPost.appearance = "wrap" default: managedPost.appearance = "serif" } if let languageCode = Locale.current.languageCode { managedPost.language = languageCode //swiftlint:disable:next line_length managedPost.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft } withAnimation { - self.selectedCollection = nil - self.showAllPosts = false + self.model.showAllPosts = false + self.model.selectedCollection = nil self.model.selectedPost = managedPost } }, label: { ZStack { Image("does.not.exist") .accessibilityHidden(true) Image(systemName: "square.and.pencil") .accessibilityHidden(true) .imageScale(.large) // These modifiers compensate for the resizing .padding(.vertical, 12) // done to the Image (and the button tap target) .padding(.leading, 12) // by the SwiftUI layout system from adding a .padding(.trailing, 8) // Spacer in this ZStack (FB8956392). } .frame(maxWidth: .infinity, maxHeight: .infinity) }) .accessibilityLabel(Text("Compose")) .accessibilityHint(Text("Compose a new local draft")) } } } VStack { HStack(spacing: 0) { Button(action: { model.isPresentingSettingsView = true }, label: { Image(systemName: "gear") .padding(.vertical, 4) .padding(.horizontal, 8) }) .accessibilityLabel(Text("Settings")) .accessibilityHint(Text("Open the Settings sheet")) Spacer() Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts") .foregroundColor(.secondary) Spacer() if model.isProcessingRequest { ProgressView() .padding(.vertical, 4) .padding(.horizontal, 8) } else { Button(action: { DispatchQueue.main.async { model.fetchUserCollections() model.fetchUserPosts() } }, label: { Image(systemName: "arrow.clockwise") .padding(.vertical, 4) .padding(.horizontal, 8) }) .accessibilityLabel(Text("Refresh Posts")) .accessibilityHint(Text("Fetch changes from the server")) .disabled(!model.account.isLoggedIn) } } .padding(.top, 8) .padding(.horizontal, 8) Spacer() } .frame(height: frameHeight) .background(Color(UIColor.systemGray5)) .overlay(Divider(), alignment: .top) } .ignoresSafeArea() #else //if os(macOS) PostListFilteredView( - collection: selectedCollection, - showAllPosts: showAllPosts, + collection: model.selectedCollection, + showAllPosts: model.showAllPosts, postCount: $postCount ) .toolbar { ToolbarItemGroup(placement: .primaryAction) { if let selectedPost = model.selectedPost { ActivePostToolbarView(activePost: selectedPost) .alert(isPresented: $model.isPresentingNetworkErrorAlert, content: { Alert( title: Text("Connection Error"), message: Text(""" There is no internet connection at the moment. \ Please reconnect or try again later. """), dismissButton: .default(Text("OK"), action: { model.isPresentingNetworkErrorAlert = false }) ) }) } } } - .onDisappear { - DispatchQueue.main.async { - self.model.selectedCollection = nil - self.model.showAllPosts = true - self.model.selectedPost = nil - } - } .navigationTitle( - showAllPosts ? "All Posts" : selectedCollection?.title ?? ( + model.showAllPosts ? "All Posts" : model.selectedCollection?.title ?? ( model.account.server == "https://write.as" ? "Anonymous" : "Drafts" ) ) #endif } } struct PostListView_Previews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let model = WriteFreelyModel() return PostListView() .environment(\.managedObjectContext, context) .environmentObject(model) } }