Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F12658823
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
47 KB
Subscribers
None
View Options
diff --git a/Shared/Account/AccountLoginView.swift b/Shared/Account/AccountLoginView.swift
index de0390b..b77d915 100644
--- a/Shared/Account/AccountLoginView.swift
+++ b/Shared/Account/AccountLoginView.swift
@@ -1,109 +1,111 @@
import SwiftUI
struct AccountLoginView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@State private var alertMessage: String = ""
@State private var username: String = ""
@State private var password: String = ""
@State private var server: String = ""
var body: some View {
VStack {
Text("Log in to publish and share your posts.")
.font(.caption)
.foregroundColor(.secondary)
HStack {
Image(systemName: "person.circle")
.foregroundColor(.gray)
#if os(iOS)
TextField("Username", text: $username)
.autocapitalization(.none)
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
#else
TextField("Username", text: $username)
#endif
}
HStack {
Image(systemName: "lock.circle")
.foregroundColor(.gray)
#if os(iOS)
SecureField("Password", text: $password)
.autocapitalization(.none)
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
#else
SecureField("Password", text: $password)
#endif
}
HStack {
Image(systemName: "link.circle")
.foregroundColor(.gray)
#if os(iOS)
TextField("Server URL", text: $server)
.keyboardType(.URL)
.autocapitalization(.none)
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
#else
TextField("Server URL", text: $server)
#endif
}
Spacer()
if model.isLoggingIn {
ProgressView("Logging in...")
.padding()
} else {
Button(action: {
#if os(iOS)
hideKeyboard()
#endif
// If the server string is not prefixed with a scheme, prepend "https://" to it.
if !(server.hasPrefix("https://") || server.hasPrefix("http://")) {
server = "https://\(server)"
}
// We only need the protocol and host from the URL, so drop anything else.
let url = URLComponents(string: server)
if let validURL = url {
let scheme = validURL.scheme
let host = validURL.host
var hostURL = URLComponents()
hostURL.scheme = scheme
hostURL.host = host
server = hostURL.string ?? server
model.login(
to: URL(string: server)!,
as: username, password: password
)
} else {
self.errorHandling.handle(error: AccountError.invalidServerURL)
}
}, label: {
Text("Log In")
})
.disabled(
model.account.isLoggedIn || (username.isEmpty || password.isEmpty || server.isEmpty)
)
.padding()
}
}
.onChange(of: model.hasError) { value in
if value {
- if let error = model.currentError {
- self.errorHandling.handle(error: error)
- } else {
- self.errorHandling.handle(error: AppError.genericError())
+ if model.hasNetworkConnection {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
}
model.hasError = false
}
}
}
}
struct AccountLoginView_Previews: PreviewProvider {
static var previews: some View {
AccountLoginView()
.environmentObject(WriteFreelyModel())
}
}
diff --git a/Shared/Account/AccountView.swift b/Shared/Account/AccountView.swift
index 0b54f0e..e373aed 100644
--- a/Shared/Account/AccountView.swift
+++ b/Shared/Account/AccountView.swift
@@ -1,40 +1,42 @@
import SwiftUI
struct AccountView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
var body: some View {
if model.account.isLoggedIn {
HStack {
Spacer()
AccountLogoutView()
.withErrorHandling()
Spacer()
}
.padding()
} else {
AccountLoginView()
.withErrorHandling()
.padding(.top)
}
EmptyView()
.onChange(of: model.hasError) { value in
if value {
- if let error = model.currentError {
- self.errorHandling.handle(error: error)
- } else {
- self.errorHandling.handle(error: AppError.genericError())
+ if model.hasNetworkConnection {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
}
model.hasError = false
}
}
}
}
struct AccountLogin_Previews: PreviewProvider {
static var previews: some View {
AccountView()
.environmentObject(WriteFreelyModel())
}
}
diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift
index 3a088d3..f48e260 100644
--- a/Shared/Navigation/ContentView.swift
+++ b/Shared/Navigation/ContentView.swift
@@ -1,94 +1,96 @@
import SwiftUI
struct ContentView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
var body: some View {
NavigationView {
#if os(macOS)
CollectionListView()
.withErrorHandling()
.toolbar {
Button(
action: {
NSApp.keyWindow?.contentViewController?.tryToPerform(
#selector(NSSplitViewController.toggleSidebar(_:)), with: nil
)
},
label: { Image(systemName: "sidebar.left") }
)
.help("Toggle the sidebar's visibility.")
Spacer()
Button(action: {
withAnimation {
// Un-set the currently selected post
self.model.selectedPost = nil
}
// Create the new-post managed object
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
DispatchQueue.main.async {
// Load the new post in the editor
self.model.selectedPost = managedPost
}
}
}, label: { Image(systemName: "square.and.pencil") })
.help("Create a new local draft.")
}
.frame(width: 200)
#else
CollectionListView()
.withErrorHandling()
#endif
#if os(macOS)
ZStack {
PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
.withErrorHandling()
.frame(width: 300)
if model.isProcessingRequest {
ZStack {
Color(NSColor.controlBackgroundColor).opacity(0.75)
ProgressView()
}
}
}
#else
PostListView(selectedCollection: model.selectedCollection, showAllPosts: model.showAllPosts)
.withErrorHandling()
#endif
#if os(macOS)
Text("Select a post, or create a new local draft.")
.foregroundColor(.secondary)
.frame(width: 500, height: 500)
#else
Text("Select a post, or create a new local draft.")
.foregroundColor(.secondary)
#endif
}
.environmentObject(model)
.onChange(of: model.hasError) { value in
if value {
- if let error = model.currentError {
- self.errorHandling.handle(error: error)
- } else {
- self.errorHandling.handle(error: AppError.genericError())
+ if model.hasNetworkConnection {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
}
model.hasError = false
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
return ContentView()
.environment(\.managedObjectContext, context)
.environmentObject(model)
}
}
diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift
index 2c3dab7..fc41637 100644
--- a/Shared/PostCollection/CollectionListView.swift
+++ b/Shared/PostCollection/CollectionListView.swift
@@ -1,69 +1,71 @@
import SwiftUI
struct CollectionListView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@FetchRequest(sortDescriptors: []) var collections: FetchedResults<WFACollection>
@State var selectedCollection: WFACollection?
var body: some View {
List(selection: $selectedCollection) {
if model.account.isLoggedIn {
NavigationLink("All Posts", destination: PostListView(selectedCollection: nil, showAllPosts: true))
NavigationLink(
model.account.server == "https://write.as" ? "Anonymous" : "Drafts",
destination: PostListView(selectedCollection: nil, showAllPosts: false)
)
Section(header: Text("Your Blogs")) {
ForEach(collections, id: \.self) { collection in
NavigationLink(destination: PostListView(selectedCollection: collection, showAllPosts: false),
tag: collection,
selection: $selectedCollection,
label: { Text("\(collection.title)") })
}
}
} else {
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) {
Text("Drafts")
}
}
}
.navigationTitle(
model.account.isLoggedIn ? "\(URL(string: model.account.server)?.host ?? "WriteFreely")" : "WriteFreely"
)
.listStyle(SidebarListStyle())
.onChange(of: model.selectedCollection) { collection in
model.selectedPost = nil
if collection != model.editor.fetchSelectedCollectionFromAppStorage() {
self.model.editor.selectedCollectionURL = collection?.objectID.uriRepresentation()
}
}
.onChange(of: model.showAllPosts) { value in
model.selectedPost = nil
if value != model.editor.showAllPostsFlag {
self.model.editor.showAllPostsFlag = model.showAllPosts
}
}
.onChange(of: model.hasError) { value in
if value {
- if let error = model.currentError {
- self.errorHandling.handle(error: error)
- } else {
- self.errorHandling.handle(error: AppError.genericError())
+ if model.hasNetworkConnection {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
}
model.hasError = false
}
}
}
}
struct CollectionListView_LoggedOutPreviews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
return CollectionListView()
.environment(\.managedObjectContext, context)
.environmentObject(model)
}
}
diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift
index 89fe9fa..0146f39 100644
--- a/Shared/PostList/PostListView.swift
+++ b/Shared/PostList/PostListView.swift
@@ -1,197 +1,201 @@
import SwiftUI
import Combine
struct PostListView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@Environment(\.managedObjectContext) var managedObjectContext
@State private var postCount: Int = 0
@State private var filteredListViewId: Int = 0
var selectedCollection: WFACollection?
var showAllPosts: Bool
#if os(iOS)
private var frameHeight: CGFloat {
var height: CGFloat = 50
let bottom = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0
height += bottom
return height
}
#endif
var body: some View {
#if os(iOS)
ZStack(alignment: .bottom) {
PostListFilteredView(
collection: selectedCollection,
showAllPosts: showAllPosts,
postCount: $postCount
)
.id(self.filteredListViewId)
.navigationTitle(
showAllPosts ? "All Posts" : selectedCollection?.title ?? (
model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
)
)
.toolbar {
ToolbarItem(placement: .primaryAction) {
ZStack {
// We have to add a Spacer as a sibling view to the Button in some kind of Stack so that any
// a11y modifiers are applied as expected: bug report filed as FB8956392.
if #unavailable(iOS 16) {
Spacer()
}
Button(action: {
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
self.model.showAllPosts = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.model.selectedPost = managedPost
}
}
}, label: {
ZStack {
Image("does.not.exist")
.accessibilityHidden(true)
Image(systemName: "square.and.pencil")
.accessibilityHidden(true)
.imageScale(.large) // These modifiers compensate for the resizing
.padding(.vertical, 12) // done to the Image (and the button tap target)
.padding(.leading, 12) // by the SwiftUI layout system from adding a
.padding(.trailing, 8) // Spacer in this ZStack (FB8956392).
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
})
.accessibilityLabel(Text("Compose"))
.accessibilityHint(Text("Compose a new local draft"))
}
}
}
VStack {
HStack(spacing: 0) {
Button(action: {
model.isPresentingSettingsView = true
}, label: {
Image(systemName: "gear")
.padding(.vertical, 4)
.padding(.horizontal, 8)
})
.accessibilityLabel(Text("Settings"))
.accessibilityHint(Text("Open the Settings sheet"))
.sheet(
isPresented: $model.isPresentingSettingsView,
onDismiss: { model.isPresentingSettingsView = false },
content: {
SettingsView()
.environmentObject(model)
}
)
Spacer()
Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts")
.foregroundColor(.secondary)
Spacer()
if model.isProcessingRequest {
ProgressView()
.padding(.vertical, 4)
.padding(.horizontal, 8)
} else {
if model.hasNetworkConnection {
Button(action: {
DispatchQueue.main.async {
model.fetchUserCollections()
model.fetchUserPosts()
}
}, label: {
Image(systemName: "arrow.clockwise")
.padding(.vertical, 4)
.padding(.horizontal, 8)
})
.accessibilityLabel(Text("Refresh Posts"))
.accessibilityHint(Text("Fetch changes from the server"))
.disabled(!model.account.isLoggedIn)
} else {
Image(systemName: "wifi.exclamationmark")
.padding(.vertical, 4)
.padding(.horizontal, 8)
.foregroundColor(.secondary)
}
}
}
.padding(.top, 8)
.padding(.horizontal, 8)
Spacer()
}
.frame(height: frameHeight)
.background(Color(UIColor.systemGray5))
.overlay(Divider(), alignment: .top)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
// We use this to invalidate and refresh the view, so that new posts created outside of the app (e.g.,
// in the action extension) show up.
withAnimation {
self.filteredListViewId += 1
}
}
}
.ignoresSafeArea(.all, edges: .bottom)
.onAppear {
model.selectedCollection = selectedCollection
model.showAllPosts = showAllPosts
}
.onChange(of: model.hasError) { value in
if value {
- if let error = model.currentError {
- self.errorHandling.handle(error: error)
- } else {
- self.errorHandling.handle(error: AppError.genericError())
+ if model.hasNetworkConnection {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
}
model.hasError = false
}
}
#else
PostListFilteredView(
collection: selectedCollection,
showAllPosts: showAllPosts,
postCount: $postCount
)
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
if model.selectedPost != nil {
ActivePostToolbarView(activePost: model.selectedPost!)
}
}
}
.navigationTitle(
showAllPosts ? "All Posts" : selectedCollection?.title ?? (
model.account.server == "https://write.as" ? "Anonymous" : "Drafts"
)
)
.onAppear {
model.selectedCollection = selectedCollection
model.showAllPosts = showAllPosts
}
.onChange(of: model.hasError) { value in
if value {
- if let error = model.currentError {
- self.errorHandling.handle(error: error)
- } else {
- self.errorHandling.handle(error: AppError.genericError())
+ if model.hasNetworkConnection {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
}
model.hasError = false
}
}
#endif
}
}
struct PostListView_Previews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
return PostListView(showAllPosts: true)
.environment(\.managedObjectContext, context)
.environmentObject(model)
}
}
diff --git a/iOS/PostEditor/PostEditorView.swift b/iOS/PostEditor/PostEditorView.swift
index 070b421..97b0927 100644
--- a/iOS/PostEditor/PostEditorView.swift
+++ b/iOS/PostEditor/PostEditorView.swift
@@ -1,286 +1,288 @@
import SwiftUI
struct PostEditorView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.managedObjectContext) var moc
@Environment(\.presentationMode) var presentationMode
@ObservedObject var post: WFAPost
@State private var updatingTitleFromServer: Bool = false
@State private var updatingBodyFromServer: Bool = false
@State private var selectedCollection: WFACollection?
@FetchRequest(
entity: WFACollection.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)]
) var collections: FetchedResults<WFACollection>
var body: some View {
VStack {
if post.hasNewerRemoteCopy {
RemoteChangePromptView(
remoteChangeType: .remoteCopyUpdated,
buttonHandler: { model.updateFromServer(post: post) }
)
} else if post.wasDeletedFromServer {
RemoteChangePromptView(
remoteChangeType: .remoteCopyDeleted,
buttonHandler: {
self.presentationMode.wrappedValue.dismiss()
DispatchQueue.main.async {
model.posts.remove(post)
}
}
)
}
PostTextEditingView(
post: _post,
updatingTitleFromServer: $updatingTitleFromServer,
updatingBodyFromServer: $updatingBodyFromServer
)
.withErrorHandling()
}
.navigationBarTitleDisplayMode(.inline)
.padding()
.toolbar {
ToolbarItem(placement: .principal) {
PostEditorStatusToolbarView(post: post)
}
ToolbarItem(placement: .primaryAction) {
if !model.hasNetworkConnection {
Image(systemName: "wifi.exclamationmark")
.foregroundColor(.secondary)
} else if model.isProcessingRequest {
ProgressView()
} else {
Menu(content: {
if post.status == PostStatus.local.rawValue {
Menu(content: {
Label("Publish to…", systemImage: "paperplane")
Button(action: {
if model.account.isLoggedIn {
post.collectionAlias = nil
publishPost()
} else {
self.model.isPresentingSettingsView = true
}
}, label: {
Text(" \(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")")
})
ForEach(collections) { collection in
Button(action: {
if model.account.isLoggedIn {
post.collectionAlias = collection.alias
publishPost()
} else {
self.model.isPresentingSettingsView = true
}
}, label: {
Text(" \(collection.title)")
})
}
}, label: {
Label("Publish…", systemImage: "paperplane")
})
.accessibilityHint(Text("Choose the blog you want to publish this post to"))
.disabled(post.body.count == 0)
} else if post.status == PostStatus.edited.rawValue {
Button(action: {
if model.account.isLoggedIn {
publishPost()
} else {
self.model.isPresentingSettingsView = true
}
}, label: {
Label("Publish", systemImage: "paperplane")
})
.disabled(post.status == PostStatus.published.rawValue || post.body.count == 0)
Button(action: {
model.updateFromServer(post: post)
}, label: {
Label("Revert", systemImage: "clock.arrow.circlepath")
})
.accessibilityHint(Text("Replace the edited post with the published version from the server"))
.disabled(post.status != PostStatus.edited.rawValue)
}
Button(action: {
sharePost()
}, label: {
Label("Share", systemImage: "square.and.arrow.up")
})
.accessibilityHint(Text("Open the system share sheet to share a link to this post"))
.disabled(post.postId == nil)
if model.account.isLoggedIn && post.status != PostStatus.local.rawValue {
Section(header: Text("Move To Collection")) {
Label("Move to:", systemImage: "arrowshape.zigzag.right")
Picker(selection: $selectedCollection, label: Text("Move to…")) {
Text(
" \(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")"
).tag(nil as WFACollection?)
ForEach(collections) { collection in
Text(" \(collection.title)").tag(collection as WFACollection?)
}
}
}
}
}, label: {
ZStack {
Image("does.not.exist")
.accessibilityHidden(true)
Image(systemName: "ellipsis.circle")
.imageScale(.large)
.accessibilityHidden(true)
}
})
.accessibilityLabel(Text("Menu"))
.accessibilityHint(Text("Opens a context menu to publish, share, or move the post"))
.onTapGesture {
hideKeyboard()
}
.disabled(post.body.count == 0)
}
}
}
.onChange(of: post.hasNewerRemoteCopy, perform: { _ in
if !post.hasNewerRemoteCopy {
updatingTitleFromServer = true
updatingBodyFromServer = true
}
})
.onChange(of: selectedCollection, perform: { [selectedCollection] newCollection in
if post.collectionAlias == newCollection?.alias {
return
} else {
post.collectionAlias = newCollection?.alias
model.move(post: post, from: selectedCollection, to: newCollection)
}
})
.onChange(of: post.status, perform: { value in
if value != PostStatus.published.rawValue {
self.model.editor.saveLastDraft(post)
} else {
self.model.editor.clearLastDraft()
}
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
})
.onAppear(perform: {
self.selectedCollection = collections.first { $0.alias == post.collectionAlias }
model.editor.setInitialValues(for: post)
if post.status != PostStatus.published.rawValue {
DispatchQueue.main.async {
self.model.editor.saveLastDraft(post)
}
} else {
self.model.editor.clearLastDraft()
}
})
.onChange(of: model.hasError) { value in
if value {
- if let error = model.currentError {
- self.errorHandling.handle(error: error)
- } else {
- self.errorHandling.handle(error: AppError.genericError())
+ if model.hasNetworkConnection {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
}
model.hasError = false
}
}
.onDisappear(perform: {
self.model.editor.clearLastDraft()
if post.title.count == 0
&& post.body.count == 0
&& post.status == PostStatus.local.rawValue
&& post.updatedDate == nil
&& post.postId == nil {
DispatchQueue.main.async {
model.posts.remove(post)
}
} else if post.status != PostStatus.published.rawValue {
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
}
})
}
private func publishPost() {
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
model.publish(post: post)
}
model.editor.setInitialValues(for: post)
self.hideKeyboard()
}
private func sharePost() {
// If the post doesn't have a post ID, it isn't published, and therefore can't be shared, so return early.
guard let postId = post.postId else { return }
var urlString: String
if let postSlug = post.slug,
let postCollectionAlias = post.collectionAlias {
// This post is in a collection, so share the URL as baseURL/postSlug.
let urls = collections.filter { $0.alias == postCollectionAlias }
let baseURL = urls.first?.url ?? "\(model.account.server)/\(postCollectionAlias)/"
urlString = "\(baseURL)\(postSlug)"
} else {
// This is a draft post, so share the URL as server/postID
urlString = "\(model.account.server)/\(postId)"
}
guard let data = URL(string: urlString) else { return }
let activityView = UIActivityViewController(activityItems: [data], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(activityView, animated: true, completion: nil)
if UIDevice.current.userInterfaceIdiom == .pad {
activityView.popoverPresentationController?.permittedArrowDirections = .up
activityView.popoverPresentationController?.sourceView = UIApplication.shared.windows.first
activityView.popoverPresentationController?.sourceRect = CGRect(
x: UIScreen.main.bounds.width,
y: -125,
width: 200,
height: 200
)
}
}
}
struct PostEditorView_EmptyPostPreviews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.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.standard.container.viewContext
let testPost = WFAPost(context: context)
testPost.title = "Test Post Title"
testPost.body = "Here's some cool sample body text."
testPost.createdDate = Date()
testPost.appearance = "code"
testPost.hasNewerRemoteCopy = true
let model = WriteFreelyModel()
return PostEditorView(post: testPost)
.environment(\.managedObjectContext, context)
.environmentObject(model)
}
}
diff --git a/macOS/PostEditor/PostEditorView.swift b/macOS/PostEditor/PostEditorView.swift
index a6d928c..9ef80b3 100644
--- a/macOS/PostEditor/PostEditorView.swift
+++ b/macOS/PostEditor/PostEditorView.swift
@@ -1,104 +1,106 @@
import SwiftUI
struct PostEditorView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@ObservedObject var post: WFAPost
@State private var isHovering: Bool = false
@State private var updatingFromServer: Bool = false
var body: some View {
PostTextEditingView(
post: post,
updatingFromServer: $updatingFromServer
)
.padding()
.background(Color(NSColor.controlBackgroundColor))
.onAppear(perform: {
model.editor.setInitialValues(for: post)
if post.status != PostStatus.published.rawValue {
DispatchQueue.main.async {
self.model.editor.saveLastDraft(post)
}
} else {
self.model.editor.clearLastDraft()
}
})
.onChange(of: post.hasNewerRemoteCopy, perform: { _ in
if !post.hasNewerRemoteCopy {
self.updatingFromServer = true
}
})
.onChange(of: post.status, perform: { value in
if value != PostStatus.published.rawValue {
self.model.editor.saveLastDraft(post)
} else {
self.model.editor.clearLastDraft()
}
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
})
.onChange(of: model.hasError) { value in
if value {
- if let error = model.currentError {
- self.errorHandling.handle(error: error)
- } else {
- self.errorHandling.handle(error: AppError.genericError())
+ if model.hasNetworkConnection {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
}
model.hasError = false
}
}
.onDisappear(perform: {
DispatchQueue.main.async {
model.editor.clearLastDraft()
}
if post.title.count == 0
&& post.body.count == 0
&& post.status == PostStatus.local.rawValue
&& post.updatedDate == nil
&& post.postId == nil {
DispatchQueue.main.async {
model.posts.remove(post)
}
} else if post.status != PostStatus.published.rawValue {
DispatchQueue.main.async {
LocalStorageManager.standard.saveContext()
}
}
})
}
}
struct PostEditorView_EmptyPostPreviews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.standard.container.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.standard.container.viewContext
let testPost = WFAPost(context: context)
testPost.title = "Test Post Title"
testPost.body = "Here's some cool sample body text."
testPost.createdDate = Date()
testPost.appearance = "code"
let model = WriteFreelyModel()
return PostEditorView(post: testPost)
.environment(\.managedObjectContext, context)
.environmentObject(model)
}
}
diff --git a/macOS/Settings/MacAccountView.swift b/macOS/Settings/MacAccountView.swift
index 9939e99..44fa8d9 100644
--- a/macOS/Settings/MacAccountView.swift
+++ b/macOS/Settings/MacAccountView.swift
@@ -1,29 +1,31 @@
import SwiftUI
struct MacAccountView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
var body: some View {
Form {
AccountView()
}
.onChange(of: model.hasError) { value in
if value {
- if let error = model.currentError {
- self.errorHandling.handle(error: error)
- } else {
- self.errorHandling.handle(error: AppError.genericError())
+ if model.hasNetworkConnection {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
}
model.hasError = false
}
}
}
}
struct MacAccountView_Previews: PreviewProvider {
static var previews: some View {
MacAccountView()
.environmentObject(WriteFreelyModel())
}
}
diff --git a/macOS/Settings/MacPreferencesView.swift b/macOS/Settings/MacPreferencesView.swift
index feb91e5..ad48e73 100644
--- a/macOS/Settings/MacPreferencesView.swift
+++ b/macOS/Settings/MacPreferencesView.swift
@@ -1,31 +1,33 @@
import SwiftUI
struct MacPreferencesView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@ObservedObject var preferences: PreferencesModel
var body: some View {
VStack {
PreferencesView(preferences: preferences)
Spacer()
}
.onChange(of: model.hasError) { value in
if value {
- if let error = model.currentError {
- self.errorHandling.handle(error: error)
- } else {
- self.errorHandling.handle(error: AppError.genericError())
+ if model.hasNetworkConnection {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
}
model.hasError = false
}
}
}
}
struct MacPreferencesView_Previews: PreviewProvider {
static var previews: some View {
MacPreferencesView(preferences: PreferencesModel())
}
}
diff --git a/macOS/Settings/MacUpdatesView.swift b/macOS/Settings/MacUpdatesView.swift
index ba9f6a3..1f13f52 100644
--- a/macOS/Settings/MacUpdatesView.swift
+++ b/macOS/Settings/MacUpdatesView.swift
@@ -1,104 +1,106 @@
import SwiftUI
import Sparkle
struct MacUpdatesView: View {
@EnvironmentObject var model: WriteFreelyModel
@EnvironmentObject var errorHandling: ErrorHandling
@ObservedObject var updaterViewModel: MacUpdatesViewModel
@AppStorage(WFDefaults.automaticallyChecksForUpdates, store: UserDefaults.shared)
var automaticallyChecksForUpdates: Bool = false
@AppStorage(WFDefaults.subscribeToBetaUpdates, store: UserDefaults.shared)
var subscribeToBetaUpdates: Bool = false
@State private var lastUpdateCheck: Date?
private let betaWarningString = """
To get brand new features before each official release, choose "Test versions." Note that test versions may have bugs \
that can cause crashes and data loss.
"""
static let lastUpdateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
formatter.doesRelativeDateFormatting = true
return formatter
}()
var body: some View {
VStack(spacing: 24) {
Toggle(isOn: $automaticallyChecksForUpdates, label: {
Text("Check for updates automatically")
})
VStack {
Button(action: {
updaterViewModel.checkForUpdates()
// There's a delay between requesting an update, and the timestamp for that update request being
// written to user defaults; we therefore delay updating the "Last checked" UI for one second.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
lastUpdateCheck = updaterViewModel.getLastUpdateCheckDate()
}
}, label: {
Text("Check For Updates")
})
HStack {
Text("Last checked:")
.font(.caption)
if let lastUpdateCheck = lastUpdateCheck {
Text(lastUpdateCheck, formatter: Self.lastUpdateFormatter)
.font(.caption)
} else {
Text("Never")
.font(.caption)
}
}
}
VStack(spacing: 16) {
HStack(alignment: .top) {
Text("Download:")
Picker(selection: $subscribeToBetaUpdates, label: Text("Download:"), content: {
Text("Release versions").tag(false)
Text("Test versions").tag(true)
})
.pickerStyle(RadioGroupPickerStyle())
.labelsHidden()
}
Text(betaWarningString)
.frame(width: 350)
.foregroundColor(.secondary)
}
}
.padding()
.onAppear {
lastUpdateCheck = updaterViewModel.getLastUpdateCheckDate()
}
.onChange(of: automaticallyChecksForUpdates) { value in
updaterViewModel.automaticallyCheckForUpdates = value
}
.onChange(of: subscribeToBetaUpdates) { _ in
updaterViewModel.toggleAllowedChannels()
}
.onChange(of: model.hasError) { value in
if value {
- if let error = model.currentError {
- self.errorHandling.handle(error: error)
- } else {
- self.errorHandling.handle(error: AppError.genericError())
+ if model.hasNetworkConnection {
+ if let error = model.currentError {
+ self.errorHandling.handle(error: error)
+ } else {
+ self.errorHandling.handle(error: AppError.genericError())
+ }
}
model.hasError = false
}
}
}
}
struct MacUpdatesView_Previews: PreviewProvider {
static var previews: some View {
MacUpdatesView(updaterViewModel: MacUpdatesViewModel())
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Dec 9, 12:39 PM (12 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3531176
Attached To
rWFSUI WriteFreely SwiftUI
Event Timeline
Log In to Comment