Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F13799795
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
69 KB
Subscribers
None
View Options
diff --git a/Shared/Extensions/WriteFreelyModel+API.swift b/Shared/Extensions/WriteFreelyModel+API.swift
index 1a574ab..47ec63e 100644
--- a/Shared/Extensions/WriteFreelyModel+API.swift
+++ b/Shared/Extensions/WriteFreelyModel+API.swift
@@ -1,182 +1,182 @@
import Foundation
import WriteFreely
extension WriteFreelyModel {
func login(to server: URL, as username: String, password: String) {
if !hasNetworkConnection {
self.currentError = NetworkError.noConnectionError
return
}
let secureProtocolPrefix = "https://"
let insecureProtocolPrefix = "http://"
var serverString = server.absoluteString
// If there's neither an http or https prefix, prepend "https://" to the server string.
if !(serverString.hasPrefix(secureProtocolPrefix) || serverString.hasPrefix(insecureProtocolPrefix)) {
serverString = secureProtocolPrefix + serverString
}
// If the server string is prefixed with http, upgrade to https before attempting to login.
if serverString.hasPrefix(insecureProtocolPrefix) {
serverString = serverString.replacingOccurrences(of: insecureProtocolPrefix, with: secureProtocolPrefix)
}
isLoggingIn = true
var serverURL = URL(string: serverString)!
if !serverURL.path.isEmpty {
serverURL.deleteLastPathComponent()
}
account.server = serverURL.absoluteString
client = WFClient(for: serverURL)
client?.login(username: username, password: password, completion: loginHandler)
}
func logout() {
if !hasNetworkConnection {
self.currentError = NetworkError.noConnectionError
return
}
guard let loggedInClient = client else {
do {
try purgeTokenFromKeychain(username: account.username, server: account.server)
account.logout()
} catch {
self.currentError = KeychainError.couldNotPurgeAccessToken
}
return
}
loggedInClient.logout(completion: logoutHandler)
}
func fetchUserCollections() {
if !hasNetworkConnection {
self.currentError = NetworkError.noConnectionError
return
}
guard let loggedInClient = client else {
self.currentError = AppError.couldNotGetLoggedInClient
return
}
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
loggedInClient.getUserCollections(completion: fetchUserCollectionsHandler)
}
func fetchUserPosts() {
if !hasNetworkConnection {
self.currentError = NetworkError.noConnectionError
return
}
guard let loggedInClient = client else {
self.currentError = AppError.couldNotGetLoggedInClient
return
}
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
loggedInClient.getPosts(completion: fetchUserPostsHandler)
}
func publish(post: WFAPost) {
postToUpdate = nil
if !hasNetworkConnection {
self.currentError = NetworkError.noConnectionError
return
}
guard let loggedInClient = client else {
self.currentError = AppError.couldNotGetLoggedInClient
return
}
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
if post.language == nil {
if #available(iOS 16, macOS 13, *) {
if let languageCode = Locale.current.language.languageCode?.identifier {
post.language = languageCode
post.rtl = Locale.Language(identifier: languageCode).characterDirection == .rightToLeft
}
} else {
if let languageCode = Locale.current.languageCode {
post.language = languageCode
post.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft
}
}
}
var wfPost = WFPost(
body: post.body,
title: post.title.isEmpty ? "" : post.title,
appearance: post.appearance,
language: post.language,
rtl: post.rtl,
createdDate: post.status == PostStatus.local.rawValue ? Date() : post.createdDate
)
if let existingPostId = post.postId {
// This is an existing post.
postToUpdate = post
wfPost.postId = post.postId
loggedInClient.updatePost(
postId: existingPostId,
updatedPost: wfPost,
completion: publishHandler
)
} else {
// This is a new local draft.
loggedInClient.createPost(
post: wfPost, in: post.collectionAlias, completion: publishHandler
)
}
}
func updateFromServer(post: WFAPost) {
if !hasNetworkConnection {
self.currentError = NetworkError.noConnectionError
return
}
guard let loggedInClient = client else {
self.currentError = AppError.couldNotGetLoggedInClient
return
}
guard let postId = post.postId else {
self.currentError = AppError.couldNotGetPostId
return
}
// We're starting the network request.
DispatchQueue.main.async {
#if os(iOS)
- self.selectedPost = post
+ self.navState.selectedPost = post
#endif
self.isProcessingRequest = true
}
loggedInClient.getPost(byId: postId, completion: updateFromServerHandler)
}
func move(post: WFAPost, from oldCollection: WFACollection?, to newCollection: WFACollection?) {
if !hasNetworkConnection {
self.currentError = NetworkError.noConnectionError
return
}
guard let loggedInClient = client else {
self.currentError = AppError.couldNotGetLoggedInClient
return
}
guard let postId = post.postId else {
self.currentError = AppError.couldNotGetPostId
return
}
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
- selectedPost = post
+ navState.selectedPost = post
post.collectionAlias = newCollection?.alias
loggedInClient.movePost(postId: postId, to: newCollection?.alias, completion: movePostHandler)
}
}
diff --git a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
index 8698977..beb186b 100644
--- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
+++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift
@@ -1,288 +1,288 @@
import Foundation
import WriteFreely
extension WriteFreelyModel {
func loginHandler(result: Result<WFUser, Error>) {
DispatchQueue.main.async {
self.isLoggingIn = false
}
do {
let user = try result.get()
fetchUserCollections()
fetchUserPosts()
do {
try saveTokenToKeychain(user.token, username: user.username, server: account.server)
DispatchQueue.main.async {
self.account.login(user)
}
} catch {
self.currentError = KeychainError.couldNotStoreAccessToken
}
} catch WFError.notFound {
self.currentError = AccountError.usernameNotFound
} catch WFError.unauthorized {
self.currentError = AccountError.invalidPassword
} catch {
if (error as NSError).domain == NSURLErrorDomain,
(error as NSError).code == -1003 {
self.currentError = AccountError.serverNotFound
} else {
self.currentError = error
}
}
}
func logoutHandler(result: Result<Bool, Error>) {
do {
_ = try result.get()
do {
try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
client = nil
DispatchQueue.main.async {
self.account.logout()
do {
try LocalStorageManager.standard.purgeUserCollections()
try self.posts.purgePublishedPosts()
} catch {
self.currentError = error
}
}
} catch {
self.currentError = KeychainError.couldNotPurgeAccessToken
}
} catch WFError.notFound {
// The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with
// purging the token from the Keychain, destroying the client object, and setting the AccountModel to its
// logged-out state.
do {
try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
client = nil
DispatchQueue.main.async {
self.account.logout()
do {
try LocalStorageManager.standard.purgeUserCollections()
try self.posts.purgePublishedPosts()
} catch {
self.currentError = error
}
}
} catch {
self.currentError = KeychainError.couldNotPurgeAccessToken
}
} catch {
// We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here,
// so we're using a hacky workaround — if we get the NSURLError, but the AccountModel still thinks we're
// logged in, try calling the logout function again and see what we get.
// Conditional cast from 'Error' to 'NSError' always succeeds but is the only way to check error properties.
if (error as NSError).domain == NSURLErrorDomain,
(error as NSError).code == NSURLErrorCannotParseResponse {
if account.isLoggedIn {
self.logout()
}
}
}
}
func fetchUserCollectionsHandler(result: Result<[WFCollection], Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
do {
let fetchedCollections = try result.get()
for fetchedCollection in fetchedCollections {
DispatchQueue.main.async {
let localCollection = WFACollection(context: LocalStorageManager.standard.container.viewContext)
localCollection.alias = fetchedCollection.alias
localCollection.blogDescription = fetchedCollection.description
localCollection.email = fetchedCollection.email
localCollection.isPublic = fetchedCollection.isPublic ?? false
localCollection.styleSheet = fetchedCollection.styleSheet
localCollection.title = fetchedCollection.title
localCollection.url = fetchedCollection.url
}
}
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} catch WFError.unauthorized {
self.currentError = AccountError.genericAuthError
self.logout()
} catch {
self.currentError = AppError.genericError(error.localizedDescription)
}
}
// swiftlint:disable function_body_length
func fetchUserPostsHandler(result: Result<[WFPost], Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
let request = WFAPost.createFetchRequest()
do {
let locallyCachedPosts = try LocalStorageManager.standard.container.viewContext.fetch(request)
do {
var postsToDelete = locallyCachedPosts.filter { $0.status != PostStatus.local.rawValue }
let fetchedPosts = try result.get()
for fetchedPost in fetchedPosts {
if let managedPost = locallyCachedPosts.first(where: { $0.postId == fetchedPost.postId }) {
DispatchQueue.main.async {
managedPost.wasDeletedFromServer = false
if let fetchedPostUpdatedDate = fetchedPost.updatedDate,
let localPostUpdatedDate = managedPost.updatedDate {
managedPost.hasNewerRemoteCopy = fetchedPostUpdatedDate > localPostUpdatedDate
} else {
self.currentError = AppError.genericError(
"Error updating post: could not determine which copy of post is newer."
)
}
if managedPost.collectionAlias != fetchedPost.collectionAlias {
// The post has been moved so we update the managed post's collectionAlias property.
DispatchQueue.main.async {
- if self.selectedPost == managedPost {
- self.selectedPost = nil
+ if self.navState.selectedPost == managedPost {
+ self.navState.selectedPost = nil
}
managedPost.collectionAlias = fetchedPost.collectionAlias
}
}
postsToDelete.removeAll(where: { $0.postId == fetchedPost.postId })
}
} else {
DispatchQueue.main.async {
let managedPost = WFAPost(context: LocalStorageManager.standard.container.viewContext)
self.importData(from: fetchedPost, into: managedPost)
managedPost.collectionAlias = fetchedPost.collectionAlias
managedPost.wasDeletedFromServer = false
}
}
}
DispatchQueue.main.async {
for post in postsToDelete { post.wasDeletedFromServer = true }
LocalStorageManager.standard.saveContext()
}
} catch {
self.currentError = AppError.genericError(error.localizedDescription)
}
} catch WFError.unauthorized {
self.currentError = AccountError.genericAuthError
self.logout()
} catch {
self.currentError = LocalStoreError.couldNotFetchPosts("cached")
}
}
// swiftlint:enable function_body_length
func publishHandler(result: Result<WFPost, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
// ⚠️ NOTE:
// The API does not return a collection alias, so we take care not to overwrite the
// cached post's collection alias with the 'nil' value from the fetched post.
// See: https://github.com/writeas/writefreely-swift/issues/20
do {
let fetchedPost = try result.get()
// If this is an updated post, check it against postToUpdate.
if let updatingPost = self.postToUpdate {
importData(from: fetchedPost, into: updatingPost)
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} else {
// Otherwise if it's a newly-published post, find it in the local store.
let request = WFAPost.createFetchRequest()
let matchBodyPredicate = NSPredicate(format: "body == %@", fetchedPost.body)
if let fetchedPostTitle = fetchedPost.title {
let matchTitlePredicate = NSPredicate(format: "title == %@", fetchedPostTitle)
request.predicate = NSCompoundPredicate(
andPredicateWithSubpredicates: [
matchTitlePredicate,
matchBodyPredicate
]
)
} else {
request.predicate = matchBodyPredicate
}
do {
let cachedPostsResults = try LocalStorageManager.standard.container.viewContext.fetch(request)
guard let cachedPost = cachedPostsResults.first else { return }
importData(from: fetchedPost, into: cachedPost)
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
} catch {
self.currentError = LocalStoreError.couldNotFetchPosts("cached")
}
}
} catch {
self.currentError = AppError.genericError(error.localizedDescription)
}
}
func updateFromServerHandler(result: Result<WFPost, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
// ⚠️ NOTE:
// The API does not return a collection alias, so we take care not to overwrite the
// cached post's collection alias with the 'nil' value from the fetched post.
// See: https://github.com/writeas/writefreely-swift/issues/20
do {
let fetchedPost = try result.get()
#if os(iOS)
- guard let cachedPost = self.selectedPost else { return }
+ guard let cachedPost = self.navState.selectedPost else { return }
#else
guard let cachedPost = self.editor.postToUpdate else { return }
#endif
importData(from: fetchedPost, into: cachedPost)
cachedPost.hasNewerRemoteCopy = false
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
#if os(macOS)
self.selectedPost = cachedPost
#endif
cachedPost.status = PostStatus.published.rawValue
}
} catch {
self.currentError = AppError.genericError(error.localizedDescription)
}
}
func movePostHandler(result: Result<Bool, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
do {
let succeeded = try result.get()
if succeeded {
- if let post = selectedPost {
+ if let post = navState.selectedPost {
updateFromServer(post: post)
} else {
return
}
}
} catch {
DispatchQueue.main.async {
LocalStorageManager.standard.container.viewContext.rollback()
}
self.currentError = AppError.genericError(error.localizedDescription)
}
}
private func importData(from fetchedPost: WFPost, into cachedPost: WFAPost) {
cachedPost.appearance = fetchedPost.appearance
cachedPost.body = fetchedPost.body
cachedPost.createdDate = fetchedPost.createdDate
cachedPost.language = fetchedPost.language
cachedPost.postId = fetchedPost.postId
cachedPost.rtl = fetchedPost.rtl ?? false
cachedPost.slug = fetchedPost.slug
cachedPost.status = PostStatus.published.rawValue
cachedPost.title = fetchedPost.title ?? ""
cachedPost.updatedDate = fetchedPost.updatedDate
}
}
diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift
index 5b9d89a..e67e145 100644
--- a/Shared/Navigation/ContentView.swift
+++ b/Shared/Navigation/ContentView.swift
@@ -1,112 +1,115 @@
import SwiftUI
struct ContentView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
var body: some View {
#if os(macOS)
WFNavigation(
collectionList: {
CollectionListView()
.withErrorHandling()
.toolbar {
if #available(macOS 13, *) {
EmptyView()
} else {
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 {
// Un-set the currently selected post
self.model.selectedPost = nil
}
// Create the new-post managed object
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
DispatchQueue.main.async {
// Load the new post in the editor
self.model.selectedPost = managedPost
}
}
}, label: { Image(systemName: "square.and.pencil") })
.help("Create a new local draft.")
}
.frame(width: 200)
},
postList: {
ZStack {
PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
.withErrorHandling()
.frame(width: 300)
if model.isProcessingRequest {
ZStack {
Color(NSColor.controlBackgroundColor).opacity(0.75)
ProgressView()
}
}
}
},
postDetail: {
NoSelectedPostView(isConnected: $model.hasNetworkConnection)
}
)
.environmentObject(model)
.onChange(of: model.hasError) { value in
if value {
if let error = model.currentError {
self.errorHandling.handle(error: error)
} else {
self.errorHandling.handle(error: AppError.genericError())
}
model.hasError = false
}
}
#else
WFNavigation(
collectionList: {
CollectionListView()
.withErrorHandling()
},
postList: {
- PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
- .withErrorHandling()
+ PostListView(
+ selectedCollection: model.navState.selectedCollection,
+ showAllPosts: model.navState.showAllPosts
+ )
+ .withErrorHandling()
},
postDetail: {
NoSelectedPostView(isConnected: $model.hasNetworkConnection)
}
)
.environmentObject(model)
.onChange(of: model.hasError) { value in
if value {
if let error = model.currentError {
self.errorHandling.handle(error: error)
} else {
self.errorHandling.handle(error: AppError.genericError())
}
model.hasError = false
}
}
#endif
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
return ContentView()
.environment(\.managedObjectContext, context)
.environmentObject(model)
}
}
diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift
index 2c3dab7..a5f9300 100644
--- a/Shared/PostCollection/CollectionListView.swift
+++ b/Shared/PostCollection/CollectionListView.swift
@@ -1,69 +1,69 @@
import SwiftUI
struct CollectionListView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@FetchRequest(sortDescriptors: []) var collections: FetchedResults<WFACollection>
@State var selectedCollection: WFACollection?
var body: some View {
List(selection: $selectedCollection) {
if model.account.isLoggedIn {
NavigationLink("All Posts", destination: PostListView(selectedCollection: nil, showAllPosts: true))
NavigationLink(
model.account.server == "https://write.as" ? "Anonymous" : "Drafts",
destination: PostListView(selectedCollection: nil, showAllPosts: false)
)
Section(header: Text("Your Blogs")) {
ForEach(collections, id: \.self) { collection in
NavigationLink(destination: PostListView(selectedCollection: collection, showAllPosts: false),
tag: collection,
selection: $selectedCollection,
label: { Text("\(collection.title)") })
}
}
} else {
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) {
Text("Drafts")
}
}
}
.navigationTitle(
model.account.isLoggedIn ? "\(URL(string: model.account.server)?.host ?? "WriteFreely")" : "WriteFreely"
)
.listStyle(SidebarListStyle())
- .onChange(of: model.selectedCollection) { collection in
- model.selectedPost = nil
+ .onChange(of: model.navState.selectedCollection) { collection in
+ model.navState.selectedPost = nil
if collection != model.editor.fetchSelectedCollectionFromAppStorage() {
self.model.editor.selectedCollectionURL = collection?.objectID.uriRepresentation()
}
}
- .onChange(of: model.showAllPosts) { value in
- model.selectedPost = nil
+ .onChange(of: model.navState.showAllPosts) { value in
+ model.navState.selectedPost = nil
if value != model.editor.showAllPostsFlag {
- self.model.editor.showAllPostsFlag = model.showAllPosts
+ self.model.editor.showAllPostsFlag = model.navState.showAllPosts
}
}
.onChange(of: model.hasError) { value in
if value {
if let error = model.currentError {
self.errorHandling.handle(error: error)
} else {
self.errorHandling.handle(error: AppError.genericError())
}
model.hasError = false
}
}
}
}
struct CollectionListView_LoggedOutPreviews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
return CollectionListView()
.environment(\.managedObjectContext, context)
.environmentObject(model)
}
}
diff --git a/Shared/PostEditor/PostEditorModel.swift b/Shared/PostEditor/PostEditorModel.swift
index c6fda4c..e961b51 100644
--- a/Shared/PostEditor/PostEditorModel.swift
+++ b/Shared/PostEditor/PostEditorModel.swift
@@ -1,92 +1,92 @@
import SwiftUI
import CoreData
enum PostAppearance: String {
case sans = "OpenSans-Regular"
case mono = "Hack-Regular"
case serif = "Lora-Regular"
}
struct PostEditorModel {
@AppStorage(WFDefaults.showAllPostsFlag, store: UserDefaults.shared) var showAllPostsFlag: Bool = false
@AppStorage(WFDefaults.selectedCollectionURL, store: UserDefaults.shared) var selectedCollectionURL: URL?
@AppStorage(WFDefaults.lastDraftURL, store: UserDefaults.shared) var lastDraftURL: URL?
private(set) var initialPostTitle: String?
private(set) var initialPostBody: String?
#if os(macOS)
var postToUpdate: WFAPost?
#endif
func saveLastDraft(_ post: WFAPost) {
self.lastDraftURL = post.status != PostStatus.published.rawValue ? post.objectID.uriRepresentation() : nil
}
func clearLastDraft() {
self.lastDraftURL = nil
}
func fetchLastDraftFromAppStorage() -> WFAPost? {
guard let postURL = lastDraftURL else { return nil }
guard let post = fetchManagedObject(from: postURL) as? WFAPost else { return nil }
return post
}
func generateNewLocalPost(withFont appearance: Int) -> WFAPost {
let managedPost = WFAPost(context: LocalStorageManager.standard.container.viewContext)
managedPost.createdDate = Date()
managedPost.title = ""
managedPost.body = ""
managedPost.status = PostStatus.local.rawValue
- managedPost.collectionAlias = WriteFreelyModel.shared.selectedCollection?.alias
+ managedPost.collectionAlias = WriteFreelyModel.shared.navState.selectedCollection?.alias
switch appearance {
case 1:
managedPost.appearance = "sans"
case 2:
managedPost.appearance = "wrap"
default:
managedPost.appearance = "serif"
}
if #available(iOS 16, macOS 13, *) {
if let languageCode = Locale.current.language.languageCode?.identifier {
managedPost.language = languageCode
managedPost.rtl = Locale.Language(identifier: languageCode).characterDirection == .rightToLeft
}
} else {
if let languageCode = Locale.current.languageCode {
managedPost.language = languageCode
managedPost.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft
}
}
return managedPost
}
func fetchSelectedCollectionFromAppStorage() -> WFACollection? {
guard let collectionURL = selectedCollectionURL else { return nil }
guard let collection = fetchManagedObject(from: collectionURL) as? WFACollection else { return nil }
return collection
}
/// Sets the initial values for title and body on a published post.
///
/// Used to detect if the title and body have changed back to their initial values. If the passed `WFAPost` isn't
/// published, any title and post values already stored are reset to `nil`.
/// - Parameter post: The `WFAPost` for which we're setting initial title/body values.
mutating func setInitialValues(for post: WFAPost) {
if post.status != PostStatus.published.rawValue {
initialPostTitle = nil
initialPostBody = nil
return
}
initialPostTitle = post.title
initialPostBody = post.body
}
private func fetchManagedObject(from objectURL: URL) -> NSManagedObject? {
let coordinator = LocalStorageManager.standard.container.persistentStoreCoordinator
guard let managedObjectID = coordinator.managedObjectID(forURIRepresentation: objectURL) else { return nil }
let object = LocalStorageManager.standard.container.viewContext.object(with: managedObjectID)
return object
}
}
diff --git a/Shared/PostList/DeprecatedListView.swift b/Shared/PostList/DeprecatedListView.swift
index f2a6e5a..1c4fb10 100644
--- a/Shared/PostList/DeprecatedListView.swift
+++ b/Shared/PostList/DeprecatedListView.swift
@@ -1,57 +1,57 @@
import SwiftUI
@available(iOS 15, macOS 12.0, *)
struct DeprecatedListView: View {
@EnvironmentObject var model: WriteFreelyModel
@Binding var searchString: String
var collections: FetchedResults<WFACollection>
var fetchRequest: FetchRequest<WFAPost>
var onDelete: (WFAPost) -> Void
var body: some View {
- List(selection: $model.selectedPost) {
+ List(selection: $model.navState.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,
+ selection: $model.navState.selectedPost,
label: {
- if model.showAllPosts {
+ if model.navState.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/Shared/PostList/PostCellView.swift b/Shared/PostList/PostCellView.swift
index 9f3a622..acbb49f 100644
--- a/Shared/PostList/PostCellView.swift
+++ b/Shared/PostList/PostCellView.swift
@@ -1,105 +1,105 @@
import SwiftUI
struct PostCellView: View {
@EnvironmentObject var model: WriteFreelyModel
@ObservedObject var post: WFAPost
var collectionName: String?
static let createdDateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.dateStyle = .long
formatter.timeStyle = .short
return formatter
}()
var titleText: String {
if post.title.isEmpty {
return model.posts.getBodyPreview(of: post)
}
return post.title
}
var body: some View {
HStack {
VStack(alignment: .leading) {
if let collectionName = collectionName {
Text(collectionName)
.font(.caption)
.foregroundColor(.secondary)
.padding(EdgeInsets(top: 3, leading: 4, bottom: 3, trailing: 4))
.overlay(RoundedRectangle(cornerRadius: 2).stroke(Color.secondary, lineWidth: 1))
}
Text(titleText)
.font(.headline)
Text(post.createdDate ?? Date(), formatter: Self.createdDateFormat)
.font(.caption)
.foregroundColor(.secondary)
.padding(.top, -3)
}
Spacer()
PostStatusBadgeView(post: post)
}
.padding(5)
.contextMenu {
Button(
action: didTapDeleteContextMenuItem,
label: { Label("Delete", systemImage: "trash") }
)
.disabled(post.status != PostStatus.local.rawValue)
}
}
private func didTapDeleteContextMenuItem() {
guard post.status == PostStatus.local.rawValue else { return }
- if post === model.selectedPost {
- model.selectedPost = nil
+ if post === model.navState.selectedPost {
+ model.navState.selectedPost = nil
model.editor.clearLastDraft()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
model.posts.remove(post)
}
}
}
struct PostCell_AllPostsPreviews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let testPost = WFAPost(context: context)
testPost.title = "Test Post Title"
testPost.body = "Here's some cool sample body text."
testPost.createdDate = Date()
return PostCellView(post: testPost, collectionName: "My Cool Blog")
.environment(\.managedObjectContext, context)
}
}
struct PostCell_NormalPreviews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let testPost = WFAPost(context: context)
testPost.title = "Test Post Title"
testPost.body = "Here's some cool sample body text."
testPost.collectionAlias = "My Cool Blog"
testPost.createdDate = Date()
return PostCellView(post: testPost)
.environment(\.managedObjectContext, context)
}
}
struct PostCell_NoTitlePreviews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let testPost = WFAPost(context: context)
testPost.title = ""
testPost.body = "Here's some cool sample body text."
testPost.collectionAlias = "My Cool Blog"
testPost.createdDate = Date()
return PostCellView(post: testPost)
.environment(\.managedObjectContext, context)
}
}
diff --git a/Shared/PostList/PostListFilteredView.swift b/Shared/PostList/PostListFilteredView.swift
index 24b2453..3af723a 100644
--- a/Shared/PostList/PostListFilteredView.swift
+++ b/Shared/PostList/PostListFilteredView.swift
@@ -1,143 +1,143 @@
import SwiftUI
struct PostListFilteredView: View {
@EnvironmentObject var model: WriteFreelyModel
@Binding var postCount: Int
@FetchRequest(entity: WFACollection.entity(), sortDescriptors: []) var collections: FetchedResults<WFACollection>
var fetchRequest: FetchRequest<WFAPost>
init(collection: WFACollection?, showAllPosts: Bool, postCount: Binding<Int>) {
if showAllPosts {
fetchRequest = FetchRequest<WFAPost>(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)]
)
} else {
if let collectionAlias = collection?.alias {
fetchRequest = FetchRequest<WFAPost>(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)],
predicate: NSPredicate(format: "collectionAlias == %@", collectionAlias)
)
} else {
fetchRequest = FetchRequest<WFAPost>(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)],
predicate: NSPredicate(format: "collectionAlias == nil")
)
}
}
_postCount = postCount
}
var body: some View {
#if os(iOS)
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) {
+ List(selection: $model.navState.selectedPost) {
ForEach(fetchRequest.wrappedValue, id: \.self) { post in
NavigationLink(
destination: PostEditorView(post: post),
tag: post,
- selection: $model.selectedPost,
+ selection: $model.navState.selectedPost,
label: {
- if model.showAllPosts {
+ if model.navState.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)
}
})
}
.onAppear(perform: {
self.postCount = fetchRequest.wrappedValue.count
})
.onChange(of: fetchRequest.wrappedValue.count, perform: { value in
self.postCount = value
})
}
#else
SearchablePostListFilteredView(
postCount: $postCount,
collections: collections,
fetchRequest: fetchRequest,
onDelete: delete(_:)
)
.environmentObject(model)
.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
+ if post == model.navState.selectedPost {
+ model.navState.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)
)
.environmentObject(WriteFreelyModel())
}
}
diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift
index 06b4714..d47cafa 100644
--- a/Shared/PostList/PostListView.swift
+++ b/Shared/PostList/PostListView.swift
@@ -1,197 +1,197 @@
import SwiftUI
import Combine
struct PostListView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@Environment(\.managedObjectContext) var managedObjectContext
@State private var postCount: Int = 0
@State private var filteredListViewId: Int = 0
var selectedCollection: WFACollection?
var showAllPosts: Bool
#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
)
.id(self.filteredListViewId)
.navigationTitle(
showAllPosts ? "All Posts" : selectedCollection?.title ?? (
model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
)
)
.toolbar {
ToolbarItem(placement: .primaryAction) {
ZStack {
// 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.
if #unavailable(iOS 16) {
Spacer()
}
Button(action: {
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
- self.model.showAllPosts = false
+ self.model.navState.showAllPosts = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
- self.model.selectedPost = managedPost
+ self.model.navState.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"))
.sheet(
isPresented: $model.isPresentingSettingsView,
onDismiss: { model.isPresentingSettingsView = false },
content: {
SettingsView()
.environmentObject(model)
}
)
Spacer()
Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts")
.foregroundColor(.secondary)
Spacer()
if model.isProcessingRequest {
ProgressView()
.padding(.vertical, 4)
.padding(.horizontal, 8)
} else {
if model.hasNetworkConnection {
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)
} else {
Image(systemName: "wifi.exclamationmark")
.padding(.vertical, 4)
.padding(.horizontal, 8)
.foregroundColor(.secondary)
}
}
}
.padding(.top, 8)
.padding(.horizontal, 8)
Spacer()
}
.frame(height: frameHeight)
.background(Color(UIColor.systemGray5))
.overlay(Divider(), alignment: .top)
}
.ignoresSafeArea(.all, edges: .bottom)
.onAppear {
// Set the selected collection and whether or not we want to show all posts
- model.selectedCollection = selectedCollection
- model.showAllPosts = showAllPosts
+ model.navState.selectedCollection = selectedCollection
+ model.navState.showAllPosts = showAllPosts
// We use this to invalidate and refresh the view, so that new posts created outside of the app (e.g.,
// in the action extension) show up.
withAnimation {
self.filteredListViewId += 1
}
}
.onChange(of: model.hasError) { value in
if value {
if let error = model.currentError {
self.errorHandling.handle(error: error)
} else {
self.errorHandling.handle(error: AppError.genericError())
}
model.hasError = false
}
}
#else
PostListFilteredView(
collection: selectedCollection,
showAllPosts: showAllPosts,
postCount: $postCount
)
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
if model.selectedPost != nil {
ActivePostToolbarView(activePost: model.selectedPost!)
}
}
}
.navigationTitle(
showAllPosts ? "All Posts" : selectedCollection?.title ?? (
model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
)
)
.onAppear {
model.selectedCollection = selectedCollection
model.showAllPosts = showAllPosts
}
.onChange(of: model.hasError) { value in
if value {
if let error = model.currentError {
self.errorHandling.handle(error: error)
} else {
self.errorHandling.handle(error: AppError.genericError())
}
model.hasError = false
}
}
#endif
}
}
struct PostListView_Previews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
return PostListView(showAllPosts: true)
.environment(\.managedObjectContext, context)
.environmentObject(model)
}
}
diff --git a/Shared/PostList/SearchablePostListFilteredView.swift b/Shared/PostList/SearchablePostListFilteredView.swift
index 5279fea..8c79d5e 100644
--- a/Shared/PostList/SearchablePostListFilteredView.swift
+++ b/Shared/PostList/SearchablePostListFilteredView.swift
@@ -1,39 +1,39 @@
import SwiftUI
@available(iOS 15, macOS 12.0, *)
struct SearchablePostListFilteredView: View {
@EnvironmentObject var model: WriteFreelyModel
@Binding var postCount: Int
@State private var searchString = ""
// Only used for NavigationStack in iOS 16/macOS 13 or later
// @State private var path: [WFAPost] = []
var collections: FetchedResults<WFACollection>
var fetchRequest: FetchRequest<WFAPost>
var onDelete: (WFAPost) -> Void
var body: some View {
if #available(iOS 16, macOS 13, *) {
NavigationStack {
- List(fetchRequest.wrappedValue, id: \.self, selection: $model.selectedPost) { post in
+ List(fetchRequest.wrappedValue, id: \.self, selection: $model.navState.selectedPost) { post in
NavigationLink(
"\(post.title.isEmpty ? "UNTITLED" : post.title)",
destination: PostEditorView(post: post)
)
}
}
} else {
DeprecatedListView(
searchString: $searchString,
collections: collections,
fetchRequest: fetchRequest,
onDelete: onDelete
)
}
}
func delete(_ post: WFAPost) {
onDelete(post)
}
}
diff --git a/Shared/WriteFreely_MultiPlatformApp.swift b/Shared/WriteFreely_MultiPlatformApp.swift
index b121314..fae66c4 100644
--- a/Shared/WriteFreely_MultiPlatformApp.swift
+++ b/Shared/WriteFreely_MultiPlatformApp.swift
@@ -1,212 +1,213 @@
import SwiftUI
#if os(macOS)
import Sparkle
#endif
@main
struct CheckForDebugModifier {
static func main() {
#if os(macOS)
if NSEvent.modifierFlags.contains(.shift) {
// Clear the launch-to-last-draft values to load a new draft.
UserDefaults.shared.setValue(false, forKey: WFDefaults.showAllPostsFlag)
UserDefaults.shared.setValue(nil, forKey: WFDefaults.selectedCollectionURL)
UserDefaults.shared.setValue(nil, forKey: WFDefaults.lastDraftURL)
} else {
// No-op
}
#endif
WriteFreely_MultiPlatformApp.main()
}
}
struct WriteFreely_MultiPlatformApp: App {
@StateObject private var model = WriteFreelyModel.shared
private let logger = Logging(for: String(describing: WriteFreely_MultiPlatformApp.self))
#if os(macOS)
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject var updaterViewModel = MacUpdatesViewModel()
@State private var selectedTab = 0
#endif
@State private var didCrash = UserDefaults.shared.bool(forKey: WFDefaults.didHaveFatalError)
var body: some Scene {
WindowGroup {
ContentView()
.onAppear(perform: {
if model.editor.showAllPostsFlag {
DispatchQueue.main.async {
- self.model.selectedCollection = nil
- self.model.showAllPosts = true
+ self.model.navState.selectedCollection = nil
+ self.model.navState.showAllPosts = true
showLastDraftOrCreateNewLocalPost()
}
} else {
DispatchQueue.main.async {
- self.model.selectedCollection = model.editor.fetchSelectedCollectionFromAppStorage()
- self.model.showAllPosts = false
+ self.model.navState.selectedCollection =
+ model.editor.fetchSelectedCollectionFromAppStorage()
+ self.model.navState.showAllPosts = false
showLastDraftOrCreateNewLocalPost()
}
}
})
.alert(isPresented: $didCrash) {
var helpMsg = "Alert the humans by sharing what happened on the help forum."
if let errorMsg = UserDefaults.shared.object(forKey: WFDefaults.fatalErrorDescription) as? String {
helpMsg.append("\n\n\(errorMsg)")
}
return Alert(
title: Text("Crash Detected"),
message: Text(helpMsg),
primaryButton: .default(
Text("Let us know"), action: didPressCrashAlertButton
),
secondaryButton: .cancel(
Text("Dismiss"),
action: resetCrashFlags
)
)
}
.onAppear {
if #available(iOS 15, *) {
if didCrash { generateCrashLogPost() }
}
}
.withErrorHandling()
.environmentObject(model)
.environment(\.managedObjectContext, LocalStorageManager.standard.container.viewContext)
// .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info.
}
.commands {
#if os(macOS)
CommandGroup(after: .appInfo) {
CheckForUpdatesView(updaterViewModel: updaterViewModel)
}
#endif
CommandGroup(replacing: .newItem, addition: {
Button("New Post") {
createNewLocalPost()
}
.keyboardShortcut("n", modifiers: [.command])
})
CommandGroup(after: .newItem) {
Button("Refresh Posts") {
DispatchQueue.main.async {
model.fetchUserCollections()
model.fetchUserPosts()
}
}
.disabled(!model.account.isLoggedIn)
.keyboardShortcut("r", modifiers: [.command])
}
SidebarCommands()
#if os(macOS)
PostCommands(model: model)
HelpCommands(model: model)
#endif
ToolbarCommands()
TextEditingCommands()
}
#if os(macOS)
Settings {
TabView(selection: $selectedTab) {
MacAccountView()
.environmentObject(model)
.tabItem {
Image(systemName: "person.crop.circle")
Text("Account")
}
.tag(0)
MacPreferencesView(preferences: model.preferences)
.tabItem {
Image(systemName: "gear")
Text("Preferences")
}
.tag(1)
MacUpdatesView(updaterViewModel: updaterViewModel)
.tabItem {
Image(systemName: "arrow.down.circle")
Text("Updates")
}
.tag(2)
}
.environmentObject(model)
.withErrorHandling()
.frame(minWidth: 500, maxWidth: 500, minHeight: 200)
.padding()
// .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info.
}
#endif
}
private func showLastDraftOrCreateNewLocalPost() {
if model.editor.lastDraftURL != nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
- self.model.selectedPost = model.editor.fetchLastDraftFromAppStorage()
+ self.model.navState.selectedPost = model.editor.fetchLastDraftFromAppStorage()
}
} else {
createNewLocalPost()
}
}
private func createNewLocalPost() {
withAnimation {
// Un-set the currently selected post
- self.model.selectedPost = nil
+ self.model.navState.selectedPost = nil
}
// Create the new-post managed object
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
// Set it as the selectedPost
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
- self.model.selectedPost = managedPost
+ self.model.navState.selectedPost = managedPost
}
}
}
@available(iOS 15, *)
private func generateCrashLogPost() {
logger.log("Generating local log post...")
DispatchQueue.main.asyncAfter(deadline: .now()) {
// Unset selected post and collection and navigate to local drafts.
- self.model.selectedPost = nil
- self.model.selectedCollection = nil
- self.model.showAllPosts = false
+ self.model.navState.selectedPost = nil
+ self.model.navState.selectedCollection = nil
+ self.model.navState.showAllPosts = false
// Create the new log post.
let newLogPost = model.editor.generateNewLocalPost(withFont: 2)
newLogPost.title = "Logs For Support"
var postBody: [String] = [
"WriteFreely-Multiplatform v\(Bundle.main.appMarketingVersion) (\(Bundle.main.appBuildVersion))",
"Generated \(Date())",
""
]
postBody.append(contentsOf: logger.fetchLogs())
newLogPost.body = postBody.joined(separator: "\n")
- self.model.selectedPost = newLogPost
+ self.model.navState.selectedPost = newLogPost
}
logger.log("Generated local log post.")
}
private func resetCrashFlags() {
UserDefaults.shared.set(false, forKey: WFDefaults.didHaveFatalError)
UserDefaults.shared.removeObject(forKey: WFDefaults.fatalErrorDescription)
}
private func didPressCrashAlertButton() {
resetCrashFlags()
#if os(macOS)
NSWorkspace().open(model.helpURL)
#else
UIApplication.shared.open(model.helpURL)
#endif
}
}
diff --git a/iOS/Settings/SettingsView.swift b/iOS/Settings/SettingsView.swift
index cfd0211..e75b2d2 100644
--- a/iOS/Settings/SettingsView.swift
+++ b/iOS/Settings/SettingsView.swift
@@ -1,105 +1,105 @@
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var model: WriteFreelyModel
@State private var isShowingAlert = false
private let logger = Logging(for: String(describing: SettingsView.self))
var body: some View {
VStack {
SettingsHeaderView()
Form {
Section(header: Text("Login Details")) {
AccountView()
.withErrorHandling()
}
Section(header: Text("Appearance")) {
PreferencesView(preferences: model.preferences)
}
Section(header: Text("Help and Support")) {
Link("View the Guide", destination: model.howToURL)
Link("Visit the Help Forum", destination: model.helpURL)
Link("Write a Review on the App Store", destination: model.reviewURL)
if #available(iOS 15.0, *) {
VStack(alignment: .leading, spacing: 8) {
Button(
action: didTapGenerateLogPostButton,
label: {
Text("Create Log Post")
}
)
Text("Generates a local post using recent logs. You can share this for troubleshooting.")
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
Section(header: Text("Acknowledgements")) {
VStack {
VStack(alignment: .leading) {
Text("This application makes use of the following open-source projects:")
.padding(.bottom)
Text("• Lora typeface")
.padding(.leading)
Text("• Open Sans typeface")
.padding(.leading)
Text("• Hack typeface")
.padding(.leading)
}
.padding(.bottom)
.foregroundColor(.secondary)
HStack {
Spacer()
Link("View the licenses", destination: model.licensesURL)
Spacer()
}
}
.padding()
}
}
}
.alert(isPresented: $isShowingAlert) {
Alert(
title: Text("Log Post Created"),
message: Text("Check your local drafts for app logs from the past 24 hours.")
)
}
// .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info.
}
@available(iOS 15, *)
private func didTapGenerateLogPostButton() {
logger.log("Generating local log post...")
DispatchQueue.main.asyncAfter(deadline: .now()) {
// Unset selected post and collection and navigate to local drafts.
- self.model.selectedPost = nil
- self.model.selectedCollection = nil
- self.model.showAllPosts = false
+ self.model.navState.selectedPost = nil
+ self.model.navState.selectedCollection = nil
+ self.model.navState.showAllPosts = false
// Create the new log post.
let newLogPost = model.editor.generateNewLocalPost(withFont: 2)
newLogPost.title = "Logs For Support"
var postBody: [String] = [
"WriteFreely-Multiplatform v\(Bundle.main.appMarketingVersion) (\(Bundle.main.appBuildVersion))",
"Generated \(Date())",
""
]
postBody.append(contentsOf: logger.fetchLogs())
newLogPost.body = postBody.joined(separator: "\n")
self.isShowingAlert = true
}
logger.log("Generated local log post.")
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView()
.environmentObject(WriteFreelyModel())
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Feb 6, 7:55 PM (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3619776
Attached To
rWFSUI WriteFreely SwiftUI
Event Timeline
Log In to Comment