diff --git a/Shared/Extensions/Bundle+AppVersion.swift b/Shared/Extensions/Bundle+AppVersion.swift
new file mode 100644
index 0000000..d5020eb
--- /dev/null
+++ b/Shared/Extensions/Bundle+AppVersion.swift
@@ -0,0 +1,22 @@
+import Foundation
+
+private struct InfoPlistConstants {
+ static let versionNumber = "CFBundleShortVersionString"
+ static let buildNumber = "CFBundleVersion"
+}
+
+extension Bundle {
+ public var appMarketingVersion: String {
+ guard let result = infoDictionary?[InfoPlistConstants.versionNumber] as? String else {
+ return "⚠️"
+ }
+ return result
+ }
+
+ public var appBuildVersion: String {
+ guard let result = infoDictionary?[InfoPlistConstants.buildNumber] as? String else {
+ return "⚠️"
+ }
+ return result
+ }
+}
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 bb85444..ce9bf20 100644
--- a/Shared/Models/WriteFreelyModel.swift
+++ b/Shared/Models/WriteFreelyModel.swift
@@ -1,377 +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 selectedPost: WFAPost?
-
+ @Published var hasNetworkConnection: Bool = false
+ @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() {
- // Set the color scheme based on what's been saved in UserDefaults.
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 8cbefca..26a341f 100644
--- a/Shared/Navigation/ContentView.swift
+++ b/Shared/Navigation/ContentView.swift
@@ -1,40 +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/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift
index 1f65af6..e8e50e9 100644
--- a/Shared/PostCollection/CollectionListView.swift
+++ b/Shared/PostCollection/CollectionListView.swift
@@ -1,44 +1,48 @@
import SwiftUI
struct CollectionListView: View {
@EnvironmentObject var model: WriteFreelyModel
@Environment(\.managedObjectContext) var moc
@FetchRequest(
entity: WFACollection.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)]
) var collections: FetchedResults
var body: some View {
List {
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: true)) {
Text("All Posts")
}
- NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) {
- Text(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")
- }
- Section(header: Text("Your Blogs")) {
- ForEach(collections, id: \.alias) { collection in
- NavigationLink(
- destination: PostListView(selectedCollection: collection, showAllPosts: false)
- ) {
- Text(collection.title)
+ if model.account.isLoggedIn {
+ NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) {
+ Text(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")
+ }
+ Section(header: Text("Your Blogs")) {
+ ForEach(collections, id: \.alias) { collection in
+ NavigationLink(
+ destination: PostListView(selectedCollection: collection, showAllPosts: false)
+ ) {
+ Text(collection.title)
+ }
}
}
}
}
- .navigationTitle("Collections")
+ .navigationTitle(
+ model.account.isLoggedIn ? "\(URL(string: model.account.server)?.host ?? "WriteFreely")" : "WriteFreely"
+ )
.listStyle(SidebarListStyle())
}
}
-struct CollectionListView_Previews: PreviewProvider {
+struct CollectionListView_LoggedOutPreviews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.persistentContainer.viewContext
let model = WriteFreelyModel()
return CollectionListView()
.environment(\.managedObjectContext, context)
.environmentObject(model)
}
}
diff --git a/Shared/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 88003be..0000000
--- a/Shared/PostEditor/PostEditorView.swift
+++ /dev/null
@@ -1,76 +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")
- })
- }
- }
- .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/PostListFilteredView.swift b/Shared/PostList/PostListFilteredView.swift
index f89f081..9f72c94 100644
--- a/Shared/PostList/PostListFilteredView.swift
+++ b/Shared/PostList/PostListFilteredView.swift
@@ -1,50 +1,89 @@
import SwiftUI
struct PostListFilteredView: View {
@EnvironmentObject var model: WriteFreelyModel
+
var fetchRequest: FetchRequest
init(filter: String?, showAllPosts: Bool) {
if showAllPosts {
fetchRequest = FetchRequest(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)]
)
} else {
if let filter = filter {
fetchRequest = FetchRequest(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)],
predicate: NSPredicate(format: "collectionAlias == %@", filter)
)
} else {
fetchRequest = FetchRequest(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)],
predicate: NSPredicate(format: "collectionAlias == nil")
)
}
}
}
var body: some View {
- List(fetchRequest.wrappedValue, id: \.self) { post in
- NavigationLink(
- destination: PostEditorView(post: post),
- tag: post,
- selection: $model.selectedPost
- ) {
- PostCellView(post: post)
+ #if os(iOS)
+ List {
+ ForEach(fetchRequest.wrappedValue, id: \.self) { post in
+ NavigationLink(
+ destination: PostEditorView(post: post),
+ tag: post,
+ selection: $model.selectedPost
+ ) {
+ PostCellView(post: post)
+ }
+ .deleteDisabled(post.status != PostStatus.local.rawValue)
+ }
+ .onDelete(perform: { indexSet in
+ for index in indexSet {
+ let post = fetchRequest.wrappedValue[index]
+ delete(post)
+ }
+ })
+ }
+ #else
+ List {
+ ForEach(fetchRequest.wrappedValue, id: \.self) { post in
+ NavigationLink(
+ destination: PostEditorView(post: post),
+ tag: post,
+ selection: $model.selectedPost
+ ) {
+ PostCellView(post: post)
+ }
+ .deleteDisabled(post.status != PostStatus.local.rawValue)
}
+ .onDelete(perform: { indexSet in
+ for index in indexSet {
+ let post = fetchRequest.wrappedValue[index]
+ delete(post)
+ }
+ })
}
+ .onDeleteCommand(perform: {
+ guard let selectedPost = model.selectedPost else { return }
+ if selectedPost.status == PostStatus.local.rawValue {
+ model.postToDelete = selectedPost
+ model.isPresentingDeleteAlert = true
+ }
+ })
+ #endif
+ }
+
+ func delete(_ post: WFAPost) {
+ model.posts.remove(post)
}
}
struct PostListFilteredView_Previews: PreviewProvider {
static var previews: some View {
- let context = LocalStorageManager.persistentContainer.viewContext
-
return PostListFilteredView(filter: nil, showAllPosts: false)
- .environment(\.managedObjectContext, context)
}
}
diff --git a/Shared/PostList/PostListModel.swift b/Shared/PostList/PostListModel.swift
index aab1ebd..9dea70a 100644
--- a/Shared/PostList/PostListModel.swift
+++ b/Shared/PostList/PostListModel.swift
@@ -1,36 +1,41 @@
import SwiftUI
import CoreData
class PostListModel: ObservableObject {
@Published var userPosts = [WFAPost]()
init() {
loadCachedPosts()
}
func loadCachedPosts() {
let request = WFAPost.createFetchRequest()
let sort = NSSortDescriptor(key: "createdDate", ascending: false)
request.sortDescriptors = [sort]
userPosts = []
do {
let cachedPosts = try LocalStorageManager.persistentContainer.viewContext.fetch(request)
userPosts.append(contentsOf: cachedPosts)
} catch {
print("Error: Failed to fetch cached posts.")
}
}
+ func remove(_ post: WFAPost) {
+ LocalStorageManager.persistentContainer.viewContext.delete(post)
+ LocalStorageManager().saveContext()
+ }
+
func purgeAllPosts() {
userPosts = []
let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "WFAPost")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try LocalStorageManager.persistentContainer.viewContext.executeAndMergeChanges(using: deleteRequest)
} catch {
print("Error: Failed to purge cached posts.")
}
}
}
diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift
index bafb050..2103891 100644
--- a/Shared/PostList/PostListView.swift
+++ b/Shared/PostList/PostListView.swift
@@ -1,127 +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)
+ .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)
+ .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 35fc831..ca9570b 100644
--- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj
+++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj
@@ -1,1064 +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/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/SettingsHeaderView.swift b/iOS/Settings/SettingsHeaderView.swift
index 9177ec4..ca65578 100644
--- a/iOS/Settings/SettingsHeaderView.swift
+++ b/iOS/Settings/SettingsHeaderView.swift
@@ -1,26 +1,32 @@
import SwiftUI
struct SettingsHeaderView: View {
@Environment(\.presentationMode) var presentationMode
var body: some View {
- HStack {
- Text("Settings")
- .font(.largeTitle)
- .fontWeight(.bold)
- Spacer()
- Button(action: {
- presentationMode.wrappedValue.dismiss()
- }, label: {
- Image(systemName: "xmark.circle")
- })
+ VStack {
+ HStack {
+ Text("Settings")
+ .font(.largeTitle)
+ .fontWeight(.bold)
+ Spacer()
+ Button(action: {
+ presentationMode.wrappedValue.dismiss()
+ }, label: {
+ Image(systemName: "xmark.circle")
+ })
+ }
+ Text("WriteFreely v\(Bundle.main.appMarketingVersion) (build \(Bundle.main.appBuildVersion))")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .padding(.top)
}
.padding()
}
}
struct SettingsHeaderView_Previews: PreviewProvider {
static var previews: some View {
SettingsHeaderView()
}
}
diff --git a/iOS/Settings/SettingsView.swift b/iOS/Settings/SettingsView.swift
index f6b60fc..d67a90f 100644
--- a/iOS/Settings/SettingsView.swift
+++ b/iOS/Settings/SettingsView.swift
@@ -1,27 +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)
+ }
+}