Source code for the WriteFreely SwiftUI app for iOS, iPadOS, and macOS
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 

187 wiersze
7.6 KiB

  1. import SwiftUI
  2. import Combine
  3. struct PostListView: View {
  4. @EnvironmentObject var model: WriteFreelyModel
  5. @EnvironmentObject var errorHandling: ErrorHandling
  6. @Environment(\.managedObjectContext) var managedObjectContext
  7. @State private var postCount: Int = 0
  8. @State private var filteredListViewId: Int = 0
  9. var selectedCollection: WFACollection?
  10. var showAllPosts: Bool
  11. #if os(iOS)
  12. private var frameHeight: CGFloat {
  13. var height: CGFloat = 50
  14. let bottom = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0
  15. height += bottom
  16. return height
  17. }
  18. #endif
  19. var body: some View {
  20. #if os(iOS)
  21. ZStack(alignment: .bottom) {
  22. PostListFilteredView(
  23. collection: selectedCollection,
  24. showAllPosts: showAllPosts,
  25. postCount: $postCount
  26. )
  27. .id(self.filteredListViewId)
  28. .navigationTitle(
  29. showAllPosts ? "All Posts" : selectedCollection?.title ?? (
  30. model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
  31. )
  32. )
  33. .toolbar {
  34. ToolbarItem(placement: .primaryAction) {
  35. // We have to add a Spacer as a sibling view to the Button in some kind of Stack, so that any
  36. // a11y modifiers are applied as expected: bug report filed as FB8956392.
  37. ZStack {
  38. Spacer()
  39. Button(action: {
  40. let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
  41. withAnimation {
  42. self.model.showAllPosts = false
  43. self.model.selectedPost = managedPost
  44. }
  45. }, label: {
  46. ZStack {
  47. Image("does.not.exist")
  48. .accessibilityHidden(true)
  49. Image(systemName: "square.and.pencil")
  50. .accessibilityHidden(true)
  51. .imageScale(.large) // These modifiers compensate for the resizing
  52. .padding(.vertical, 12) // done to the Image (and the button tap target)
  53. .padding(.leading, 12) // by the SwiftUI layout system from adding a
  54. .padding(.trailing, 8) // Spacer in this ZStack (FB8956392).
  55. }
  56. .frame(maxWidth: .infinity, maxHeight: .infinity)
  57. })
  58. .accessibilityLabel(Text("Compose"))
  59. .accessibilityHint(Text("Compose a new local draft"))
  60. }
  61. }
  62. }
  63. VStack {
  64. HStack(spacing: 0) {
  65. Button(action: {
  66. model.isPresentingSettingsView = true
  67. }, label: {
  68. Image(systemName: "gear")
  69. .padding(.vertical, 4)
  70. .padding(.horizontal, 8)
  71. })
  72. .accessibilityLabel(Text("Settings"))
  73. .accessibilityHint(Text("Open the Settings sheet"))
  74. .sheet(
  75. isPresented: $model.isPresentingSettingsView,
  76. onDismiss: { model.isPresentingSettingsView = false },
  77. content: {
  78. SettingsView()
  79. .environmentObject(model)
  80. }
  81. )
  82. Spacer()
  83. Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts")
  84. .foregroundColor(.secondary)
  85. Spacer()
  86. if model.isProcessingRequest {
  87. ProgressView()
  88. .padding(.vertical, 4)
  89. .padding(.horizontal, 8)
  90. } else {
  91. Button(action: {
  92. DispatchQueue.main.async {
  93. model.fetchUserCollections()
  94. model.fetchUserPosts()
  95. }
  96. }, label: {
  97. Image(systemName: "arrow.clockwise")
  98. .padding(.vertical, 4)
  99. .padding(.horizontal, 8)
  100. })
  101. .accessibilityLabel(Text("Refresh Posts"))
  102. .accessibilityHint(Text("Fetch changes from the server"))
  103. .disabled(!model.account.isLoggedIn)
  104. }
  105. }
  106. .padding(.top, 8)
  107. .padding(.horizontal, 8)
  108. Spacer()
  109. }
  110. .frame(height: frameHeight)
  111. .background(Color(UIColor.systemGray5))
  112. .overlay(Divider(), alignment: .top)
  113. .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
  114. // We use this to invalidate and refresh the view, so that new posts created outside of the app (e.g.,
  115. // in the action extension) show up.
  116. withAnimation {
  117. self.filteredListViewId += 1
  118. }
  119. }
  120. }
  121. .ignoresSafeArea(.all, edges: .bottom)
  122. .onAppear {
  123. model.selectedCollection = selectedCollection
  124. model.showAllPosts = showAllPosts
  125. }
  126. .onChange(of: model.hasError) { value in
  127. if value {
  128. if let error = model.currentError {
  129. self.errorHandling.handle(error: error)
  130. } else {
  131. self.errorHandling.handle(error: AppError.genericError())
  132. }
  133. model.hasError = false
  134. }
  135. }
  136. #else
  137. PostListFilteredView(
  138. collection: selectedCollection,
  139. showAllPosts: showAllPosts,
  140. postCount: $postCount
  141. )
  142. .toolbar {
  143. ToolbarItemGroup(placement: .primaryAction) {
  144. if model.selectedPost != nil {
  145. ActivePostToolbarView(activePost: model.selectedPost!)
  146. }
  147. }
  148. }
  149. .navigationTitle(
  150. showAllPosts ? "All Posts" : selectedCollection?.title ?? (
  151. model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
  152. )
  153. )
  154. .onAppear {
  155. model.selectedCollection = selectedCollection
  156. model.showAllPosts = showAllPosts
  157. }
  158. .onChange(of: model.hasError) { value in
  159. if value {
  160. if let error = model.currentError {
  161. self.errorHandling.handle(error: error)
  162. } else {
  163. self.errorHandling.handle(error: AppError.genericError())
  164. }
  165. model.hasError = false
  166. }
  167. }
  168. #endif
  169. }
  170. }
  171. struct PostListView_Previews: PreviewProvider {
  172. static var previews: some View {
  173. let context = LocalStorageManager.standard.container.viewContext
  174. let model = WriteFreelyModel()
  175. return PostListView(showAllPosts: true)
  176. .environment(\.managedObjectContext, context)
  177. .environmentObject(model)
  178. }
  179. }