Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F10455643
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
39 KB
Subscribers
None
View Options
diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift
index baf1dd3..b6a17b9 100644
--- a/Shared/Models/WriteFreelyModel.swift
+++ b/Shared/Models/WriteFreelyModel.swift
@@ -1,360 +1,367 @@
import Foundation
import WriteFreely
import Security
// MARK: - WriteFreelyModel
class WriteFreelyModel: ObservableObject {
@Published var account = AccountModel()
@Published var preferences = PreferencesModel()
@Published var store = PostListModel()
@Published var collections = CollectionListModel()
@Published var isLoggingIn: Bool = false
@Published var selectedPost: WFAPost?
private var client: WFClient?
private let defaults = UserDefaults.standard
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)
- }
-
- #if DEBUG
-// for post in testPostData { store.add(post) }
- #endif
-
- DispatchQueue.main.async {
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
if self.collections.userCollections.count == 0 {
self.fetchUserCollections()
}
self.fetchUserPosts()
}
}
}
}
// 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 }
var wfPost = WFPost(
- body: post.body ?? "",
- title: post.title,
+ 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
}
loggedInClient.getPost(byId: postId, completion: updateFromServerHandler)
}
}
private extension WriteFreelyModel {
func loginHandler(result: Result<WFUser, Error>) {
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<Bool, Error>) {
do {
_ = try result.get()
do {
try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
client = nil
DispatchQueue.main.async {
self.account.logout()
self.collections.clearUserCollection()
self.store.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.collections.clearUserCollection()
self.account.logout()
self.store.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: PersistenceManager.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 {
PersistenceManager().saveContext()
}
} catch {
print(error)
}
}
func fetchUserPostsHandler(result: Result<[WFPost], Error>) {
do {
let fetchedPosts = try result.get()
for fetchedPost in fetchedPosts {
- let managedPost = WFAPost(context: PersistenceManager.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
+ // For each fetched post, we
+ // 1. check to see if a matching post exists
+ if let managedPost = store.posts.first(where: { $0.postId == fetchedPost.postId }) {
+ // If it exists, we set the hasNewerRemoteCopy flag as appropriate.
+ 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")
+ }
+ } else {
+ // If it doesn't exist, we create the managed object.
+ let managedPost = WFAPost(context: PersistenceManager.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
+ }
}
DispatchQueue.main.async {
PersistenceManager().saveContext()
}
} catch {
print(error)
}
}
func publishHandler(result: Result<WFPost, Error>) {
do {
let fetchedPost = try result.get()
let foundPostIndex = store.posts.firstIndex(where: {
$0.title == fetchedPost.title && $0.body == fetchedPost.body
})
guard let index = foundPostIndex else { return }
let cachedPost = self.store.posts[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.title = fetchedPost.title ?? ""
cachedPost.updatedDate = fetchedPost.updatedDate
DispatchQueue.main.async {
PersistenceManager().saveContext()
}
} catch {
print(error)
}
}
func updateFromServerHandler(result: Result<WFPost, Error>) {
do {
let fetchedPost = try result.get()
guard let cachedPost = self.selectedPost else { return }
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.title = fetchedPost.title ?? ""
cachedPost.updatedDate = fetchedPost.updatedDate
+ cachedPost.hasNewerRemoteCopy = false
DispatchQueue.main.async {
PersistenceManager().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/PostEditor/PostEditorStatusToolbarView.swift b/Shared/PostEditor/PostEditorStatusToolbarView.swift
index 85c5e65..7f19f7a 100644
--- a/Shared/PostEditor/PostEditorStatusToolbarView.swift
+++ b/Shared/PostEditor/PostEditorStatusToolbarView.swift
@@ -1,136 +1,136 @@
import SwiftUI
struct PostEditorStatusToolbarView: View {
#if os(iOS)
@Environment(\.horizontalSizeClass) var horizontalSizeClass
#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)
+ model.updateFromServer(post: post) // FIXME: This shouldn't change post status after update
}, 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 {
PostStatusBadgeView(post: post)
}
}
}
//#if DEBUG
//let testPost = Post(
// title: "Test Post Title",
// body: """
// Here's some cool sample body text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ultrices \
// posuere dignissim. Vestibulum a libero tempor, lacinia nulla vitae, congue purus. Nunc ac nulla quam. Duis \
// tincidunt eros augue, et volutpat tortor pulvinar ut. Nullam sit amet maximus urna. Phasellus non dignissim \
// lacus. Nulla ac posuere ex. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus \
// mus. Donec non molestie mauris. Suspendisse potenti. Vivamus at erat turpis.
//
// Pellentesque porttitor gravida tincidunt. Sed vitae eros non metus aliquam hendrerit. Aliquam sed risus suscipit \
// turpis dictum dictum. Duis lacus lectus, dictum vel felis in, rhoncus fringilla felis. Nunc id dolor nisl. \
// Aliquam euismod purus elit. Nullam egestas neque leo, sed aliquet ligula ultrices nec.
// """,
// createdDate: Date()
//)
//#endif
//
//struct ToolbarView_LocalPreviews: PreviewProvider {
// static var previews: some View {
// let model = WriteFreelyModel()
// let post = testPost
// return PostEditorStatusToolbarView(post: post)
// .environmentObject(model)
// }
//}
//
//struct ToolbarView_RemotePreviews: PreviewProvider {
// static var previews: some View {
// let model = WriteFreelyModel()
// let newerRemotePost = Post(
// title: testPost.wfPost.title ?? "",
// body: testPost.wfPost.body,
// createdDate: testPost.wfPost.createdDate ?? Date(),
// status: testPost.status,
// collection: testPost.collection
// )
// newerRemotePost.hasNewerRemoteCopy = true
// return PostEditorStatusToolbarView(post: newerRemotePost)
// .environmentObject(model)
// }
//}
//
//#if os(iOS)
//struct ToolbarView_CompactLocalPreviews: PreviewProvider {
// static var previews: some View {
// let model = WriteFreelyModel()
// let post = testPost
// return PostEditorStatusToolbarView(post: post)
// .environmentObject(model)
// .environment(\.horizontalSizeClass, .compact)
// }
//}
//#endif
//
//#if os(iOS)
//struct ToolbarView_CompactRemotePreviews: PreviewProvider {
// static var previews: some View {
// let model = WriteFreelyModel()
// let newerRemotePost = Post(
// title: testPost.wfPost.title ?? "",
// body: testPost.wfPost.body,
// createdDate: testPost.wfPost.createdDate ?? Date(),
// status: testPost.status,
// collection: testPost.collection
// )
// newerRemotePost.hasNewerRemoteCopy = true
// return PostEditorStatusToolbarView(post: newerRemotePost)
// .environmentObject(model)
// .environment(\.horizontalSizeClass, .compact)
// }
//}
//#endif
diff --git a/Shared/PostEditor/PostEditorView.swift b/Shared/PostEditor/PostEditorView.swift
index 9fcd55d..b638e01 100644
--- a/Shared/PostEditor/PostEditorView.swift
+++ b/Shared/PostEditor/PostEditorView.swift
@@ -1,87 +1,77 @@
import SwiftUI
struct PostEditorView: View {
@EnvironmentObject var model: WriteFreelyModel
- @Environment(\.managedObjectContext) var moc
@ObservedObject var post: WFAPost
- @State private var postTitle = ""
- @State private var postBody = ""
-
var body: some View {
VStack {
- TextEditor(text: $postTitle)
+ TextEditor(text: $post.title)
.font(.title)
.frame(height: 100)
- .onChange(of: postTitle) { _ in
- if post.status == PostStatus.published.rawValue && post.title != postTitle {
+ .onChange(of: post.title) { _ in
+ if post.status == PostStatus.published.rawValue {
post.status = PostStatus.edited.rawValue
}
- post.title = postTitle
}
- TextEditor(text: $postBody)
+ TextEditor(text: $post.body)
.font(.body)
- .onChange(of: postBody) { _ in
+ .onChange(of: post.body) { _ in
if post.status == PostStatus.published.rawValue {
post.status = PostStatus.edited.rawValue
}
- post.body = postBody
}
}
.padding()
.toolbar {
ToolbarItem(placement: .status) {
PostEditorStatusToolbarView(post: post)
}
ToolbarItem(placement: .primaryAction) {
Button(action: {
model.publish(post: post)
post.status = PostStatus.published.rawValue
}, label: {
Image(systemName: "paperplane")
})
}
}
- .onAppear(perform: {
- postTitle = post.title ?? ""
- postBody = post.body ?? ""
- })
.onDisappear(perform: {
if post.status == PostStatus.edited.rawValue {
DispatchQueue.main.async {
PersistenceManager().saveContext()
}
}
})
}
}
//struct PostEditorView_NewLocalDraftPreviews: PreviewProvider {
// static var previews: some View {
// PostEditorView(post: Post())
// .environmentObject(WriteFreelyModel())
// }
//}
//
//struct PostEditorView_NewerLocalPostPreviews: PreviewProvider {
// static var previews: some View {
// return PostEditorView(post: testPost)
// .environmentObject(WriteFreelyModel())
// }
//}
//
//struct PostEditorView_NewerRemotePostPreviews: PreviewProvider {
// static var previews: some View {
// let newerRemotePost = Post(
// title: testPost.wfPost.title ?? "",
// body: testPost.wfPost.body,
// createdDate: testPost.wfPost.createdDate ?? Date(),
// status: testPost.status,
// collection: testPost.collection
// )
// newerRemotePost.hasNewerRemoteCopy = true
// return PostEditorView(post: newerRemotePost)
// .environmentObject(WriteFreelyModel())
// }
//}
diff --git a/Shared/PostList/PostCellView.swift b/Shared/PostList/PostCellView.swift
index 535260e..3f65070 100644
--- a/Shared/PostList/PostCellView.swift
+++ b/Shared/PostList/PostCellView.swift
@@ -1,40 +1,40 @@
import SwiftUI
struct PostCellView: View {
@ObservedObject var post: WFAPost
var body: some View {
HStack {
VStack(alignment: .leading) {
- Text(post.title ?? "")
+ Text(post.title)
.font(.headline)
.lineLimit(1)
Text(buildDateString(from: post.createdDate ?? Date()))
.font(.caption)
.lineLimit(1)
}
Spacer()
PostStatusBadgeView(post: post)
}
.padding(5)
}
func buildDateString(from date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .short
return dateFormatter.string(from: date)
}
}
//struct PostCell_Previews: PreviewProvider {
// static var previews: some View {
// let testPost = Post(
// title: "Test Post Title",
// body: "Here's some cool sample body text.",
// createdDate: Date()
// )
// return PostCellView(post: testPost)
// }
//}
diff --git a/Shared/PostList/PostListModel.swift b/Shared/PostList/PostListModel.swift
index 3f2d9c9..29ad70c 100644
--- a/Shared/PostList/PostListModel.swift
+++ b/Shared/PostList/PostListModel.swift
@@ -1,91 +1,39 @@
import Foundation
import WriteFreely
import CoreData
class PostListModel: ObservableObject {
@Published var posts = [WFAPost]()
init() {
loadCachedPosts()
}
func loadCachedPosts() {
let request = WFAPost.createFetchRequest()
let sort = NSSortDescriptor(key: "createdDate", ascending: false)
request.sortDescriptors = [sort]
posts = []
do {
let cachedPosts = try PersistenceManager.persistentContainer.viewContext.fetch(request)
posts.append(contentsOf: cachedPosts)
} catch {
print("Error: Failed to fetch cached posts.")
}
}
func purgeAllPosts() {
posts = []
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "WFAPost")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try PersistenceManager.persistentContainer.persistentStoreCoordinator.execute(
deleteRequest, with: PersistenceManager.persistentContainer.viewContext
)
} catch {
print("Error: Failed to purge cached posts.")
}
}
-
-// func add(_ post: WFAPost) {
-// posts.append(post)
-// }
-
-// func update(_ post: WFAPost) {
-// // Find the local copy in the store
-// let localCopy = posts.first(where: { $0.id == post.id })
-//
-// // If there's a local copy, update the updatedDate property of its WFPost
-// if let localCopy = localCopy {
-// localCopy.wfPost.updatedDate = Date()
-// } else {
-// print("Error: Local copy not found")
-// }
-// }
-
-// func replace(post: Post, with fetchedPost: WFPost) {
-// // Find the local copy in the store.
-// let localCopy = posts.first(where: { $0.id == post.id })
-//
-// // Replace the local copy's wfPost property with the fetched copy.
-// if let localCopy = localCopy {
-// localCopy.wfPost = fetchedPost
-// DispatchQueue.main.async {
-// localCopy.hasNewerRemoteCopy = false
-// localCopy.status = .published
-// }
-// } else {
-// print("Error: Local copy not found")
-// }
-// }
-
-// func updateStore(with fetchedPosts: [Post]) {
-// for fetchedPost in fetchedPosts {
-// // Find the local copy in the store.
-// let localCopy = posts.first(where: { $0.wfPost.postId == fetchedPost.wfPost.postId })
-//
-// // If there's a local copy, check which is newer; if not, add the fetched post to the store.
-// if let localCopy = localCopy {
-// // We do not discard the local copy; we simply set the hasNewerRemoteCopy flag accordingly.
-// if let remoteCopyUpdatedDate = fetchedPost.wfPost.updatedDate,
-// let localCopyUpdatedDate = localCopy.wfPost.updatedDate {
-// localCopy.hasNewerRemoteCopy = remoteCopyUpdatedDate > localCopyUpdatedDate
-// } else {
-// print("Error: could not determine which copy of post is newer")
-// }
-// } else {
-// add(fetchedPost)
-// }
-// }
-// }
}
diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift
index 81d23b6..c3571b0 100644
--- a/Shared/PostList/PostListView.swift
+++ b/Shared/PostList/PostListView.swift
@@ -1,206 +1,204 @@
import SwiftUI
struct PostListView: View {
@EnvironmentObject var model: WriteFreelyModel
@Environment(\.managedObjectContext) var moc
@FetchRequest(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \WFAPost.createdDate, ascending: true)]
) var posts: FetchedResults<WFAPost>
@State var selectedCollection: WFACollection?
@State var showAllPosts: Bool = false
#if os(iOS)
@State private var isPresentingSettings = false
#endif
var body: some View {
#if os(iOS)
GeometryReader { geometry in
List {
ForEach(showPosts(for: selectedCollection)) { post in
NavigationLink(destination: PostEditorView(post: post)) {
PostCellView(post: post)
}
}
}
.environmentObject(model)
.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: {
isPresentingSettings = true
}, label: {
Image(systemName: "gear")
}).sheet(
isPresented: $isPresentingSettings,
onDismiss: {
isPresentingSettings = false
},
content: {
SettingsView(isPresented: $isPresentingSettings)
}
)
.padding(.leading)
Spacer()
Text(pluralizedPostCount(for: showPosts(for: selectedCollection)))
.foregroundColor(.secondary)
Spacer()
Button(action: {
reloadFromServer()
}, label: {
Image(systemName: "arrow.clockwise")
})
.disabled(!model.account.isLoggedIn)
}
.padding()
.frame(width: geometry.size.width)
}
}
}
#else //if os(macOS)
List {
ForEach(showPosts(for: selectedCollection)) { post in
NavigationLink(destination: PostEditorView(post: post)) {
PostCellView(post: post)
}
}
}
.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)
}
#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.store.posts
} else {
if let selectedCollection = collection {
return model.store.posts.filter { $0.collectionAlias == selectedCollection.alias }
} else {
return model.store.posts.filter { $0.collectionAlias == nil }
}
}
}
private func reloadFromServer() {
DispatchQueue.main.async {
model.collections.clearUserCollection()
model.fetchUserCollections()
model.fetchUserPosts()
}
}
private func createNewLocalDraft() {
- let post = Post()
let managedPost = WFAPost(context: PersistenceManager.persistentContainer.viewContext)
- managedPost.createdDate = post.wfPost.createdDate
- managedPost.title = post.wfPost.title
- managedPost.body = post.wfPost.body
+ managedPost.createdDate = Date()
+ managedPost.title = ""
+ managedPost.body = ""
managedPost.status = PostStatus.local.rawValue
DispatchQueue.main.async {
-// model.store.add(post)
PersistenceManager().saveContext()
}
}
}
//struct PostList_Previews: PreviewProvider {
// static var previews: some View {
// let userCollection1 = WFACollection(context: PersistenceManager.persistentContainer.viewContext)
// let userCollection2 = WFACollection(context: PersistenceManager.persistentContainer.viewContext)
// let userCollection3 = WFACollection(context: PersistenceManager.persistentContainer.viewContext)
//
// userCollection1.title = "Collection 1"
// userCollection2.title = "Collection 2"
// userCollection3.title = "Collection 3"
//
// let testPostData = [
// Post(
// title: "My First Post",
// body: "Look at me, creating a first post! That's cool.",
// createdDate: Date(timeIntervalSince1970: 1595429452),
// status: .published,
// collection: userCollection1
// ),
// Post(
// title: "Post 2: The Quickening",
// body: "See, here's the rule about Highlander jokes: _there can be only one_.",
// createdDate: Date(timeIntervalSince1970: 1595514125),
// status: .edited,
// collection: userCollection1
// ),
// Post(
// title: "The Post Revolutions",
// body: "I can never keep the Matrix movie order straight. Why not just call them part 2 and part 3?",
// createdDate: Date(timeIntervalSince1970: 1595600006)
// ),
// Post(
// title: "Episode IV: A New Post",
// body: "How many movies does this person watch? How many movie-title jokes will they make?",
// createdDate: Date(timeIntervalSince1970: 1596219877),
// status: .published,
// collection: userCollection2
// ),
// Post(
// title: "Fast (Post) Five",
// body: "Look, it was either a Fast and the Furious reference, or a Resident Evil reference."
// ),
// Post(
// title: "Post: The Final Chapter",
// body: "And there you have it, a Resident Evil movie reference.",
// createdDate: Date(timeIntervalSince1970: 1596043684),
// status: .edited,
// collection: userCollection3
// )
// ]
//
// let model = WriteFreelyModel()
// for post in testPostData {
// model.store.add(post)
// }
// return Group {
// PostListView(selectedCollection: userCollection1)
// .environmentObject(model)
// }
// }
//}
diff --git a/WFAPost+CoreDataProperties.swift b/WFAPost+CoreDataProperties.swift
index d7ef424..48d4d2b 100644
--- a/WFAPost+CoreDataProperties.swift
+++ b/WFAPost+CoreDataProperties.swift
@@ -1,35 +1,35 @@
//
// 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<WFAPost> {
return NSFetchRequest<WFAPost>(entityName: "WFAPost")
}
@NSManaged public var appearance: String?
- @NSManaged public var body: 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 title: String
@NSManaged public var updatedDate: Date?
@NSManaged public var hasNewerRemoteCopy: Bool
}
extension WFAPost: Identifiable {
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Jan 31, 3:32 PM (17 h, 16 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3145869
Attached To
rWFSUI WriteFreely SwiftUI
Event Timeline
Log In to Comment