diff --git a/Shared/Account/AccountLoginView.swift b/Shared/Account/AccountLoginView.swift index 774028e..4e1e2c5 100644 --- a/Shared/Account/AccountLoginView.swift +++ b/Shared/Account/AccountLoginView.swift @@ -1,83 +1,86 @@ import SwiftUI struct AccountLoginView: View { @EnvironmentObject var model: WriteFreelyModel @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: { model.login( to: URL(string: server)!, as: username, password: password ) }, label: { - Text("Login") + Text("Log In") }) .disabled( model.account.isLoggedIn || (username.isEmpty || password.isEmpty || server.isEmpty) ) .padding() } } .alert(isPresented: $model.isPresentingLoginErrorAlert) { Alert( title: Text("Error Logging In"), message: Text(model.loginErrorMessage ?? "An unknown error occurred while trying to login."), dismissButton: .default(Text("OK")) ) } } } struct AccountLoginView_Previews: PreviewProvider { static var previews: some View { AccountLoginView() .environmentObject(WriteFreelyModel()) } } diff --git a/Shared/Account/AccountLogoutView.swift b/Shared/Account/AccountLogoutView.swift index a90412c..0b2bd82 100644 --- a/Shared/Account/AccountLogoutView.swift +++ b/Shared/Account/AccountLogoutView.swift @@ -1,30 +1,30 @@ import SwiftUI struct AccountLogoutView: View { @EnvironmentObject var model: WriteFreelyModel var body: some View { VStack { Spacer() VStack { Text("Logged in as \(model.account.username)") Text("on \(model.account.server)") } Spacer() Button(action: logoutHandler, label: { - Text("Logout") + Text("Log Out") }) } } func logoutHandler() { model.logout() } } struct AccountLogoutView_Previews: PreviewProvider { static var previews: some View { AccountLogoutView() .environmentObject(WriteFreelyModel()) } } diff --git a/Shared/Account/AccountView.swift b/Shared/Account/AccountView.swift index 64f23a5..4ff4527 100644 --- a/Shared/Account/AccountView.swift +++ b/Shared/Account/AccountView.swift @@ -1,26 +1,26 @@ import SwiftUI struct AccountView: View { @EnvironmentObject var model: WriteFreelyModel var body: some View { if model.account.isLoggedIn { HStack { Spacer() AccountLogoutView() Spacer() } .padding() } else { AccountLoginView() - .padding() + .padding(.top) } } } struct AccountLogin_Previews: PreviewProvider { static var previews: some View { AccountView() .environmentObject(WriteFreelyModel()) } } diff --git a/iOS/PostEditor/PostEditorView.swift b/iOS/PostEditor/PostEditorView.swift index 884d9f8..98a646a 100644 --- a/iOS/PostEditor/PostEditorView.swift +++ b/iOS/PostEditor/PostEditorView.swift @@ -1,243 +1,245 @@ import SwiftUI struct PostEditorView: View { @EnvironmentObject var model: WriteFreelyModel @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.presentationMode) var presentationMode @ObservedObject var post: WFAPost var body: some View { VStack { if post.hasNewerRemoteCopy { HStack { Text("⚠️ Newer copy on server. Replace local copy?") .font(horizontalSizeClass == .compact ? .caption : .body) .foregroundColor(.secondary) Button(action: { model.updateFromServer(post: post) }, label: { Image(systemName: "square.and.arrow.down") }) } .padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)) .background(Color(UIColor.secondarySystemBackground)) .clipShape(Capsule()) .padding(.bottom) } else if post.wasDeletedFromServer { HStack { Text("⚠️ Post deleted from server. Delete local copy?") .font(horizontalSizeClass == .compact ? .caption : .body) .foregroundColor(.secondary) Button(action: { self.presentationMode.wrappedValue.dismiss() DispatchQueue.main.async { model.posts.remove(post) } }, label: { Image(systemName: "trash") }) } .padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)) .background(Color(UIColor.secondarySystemBackground)) .clipShape(Capsule()) .padding(.bottom) } 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) } ToolbarItemGroup(placement: .navigationBarTrailing) { Button(action: { - publishPost() + if model.account.isLoggedIn { + publishPost() + } else { + self.model.isPresentingSettingsView = true + } }, label: { Image(systemName: "paperplane") }) .disabled( - post.status == PostStatus.published.rawValue || - !model.account.isLoggedIn || - !model.hasNetworkConnection + post.status == PostStatus.published.rawValue || !model.hasNetworkConnection || post.body.count == 0 ) Button(action: { sharePost() }, label: { Image(systemName: "square.and.arrow.up") }) .disabled(post.postId == nil) } } .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.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) model.posts.loadCachedPosts() } } else 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 } private func sharePost() { guard let urlString = model.selectedPost?.slug != nil ? "\(model.account.server)/\((model.selectedPost?.collectionAlias)!)/\((model.selectedPost?.slug)!)" : "\(model.account.server)/\((model.selectedPost?.postId)!)" else { return } 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.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" 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 3ba7381..ed07ce6 100644 --- a/macOS/PostEditor/PostEditorView.swift +++ b/macOS/PostEditor/PostEditorView.swift @@ -1,199 +1,201 @@ 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() + if model.account.isLoggedIn { + publishPost() + } else { + // TODO: Open the Preferences window. + } }, label: { Image(systemName: "paperplane") }) .disabled( - post.status == PostStatus.published.rawValue || - !model.account.isLoggedIn || - !model.hasNetworkConnection + post.status == PostStatus.published.rawValue || !model.hasNetworkConnection || post.body.count == 0 ) } } .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.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) model.posts.loadCachedPosts() } } else 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_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) } }