Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F10384354
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
41 KB
Subscribers
None
View Options
diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift
index 525f4ee..b53da10 100644
--- a/Shared/Models/WriteFreelyModel.swift
+++ b/Shared/Models/WriteFreelyModel.swift
@@ -1,549 +1,551 @@
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 isProcessingRequest: Bool = false
@Published var hasNetworkConnection: Bool = true
@Published var selectedPost: WFAPost?
+ @Published var selectedCollection: WFACollection?
+ @Published var showAllPosts: Bool = true
@Published var isPresentingDeleteAlert: Bool = false
@Published var isPresentingLoginErrorAlert: Bool = false
@Published var isPresentingNetworkErrorAlert: Bool = false
@Published var postToDelete: WFAPost?
#if os(iOS)
@Published var isPresentingSettingsView: Bool = false
#endif
var loginErrorMessage: String?
// swiftlint:disable line_length
let helpURL = URL(string: "https://discuss.write.as/c/help/5")!
let howToURL = URL(string: "https://discuss.write.as/t/using-the-writefreely-ios-app/1946")!
let licensesURL = URL(string: "https://github.com/writeas/writefreely-swiftui-multiplatform/tree/main/Shared/Resources/Licenses")!
// swiftlint:enable line_length
private var client: WFClient?
private let defaults = UserDefaults.standard
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")
init() {
DispatchQueue.main.async {
self.preferences.appearance = self.defaults.integer(forKey: self.preferences.colorSchemeIntegerKey)
self.preferences.font = self.defaults.integer(forKey: self.preferences.defaultFontIntegerKey)
self.account.restoreState()
if self.account.isLoggedIn {
guard let serverURL = URL(string: self.account.server) else {
print("Server URL not found")
return
}
guard let token = self.fetchTokenFromKeychain(
username: self.account.username,
server: self.account.server
) else {
print("Could not fetch token from Keychain")
return
}
self.account.login(WFUser(token: token, username: self.account.username))
self.client = WFClient(for: serverURL)
self.client?.user = self.account.user
self.fetchUserCollections()
self.fetchUserPosts()
}
}
monitor.pathUpdateHandler = { path in
DispatchQueue.main.async {
self.hasNetworkConnection = path.status == .satisfied
}
}
monitor.start(queue: queue)
}
}
// MARK: - WriteFreelyModel API
extension WriteFreelyModel {
func login(to server: URL, as username: String, password: String) {
if !hasNetworkConnection {
isPresentingNetworkErrorAlert = true
return
}
let secureProtocolPrefix = "https://"
let insecureProtocolPrefix = "http://"
var serverString = server.absoluteString
// If there's neither an http or https prefix, prepend "https://" to the server string.
if !(serverString.hasPrefix(secureProtocolPrefix) || serverString.hasPrefix(insecureProtocolPrefix)) {
serverString = secureProtocolPrefix + serverString
}
// If the server string is prefixed with http, upgrade to https before attempting to login.
if serverString.hasPrefix(insecureProtocolPrefix) {
serverString = serverString.replacingOccurrences(of: insecureProtocolPrefix, with: secureProtocolPrefix)
}
isLoggingIn = true
var serverURL = URL(string: serverString)!
if !serverURL.path.isEmpty {
serverURL.deleteLastPathComponent()
}
account.server = serverURL.absoluteString
client = WFClient(for: serverURL)
client?.login(username: username, password: password, completion: loginHandler)
}
func logout() {
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
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() {
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
guard let loggedInClient = client else { return }
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
loggedInClient.getUserCollections(completion: fetchUserCollectionsHandler)
}
func fetchUserPosts() {
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
guard let loggedInClient = client else { return }
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
loggedInClient.getPosts(completion: fetchUserPostsHandler)
}
func publish(post: WFAPost) {
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
guard let loggedInClient = client else { return }
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
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) {
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
guard let loggedInClient = client else { return }
guard let postId = post.postId else { return }
// We're starting the network request.
DispatchQueue.main.async {
self.selectedPost = post
self.isProcessingRequest = true
}
loggedInClient.getPost(byId: postId, completion: updateFromServerHandler)
}
func move(post: WFAPost, from oldCollection: WFACollection?, to newCollection: WFACollection?) {
if !hasNetworkConnection {
DispatchQueue.main.async { self.isPresentingNetworkErrorAlert = true }
return
}
guard let loggedInClient = client,
let postId = post.postId else { return }
// We're starting the network request.
DispatchQueue.main.async {
self.isProcessingRequest = true
}
selectedPost = post
post.collectionAlias = newCollection?.alias
loggedInClient.movePost(postId: postId, to: newCollection?.alias, completion: movePostHandler)
}
}
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.loginErrorMessage = AccountError.usernameNotFound.localizedDescription
self.isPresentingLoginErrorAlert = true
}
} catch WFError.unauthorized {
DispatchQueue.main.async {
self.loginErrorMessage = AccountError.invalidPassword.localizedDescription
self.isPresentingLoginErrorAlert = true
}
} catch {
if (error as NSError).domain == NSURLErrorDomain,
(error as NSError).code == -1003 {
DispatchQueue.main.async {
self.loginErrorMessage = AccountError.serverNotFound.localizedDescription
self.isPresentingLoginErrorAlert = true
}
} else {
DispatchQueue.main.async {
self.loginErrorMessage = error.localizedDescription
self.isPresentingLoginErrorAlert = true
}
}
}
}
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()
LocalStorageManager().purgeUserCollections()
self.posts.purgePublishedPosts()
}
} 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.purgePublishedPosts()
}
} 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>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
do {
let fetchedCollections = try result.get()
for fetchedCollection in fetchedCollections {
DispatchQueue.main.async {
let localCollection = WFACollection(context: LocalStorageManager.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 WFError.unauthorized {
DispatchQueue.main.async {
self.loginErrorMessage = "Something went wrong, please try logging in again."
self.isPresentingLoginErrorAlert = true
}
self.logout()
} catch {
print(error)
}
}
func fetchUserPostsHandler(result: Result<[WFPost], Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
let request = WFAPost.createFetchRequest()
do {
let locallyCachedPosts = try LocalStorageManager.persistentContainer.viewContext.fetch(request)
do {
var postsToDelete = locallyCachedPosts.filter { $0.status != PostStatus.local.rawValue }
let fetchedPosts = try result.get()
for fetchedPost in fetchedPosts {
if let managedPost = locallyCachedPosts.first(where: { $0.postId == fetchedPost.postId }) {
DispatchQueue.main.async {
managedPost.wasDeletedFromServer = false
if let fetchedPostUpdatedDate = fetchedPost.updatedDate,
let localPostUpdatedDate = managedPost.updatedDate {
managedPost.hasNewerRemoteCopy = fetchedPostUpdatedDate > localPostUpdatedDate
} else { print("Error: could not determine which copy of post is newer") }
postsToDelete.removeAll(where: { $0.postId == fetchedPost.postId })
}
} else {
DispatchQueue.main.async {
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
}
}
}
DispatchQueue.main.async {
for post in postsToDelete { post.wasDeletedFromServer = true }
LocalStorageManager().saveContext()
}
} catch {
print(error)
}
} catch WFError.unauthorized {
DispatchQueue.main.async {
self.loginErrorMessage = "Something went wrong, please try logging in again."
self.isPresentingLoginErrorAlert = true
}
self.logout()
} catch {
print("Error: Failed to fetch cached posts")
}
}
func publishHandler(result: Result<WFPost, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
// ⚠️ NOTE:
// The API does not return a collection alias, so we take care not to overwrite the
// cached post's collection alias with the 'nil' value from the fetched post.
// See: https://github.com/writeas/writefreely-swift/issues/20
do {
let fetchedPost = try result.get()
let request = WFAPost.createFetchRequest()
let matchBodyPredicate = NSPredicate(format: "body == %@", fetchedPost.body)
if let fetchedPostTitle = fetchedPost.title {
let matchTitlePredicate = NSPredicate(format: "title == %@", fetchedPostTitle)
request.predicate = NSCompoundPredicate(
andPredicateWithSubpredicates: [
matchTitlePredicate,
matchBodyPredicate
]
)
} else {
request.predicate = matchBodyPredicate
}
do {
let cachedPostsResults = try LocalStorageManager.persistentContainer.viewContext.fetch(request)
guard let cachedPost = cachedPostsResults.first 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
DispatchQueue.main.async {
LocalStorageManager().saveContext()
}
} catch {
print("Error: Failed to fetch cached posts")
}
} catch {
print(error)
}
}
func updateFromServerHandler(result: Result<WFPost, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
// ⚠️ NOTE:
// The API does not return a collection alias, so we take care not to overwrite the
// cached post's collection alias with the 'nil' value from the fetched post.
// See: https://github.com/writeas/writefreely-swift/issues/20
do {
let fetchedPost = try result.get()
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)
}
}
func movePostHandler(result: Result<Bool, Error>) {
// We're done with the network request.
DispatchQueue.main.async {
self.isProcessingRequest = false
}
do {
let succeeded = try result.get()
if succeeded {
if let post = selectedPost {
updateFromServer(post: post)
} else {
return
}
}
} catch {
DispatchQueue.main.async {
LocalStorageManager.persistentContainer.viewContext.rollback()
}
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 ce8f253..42b036f 100644
--- a/Shared/Navigation/ContentView.swift
+++ b/Shared/Navigation/ContentView.swift
@@ -1,116 +1,98 @@
import SwiftUI
struct ContentView: View {
@EnvironmentObject var model: WriteFreelyModel
var body: some View {
NavigationView {
#if os(macOS)
SidebarView()
.toolbar {
Button(
action: {
NSApp.keyWindow?.contentViewController?.tryToPerform(
#selector(NSSplitViewController.toggleSidebar(_:)), with: nil
)
},
label: { Image(systemName: "sidebar.left") }
)
Spacer()
Button(action: {
withAnimation {
self.model.selectedPost = nil
}
let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext)
managedPost.createdDate = Date()
managedPost.title = ""
managedPost.body = ""
managedPost.status = PostStatus.local.rawValue
managedPost.collectionAlias = nil
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
}
withAnimation {
DispatchQueue.main.async {
self.model.selectedPost = managedPost
}
}
}, label: { Image(systemName: "square.and.pencil") })
}
#else
SidebarView()
#endif
#if os(macOS)
- PostListView(selectedCollection: nil, showAllPosts: model.account.isLoggedIn)
- .toolbar {
- ToolbarItemGroup(placement: .primaryAction) {
- if let selectedPost = model.selectedPost {
- ActivePostToolbarView(activePost: selectedPost)
- .alert(isPresented: $model.isPresentingNetworkErrorAlert, content: {
- Alert(
- title: Text("Connection Error"),
- message: Text("""
- There is no internet connection at the moment. Please reconnect or try again later.
- """),
- dismissButton: .default(Text("OK"), action: {
- model.isPresentingNetworkErrorAlert = false
- })
- )
- })
- }
- }
- }
+ PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
#else
PostListView(selectedCollection: nil, showAllPosts: model.account.isLoggedIn)
#endif
Text("Select a post, or create a new local draft.")
.foregroundColor(.secondary)
}
.environmentObject(model)
#if os(iOS)
EmptyView()
.sheet(
isPresented: $model.isPresentingSettingsView,
onDismiss: { model.isPresentingSettingsView = false },
content: {
SettingsView()
.environmentObject(model)
}
)
.alert(isPresented: $model.isPresentingNetworkErrorAlert, content: {
Alert(
title: Text("Connection Error"),
message: Text("""
There is no internet connection at the moment. Please reconnect or try again later.
"""),
dismissButton: .default(Text("OK"), action: {
model.isPresentingNetworkErrorAlert = false
})
)
})
#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/PostList/PostListFilteredView.swift b/Shared/PostList/PostListFilteredView.swift
index cc65779..d528176 100644
--- a/Shared/PostList/PostListFilteredView.swift
+++ b/Shared/PostList/PostListFilteredView.swift
@@ -1,139 +1,145 @@
import SwiftUI
struct PostListFilteredView: View {
@EnvironmentObject var model: WriteFreelyModel
@Binding var postCount: Int
@FetchRequest(entity: WFACollection.entity(), sortDescriptors: []) var collections: FetchedResults<WFACollection>
var fetchRequest: FetchRequest<WFAPost>
- var showAllPosts: Bool
- init(filter: String?, showAllPosts: Bool, postCount: Binding<Int>) {
+ var showAllPosts: Bool {
+ didSet {
+ model.showAllPosts = showAllPosts
+ }
+ }
+
+ var selectedCollection: WFACollection? {
+ didSet {
+ model.selectedCollection = selectedCollection
+ }
+ }
+
+ init(collection: WFACollection?, showAllPosts: Bool, postCount: Binding<Int>) {
self.showAllPosts = showAllPosts
+ self.selectedCollection = collection
if showAllPosts {
fetchRequest = FetchRequest<WFAPost>(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)]
)
} else {
- if let filter = filter {
+ if let collectionAlias = collection?.alias {
fetchRequest = FetchRequest<WFAPost>(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)],
- predicate: NSPredicate(format: "collectionAlias == %@", filter)
+ predicate: NSPredicate(format: "collectionAlias == %@", collectionAlias)
)
} else {
fetchRequest = FetchRequest<WFAPost>(
entity: WFAPost.entity(),
sortDescriptors: [NSSortDescriptor(key: "createdDate", ascending: false)],
predicate: NSPredicate(format: "collectionAlias == nil")
)
}
}
_postCount = postCount
}
var body: some View {
#if os(iOS)
List {
ForEach(fetchRequest.wrappedValue, id: \.self) { post in
NavigationLink(
destination: PostEditorView(post: post),
tag: post,
selection: $model.selectedPost
) {
if showAllPosts {
if let collection = collections.filter { $0.alias == post.collectionAlias }.first {
PostCellView(post: post, collectionName: collection.title)
} else {
let collectionName = model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
PostCellView(post: post, collectionName: collectionName)
}
} else {
PostCellView(post: post)
}
}
.deleteDisabled(post.status != PostStatus.local.rawValue)
}
.onDelete(perform: { indexSet in
for index in indexSet {
let post = fetchRequest.wrappedValue[index]
delete(post)
}
})
}
.onAppear(perform: {
self.postCount = fetchRequest.wrappedValue.count
})
.onChange(of: fetchRequest.wrappedValue.count, perform: { value in
self.postCount = value
})
#else
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)
}
})
}
.alert(isPresented: $model.isPresentingDeleteAlert) {
Alert(
title: Text("Delete Post?"),
message: Text("This action cannot be undone."),
primaryButton: .cancel() {
model.postToDelete = nil
},
secondaryButton: .destructive(Text("Delete"), action: {
if let postToDelete = model.postToDelete {
model.selectedPost = nil
DispatchQueue.main.async {
model.editor.clearLastDraft()
model.posts.remove(postToDelete)
}
model.postToDelete = nil
}
})
)
}
- .onAppear(perform: {
- self.postCount = fetchRequest.wrappedValue.count
- })
- .onChange(of: fetchRequest.wrappedValue.count, perform: { value in
- self.postCount = value
- })
.onDeleteCommand(perform: {
guard let selectedPost = model.selectedPost else { return }
if selectedPost.status == PostStatus.local.rawValue {
model.postToDelete = selectedPost
model.isPresentingDeleteAlert = true
}
})
#endif
}
func delete(_ post: WFAPost) {
DispatchQueue.main.async {
if post == model.selectedPost {
model.selectedPost = nil
model.editor.clearLastDraft()
}
model.posts.remove(post)
}
}
}
struct PostListFilteredView_Previews: PreviewProvider {
static var previews: some View {
- return PostListFilteredView(filter: nil, showAllPosts: false, postCount: .constant(999))
+ return PostListFilteredView(collection: nil, showAllPosts: false, postCount: .constant(999))
}
}
diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift
index ffc545b..acd1fbe 100644
--- a/Shared/PostList/PostListView.swift
+++ b/Shared/PostList/PostListView.swift
@@ -1,102 +1,128 @@
import SwiftUI
import Combine
struct PostListView: View {
@EnvironmentObject var model: WriteFreelyModel
@Environment(\.managedObjectContext) var managedObjectContext
@State var selectedCollection: WFACollection?
@State var showAllPosts: Bool = false
@State private var postCount: Int = 0
var body: some View {
#if os(iOS)
GeometryReader { geometry in
- PostListFilteredView(filter: selectedCollection?.alias, showAllPosts: showAllPosts, postCount: $postCount)
+ PostListFilteredView(collection: selectedCollection, showAllPosts: showAllPosts, postCount: $postCount)
.navigationTitle(
showAllPosts ? "All Posts" : selectedCollection?.title ?? (
model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
)
)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
let managedPost = WFAPost(context: self.managedObjectContext)
managedPost.createdDate = Date()
managedPost.title = ""
managedPost.body = ""
managedPost.status = PostStatus.local.rawValue
managedPost.collectionAlias = nil
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
}
withAnimation {
self.selectedCollection = nil
self.showAllPosts = false
self.model.selectedPost = managedPost
}
}, label: {
Image(systemName: "square.and.pencil")
})
}
ToolbarItem(placement: .bottomBar) {
HStack {
Button(action: {
model.isPresentingSettingsView = true
}, label: {
Image(systemName: "gear")
})
Spacer()
Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts")
.foregroundColor(.secondary)
Spacer()
if model.isProcessingRequest {
ProgressView()
} else {
Button(action: {
DispatchQueue.main.async {
model.fetchUserCollections()
model.fetchUserPosts()
}
}, label: {
Image(systemName: "arrow.clockwise")
})
.disabled(!model.account.isLoggedIn)
}
}
.padding()
.frame(width: geometry.size.width)
}
}
}
#else //if os(macOS)
- PostListFilteredView(filter: selectedCollection?.alias, showAllPosts: showAllPosts, postCount: $postCount)
- .navigationTitle(
- showAllPosts ? "All Posts" : selectedCollection?.title ?? (
- model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
- )
+ PostListFilteredView(
+ collection: selectedCollection,
+ showAllPosts: showAllPosts,
+ postCount: $postCount
+ )
+ .toolbar {
+ ToolbarItemGroup(placement: .primaryAction) {
+ if let selectedPost = model.selectedPost {
+ ActivePostToolbarView(activePost: selectedPost)
+ .alert(isPresented: $model.isPresentingNetworkErrorAlert, content: {
+ Alert(
+ title: Text("Connection Error"),
+ message: Text("""
+ There is no internet connection at the moment. Please reconnect or try again later.
+ """),
+ dismissButton: .default(Text("OK"), action: {
+ model.isPresentingNetworkErrorAlert = false
+ })
+ )
+ })
+ }
+ }
+ }
+ .onDisappear {
+ DispatchQueue.main.async {
+ model.selectedPost = nil
+ }
+ }
+ .navigationTitle(
+ showAllPosts ? "All Posts" : selectedCollection?.title ?? (
+ model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
)
- .navigationSubtitle(postCount == 1 ? "\(postCount) post" : "\(postCount) posts")
+ )
#endif
}
}
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/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist
index 6cd8075..2723ebe 100644
--- a/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/WriteFreely-MultiPlatform.xcodeproj/xcuserdata/angelo.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -1,19 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>WriteFreely-MultiPlatform (iOS).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
- <integer>1</integer>
+ <integer>0</integer>
</dict>
<key>WriteFreely-MultiPlatform (macOS).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
- <integer>0</integer>
+ <integer>1</integer>
</dict>
</dict>
</dict>
</plist>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 2:05 PM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3104355
Attached To
rWFSUI WriteFreely SwiftUI
Event Timeline
Log In to Comment