diff --git a/Shared/Models/LocalStorageModel.xcdatamodeld/LocalStorageModel.xcdatamodel/contents b/Shared/Models/LocalStorageModel.xcdatamodeld/LocalStorageModel.xcdatamodel/contents index ff8b974..29499d1 100644 --- a/Shared/Models/LocalStorageModel.xcdatamodeld/LocalStorageModel.xcdatamodel/contents +++ b/Shared/Models/LocalStorageModel.xcdatamodeld/LocalStorageModel.xcdatamodel/contents @@ -1,40 +1,41 @@ + - + \ No newline at end of file diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift index 5923939..ce9bf20 100644 --- a/Shared/Models/WriteFreelyModel.swift +++ b/Shared/Models/WriteFreelyModel.swift @@ -1,397 +1,410 @@ import Foundation import WriteFreely import Security import Network // MARK: - WriteFreelyModel class WriteFreelyModel: ObservableObject { @Published var account = AccountModel() @Published var preferences = PreferencesModel() @Published var posts = PostListModel() + @Published var editor = PostEditorModel() @Published var isLoggingIn: Bool = false @Published var hasNetworkConnection: Bool = false - @Published var selectedPost: WFAPost? + @Published var selectedPost: WFAPost? { + didSet { + if let post = selectedPost { + if post.status != PostStatus.published.rawValue { + editor.setLastDraft(post) + } else { + editor.clearLastDraft() + } + } else { + editor.clearLastDraft() + } + } + } @Published var isPresentingDeleteAlert: Bool = false @Published var postToDelete: WFAPost? #if os(iOS) @Published var isPresentingSettingsView: Bool = false #endif + // swiftlint:disable line_length let helpURL = URL(string: "https://discuss.write.as/c/help/5")! + let licensesURL = URL(string: "https://github.com/writeas/writefreely-swiftui-multiplatform/tree/main/Shared/Resources/Licenses")! + // swiftlint:enable line_length private var client: WFClient? private let defaults = UserDefaults.standard private let monitor = NWPathMonitor() private let queue = DispatchQueue(label: "NetworkMonitor") init() { DispatchQueue.main.async { self.preferences.appearance = self.defaults.integer(forKey: self.preferences.colorSchemeIntegerKey) + self.preferences.font = self.defaults.integer(forKey: self.preferences.defaultFontIntegerKey) self.account.restoreState() if self.account.isLoggedIn { guard let serverURL = URL(string: self.account.server) else { print("Server URL not found") return } guard let token = self.fetchTokenFromKeychain( username: self.account.username, server: self.account.server ) else { print("Could not fetch token from Keychain") return } self.account.login(WFUser(token: token, username: self.account.username)) self.client = WFClient(for: serverURL) self.client?.user = self.account.user self.fetchUserCollections() self.fetchUserPosts() } } monitor.pathUpdateHandler = { path in DispatchQueue.main.async { self.hasNetworkConnection = path.status == .satisfied } } monitor.start(queue: queue) } } // MARK: - WriteFreelyModel API extension WriteFreelyModel { func login(to server: URL, as username: String, password: String) { isLoggingIn = true account.server = server.absoluteString client = WFClient(for: server) client?.login(username: username, password: password, completion: loginHandler) } func logout() { guard let loggedInClient = client else { do { try purgeTokenFromKeychain(username: account.username, server: account.server) account.logout() } catch { fatalError("Failed to log out persisted state") } return } loggedInClient.logout(completion: logoutHandler) } func fetchUserCollections() { guard let loggedInClient = client else { return } loggedInClient.getUserCollections(completion: fetchUserCollectionsHandler) } func fetchUserPosts() { guard let loggedInClient = client else { return } loggedInClient.getPosts(completion: fetchUserPostsHandler) } func publish(post: WFAPost) { guard let loggedInClient = client else { return } if post.language == nil { 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.createdDate ) if let existingPostId = post.postId { // This is an existing post. wfPost.postId = post.postId wfPost.slug = post.slug wfPost.updatedDate = post.updatedDate wfPost.collectionAlias = post.collectionAlias 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) { guard let loggedInClient = client else { return } guard let postId = post.postId else { return } DispatchQueue.main.async { self.selectedPost = post } if let postCollectionAlias = post.collectionAlias, let postSlug = post.slug { loggedInClient.getPost(bySlug: postSlug, from: postCollectionAlias, completion: updateFromServerHandler) } else { loggedInClient.getPost(byId: postId, completion: updateFromServerHandler) } } } private extension WriteFreelyModel { func loginHandler(result: Result) { DispatchQueue.main.async { self.isLoggingIn = false } do { let user = try result.get() fetchUserCollections() fetchUserPosts() saveTokenToKeychain(user.token, username: user.username, server: account.server) DispatchQueue.main.async { self.account.login(user) } } catch WFError.notFound { DispatchQueue.main.async { self.account.currentError = AccountError.usernameNotFound } } catch WFError.unauthorized { DispatchQueue.main.async { self.account.currentError = AccountError.invalidPassword } } catch { if (error as NSError).domain == NSURLErrorDomain, (error as NSError).code == -1003 { DispatchQueue.main.async { self.account.currentError = AccountError.serverNotFound } } } } func logoutHandler(result: Result) { do { _ = try result.get() do { try purgeTokenFromKeychain(username: account.user?.username, server: account.server) client = nil DispatchQueue.main.async { self.account.logout() LocalStorageManager().purgeUserCollections() self.posts.purgeAllPosts() } } catch { print("Something went wrong purging the token from the Keychain.") } } 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() LocalStorageManager().purgeUserCollections() self.posts.purgeAllPosts() } } catch { print("Something went wrong purging the token from the Keychain.") } } 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>) { do { let fetchedCollections = try result.get() for fetchedCollection in fetchedCollections { DispatchQueue.main.async { let localCollection = WFACollection(context: LocalStorageManager.persistentContainer.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().saveContext() } } catch { print(error) } } func fetchUserPostsHandler(result: Result<[WFPost], Error>) { do { + var postsToDelete = posts.userPosts.filter { $0.status != PostStatus.local.rawValue } let fetchedPosts = try result.get() for fetchedPost in fetchedPosts { - // For each fetched post, we - // 1. check to see if a matching post exists if let managedPost = posts.userPosts.first(where: { $0.postId == fetchedPost.postId }) { - // If it exists, we set the hasNewerRemoteCopy flag as appropriate. + managedPost.wasDeletedFromServer = false if let fetchedPostUpdatedDate = fetchedPost.updatedDate, let localPostUpdatedDate = managedPost.updatedDate { managedPost.hasNewerRemoteCopy = fetchedPostUpdatedDate > localPostUpdatedDate } else { print("Error: could not determine which copy of post is newer") } + postsToDelete.removeAll(where: { $0.postId == fetchedPost.postId }) } else { - // If it doesn't exist, we create the managed object. let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext) managedPost.postId = fetchedPost.postId managedPost.slug = fetchedPost.slug managedPost.appearance = fetchedPost.appearance managedPost.language = fetchedPost.language managedPost.rtl = fetchedPost.rtl ?? false managedPost.createdDate = fetchedPost.createdDate managedPost.updatedDate = fetchedPost.updatedDate managedPost.title = fetchedPost.title ?? "" managedPost.body = fetchedPost.body managedPost.collectionAlias = fetchedPost.collectionAlias managedPost.status = PostStatus.published.rawValue + managedPost.wasDeletedFromServer = false } } + for post in postsToDelete { + post.wasDeletedFromServer = true + } DispatchQueue.main.async { LocalStorageManager().saveContext() self.posts.loadCachedPosts() } } catch { print(error) } } func publishHandler(result: Result) { do { let fetchedPost = try result.get() let foundPostIndex = posts.userPosts.firstIndex(where: { $0.title == fetchedPost.title && $0.body == fetchedPost.body }) guard let index = foundPostIndex else { return } let cachedPost = self.posts.userPosts[index] cachedPost.appearance = fetchedPost.appearance cachedPost.body = fetchedPost.body cachedPost.collectionAlias = fetchedPost.collectionAlias 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 DispatchQueue.main.async { LocalStorageManager().saveContext() } } catch { print(error) } } func updateFromServerHandler(result: Result) { // ⚠️ 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() guard let cachedPost = self.selectedPost else { return } 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 cachedPost.hasNewerRemoteCopy = false DispatchQueue.main.async { LocalStorageManager().saveContext() } } catch { print(error) } } } private extension WriteFreelyModel { // MARK: - Keychain Helpers func saveTokenToKeychain(_ token: String, username: String?, server: String) { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecValueData as String: token.data(using: .utf8)!, kSecAttrAccount as String: username ?? "anonymous", kSecAttrService as String: server ] - let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecDuplicateItem || status == errSecSuccess else { fatalError("Error storing in Keychain with OSStatus: \(status)") } } func purgeTokenFromKeychain(username: String?, server: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: username ?? "anonymous", kSecAttrService as String: server ] - let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { fatalError("Error deleting from Keychain with OSStatus: \(status)") } } func fetchTokenFromKeychain(username: String?, server: String) -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: username ?? "anonymous", kSecAttrService as String: server, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnAttributes as String: true, kSecReturnData as String: true ] - var secItem: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &secItem) - guard status != errSecItemNotFound else { return nil } - guard status == errSecSuccess else { fatalError("Error fetching from Keychain with OSStatus: \(status)") } - guard let existingSecItem = secItem as? [String: Any], let tokenData = existingSecItem[kSecValueData as String] as? Data, let token = String(data: tokenData, encoding: .utf8) else { return nil } - return token } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 732b187..26a341f 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -1,58 +1,82 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var model: WriteFreelyModel var body: some View { NavigationView { SidebarView() PostListView(selectedCollection: nil, showAllPosts: true) Text("Select a post, or create a new local draft.") .foregroundColor(.secondary) } + .onAppear(perform: { + if let lastDraft = self.model.editor.fetchLastDraft() { + model.selectedPost = lastDraft + } else { + let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext) + managedPost.createdDate = Date() + managedPost.title = "" + managedPost.body = "" + managedPost.status = PostStatus.local.rawValue + switch self.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 + } + model.selectedPost = managedPost + } + }) .environmentObject(model) .alert(isPresented: $model.isPresentingDeleteAlert) { Alert( title: Text("Delete Post?"), message: Text("This action cannot be undone."), primaryButton: .destructive(Text("Delete"), action: { if let postToDelete = model.postToDelete { model.selectedPost = nil withAnimation { model.posts.remove(postToDelete) } model.postToDelete = nil } }), secondaryButton: .cancel() { model.postToDelete = nil } ) } #if os(iOS) EmptyView() .sheet( isPresented: $model.isPresentingSettingsView, onDismiss: { model.isPresentingSettingsView = false }, content: { SettingsView() .environmentObject(model) } ) #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/PostEditor/PostEditorModel.swift b/Shared/PostEditor/PostEditorModel.swift new file mode 100644 index 0000000..d080927 --- /dev/null +++ b/Shared/PostEditor/PostEditorModel.swift @@ -0,0 +1,32 @@ +import Foundation +import CoreData + +struct PostEditorModel { + let lastDraftObjectURLKey = "lastDraftObjectURLKey" + private(set) var lastDraft: WFAPost? + + mutating func setLastDraft(_ post: WFAPost) { + lastDraft = post + UserDefaults.standard.set(post.objectID.uriRepresentation(), forKey: lastDraftObjectURLKey) + } + + mutating func fetchLastDraft() -> WFAPost? { + let coordinator = LocalStorageManager.persistentContainer.persistentStoreCoordinator + + // See if we have a lastDraftObjectURI + guard let lastDraftObjectURI = UserDefaults.standard.url(forKey: lastDraftObjectURLKey) else { return nil } + + // See if we can get an ObjectID from the URI representation + guard let lastDraftObjectID = coordinator.managedObjectID(forURIRepresentation: lastDraftObjectURI) else { + return nil + } + + lastDraft = LocalStorageManager.persistentContainer.viewContext.object(with: lastDraftObjectID) as? WFAPost + return lastDraft + } + + mutating func clearLastDraft() { + lastDraft = nil + UserDefaults.standard.removeObject(forKey: lastDraftObjectURLKey) + } +} diff --git a/Shared/PostEditor/PostEditorStatusToolbarView.swift b/Shared/PostEditor/PostEditorStatusToolbarView.swift index 5174b04..e76e2b1 100644 --- a/Shared/PostEditor/PostEditorStatusToolbarView.swift +++ b/Shared/PostEditor/PostEditorStatusToolbarView.swift @@ -1,87 +1,152 @@ import SwiftUI struct PostEditorStatusToolbarView: View { #if os(iOS) @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.presentationMode) var presentationMode #endif @EnvironmentObject var model: WriteFreelyModel @ObservedObject var post: WFAPost var body: some View { if post.hasNewerRemoteCopy { #if os(iOS) if horizontalSizeClass == .compact { VStack { PostStatusBadgeView(post: post) HStack { Text("⚠️ Newer copy on server. Replace local copy?") .font(.caption) .foregroundColor(.secondary) Button(action: { model.updateFromServer(post: post) }, label: { Image(systemName: "square.and.arrow.down") }) } .padding(.bottom) } .padding(.top) } else { HStack { PostStatusBadgeView(post: post) .padding(.trailing) Text("⚠️ Newer copy on server. Replace local copy?") .font(.callout) .foregroundColor(.secondary) Button(action: { model.updateFromServer(post: post) }, label: { Image(systemName: "square.and.arrow.down") }) } } #else HStack { PostStatusBadgeView(post: post) .padding(.trailing) Text("⚠️ Newer copy on server. Replace local copy?") .font(.callout) .foregroundColor(.secondary) Button(action: { model.updateFromServer(post: post) }, label: { Image(systemName: "square.and.arrow.down") }) } #endif + } else if post.wasDeletedFromServer && post.status != PostStatus.local.rawValue { + #if os(iOS) + if horizontalSizeClass == .compact { + VStack { + PostStatusBadgeView(post: post) + HStack { + Text("‼️ Post deleted from server. Delete local copy?") + .font(.caption) + .foregroundColor(.secondary) + Button(action: { + self.presentationMode.wrappedValue.dismiss() + model.selectedPost = nil + model.posts.remove(post) + }, label: { + Image(systemName: "trash") + }) + } + .padding(.bottom) + } + .padding(.top) + } else { + HStack { + PostStatusBadgeView(post: post) + .padding(.trailing) + Text("‼️ Post deleted from server. Delete local copy?") + .font(.callout) + .foregroundColor(.secondary) + Button(action: { + self.presentationMode.wrappedValue.dismiss() + model.selectedPost = nil + model.posts.remove(post) + }, label: { + Image(systemName: "trash") + }) + } + } + #else + HStack { + PostStatusBadgeView(post: post) + .padding(.trailing) + Text("‼️ Post deleted from server. Delete local copy?") + .font(.callout) + .foregroundColor(.secondary) + Button(action: { + model.selectedPost = nil + model.posts.remove(post) + }, label: { + Image(systemName: "trash") + }) + } + #endif } else { PostStatusBadgeView(post: post) } } } struct PESTView_StandardPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let model = WriteFreelyModel() let testPost = WFAPost(context: context) testPost.status = PostStatus.published.rawValue return PostEditorStatusToolbarView(post: testPost) .environmentObject(model) } } struct PESTView_OutdatedLocalCopyPreviews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let model = WriteFreelyModel() - let testPost = WFAPost(context: context) - testPost.status = PostStatus.published.rawValue - testPost.hasNewerRemoteCopy = true + let updatedPost = WFAPost(context: context) + updatedPost.status = PostStatus.published.rawValue + updatedPost.hasNewerRemoteCopy = true - return PostEditorStatusToolbarView(post: testPost) + return PostEditorStatusToolbarView(post: updatedPost) + .environmentObject(model) + } +} + +struct PESTView_DeletedRemoteCopyPreviews: PreviewProvider { + static var previews: some View { + let context = LocalStorageManager.persistentContainer.viewContext + let model = WriteFreelyModel() + let deletedPost = WFAPost(context: context) + deletedPost.status = PostStatus.published.rawValue + deletedPost.wasDeletedFromServer = true + + return PostEditorStatusToolbarView(post: deletedPost) .environmentObject(model) } } diff --git a/Shared/PostEditor/PostEditorView.swift b/Shared/PostEditor/PostEditorView.swift deleted file mode 100644 index 6182e87..0000000 --- a/Shared/PostEditor/PostEditorView.swift +++ /dev/null @@ -1,81 +0,0 @@ -import SwiftUI - -struct PostEditorView: View { - @EnvironmentObject var model: WriteFreelyModel - - @ObservedObject var post: WFAPost - - var body: some View { - VStack { - TextEditor(text: $post.title) - .font(.title) - .frame(height: 100) - .onChange(of: post.title) { _ in - if post.status == PostStatus.published.rawValue { - post.status = PostStatus.edited.rawValue - } - } - TextEditor(text: $post.body) - .font(.body) - .onChange(of: post.body) { _ in - if post.status == PostStatus.published.rawValue { - post.status = PostStatus.edited.rawValue - } - } - } - .padding() - .toolbar { - ToolbarItem(placement: .status) { - PostEditorStatusToolbarView(post: post) - } - ToolbarItem(placement: .primaryAction) { - Button(action: { - publishPost() - }, label: { - Image(systemName: "paperplane") - }) - .disabled( - post.status == PostStatus.published.rawValue || - !model.account.isLoggedIn || - !model.hasNetworkConnection - ) - } - } - .onChange(of: post.hasNewerRemoteCopy, perform: { _ in - if post.status == PostStatus.edited.rawValue && !post.hasNewerRemoteCopy { - post.status = PostStatus.published.rawValue - } - }) - .onDisappear(perform: { - if post.status < PostStatus.published.rawValue { - DispatchQueue.main.async { - LocalStorageManager().saveContext() - } - } - }) - } - - private func publishPost() { - DispatchQueue.main.async { - LocalStorageManager().saveContext() - model.posts.loadCachedPosts() - model.publish(post: post) - } - } -} - -struct PostEditorView_Previews: PreviewProvider { - static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext - let testPost = WFAPost(context: context) - testPost.title = "Test Post Title" - testPost.body = "Here's some cool sample body text." - testPost.createdDate = Date() - - let model = WriteFreelyModel() - - return PostEditorView(post: testPost) - .environment(\.managedObjectContext, context) - .environmentObject(model) - } -} diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index 32df675..2103891 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -1,131 +1,138 @@ import SwiftUI struct PostListView: View { @EnvironmentObject var model: WriteFreelyModel @Environment(\.managedObjectContext) var moc @State var selectedCollection: WFACollection? @State var showAllPosts: Bool = false var body: some View { #if os(iOS) GeometryReader { geometry in PostListFilteredView(filter: selectedCollection?.alias, showAllPosts: showAllPosts) .navigationTitle( showAllPosts ? "All Posts" : selectedCollection?.title ?? ( model.account.server == "https://write.as" ? "Anonymous" : "Drafts" ) ) .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: { createNewLocalDraft() }, label: { Image(systemName: "square.and.pencil") }) } ToolbarItem(placement: .bottomBar) { HStack { Button(action: { model.isPresentingSettingsView = true }, label: { Image(systemName: "gear") }) - .padding(.leading) Spacer() Text(pluralizedPostCount(for: showPosts(for: selectedCollection))) .foregroundColor(.secondary) Spacer() Button(action: { reloadFromServer() }, label: { Image(systemName: "arrow.clockwise") }) .disabled(!model.account.isLoggedIn || !model.hasNetworkConnection) } .padding() .frame(width: geometry.size.width) } } } #else //if os(macOS) PostListFilteredView(filter: selectedCollection?.alias, showAllPosts: showAllPosts) .navigationTitle( showAllPosts ? "All Posts" : selectedCollection?.title ?? ( model.account.server == "https://write.as" ? "Anonymous" : "Drafts" ) ) .navigationSubtitle(pluralizedPostCount(for: showPosts(for: selectedCollection))) .toolbar { Button(action: { createNewLocalDraft() }, label: { Image(systemName: "square.and.pencil") }) Button(action: { reloadFromServer() }, label: { Image(systemName: "arrow.clockwise") }) .disabled(!model.account.isLoggedIn || !model.hasNetworkConnection) } #endif } private func pluralizedPostCount(for posts: [WFAPost]) -> String { if posts.count == 1 { return "1 post" } else { return "\(posts.count) posts" } } private func showPosts(for collection: WFACollection?) -> [WFAPost] { if showAllPosts { return model.posts.userPosts } else { if let selectedCollection = collection { return model.posts.userPosts.filter { $0.collectionAlias == selectedCollection.alias } } else { return model.posts.userPosts.filter { $0.collectionAlias == nil } } } } private func reloadFromServer() { DispatchQueue.main.async { model.fetchUserCollections() model.fetchUserPosts() } } private func createNewLocalDraft() { let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext) managedPost.createdDate = Date() managedPost.title = "" managedPost.body = "" managedPost.status = PostStatus.local.rawValue + 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 } if let selectedCollectionAlias = selectedCollection?.alias { managedPost.collectionAlias = selectedCollectionAlias } DispatchQueue.main.async { LocalStorageManager().saveContext() + model.selectedPost = managedPost } - model.selectedPost = managedPost } } struct PostListView_Previews: PreviewProvider { static var previews: some View { let context = LocalStorageManager.persistentContainer.viewContext let model = WriteFreelyModel() return PostListView() .environment(\.managedObjectContext, context) .environmentObject(model) } } diff --git a/Shared/Preferences/PreferencesModel.swift b/Shared/Preferences/PreferencesModel.swift index c4ac721..a7acf85 100644 --- a/Shared/Preferences/PreferencesModel.swift +++ b/Shared/Preferences/PreferencesModel.swift @@ -1,59 +1,65 @@ import SwiftUI class PreferencesModel: ObservableObject { private let defaults = UserDefaults.standard let colorSchemeIntegerKey = "colorSchemeIntegerKey" + let defaultFontIntegerKey = "defaultFontIntegerKey" /* We're stuck dropping into AppKit/UIKit to set light/dark schemes for now, * because setting the .preferredColorScheme modifier on views in SwiftUI is * currently unreliable. * * Feedback submitted to Apple: * * FB8382883: "On macOS 11β4, preferredColorScheme modifier does not respect .light ColorScheme" * FB8383053: "On iOS 14β4/macOS 11β4, it is not possible to unset preferredColorScheme after setting * it to either .light or .dark" */ #if os(iOS) var window: UIWindow? { guard let scene = UIApplication.shared.connectedScenes.first, let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate, let window = windowSceneDelegate.window else { return nil } return window } #endif @Published var selectedColorScheme: ColorScheme? @Published var appearance: Int = 0 { didSet { switch appearance { case 1: // selectedColorScheme = .light #if os(macOS) NSApp.appearance = NSAppearance(named: .aqua) #else window?.overrideUserInterfaceStyle = .light #endif case 2: // selectedColorScheme = .dark #if os(macOS) NSApp.appearance = NSAppearance(named: .darkAqua) #else window?.overrideUserInterfaceStyle = .dark #endif default: // selectedColorScheme = .none #if os(macOS) NSApp.appearance = nil #else window?.overrideUserInterfaceStyle = .unspecified #endif } defaults.set(appearance, forKey: colorSchemeIntegerKey) } } + @Published var font: Int = 0 { + didSet { + defaults.set(font, forKey: defaultFontIntegerKey) + } + } } diff --git a/Shared/Preferences/PreferencesView.swift b/Shared/Preferences/PreferencesView.swift index 26168e3..1522450 100644 --- a/Shared/Preferences/PreferencesView.swift +++ b/Shared/Preferences/PreferencesView.swift @@ -1,28 +1,56 @@ import SwiftUI struct PreferencesView: View { @ObservedObject var preferences: PreferencesModel var body: some View { - #if os(iOS) - Picker(selection: $preferences.appearance, label: Text("Appearance")) { - Text("System").tag(0) - Text("Light").tag(1) - Text("Dark").tag(2) - } - .pickerStyle(SegmentedPickerStyle()) - #elseif os(macOS) - Picker(selection: $preferences.appearance, label: Text("Appearance")) { - Text("System").tag(0) - Text("Light").tag(1) - Text("Dark").tag(2) + VStack { + VStack { + Text("Choose the preferred appearance for the app.") + .font(.caption) + .foregroundColor(.secondary) + Picker(selection: $preferences.appearance, label: Text("Appearance")) { + Text("System").tag(0) + Text("Light").tag(1) + Text("Dark").tag(2) + } + .pickerStyle(SegmentedPickerStyle()) + } + .padding(.bottom) + + VStack { + Text("Choose the default font for new posts.") + .font(.caption) + .foregroundColor(.secondary) + Picker(selection: $preferences.font, label: Text("Default Font")) { + Text("Serif").tag(0) + Text("Sans-Serif").tag(1) + Text("Monospace").tag(2) + } + .pickerStyle(SegmentedPickerStyle()) + .padding(.bottom) + switch preferences.font { + case 1: + Text("Sample Text") + .frame(width: 240, height: 50, alignment: .center) + .font(.custom("OpenSans-Regular", size: 20)) + case 2: + Text("Sample Text") + .frame(width: 240, height: 50, alignment: .center) + .font(.custom("Hack-Regular", size: 20)) + default: + Text("Sample Text") + .frame(width: 240, height: 50, alignment: .center) + .font(.custom("Lora", size: 20)) + } + } + .padding(.bottom) } - #endif } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { PreferencesView(preferences: PreferencesModel()) } } diff --git a/Shared/Resources/Hack-Regular.ttf b/Shared/Resources/Hack-Regular.ttf new file mode 100644 index 0000000..92a90cb Binary files /dev/null and b/Shared/Resources/Hack-Regular.ttf differ diff --git a/Shared/Resources/Licenses/Hack-License.txt b/Shared/Resources/Licenses/Hack-License.txt new file mode 100644 index 0000000..f337012 --- /dev/null +++ b/Shared/Resources/Licenses/Hack-License.txt @@ -0,0 +1,62 @@ +The work in the Hack project is Copyright 2018 Source Foundry Authors and licensed under the MIT License + +The work in the DejaVu project was committed to the public domain. + +Bitstream Vera Sans Mono Copyright 2003 Bitstream Inc. and licensed under the Bitstream Vera License with Reserved Font +Names "Bitstream" and "Vera" + +### MIT License + +Copyright (c) 2018 Source Foundry Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +### BITSTREAM VERA LICENSE + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license +("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, +including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font +Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of +the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the +Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to +names not containing either the words "Bitstream" or the word "Vera". + +This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is +distributed under the "Bitstream Vera" names. + +The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software +typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in +advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written +authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: +fonts at gnome dot org. diff --git a/Shared/Resources/Licenses/Lora-Cyrillic-OFL.txt b/Shared/Resources/Licenses/Lora-Cyrillic-OFL.txt new file mode 100644 index 0000000..868bbd4 --- /dev/null +++ b/Shared/Resources/Licenses/Lora-Cyrillic-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name "Lora". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Shared/Resources/Licenses/OpenSans-License.txt b/Shared/Resources/Licenses/OpenSans-License.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/Shared/Resources/Licenses/OpenSans-License.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Shared/Resources/LoraGX.ttf b/Shared/Resources/LoraGX.ttf new file mode 100644 index 0000000..2e5f1bb Binary files /dev/null and b/Shared/Resources/LoraGX.ttf differ diff --git a/Shared/Resources/OpenSans-Regular.ttf b/Shared/Resources/OpenSans-Regular.ttf new file mode 100644 index 0000000..29bfd35 Binary files /dev/null and b/Shared/Resources/OpenSans-Regular.ttf differ diff --git a/Technotes/EditorLaunchingPolicy.md b/Technotes/EditorLaunchingPolicy.md new file mode 100644 index 0000000..0f84e22 --- /dev/null +++ b/Technotes/EditorLaunchingPolicy.md @@ -0,0 +1,21 @@ +# Editor Launching Policy + +_Last updated: Wednesday, 23 September, 2020_ + +This technote defines the policy for what is loaded in the post editor on app launch. + +The app shall always launch to the post editor. Determining what post should be loaded in the editor requires defining the following: + +- **Last Draft:** The last post with either a `local` or `edited` status to have been loaded into the post editor. It's important to note that a +`published` post that is loaded into the post editor and is then changed becomes an `edited` post, and therefore qualifies as a last draft. + +The launch policy is as follows: + +The app shall launch to the last draft, _except_ when: + +- There is no last draft (i.e., on the first launch of the app); or +- The user's actions signal that they are done working with this last draft: + - The last draft was `published` before quitting the app + - The user's last action in the app was to leave the post editor (iOS) or deselect any post from the post list (macOS). + +In these cases, the app shall launch to a new, blank, `local` post. diff --git a/WFAPost+CoreDataProperties.swift b/WFAPost+CoreDataProperties.swift index 48d4d2b..50d64be 100644 --- a/WFAPost+CoreDataProperties.swift +++ b/WFAPost+CoreDataProperties.swift @@ -1,35 +1,36 @@ // // WFAPost+CoreDataProperties.swift // WriteFreely-MultiPlatform // // Created by Angelo Stavrow on 2020-09-08. // // import Foundation import CoreData extension WFAPost { @nonobjc public class func createFetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "WFAPost") } @NSManaged public var appearance: String? @NSManaged public var body: String @NSManaged public var collectionAlias: String? @NSManaged public var createdDate: Date? @NSManaged public var language: String? @NSManaged public var postId: String? @NSManaged public var rtl: Bool @NSManaged public var slug: String? @NSManaged public var status: Int32 @NSManaged public var title: String @NSManaged public var updatedDate: Date? @NSManaged public var hasNewerRemoteCopy: Bool + @NSManaged public var wasDeletedFromServer: Bool } extension WFAPost: Identifiable { } diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index e63fecb..ca9570b 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -1,1070 +1,1166 @@ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 52; objects = { /* Begin PBXBuildFile section */ + 170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170DFA33251BBC44001D82A0 /* PostEditorModel.swift */; }; + 170DFA35251BBC44001D82A0 /* PostEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170DFA33251BBC44001D82A0 /* PostEditorModel.swift */; }; 17120DA124E19839002B9F6C /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388D24DDEC7400DEFF9A /* AccountView.swift */; }; 17120DA224E1985C002B9F6C /* AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388B24DDC83F00DEFF9A /* AccountModel.swift */; }; 17120DA324E19A42002B9F6C /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5389124DDED0000DEFF9A /* PreferencesView.swift */; }; 17120DA724E19D11002B9F6C /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DA424E19CBF002B9F6C /* SettingsView.swift */; }; 17120DA924E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DA824E1B2F5002B9F6C /* AccountLogoutView.swift */; }; 17120DAA24E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DA824E1B2F5002B9F6C /* AccountLogoutView.swift */; }; 17120DAC24E1B99F002B9F6C /* AccountLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DAB24E1B99F002B9F6C /* AccountLoginView.swift */; }; 17120DAD24E1B99F002B9F6C /* AccountLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DAB24E1B99F002B9F6C /* AccountLoginView.swift */; }; 17120DB224E1E19C002B9F6C /* SettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17120DB124E1E19C002B9F6C /* SettingsHeaderView.swift */; }; 171BFDFA24D4AF8300888236 /* CollectionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BFDF924D4AF8300888236 /* CollectionListView.swift */; }; 171BFDFB24D4AF8300888236 /* CollectionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BFDF924D4AF8300888236 /* CollectionListView.swift */; }; 17480CA5251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17480CA4251272EE00EB7765 /* Bundle+AppVersion.swift */; }; 17480CA6251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17480CA4251272EE00EB7765 /* Bundle+AppVersion.swift */; }; 174D313224EC2831006CA9EE /* WriteFreelyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174D313124EC2831006CA9EE /* WriteFreelyModel.swift */; }; 174D313324EC2831006CA9EE /* WriteFreelyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174D313124EC2831006CA9EE /* WriteFreelyModel.swift */; }; 1753F6AC24E431CC00309365 /* MacPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1753F6AB24E431CC00309365 /* MacPreferencesView.swift */; }; 1756AE6E24CB255B00FD7257 /* PostListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE6D24CB255B00FD7257 /* PostListModel.swift */; }; 1756AE6F24CB255B00FD7257 /* PostListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE6D24CB255B00FD7257 /* PostListModel.swift */; }; 1756AE7424CB26FA00FD7257 /* PostCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7324CB26FA00FD7257 /* PostCellView.swift */; }; 1756AE7524CB26FA00FD7257 /* PostCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7324CB26FA00FD7257 /* PostCellView.swift */; }; 1756AE7724CB2EDD00FD7257 /* PostEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */; }; - 1756AE7824CB2EDD00FD7257 /* PostEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */; }; 1756AE7A24CB65DF00FD7257 /* PostListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7924CB65DF00FD7257 /* PostListView.swift */; }; 1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE7924CB65DF00FD7257 /* PostListView.swift */; }; 1756AE8124CB844500FD7257 /* View+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756AE8024CB844500FD7257 /* View+Keyboard.swift */; }; 1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */; }; 1756DBB424FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */; }; 1756DBB724FED3A400207AB8 /* LocalStorageModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB524FED3A400207AB8 /* LocalStorageModel.xcdatamodeld */; }; 1756DBB824FED3A400207AB8 /* LocalStorageModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB524FED3A400207AB8 /* LocalStorageModel.xcdatamodeld */; }; 1756DBBA24FED45500207AB8 /* LocalStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB924FED45500207AB8 /* LocalStorageManager.swift */; }; 1756DBBB24FED45500207AB8 /* LocalStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBB924FED45500207AB8 /* LocalStorageManager.swift */; }; 1756DC0124FEE18400207AB8 /* WFACollection+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBFF24FEE18400207AB8 /* WFACollection+CoreDataClass.swift */; }; 1756DC0224FEE18400207AB8 /* WFACollection+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DBFF24FEE18400207AB8 /* WFACollection+CoreDataClass.swift */; }; 1756DC0324FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DC0024FEE18400207AB8 /* WFACollection+CoreDataProperties.swift */; }; 1756DC0424FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756DC0024FEE18400207AB8 /* WFACollection+CoreDataProperties.swift */; }; + 17582194251A4E53004FC441 /* UITextView+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17582193251A4E53004FC441 /* UITextView+Appearance.swift */; }; 1765F62A24E18EA200C9EBF0 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1765F62924E18EA200C9EBF0 /* SidebarView.swift */; }; 1765F62B24E18EA200C9EBF0 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1765F62924E18EA200C9EBF0 /* SidebarView.swift */; }; + 17681E412519410E00D394AE /* UINavigationController+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17681E402519410E00D394AE /* UINavigationController+Appearance.swift */; }; 17A5388824DDA31F00DEFF9A /* MacAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388724DDA31F00DEFF9A /* MacAccountView.swift */; }; 17A5388C24DDC83F00DEFF9A /* AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388B24DDC83F00DEFF9A /* AccountModel.swift */; }; 17A5388F24DDEC7400DEFF9A /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388D24DDEC7400DEFF9A /* AccountView.swift */; }; 17A5389324DDED0000DEFF9A /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5389124DDED0000DEFF9A /* PreferencesView.swift */; }; + 17A67CAF251A5DD7002F163D /* PostEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A67CAE251A5DD7002F163D /* PostEditorView.swift */; }; 17B3E965250FAA9000EE9748 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */; }; + 17B5103B2515448D00E9631F /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 17B5103A2515448D00E9631F /* Credits.rtf */; }; + 17B7827425152CF8008D96C9 /* Hack-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17B7827125152CF8008D96C9 /* Hack-License.txt */; }; + 17B7827525152CF8008D96C9 /* Hack-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17B7827125152CF8008D96C9 /* Hack-License.txt */; }; + 17B7827625152CF8008D96C9 /* Lora-Cyrillic-OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17B7827225152CF8008D96C9 /* Lora-Cyrillic-OFL.txt */; }; + 17B7827725152CF8008D96C9 /* Lora-Cyrillic-OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17B7827225152CF8008D96C9 /* Lora-Cyrillic-OFL.txt */; }; + 17B7827825152CF8008D96C9 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17B7827325152CF8008D96C9 /* OpenSans-License.txt */; }; + 17B7827925152CF8008D96C9 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17B7827325152CF8008D96C9 /* OpenSans-License.txt */; }; 17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */; }; 17B996D92502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */; }; 17B996DA2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D72502D23E0017B536 /* WFAPost+CoreDataProperties.swift */; }; 17B996DB2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D72502D23E0017B536 /* WFAPost+CoreDataProperties.swift */; }; 17C42E622507D8E600072984 /* PostStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E612507D8E600072984 /* PostStatus.swift */; }; 17C42E632507D8E600072984 /* PostStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E612507D8E600072984 /* PostStatus.swift */; }; 17C42E652509237800072984 /* PostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E642509237800072984 /* PostListFilteredView.swift */; }; 17C42E662509237800072984 /* PostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E642509237800072984 /* PostListFilteredView.swift */; }; 17C42E70250AA12300072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E6F250AA12200072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */; }; 17C42E71250AAFD500072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C42E6F250AA12200072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */; }; 17D435E824E3128F0036B539 /* PreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D435E724E3128F0036B539 /* PreferencesModel.swift */; }; 17D435E924E3128F0036B539 /* PreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D435E724E3128F0036B539 /* PreferencesModel.swift */; }; + 17D4F36C2514EE2F00517CE6 /* LoraGX.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F36B2514EE2F00517CE6 /* LoraGX.ttf */; }; + 17D4F36D2514EE2F00517CE6 /* LoraGX.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F36B2514EE2F00517CE6 /* LoraGX.ttf */; }; + 17D4F39E2514F0E500517CE6 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F39D2514F0E500517CE6 /* OpenSans-Regular.ttf */; }; + 17D4F39F2514F0E500517CE6 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F39D2514F0E500517CE6 /* OpenSans-Regular.ttf */; }; + 17D4F3A52514F1E900517CE6 /* Hack-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F3A42514F1E900517CE6 /* Hack-Regular.ttf */; }; + 17D4F3A62514F1E900517CE6 /* Hack-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 17D4F3A42514F1E900517CE6 /* Hack-Regular.ttf */; }; 17DF329D24C87D3500BCE2E3 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF329C24C87D3500BCE2E3 /* Tests_iOS.swift */; }; 17DF32A824C87D3500BCE2E3 /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF32A724C87D3500BCE2E3 /* Tests_macOS.swift */; }; 17DF32AA24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF328124C87D3300BCE2E3 /* WriteFreely_MultiPlatformApp.swift */; }; 17DF32AB24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF328124C87D3300BCE2E3 /* WriteFreely_MultiPlatformApp.swift */; }; 17DF32AC24C87D3500BCE2E3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF328224C87D3300BCE2E3 /* ContentView.swift */; }; 17DF32AD24C87D3500BCE2E3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF328224C87D3300BCE2E3 /* ContentView.swift */; }; 17DF32AE24C87D3500BCE2E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17DF328324C87D3500BCE2E3 /* Assets.xcassets */; }; 17DF32AF24C87D3500BCE2E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17DF328324C87D3500BCE2E3 /* Assets.xcassets */; }; 17DF32C024C87D7B00BCE2E3 /* WriteFreely in Frameworks */ = {isa = PBXBuildFile; productRef = 17DF32BF24C87D7B00BCE2E3 /* WriteFreely */; }; 17DF32C324C87D8D00BCE2E3 /* WriteFreely in Frameworks */ = {isa = PBXBuildFile; productRef = 17DF32C224C87D8D00BCE2E3 /* WriteFreely */; }; 17DF32D524C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF32D424C8CA3400BCE2E3 /* PostStatusBadgeView.swift */; }; 17DF32D624C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF32D424C8CA3400BCE2E3 /* PostStatusBadgeView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 17DF329924C87D3500BCE2E3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 17DF327C24C87D3300BCE2E3 /* Project object */; proxyType = 1; remoteGlobalIDString = 17DF328724C87D3500BCE2E3; remoteInfo = "WriteFreely-MultiPlatform (iOS)"; }; 17DF32A424C87D3500BCE2E3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 17DF327C24C87D3300BCE2E3 /* Project object */; proxyType = 1; remoteGlobalIDString = 17DF328F24C87D3500BCE2E3; remoteInfo = "WriteFreely-MultiPlatform (macOS)"; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 1709ADDF251B9A110053AF79 /* EditorLaunchingPolicy.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = EditorLaunchingPolicy.md; sourceTree = ""; }; + 170DFA33251BBC44001D82A0 /* PostEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorModel.swift; sourceTree = ""; }; 17120DA424E19CBF002B9F6C /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 17120DA824E1B2F5002B9F6C /* AccountLogoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountLogoutView.swift; sourceTree = ""; }; 17120DAB24E1B99F002B9F6C /* AccountLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountLoginView.swift; sourceTree = ""; }; 17120DB124E1E19C002B9F6C /* SettingsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = ""; }; 171BFDF924D4AF8300888236 /* CollectionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionListView.swift; sourceTree = ""; }; 17480CA4251272EE00EB7765 /* Bundle+AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+AppVersion.swift"; sourceTree = ""; }; 174D313124EC2831006CA9EE /* WriteFreelyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteFreelyModel.swift; sourceTree = ""; }; 1753F6AB24E431CC00309365 /* MacPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacPreferencesView.swift; sourceTree = ""; }; 1756AE6D24CB255B00FD7257 /* PostListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListModel.swift; sourceTree = ""; }; 1756AE7324CB26FA00FD7257 /* PostCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCellView.swift; sourceTree = ""; }; 1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorView.swift; sourceTree = ""; }; 1756AE7924CB65DF00FD7257 /* PostListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListView.swift; sourceTree = ""; }; 1756AE8024CB844500FD7257 /* View+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Keyboard.swift"; sourceTree = ""; }; 1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorStatusToolbarView.swift; sourceTree = ""; }; 1756DBB624FED3A400207AB8 /* LocalStorageModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LocalStorageModel.xcdatamodel; sourceTree = ""; }; 1756DBB924FED45500207AB8 /* LocalStorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalStorageManager.swift; sourceTree = ""; }; 1756DBFF24FEE18400207AB8 /* WFACollection+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFACollection+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; }; 1756DC0024FEE18400207AB8 /* WFACollection+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFACollection+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; }; + 17582193251A4E53004FC441 /* UITextView+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+Appearance.swift"; sourceTree = ""; }; 1765F62924E18EA200C9EBF0 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; + 17681E402519410E00D394AE /* UINavigationController+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Appearance.swift"; sourceTree = ""; }; 17A5388724DDA31F00DEFF9A /* MacAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAccountView.swift; sourceTree = ""; }; 17A5388B24DDC83F00DEFF9A /* AccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountModel.swift; sourceTree = ""; }; 17A5388D24DDEC7400DEFF9A /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = ""; }; 17A5389124DDED0000DEFF9A /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; + 17A67CAE251A5DD7002F163D /* PostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorView.swift; sourceTree = ""; }; 17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 17B5103A2515448D00E9631F /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; + 17B7827125152CF8008D96C9 /* Hack-License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Hack-License.txt"; sourceTree = ""; }; + 17B7827225152CF8008D96C9 /* Lora-Cyrillic-OFL.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Lora-Cyrillic-OFL.txt"; sourceTree = ""; }; + 17B7827325152CF8008D96C9 /* OpenSans-License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "OpenSans-License.txt"; sourceTree = ""; }; 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFAPost+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; }; 17B996D72502D23E0017B536 /* WFAPost+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFAPost+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; }; 17C42E612507D8E600072984 /* PostStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostStatus.swift; sourceTree = ""; }; 17C42E642509237800072984 /* PostListFilteredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListFilteredView.swift; sourceTree = ""; }; 17C42E6F250AA12200072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+ExecuteAndMergeChanges.swift"; sourceTree = ""; }; 17D435E724E3128F0036B539 /* PreferencesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesModel.swift; sourceTree = ""; }; + 17D4F36B2514EE2F00517CE6 /* LoraGX.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = LoraGX.ttf; sourceTree = ""; }; + 17D4F39D2514F0E500517CE6 /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "OpenSans-Regular.ttf"; sourceTree = ""; }; + 17D4F3A42514F1E900517CE6 /* Hack-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Hack-Regular.ttf"; sourceTree = ""; }; 17DF328124C87D3300BCE2E3 /* WriteFreely_MultiPlatformApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteFreely_MultiPlatformApp.swift; sourceTree = ""; }; 17DF328224C87D3300BCE2E3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 17DF328324C87D3500BCE2E3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 17DF328824C87D3500BCE2E3 /* WriteFreely-MultiPlatform.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WriteFreely-MultiPlatform.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 17DF328B24C87D3500BCE2E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 17DF329024C87D3500BCE2E3 /* WriteFreely-MultiPlatform.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WriteFreely-MultiPlatform.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 17DF329224C87D3500BCE2E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 17DF329324C87D3500BCE2E3 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; 17DF329824C87D3500BCE2E3 /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 17DF329C24C87D3500BCE2E3 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = ""; }; 17DF329E24C87D3500BCE2E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 17DF32A324C87D3500BCE2E3 /* Tests macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 17DF32A724C87D3500BCE2E3 /* Tests_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOS.swift; sourceTree = ""; }; 17DF32A924C87D3500BCE2E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 17DF32C624C884FF00BCE2E3 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 17DF32C724C8853700BCE2E3 /* CODE_OF_CONDUCT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CODE_OF_CONDUCT.md; sourceTree = ""; }; 17DF32C824C8854B00BCE2E3 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; 17DF32C924C8855E00BCE2E3 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 17DF32CA24C8856C00BCE2E3 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 17DF32D424C8CA3400BCE2E3 /* PostStatusBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostStatusBadgeView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 17DF328524C87D3500BCE2E3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 17DF32C024C87D7B00BCE2E3 /* WriteFreely in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF328D24C87D3500BCE2E3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 17DF32C324C87D8D00BCE2E3 /* WriteFreely in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF329524C87D3500BCE2E3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 17DF32A024C87D3500BCE2E3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1709ADDE251B99D40053AF79 /* Technotes */ = { + isa = PBXGroup; + children = ( + 1709ADDF251B9A110053AF79 /* EditorLaunchingPolicy.md */, + ); + path = Technotes; + sourceTree = ""; + }; 17120DA624E19CE2002B9F6C /* Settings */ = { isa = PBXGroup; children = ( 17120DB124E1E19C002B9F6C /* SettingsHeaderView.swift */, 17120DA424E19CBF002B9F6C /* SettingsView.swift */, ); path = Settings; sourceTree = ""; }; 1739B8D324EAFAB700DA7421 /* PostEditor */ = { isa = PBXGroup; children = ( - 1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */, + 170DFA33251BBC44001D82A0 /* PostEditorModel.swift */, 1756DBB224FECDBB00207AB8 /* PostEditorStatusToolbarView.swift */, ); path = PostEditor; sourceTree = ""; }; 1756AE7F24CB841200FD7257 /* Extensions */ = { isa = PBXGroup; children = ( - 1756AE8024CB844500FD7257 /* View+Keyboard.swift */, 17C42E6F250AA12200072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */, 17480CA4251272EE00EB7765 /* Bundle+AppVersion.swift */, ); path = Extensions; sourceTree = ""; }; 1762DCB124EB07680019C4EB /* Models */ = { isa = PBXGroup; children = ( 1756DBFF24FEE18400207AB8 /* WFACollection+CoreDataClass.swift */, 1756DC0024FEE18400207AB8 /* WFACollection+CoreDataProperties.swift */, 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */, 17B996D72502D23E0017B536 /* WFAPost+CoreDataProperties.swift */, 17C42E612507D8E600072984 /* PostStatus.swift */, 174D313124EC2831006CA9EE /* WriteFreelyModel.swift */, 1756DBB524FED3A400207AB8 /* LocalStorageModel.xcdatamodeld */, ); path = Models; sourceTree = ""; }; 1765F62C24E1924800C9EBF0 /* Preferences */ = { isa = PBXGroup; children = ( 17D435E724E3128F0036B539 /* PreferencesModel.swift */, 17A5389124DDED0000DEFF9A /* PreferencesView.swift */, ); path = Preferences; sourceTree = ""; }; + 17681E3F251940F200D394AE /* Extensions */ = { + isa = PBXGroup; + children = ( + 1756AE8024CB844500FD7257 /* View+Keyboard.swift */, + 17681E402519410E00D394AE /* UINavigationController+Appearance.swift */, + 17582193251A4E53004FC441 /* UITextView+Appearance.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 17A5388924DDA50500DEFF9A /* Settings */ = { isa = PBXGroup; children = ( 17A5388724DDA31F00DEFF9A /* MacAccountView.swift */, 1753F6AB24E431CC00309365 /* MacPreferencesView.swift */, ); path = Settings; sourceTree = ""; }; + 17A67CAB251A5D7E002F163D /* PostEditor */ = { + isa = PBXGroup; + children = ( + 1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */, + ); + path = PostEditor; + sourceTree = ""; + }; + 17A67CAC251A5D8D002F163D /* PostEditor */ = { + isa = PBXGroup; + children = ( + 17A67CAE251A5DD7002F163D /* PostEditorView.swift */, + ); + path = PostEditor; + sourceTree = ""; + }; + 17D4F3722514EE4400517CE6 /* Resources */ = { + isa = PBXGroup; + children = ( + 17B7827025152BF1008D96C9 /* Licenses */, + 17D4F3A42514F1E900517CE6 /* Hack-Regular.ttf */, + 17D4F39D2514F0E500517CE6 /* OpenSans-Regular.ttf */, + 17D4F36B2514EE2F00517CE6 /* LoraGX.ttf */, + ); + path = Resources; + sourceTree = ""; + }; 17DF327B24C87D3300BCE2E3 = { isa = PBXGroup; children = ( 17DF32C624C884FF00BCE2E3 /* README.md */, 17DF32C924C8855E00BCE2E3 /* LICENSE.md */, 17DF32CA24C8856C00BCE2E3 /* CHANGELOG.md */, 17DF32C724C8853700BCE2E3 /* CODE_OF_CONDUCT.md */, 17DF32C824C8854B00BCE2E3 /* CONTRIBUTING.md */, + 1709ADDE251B99D40053AF79 /* Technotes */, 17DF328024C87D3300BCE2E3 /* Shared */, 17DF328A24C87D3500BCE2E3 /* iOS */, 17DF329124C87D3500BCE2E3 /* macOS */, 17DF329B24C87D3500BCE2E3 /* Tests iOS */, 17DF32A624C87D3500BCE2E3 /* Tests macOS */, 17DF328924C87D3500BCE2E3 /* Products */, 17DF32C124C87D8D00BCE2E3 /* Frameworks */, ); sourceTree = ""; }; 17DF328024C87D3300BCE2E3 /* Shared */ = { isa = PBXGroup; children = ( 17DF328124C87D3300BCE2E3 /* WriteFreely_MultiPlatformApp.swift */, 1756DBB924FED45500207AB8 /* LocalStorageManager.swift */, 17DF328324C87D3500BCE2E3 /* Assets.xcassets */, 17DF32D024C8B75C00BCE2E3 /* Account */, 1756AE7F24CB841200FD7257 /* Extensions */, 1762DCB124EB07680019C4EB /* Models */, 17DF32CC24C8B72300BCE2E3 /* Navigation */, 1739B8D324EAFAB700DA7421 /* PostEditor */, 17DF32D124C8B78500BCE2E3 /* PostList */, 17DF32D224C8B78D00BCE2E3 /* PostCollection */, 1765F62C24E1924800C9EBF0 /* Preferences */, + 17D4F3722514EE4400517CE6 /* Resources */, ); path = Shared; sourceTree = ""; }; 17DF328924C87D3500BCE2E3 /* Products */ = { isa = PBXGroup; children = ( 17DF328824C87D3500BCE2E3 /* WriteFreely-MultiPlatform.app */, 17DF329024C87D3500BCE2E3 /* WriteFreely-MultiPlatform.app */, 17DF329824C87D3500BCE2E3 /* Tests iOS.xctest */, 17DF32A324C87D3500BCE2E3 /* Tests macOS.xctest */, ); name = Products; sourceTree = ""; }; 17DF328A24C87D3500BCE2E3 /* iOS */ = { isa = PBXGroup; children = ( 17DF328B24C87D3500BCE2E3 /* Info.plist */, 17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */, + 17681E3F251940F200D394AE /* Extensions */, + 17A67CAB251A5D7E002F163D /* PostEditor */, 17120DA624E19CE2002B9F6C /* Settings */, ); path = iOS; sourceTree = ""; }; 17DF329124C87D3500BCE2E3 /* macOS */ = { isa = PBXGroup; children = ( 17DF329224C87D3500BCE2E3 /* Info.plist */, 17DF329324C87D3500BCE2E3 /* macOS.entitlements */, + 17A67CAC251A5D8D002F163D /* PostEditor */, 17A5388924DDA50500DEFF9A /* Settings */, + 17B5103A2515448D00E9631F /* Credits.rtf */, ); path = macOS; sourceTree = ""; }; 17DF329B24C87D3500BCE2E3 /* Tests iOS */ = { isa = PBXGroup; children = ( 17DF329C24C87D3500BCE2E3 /* Tests_iOS.swift */, 17DF329E24C87D3500BCE2E3 /* Info.plist */, ); path = "Tests iOS"; sourceTree = ""; }; 17DF32A624C87D3500BCE2E3 /* Tests macOS */ = { isa = PBXGroup; children = ( 17DF32A724C87D3500BCE2E3 /* Tests_macOS.swift */, 17DF32A924C87D3500BCE2E3 /* Info.plist */, ); path = "Tests macOS"; sourceTree = ""; }; 17DF32C124C87D8D00BCE2E3 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; 17DF32CC24C8B72300BCE2E3 /* Navigation */ = { isa = PBXGroup; children = ( 17DF328224C87D3300BCE2E3 /* ContentView.swift */, 1765F62924E18EA200C9EBF0 /* SidebarView.swift */, ); path = Navigation; sourceTree = ""; }; 17DF32D024C8B75C00BCE2E3 /* Account */ = { isa = PBXGroup; children = ( 17A5388B24DDC83F00DEFF9A /* AccountModel.swift */, 17120DAB24E1B99F002B9F6C /* AccountLoginView.swift */, 17120DA824E1B2F5002B9F6C /* AccountLogoutView.swift */, 17A5388D24DDEC7400DEFF9A /* AccountView.swift */, ); path = Account; sourceTree = ""; }; 17DF32D124C8B78500BCE2E3 /* PostList */ = { isa = PBXGroup; children = ( 1756AE7324CB26FA00FD7257 /* PostCellView.swift */, 1756AE6D24CB255B00FD7257 /* PostListModel.swift */, 1756AE7924CB65DF00FD7257 /* PostListView.swift */, 17DF32D424C8CA3400BCE2E3 /* PostStatusBadgeView.swift */, 17C42E642509237800072984 /* PostListFilteredView.swift */, ); path = PostList; sourceTree = ""; }; 17DF32D224C8B78D00BCE2E3 /* PostCollection */ = { isa = PBXGroup; children = ( 171BFDF924D4AF8300888236 /* CollectionListView.swift */, ); path = PostCollection; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 17DF328724C87D3500BCE2E3 /* WriteFreely-MultiPlatform (iOS) */ = { isa = PBXNativeTarget; buildConfigurationList = 17DF32B224C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "WriteFreely-MultiPlatform (iOS)" */; buildPhases = ( 17DF328424C87D3500BCE2E3 /* Sources */, 17DF328524C87D3500BCE2E3 /* Frameworks */, 17DF328624C87D3500BCE2E3 /* Resources */, 17DF32C424C87E6700BCE2E3 /* ShellScript */, ); buildRules = ( ); dependencies = ( ); name = "WriteFreely-MultiPlatform (iOS)"; packageProductDependencies = ( 17DF32BF24C87D7B00BCE2E3 /* WriteFreely */, ); productName = "WriteFreely-MultiPlatform (iOS)"; productReference = 17DF328824C87D3500BCE2E3 /* WriteFreely-MultiPlatform.app */; productType = "com.apple.product-type.application"; }; 17DF328F24C87D3500BCE2E3 /* WriteFreely-MultiPlatform (macOS) */ = { isa = PBXNativeTarget; buildConfigurationList = 17DF32B524C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "WriteFreely-MultiPlatform (macOS)" */; buildPhases = ( 17DF328C24C87D3500BCE2E3 /* Sources */, 17DF328D24C87D3500BCE2E3 /* Frameworks */, 17DF328E24C87D3500BCE2E3 /* Resources */, 17DF32C524C87FDB00BCE2E3 /* ShellScript */, ); buildRules = ( ); dependencies = ( ); name = "WriteFreely-MultiPlatform (macOS)"; packageProductDependencies = ( 17DF32C224C87D8D00BCE2E3 /* WriteFreely */, ); productName = "WriteFreely-MultiPlatform (macOS)"; productReference = 17DF329024C87D3500BCE2E3 /* WriteFreely-MultiPlatform.app */; productType = "com.apple.product-type.application"; }; 17DF329724C87D3500BCE2E3 /* Tests iOS */ = { isa = PBXNativeTarget; buildConfigurationList = 17DF32B824C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "Tests iOS" */; buildPhases = ( 17DF329424C87D3500BCE2E3 /* Sources */, 17DF329524C87D3500BCE2E3 /* Frameworks */, 17DF329624C87D3500BCE2E3 /* Resources */, ); buildRules = ( ); dependencies = ( 17DF329A24C87D3500BCE2E3 /* PBXTargetDependency */, ); name = "Tests iOS"; productName = "Tests iOS"; productReference = 17DF329824C87D3500BCE2E3 /* Tests iOS.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; 17DF32A224C87D3500BCE2E3 /* Tests macOS */ = { isa = PBXNativeTarget; buildConfigurationList = 17DF32BB24C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "Tests macOS" */; buildPhases = ( 17DF329F24C87D3500BCE2E3 /* Sources */, 17DF32A024C87D3500BCE2E3 /* Frameworks */, 17DF32A124C87D3500BCE2E3 /* Resources */, ); buildRules = ( ); dependencies = ( 17DF32A524C87D3500BCE2E3 /* PBXTargetDependency */, ); name = "Tests macOS"; productName = "Tests macOS"; productReference = 17DF32A324C87D3500BCE2E3 /* Tests macOS.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 17DF327C24C87D3300BCE2E3 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1200; LastUpgradeCheck = 1200; TargetAttributes = { 17DF328724C87D3500BCE2E3 = { CreatedOnToolsVersion = 12.0; }; 17DF328F24C87D3500BCE2E3 = { CreatedOnToolsVersion = 12.0; }; 17DF329724C87D3500BCE2E3 = { CreatedOnToolsVersion = 12.0; TestTargetID = 17DF328724C87D3500BCE2E3; }; 17DF32A224C87D3500BCE2E3 = { CreatedOnToolsVersion = 12.0; TestTargetID = 17DF328F24C87D3500BCE2E3; }; }; }; buildConfigurationList = 17DF327F24C87D3300BCE2E3 /* Build configuration list for PBXProject "WriteFreely-MultiPlatform" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 17DF327B24C87D3300BCE2E3; packageReferences = ( 17DF32BE24C87D7B00BCE2E3 /* XCRemoteSwiftPackageReference "writefreely-swift" */, ); productRefGroup = 17DF328924C87D3500BCE2E3 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 17DF328724C87D3500BCE2E3 /* WriteFreely-MultiPlatform (iOS) */, 17DF328F24C87D3500BCE2E3 /* WriteFreely-MultiPlatform (macOS) */, 17DF329724C87D3500BCE2E3 /* Tests iOS */, 17DF32A224C87D3500BCE2E3 /* Tests macOS */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 17DF328624C87D3500BCE2E3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 17B3E965250FAA9000EE9748 /* LaunchScreen.storyboard in Resources */, + 17B7827825152CF8008D96C9 /* OpenSans-License.txt in Resources */, 17DF32AE24C87D3500BCE2E3 /* Assets.xcassets in Resources */, + 17D4F39E2514F0E500517CE6 /* OpenSans-Regular.ttf in Resources */, + 17D4F36C2514EE2F00517CE6 /* LoraGX.ttf in Resources */, + 17D4F3A52514F1E900517CE6 /* Hack-Regular.ttf in Resources */, + 17B7827625152CF8008D96C9 /* Lora-Cyrillic-OFL.txt in Resources */, + 17B7827425152CF8008D96C9 /* Hack-License.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF328E24C87D3500BCE2E3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 17B7827525152CF8008D96C9 /* Hack-License.txt in Resources */, 17DF32AF24C87D3500BCE2E3 /* Assets.xcassets in Resources */, + 17B5103B2515448D00E9631F /* Credits.rtf in Resources */, + 17D4F39F2514F0E500517CE6 /* OpenSans-Regular.ttf in Resources */, + 17B7827725152CF8008D96C9 /* Lora-Cyrillic-OFL.txt in Resources */, + 17D4F3A62514F1E900517CE6 /* Hack-Regular.ttf in Resources */, + 17D4F36D2514EE2F00517CE6 /* LoraGX.ttf in Resources */, + 17B7827925152CF8008D96C9 /* OpenSans-License.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF329624C87D3500BCE2E3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 17DF32A124C87D3500BCE2E3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 17DF32C424C87E6700BCE2E3 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "# Run SwiftLint on builds\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; 17DF32C524C87FDB00BCE2E3 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "# Run SwiftLint on builds\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 17DF328424C87D3500BCE2E3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 17DF32AC24C87D3500BCE2E3 /* ContentView.swift in Sources */, 17C42E622507D8E600072984 /* PostStatus.swift in Sources */, 1756DBBA24FED45500207AB8 /* LocalStorageManager.swift in Sources */, 1756AE8124CB844500FD7257 /* View+Keyboard.swift in Sources */, 17C42E652509237800072984 /* PostListFilteredView.swift in Sources */, + 170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */, 17120DAC24E1B99F002B9F6C /* AccountLoginView.swift in Sources */, 17480CA5251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */, 17120DA924E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */, 171BFDFA24D4AF8300888236 /* CollectionListView.swift in Sources */, 1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */, 17120DB224E1E19C002B9F6C /* SettingsHeaderView.swift in Sources */, 1756DBB724FED3A400207AB8 /* LocalStorageModel.xcdatamodeld in Sources */, 17B996DA2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */, 1756AE7724CB2EDD00FD7257 /* PostEditorView.swift in Sources */, 17DF32D524C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */, + 17582194251A4E53004FC441 /* UITextView+Appearance.swift in Sources */, 17D435E824E3128F0036B539 /* PreferencesModel.swift in Sources */, 1765F62A24E18EA200C9EBF0 /* SidebarView.swift in Sources */, 1756AE7A24CB65DF00FD7257 /* PostListView.swift in Sources */, 17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */, 1756DC0124FEE18400207AB8 /* WFACollection+CoreDataClass.swift in Sources */, 17DF32AA24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */, 17120DA724E19D11002B9F6C /* SettingsView.swift in Sources */, 1756DC0324FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */, 17120DA224E1985C002B9F6C /* AccountModel.swift in Sources */, 17120DA324E19A42002B9F6C /* PreferencesView.swift in Sources */, 1756AE6E24CB255B00FD7257 /* PostListModel.swift in Sources */, 174D313224EC2831006CA9EE /* WriteFreelyModel.swift in Sources */, 17C42E70250AA12300072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 17120DA124E19839002B9F6C /* AccountView.swift in Sources */, 1756AE7424CB26FA00FD7257 /* PostCellView.swift in Sources */, + 17681E412519410E00D394AE /* UINavigationController+Appearance.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF328C24C87D3500BCE2E3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 17DF32AD24C87D3500BCE2E3 /* ContentView.swift in Sources */, 1765F62B24E18EA200C9EBF0 /* SidebarView.swift in Sources */, 1756DBBB24FED45500207AB8 /* LocalStorageManager.swift in Sources */, 174D313324EC2831006CA9EE /* WriteFreelyModel.swift in Sources */, - 1756AE7824CB2EDD00FD7257 /* PostEditorView.swift in Sources */, 17D435E924E3128F0036B539 /* PreferencesModel.swift in Sources */, 17120DAA24E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */, 17DF32D624C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */, 17480CA6251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */, 17C42E662509237800072984 /* PostListFilteredView.swift in Sources */, 17120DAD24E1B99F002B9F6C /* AccountLoginView.swift in Sources */, 17C42E71250AAFD500072984 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 1756AE7B24CB65DF00FD7257 /* PostListView.swift in Sources */, 1753F6AC24E431CC00309365 /* MacPreferencesView.swift in Sources */, 1756DC0424FEE18400207AB8 /* WFACollection+CoreDataProperties.swift in Sources */, 17B996DB2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */, 171BFDFB24D4AF8300888236 /* CollectionListView.swift in Sources */, + 17A67CAF251A5DD7002F163D /* PostEditorView.swift in Sources */, 17DF32AB24C87D3500BCE2E3 /* WriteFreely_MultiPlatformApp.swift in Sources */, 17A5388C24DDC83F00DEFF9A /* AccountModel.swift in Sources */, 17B996D92502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */, 1756DBB824FED3A400207AB8 /* LocalStorageModel.xcdatamodeld in Sources */, 17A5389324DDED0000DEFF9A /* PreferencesView.swift in Sources */, 1756AE6F24CB255B00FD7257 /* PostListModel.swift in Sources */, 1756DC0224FEE18400207AB8 /* WFACollection+CoreDataClass.swift in Sources */, 1756DBB424FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */, 17A5388F24DDEC7400DEFF9A /* AccountView.swift in Sources */, + 170DFA35251BBC44001D82A0 /* PostEditorModel.swift in Sources */, 1756AE7524CB26FA00FD7257 /* PostCellView.swift in Sources */, 17A5388824DDA31F00DEFF9A /* MacAccountView.swift in Sources */, 17C42E632507D8E600072984 /* PostStatus.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF329424C87D3500BCE2E3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 17DF329D24C87D3500BCE2E3 /* Tests_iOS.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 17DF329F24C87D3500BCE2E3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 17DF32A824C87D3500BCE2E3 /* Tests_macOS.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 17DF329A24C87D3500BCE2E3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 17DF328724C87D3500BCE2E3 /* WriteFreely-MultiPlatform (iOS) */; targetProxy = 17DF329924C87D3500BCE2E3 /* PBXContainerItemProxy */; }; 17DF32A524C87D3500BCE2E3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 17DF328F24C87D3500BCE2E3 /* WriteFreely-MultiPlatform (macOS) */; targetProxy = 17DF32A424C87D3500BCE2E3 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 17DF32B024C87D3500BCE2E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 17DF32B124C87D3500BCE2E3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 17DF32B324C87D3500BCE2E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 243; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1.1; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 17DF32B424C87D3500BCE2E3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 243; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1.1; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 17DF32B624C87D3500BCE2E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 245; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = macOS/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 0.1.1; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = macosx; SWIFT_VERSION = 5.0; }; name = Debug; }; 17DF32B724C87D3500BCE2E3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 245; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = macOS/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 0.1.1; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = macosx; SWIFT_VERSION = 5.0; }; name = Release; }; 17DF32B924C87D3500BCE2E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = TPPAB4YBA6; INFOPLIST_FILE = "Tests iOS/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = "WriteFreely-MultiPlatform (iOS)"; }; name = Debug; }; 17DF32BA24C87D3500BCE2E3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = TPPAB4YBA6; INFOPLIST_FILE = "Tests iOS/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = "WriteFreely-MultiPlatform (iOS)"; VALIDATE_PRODUCT = YES; }; name = Release; }; 17DF32BC24C87D3500BCE2E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = TPPAB4YBA6; INFOPLIST_FILE = "Tests macOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.Tests-macOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 5.0; TEST_TARGET_NAME = "WriteFreely-MultiPlatform (macOS)"; }; name = Debug; }; 17DF32BD24C87D3500BCE2E3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = TPPAB4YBA6; INFOPLIST_FILE = "Tests macOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.Tests-macOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 5.0; TEST_TARGET_NAME = "WriteFreely-MultiPlatform (macOS)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 17DF327F24C87D3300BCE2E3 /* Build configuration list for PBXProject "WriteFreely-MultiPlatform" */ = { isa = XCConfigurationList; buildConfigurations = ( 17DF32B024C87D3500BCE2E3 /* Debug */, 17DF32B124C87D3500BCE2E3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 17DF32B224C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "WriteFreely-MultiPlatform (iOS)" */ = { isa = XCConfigurationList; buildConfigurations = ( 17DF32B324C87D3500BCE2E3 /* Debug */, 17DF32B424C87D3500BCE2E3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 17DF32B524C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "WriteFreely-MultiPlatform (macOS)" */ = { isa = XCConfigurationList; buildConfigurations = ( 17DF32B624C87D3500BCE2E3 /* Debug */, 17DF32B724C87D3500BCE2E3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 17DF32B824C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "Tests iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 17DF32B924C87D3500BCE2E3 /* Debug */, 17DF32BA24C87D3500BCE2E3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 17DF32BB24C87D3500BCE2E3 /* Build configuration list for PBXNativeTarget "Tests macOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 17DF32BC24C87D3500BCE2E3 /* Debug */, 17DF32BD24C87D3500BCE2E3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ 17DF32BE24C87D7B00BCE2E3 /* XCRemoteSwiftPackageReference "writefreely-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "git@github.com:writeas/writefreely-swift.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 0.1.1; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 17DF32BF24C87D7B00BCE2E3 /* WriteFreely */ = { isa = XCSwiftPackageProductDependency; package = 17DF32BE24C87D7B00BCE2E3 /* XCRemoteSwiftPackageReference "writefreely-swift" */; productName = WriteFreely; }; 17DF32C224C87D8D00BCE2E3 /* WriteFreely */ = { isa = XCSwiftPackageProductDependency; package = 17DF32BE24C87D7B00BCE2E3 /* XCRemoteSwiftPackageReference "writefreely-swift" */; productName = WriteFreely; }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ 1756DBB524FED3A400207AB8 /* LocalStorageModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( 1756DBB624FED3A400207AB8 /* LocalStorageModel.xcdatamodel */, ); currentVersion = 1756DBB624FED3A400207AB8 /* LocalStorageModel.xcdatamodel */; path = LocalStorageModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; /* End XCVersionGroup section */ }; rootObject = 17DF327C24C87D3300BCE2E3 /* Project object */; } diff --git a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist index 6cd8075..2723ebe 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist @@ -1,19 +1,19 @@ SchemeUserState WriteFreely-MultiPlatform (iOS).xcscheme_^#shared#^_ orderHint - 1 + 0 WriteFreely-MultiPlatform (macOS).xcscheme_^#shared#^_ orderHint - 0 + 1 diff --git a/iOS/Extensions/UINavigationController+Appearance.swift b/iOS/Extensions/UINavigationController+Appearance.swift new file mode 100644 index 0000000..969506a --- /dev/null +++ b/iOS/Extensions/UINavigationController+Appearance.swift @@ -0,0 +1,12 @@ +import UIKit + +extension UINavigationController { + override open func viewDidLoad() { + super.viewDidLoad() + + let standardAppearance = UINavigationBarAppearance() + standardAppearance.configureWithTransparentBackground() + + navigationBar.standardAppearance = standardAppearance + } +} diff --git a/iOS/Extensions/UITextView+Appearance.swift b/iOS/Extensions/UITextView+Appearance.swift new file mode 100644 index 0000000..7c0553b --- /dev/null +++ b/iOS/Extensions/UITextView+Appearance.swift @@ -0,0 +1,9 @@ +import UIKit + +extension UITextView { + override open func draw(_ rect: CGRect) { + super.draw(rect) + let appearance = UITextView.appearance() + appearance.backgroundColor = .clear + } +} diff --git a/Shared/Extensions/View+Keyboard.swift b/iOS/Extensions/View+Keyboard.swift similarity index 86% rename from Shared/Extensions/View+Keyboard.swift rename to iOS/Extensions/View+Keyboard.swift index a24c81c..687ddac 100644 --- a/Shared/Extensions/View+Keyboard.swift +++ b/iOS/Extensions/View+Keyboard.swift @@ -1,9 +1,7 @@ import SwiftUI -#if canImport(UIKit) extension View { func hideKeyboard() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } -#endif diff --git a/iOS/Info.plist b/iOS/Info.plist index b002714..92f1bec 100644 --- a/iOS/Info.plist +++ b/iOS/Info.plist @@ -1,53 +1,59 @@ + UIAppFonts + + LoraGX.ttf + OpenSans-Regular.ttf + Hack-Regular.ttf + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName WriteFreely CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight diff --git a/iOS/PostEditor/PostEditorView.swift b/iOS/PostEditor/PostEditorView.swift new file mode 100644 index 0000000..4cb30dd --- /dev/null +++ b/iOS/PostEditor/PostEditorView.swift @@ -0,0 +1,171 @@ +import SwiftUI + +struct PostEditorView: View { + @EnvironmentObject var model: WriteFreelyModel + + @ObservedObject var post: WFAPost + + var body: some View { + VStack { + switch post.appearance { + case "sans": + TextField("Title (optional)", text: $post.title) + .font(.custom("OpenSans-Regular", size: 26, relativeTo: Font.TextStyle.largeTitle)) + .onChange(of: post.title) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + ZStack(alignment: .topLeading) { + if post.body.count == 0 { + Text("Write...") + .foregroundColor(Color(UIColor.placeholderText)) + .padding(.horizontal, 4) + .padding(.vertical, 8) + .font(.custom("OpenSans-Regular", size: 17, relativeTo: Font.TextStyle.body)) + } + TextEditor(text: $post.body) + .font(.custom("OpenSans-Regular", size: 17, relativeTo: Font.TextStyle.body)) + .onChange(of: post.body) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + } + case "wrap", "mono", "code": + TextField("Title (optional)", text: $post.title) + .font(.custom("Hack", size: 26, relativeTo: Font.TextStyle.largeTitle)) + .onChange(of: post.title) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + ZStack(alignment: .topLeading) { + if post.body.count == 0 { + Text("Write...") + .foregroundColor(Color(UIColor.placeholderText)) + .padding(.horizontal, 4) + .padding(.vertical, 8) + .font(.custom("Hack", size: 17, relativeTo: Font.TextStyle.body)) + } + TextEditor(text: $post.body) + .font(.custom("Hack", size: 17, relativeTo: Font.TextStyle.body)) + .onChange(of: post.body) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + } + default: + TextField("Title (optional)", text: $post.title) + .font(.custom("Lora", size: 26, relativeTo: Font.TextStyle.largeTitle)) + .onChange(of: post.title) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + ZStack(alignment: .topLeading) { + if post.body.count == 0 { + Text("Write...") + .foregroundColor(Color(UIColor.placeholderText)) + .padding(.horizontal, 4) + .padding(.vertical, 8) + .font(.custom("Lora", size: 17, relativeTo: Font.TextStyle.body)) + } + TextEditor(text: $post.body) + .font(.custom("Lora", size: 17, relativeTo: Font.TextStyle.body)) + .onChange(of: post.body) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + } + } + } + .navigationBarTitleDisplayMode(.inline) + .padding() + .toolbar { + ToolbarItem(placement: .principal) { + PostEditorStatusToolbarView(post: post) + } + ToolbarItem(placement: .primaryAction) { + Button(action: { + publishPost() + }, label: { + Image(systemName: "paperplane") + }) + .disabled( + post.status == PostStatus.published.rawValue || + !model.account.isLoggedIn || + !model.hasNetworkConnection + ) + } + } + .onChange(of: post.hasNewerRemoteCopy, perform: { _ in + if post.status == PostStatus.edited.rawValue && !post.hasNewerRemoteCopy { + post.status = PostStatus.published.rawValue + } + }) + .onChange(of: post.status, perform: { _ in + if post.status != PostStatus.published.rawValue { + DispatchQueue.main.async { + model.editor.setLastDraft(post) + } + } else { + DispatchQueue.main.async { + model.editor.clearLastDraft() + } + } + }) + .onDisappear(perform: { + if post.status != PostStatus.published.rawValue { + DispatchQueue.main.async { + LocalStorageManager().saveContext() + } + } + }) + } + + private func publishPost() { + DispatchQueue.main.async { + LocalStorageManager().saveContext() + model.posts.loadCachedPosts() + model.publish(post: post) + } + #if os(iOS) + self.hideKeyboard() + #endif + } +} + +struct PostEditorView_EmptyPostPreviews: PreviewProvider { + static var previews: some View { + let context = LocalStorageManager.persistentContainer.viewContext + let testPost = WFAPost(context: context) + testPost.createdDate = Date() + testPost.appearance = "norm" + + let model = WriteFreelyModel() + + return PostEditorView(post: testPost) + .environment(\.managedObjectContext, context) + .environmentObject(model) + } +} + +struct PostEditorView_ExistingPostPreviews: PreviewProvider { + static var previews: some View { + let context = LocalStorageManager.persistentContainer.viewContext + let testPost = WFAPost(context: context) + testPost.title = "Test Post Title" + testPost.body = "Here's some cool sample body text." + testPost.createdDate = Date() + testPost.appearance = "code" + + let model = WriteFreelyModel() + + return PostEditorView(post: testPost) + .environment(\.managedObjectContext, context) + .environmentObject(model) + } +} diff --git a/iOS/Settings/SettingsView.swift b/iOS/Settings/SettingsView.swift index 78ba7cf..d67a90f 100644 --- a/iOS/Settings/SettingsView.swift +++ b/iOS/Settings/SettingsView.swift @@ -1,34 +1,56 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var model: WriteFreelyModel var body: some View { VStack { SettingsHeaderView() Form { Section(header: Text("Login Details")) { AccountView() } Section(header: Text("Appearance")) { PreferencesView(preferences: model.preferences) } Section(header: Text("Support")) { HStack { Spacer() Link("Visit Help Forum", destination: model.helpURL) Spacer() } } + 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() + } } } // .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info. } } struct SettingsView_Previews: PreviewProvider { static var previews: some View { SettingsView() .environmentObject(WriteFreelyModel()) } } diff --git a/macOS/Credits.rtf b/macOS/Credits.rtf new file mode 100644 index 0000000..5d0cc51 --- /dev/null +++ b/macOS/Credits.rtf @@ -0,0 +1,20 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2571 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 SFProDisplay-Regular;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid1\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1}} +{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 + +\f0\fs26 \cf0 This application makes use of the following open-source projects:\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 +\cf0 \ +\pard\tx220\tx720\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\li720\fi-720\pardirnatural\partightenfactor0 +\ls1\ilvl0\cf0 {\listtext \uc0\u8226 }Lora typeface\ +{\listtext \uc0\u8226 }Open Sans typeface\ +{\listtext \uc0\u8226 }Hack typeface\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 +\cf0 \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 +{\field{\*\fldinst{HYPERLINK "https://github.com/writeas/writefreely-swiftui-multiplatform/tree/main/Shared/Resources/Licenses"}}{\fldrslt \cf0 View the licenses}}} \ No newline at end of file diff --git a/macOS/Info.plist b/macOS/Info.plist index 6edfdd7..55d0f90 100644 --- a/macOS/Info.plist +++ b/macOS/Info.plist @@ -1,30 +1,32 @@ + ATSApplicationFontsPath + . CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName WriteFreely CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType public.app-category.social-networking LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) diff --git a/macOS/PostEditor/PostEditorView.swift b/macOS/PostEditor/PostEditorView.swift new file mode 100644 index 0000000..5ed58af --- /dev/null +++ b/macOS/PostEditor/PostEditorView.swift @@ -0,0 +1,190 @@ +import SwiftUI + +struct PostEditorView: View { + @EnvironmentObject var model: WriteFreelyModel + + @ObservedObject var post: WFAPost + @State private var isHovering: Bool = false + + var body: some View { + VStack { + switch post.appearance { + case "sans": + TextField("Title (optional)", text: $post.title) + .textFieldStyle(PlainTextFieldStyle()) + .padding(.bottom) + .font(.custom("OpenSans-Regular", size: 26, relativeTo: Font.TextStyle.largeTitle)) + .onChange(of: post.title) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + ZStack(alignment: .topLeading) { + if post.body.count == 0 { + Text("Write...") + .foregroundColor(Color(NSColor.placeholderTextColor)) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .font(.custom("OpenSans-Regular", size: 17, relativeTo: Font.TextStyle.body)) + } + TextEditor(text: $post.body) + .font(.custom("OpenSans-Regular", size: 17, relativeTo: Font.TextStyle.body)) + .opacity(post.body.count == 0 && !isHovering ? 0.0 : 1.0) + .onChange(of: post.body) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + .onHover(perform: { hovering in + self.isHovering = hovering + }) + } + .background(Color(NSColor.controlBackgroundColor)) + case "wrap", "mono", "code": + TextField("Title (optional)", text: $post.title) + .textFieldStyle(PlainTextFieldStyle()) + .padding(.bottom) + .font(.custom("Hack", size: 26, relativeTo: Font.TextStyle.largeTitle)) + .onChange(of: post.title) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + ZStack(alignment: .topLeading) { + if post.body.count == 0 { + Text("Write...") + .foregroundColor(Color(NSColor.placeholderTextColor)) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .font(.custom("Hack", size: 17, relativeTo: Font.TextStyle.body)) + } + TextEditor(text: $post.body) + .font(.custom("Hack", size: 17, relativeTo: Font.TextStyle.body)) + .opacity(post.body.count == 0 && !isHovering ? 0.0 : 1.0) + .onChange(of: post.body) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + .onHover(perform: { hovering in + self.isHovering = hovering + }) + } + .background(Color(NSColor.controlBackgroundColor)) + default: + TextField("Title (optional)", text: $post.title) + .textFieldStyle(PlainTextFieldStyle()) + .padding(.bottom) + .font(.custom("Lora", size: 26, relativeTo: Font.TextStyle.largeTitle)) + .onChange(of: post.title) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + ZStack(alignment: .topLeading) { + if post.body.count == 0 { + Text("Write...") + .foregroundColor(Color(NSColor.placeholderTextColor)) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .font(.custom("Lora", size: 17, relativeTo: Font.TextStyle.body)) + } + TextEditor(text: $post.body) + .font(.custom("Lora", size: 17, relativeTo: Font.TextStyle.body)) + .opacity(post.body.count == 0 && !isHovering ? 0.0 : 1.0) + .onChange(of: post.body) { _ in + if post.status == PostStatus.published.rawValue { + post.status = PostStatus.edited.rawValue + } + } + .onHover(perform: { hovering in + self.isHovering = hovering + }) + } + .background(Color(NSColor.controlBackgroundColor)) + } + } + .padding() + .background(Color.white) + .toolbar { + ToolbarItem(placement: .status) { + PostEditorStatusToolbarView(post: post) + } + ToolbarItem(placement: .primaryAction) { + Button(action: { + publishPost() + }, label: { + Image(systemName: "paperplane") + }) + .disabled( + post.status == PostStatus.published.rawValue || + !model.account.isLoggedIn || + !model.hasNetworkConnection + ) + } + } + .onChange(of: post.hasNewerRemoteCopy, perform: { _ in + if post.status == PostStatus.edited.rawValue && !post.hasNewerRemoteCopy { + post.status = PostStatus.published.rawValue + } + }) + .onChange(of: post.status, perform: { _ in + if post.status != PostStatus.published.rawValue { + DispatchQueue.main.async { + model.editor.setLastDraft(post) + } + } + }) + .onDisappear(perform: { + if post.status != PostStatus.published.rawValue { + DispatchQueue.main.async { + LocalStorageManager().saveContext() + } + } else { + DispatchQueue.main.async { + model.editor.clearLastDraft() + } + } + }) + } + + private func publishPost() { + DispatchQueue.main.async { + LocalStorageManager().saveContext() + model.posts.loadCachedPosts() + model.publish(post: post) + } + } +} + +struct PostEditorView_EmptyPostPreviews: PreviewProvider { + static var previews: some View { + let context = LocalStorageManager.persistentContainer.viewContext + let testPost = WFAPost(context: context) + testPost.createdDate = Date() + testPost.appearance = "norm" + + let model = WriteFreelyModel() + + return PostEditorView(post: testPost) + .environment(\.managedObjectContext, context) + .environmentObject(model) + } +} + +struct PostEditorView_ExistingPostPreviews: PreviewProvider { + static var previews: some View { + let context = LocalStorageManager.persistentContainer.viewContext + let testPost = WFAPost(context: context) + testPost.title = "Test Post Title" + testPost.body = "Here's some cool sample body text." + testPost.createdDate = Date() + testPost.appearance = "code" + + let model = WriteFreelyModel() + + return PostEditorView(post: testPost) + .environment(\.managedObjectContext, context) + .environmentObject(model) + } +}