diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift index 617385f..942ff76 100644 --- a/Shared/Models/WriteFreelyModel.swift +++ b/Shared/Models/WriteFreelyModel.swift @@ -17,6 +17,14 @@ final class WriteFreelyModel: ObservableObject { @Published var hasError: Bool = false var currentError: Error? { didSet { + if let localizedErrorDescription = currentError?.localizedDescription, + localizedErrorDescription == "The operation couldn’t be completed. (WriteFreely.WFError error -2.)", + !hasNetworkConnection { + #if DEBUG + print("⚠️ currentError is WriteFreely.WFError -2 and there is no network connection.") + #endif + currentError = NetworkError.noConnectionError + } #if DEBUG print("⚠️ currentError -> didSet \(currentError?.localizedDescription ?? "nil")") print(" > hasError was: \(self.hasError)") @@ -66,6 +74,10 @@ final class WriteFreelyModel: ObservableObject { self.preferences.appearance = self.defaults.integer(forKey: WFDefaults.colorSchemeIntegerKey) self.preferences.font = self.defaults.integer(forKey: WFDefaults.defaultFontIntegerKey) self.account.restoreState() + + // Set the appearance + self.preferences.updateAppearance(to: Appearance(rawValue: self.preferences.appearance) ?? .system) + if self.account.isLoggedIn { guard let serverURL = URL(string: self.account.server) else { self.currentError = AccountError.invalidServerURL diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 3a088d3..f67cda9 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -59,14 +59,7 @@ struct ContentView: View { .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 + NoSelectedPostView(isConnected: $model.hasNetworkConnection) } .environmentObject(model) .onChange(of: model.hasError) { value in diff --git a/Shared/Navigation/NoSelectedPostView.swift b/Shared/Navigation/NoSelectedPostView.swift new file mode 100644 index 0000000..6c4939a --- /dev/null +++ b/Shared/Navigation/NoSelectedPostView.swift @@ -0,0 +1,29 @@ +import SwiftUI + +struct NoSelectedPostView: View { + @Binding var isConnected: Bool + + var body: some View { + VStack(spacing: 8) { + Text("Select a post, or create a new local draft.") + if !isConnected { + Label("You are not connected to the internet", systemImage: "wifi.exclamationmark") + .font(.caption) + .foregroundColor(.secondary) + } + } + .frame(width: 500, height: 500) + } +} + +struct NoSelectedPostViewIsDisconnected_Previews: PreviewProvider { + static var previews: some View { + NoSelectedPostView(isConnected: Binding.constant(true)) + } +} + +struct NoSelectedPostViewIsConnected_Previews: PreviewProvider { + static var previews: some View { + NoSelectedPostView(isConnected: Binding.constant(false)) + } +} diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index 93d0567..89fe9fa 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -97,19 +97,26 @@ struct PostListView: View { .padding(.vertical, 4) .padding(.horizontal, 8) } else { - Button(action: { - DispatchQueue.main.async { - model.fetchUserCollections() - model.fetchUserPosts() - } - }, label: { - Image(systemName: "arrow.clockwise") + 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) - }) - .accessibilityLabel(Text("Refresh Posts")) - .accessibilityHint(Text("Fetch changes from the server")) - .disabled(!model.account.isLoggedIn) + .foregroundColor(.secondary) + } } } .padding(.top, 8) diff --git a/Shared/Preferences/PreferencesModel.swift b/Shared/Preferences/PreferencesModel.swift index 0fde8a6..a63748a 100644 --- a/Shared/Preferences/PreferencesModel.swift +++ b/Shared/Preferences/PreferencesModel.swift @@ -1,67 +1,54 @@ import SwiftUI +enum Appearance: Int { + case system = 0 + case light = 1 + case dark = 2 +} + class PreferencesModel: ObservableObject { private let defaults = UserDefaults.shared - /* We're stuck dropping into AppKit/UIKit to set light/dark schemes for now, - * because setting the .preferredColorScheme modifier on views in SwiftUI is - * currently unreliable. - * - * Feedback submitted to Apple: - * - * FB8382883: "On macOS 11β4, preferredColorScheme modifier does not respect .light ColorScheme" - * FB8383053: "On iOS 14β4/macOS 11β4, it is not possible to unset preferredColorScheme after setting - * it to either .light or .dark" - */ - - #if os(iOS) - @available(iOSApplicationExtension, unavailable) - var window: UIWindow? { - guard let scene = UIApplication.shared.connectedScenes.first, - let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate, - let window = windowSceneDelegate.window else { - return nil - } - return window - } - #endif - - @available(iOSApplicationExtension, unavailable) @Published var selectedColorScheme: ColorScheme? - - @available(iOSApplicationExtension, unavailable) - @Published var appearance: Int = 0 { - didSet { - switch appearance { - case 1: -// selectedColorScheme = .light - #if os(macOS) - NSApp.appearance = NSAppearance(named: .aqua) - #else - window?.overrideUserInterfaceStyle = .light - #endif - case 2: -// selectedColorScheme = .dark - #if os(macOS) - NSApp.appearance = NSAppearance(named: .darkAqua) - #else - window?.overrideUserInterfaceStyle = .dark - #endif - default: -// selectedColorScheme = .none - #if os(macOS) - NSApp.appearance = nil - #else - window?.overrideUserInterfaceStyle = .unspecified - #endif - } - - defaults.set(appearance, forKey: WFDefaults.colorSchemeIntegerKey) - } - } + @Published var appearance: Int = 0 @Published var font: Int = 0 { didSet { defaults.set(font, forKey: WFDefaults.defaultFontIntegerKey) } } + + @available(iOSApplicationExtension, unavailable) + func updateAppearance(to appearance: Appearance) { + #if os(iOS) + var window: UIWindow? { + guard let scene = UIApplication.shared.connectedScenes.first, + let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate, + let window = windowSceneDelegate.window else { + return nil + } + return window + } + #endif + + switch appearance { + case .light: + #if os(macOS) + NSApp.appearance = NSAppearance(named: .aqua) + #else + window?.overrideUserInterfaceStyle = .light + #endif + case .dark: + #if os(macOS) + NSApp.appearance = NSAppearance(named: .darkAqua) + #else + window?.overrideUserInterfaceStyle = .dark + #endif + default: + #if os(macOS) + NSApp.appearance = nil + #else + window?.overrideUserInterfaceStyle = .unspecified + #endif + } + } } diff --git a/Shared/Preferences/PreferencesView.swift b/Shared/Preferences/PreferencesView.swift index 1522450..902d809 100644 --- a/Shared/Preferences/PreferencesView.swift +++ b/Shared/Preferences/PreferencesView.swift @@ -3,6 +3,28 @@ import SwiftUI struct PreferencesView: View { @ObservedObject var preferences: PreferencesModel + /* We're stuck dropping into AppKit/UIKit to set light/dark schemes for now, + * because setting the .preferredColorScheme modifier on views in SwiftUI is + * currently unreliable. + * + * Feedback submitted to Apple: + * + * FB8382883: "On macOS 11β4, preferredColorScheme modifier does not respect .light ColorScheme" + * FB8383053: "On iOS 14β4/macOS 11β4, it is not possible to unset preferredColorScheme after setting + * it to either .light or .dark" + */ + + #if os(iOS) + var window: UIWindow? { + guard let scene = UIApplication.shared.connectedScenes.first, + let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate, + let window = windowSceneDelegate.window else { + return nil + } + return window + } + #endif + var body: some View { VStack { VStack { @@ -46,6 +68,10 @@ struct PreferencesView: View { } .padding(.bottom) } + .onChange(of: preferences.appearance) { value in + preferences.updateAppearance(to: Appearance(rawValue: value) ?? .system) + UserDefaults.shared.set(value, forKey: WFDefaults.colorSchemeIntegerKey) + } } } diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index 91713ff..a646875 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -136,6 +136,8 @@ 17DFDE8B251D309400A25F31 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE86251D309400A25F31 /* OpenSans-License.txt */; }; 17DFDE8C251D309400A25F31 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE86251D309400A25F31 /* OpenSans-License.txt */; }; 17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */; }; + 37095AE02AA4A0E700C9C5F8 /* NoSelectedPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37113EF82A98C10A00B36B98 /* NoSelectedPostView.swift */; }; + 37113EF92A98C10A00B36B98 /* NoSelectedPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37113EF82A98C10A00B36B98 /* NoSelectedPostView.swift */; }; 375A67E828FC555C007A1AC0 /* MultilineTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375A67E728FC555C007A1AC0 /* MultilineTextView.swift */; }; 3779389729EC0C880032D6C1 /* HelpCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3779389629EC0C880032D6C1 /* HelpCommands.swift */; }; 37F749D129B4D3090087F0BF /* SearchablePostListFilteredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */; }; @@ -270,6 +272,7 @@ 17DFDE85251D309400A25F31 /* Lora-Cyrillic-OFL.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Lora-Cyrillic-OFL.txt"; sourceTree = ""; }; 17DFDE86251D309400A25F31 /* OpenSans-License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "OpenSans-License.txt"; sourceTree = ""; }; 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTextEditingView.swift; sourceTree = ""; }; + 37113EF82A98C10A00B36B98 /* NoSelectedPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoSelectedPostView.swift; sourceTree = ""; }; 375A67E728FC555C007A1AC0 /* MultilineTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextView.swift; sourceTree = ""; }; 3779389629EC0C880032D6C1 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = ""; }; 37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePostListFilteredView.swift; sourceTree = ""; }; @@ -586,6 +589,7 @@ isa = PBXGroup; children = ( 17DF328224C87D3300BCE2E3 /* ContentView.swift */, + 37113EF82A98C10A00B36B98 /* NoSelectedPostView.swift */, ); path = Navigation; sourceTree = ""; @@ -922,6 +926,7 @@ 173E19D1254318F600440F0F /* RemoteChangePromptView.swift in Sources */, 17B37C5625C8679800FE75E9 /* WriteFreelyModel+API.swift in Sources */, 17C42E622507D8E600072984 /* PostStatus.swift in Sources */, + 37095AE02AA4A0E700C9C5F8 /* NoSelectedPostView.swift in Sources */, 1756DBBA24FED45500207AB8 /* LocalStorageManager.swift in Sources */, 1727526A2809991A003D0A6A /* ErrorHandling.swift in Sources */, 1756AE8124CB844500FD7257 /* View+Keyboard.swift in Sources */, @@ -975,6 +980,7 @@ 17120DAA24E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */, 17DF32D624C8CA3400BCE2E3 /* PostStatusBadgeView.swift in Sources */, 172C492E2593981900E20ADF /* MacUpdatesView.swift in Sources */, + 37113EF92A98C10A00B36B98 /* NoSelectedPostView.swift in Sources */, 1727526728099802003D0A6A /* ErrorConstants.swift in Sources */, 17479F152583D8E40072B7FB /* PostEditorSharingPicker.swift in Sources */, 17480CA6251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */, @@ -1060,7 +1066,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 704; + CURRENT_PROJECT_VERSION = 706; DEVELOPMENT_TEAM = TPPAB4YBA6; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "ActionExtension-iOS/Info.plist"; @@ -1072,7 +1078,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.16; + MARKETING_VERSION = 1.0.17; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform.ActionExtension-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1091,7 +1097,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 704; + CURRENT_PROJECT_VERSION = 706; DEVELOPMENT_TEAM = TPPAB4YBA6; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "ActionExtension-iOS/Info.plist"; @@ -1103,7 +1109,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.16; + MARKETING_VERSION = 1.0.17; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform.ActionExtension-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1234,7 +1240,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 704; + CURRENT_PROJECT_VERSION = 706; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; @@ -1243,7 +1249,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.16; + MARKETING_VERSION = 1.0.17; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = iphoneos; @@ -1260,7 +1266,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 704; + CURRENT_PROJECT_VERSION = 706; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; @@ -1269,7 +1275,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.16; + MARKETING_VERSION = 1.0.17; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = iphoneos; diff --git a/iOS/PostEditor/PostEditorView.swift b/iOS/PostEditor/PostEditorView.swift index 0a875f3..070b421 100644 --- a/iOS/PostEditor/PostEditorView.swift +++ b/iOS/PostEditor/PostEditorView.swift @@ -50,7 +50,10 @@ struct PostEditorView: View { PostEditorStatusToolbarView(post: post) } ToolbarItem(placement: .primaryAction) { - if model.isProcessingRequest { + if !model.hasNetworkConnection { + Image(systemName: "wifi.exclamationmark") + .foregroundColor(.secondary) + } else if model.isProcessingRequest { ProgressView() } else { Menu(content: { diff --git a/macOS/PostEditor/MacEditorTextView.swift b/macOS/PostEditor/MacEditorTextView.swift index 9c11d8a..7c411e1 100644 --- a/macOS/PostEditor/MacEditorTextView.swift +++ b/macOS/PostEditor/MacEditorTextView.swift @@ -115,6 +115,7 @@ final class CustomTextView: NSView { let scrollView = NSScrollView() scrollView.drawsBackground = false scrollView.borderType = .noBorder + scrollView.autohidesScrollers = true scrollView.hasVerticalScroller = true scrollView.hasHorizontalRuler = false scrollView.autoresizingMask = [.width, .height] @@ -167,6 +168,8 @@ final class CustomTextView: NSView { .font: font ?? NSFont.systemFont(ofSize: 17), // Fall back to system font if we can't unwrap font argument .foregroundColor: NSColor.labelColor ] + textView.textContainer?.lineFragmentPadding = 16 + textView.textContainerInset = NSSize(width: 0, height: 16) return textView }() diff --git a/macOS/PostEditor/PostEditorView.swift b/macOS/PostEditor/PostEditorView.swift index a6d928c..fc6fb62 100644 --- a/macOS/PostEditor/PostEditorView.swift +++ b/macOS/PostEditor/PostEditorView.swift @@ -9,65 +9,72 @@ struct PostEditorView: View { @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() + VStack { + if !model.hasNetworkConnection { + Label("You are not connected to the internet", systemImage: "wifi.exclamationmark") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top, 8) } - }) - .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) + PostTextEditingView( + post: post, + updatingFromServer: $updatingFromServer + ) + .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.errorHandling.handle(error: AppError.genericError()) + self.model.editor.clearLastDraft() } - 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) + }) + .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() } - } else if post.status != PostStatus.published.rawValue { 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()) + } + 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() + } + } + }) + } } } diff --git a/macOS/PostEditor/PostTextEditingView.swift b/macOS/PostEditor/PostTextEditingView.swift index 8922952..6dd74bf 100644 --- a/macOS/PostEditor/PostTextEditingView.swift +++ b/macOS/PostEditor/PostTextEditingView.swift @@ -15,7 +15,8 @@ struct PostTextEditingView: View { if combinedText.count == 0 { Text("Write…") .foregroundColor(Color(NSColor.placeholderTextColor)) - .padding(.horizontal, 5) + .padding(.horizontal, 16) + .padding(.vertical, 16) .font(.custom(appearance.rawValue, size: 17, relativeTo: .body)) } if post.appearance == "sans" {