Browse Source

Improve offline experience (#255)

main
Angelo Stavrow 6 months ago
committed by GitHub
parent
commit
3f424b399a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 207 additions and 133 deletions
  1. +12
    -0
      Shared/Models/WriteFreelyModel.swift
  2. +1
    -8
      Shared/Navigation/ContentView.swift
  3. +29
    -0
      Shared/Navigation/NoSelectedPostView.swift
  4. +18
    -11
      Shared/PostList/PostListView.swift
  5. +40
    -53
      Shared/Preferences/PreferencesModel.swift
  6. +26
    -0
      Shared/Preferences/PreferencesView.swift
  7. +14
    -8
      WriteFreely-MultiPlatform.xcodeproj/project.pbxproj
  8. +4
    -1
      iOS/PostEditor/PostEditorView.swift
  9. +3
    -0
      macOS/PostEditor/MacEditorTextView.swift
  10. +58
    -51
      macOS/PostEditor/PostEditorView.swift
  11. +2
    -1
      macOS/PostEditor/PostTextEditingView.swift

+ 12
- 0
Shared/Models/WriteFreelyModel.swift View File

@@ -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


+ 1
- 8
Shared/Navigation/ContentView.swift View File

@@ -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


+ 29
- 0
Shared/Navigation/NoSelectedPostView.swift View File

@@ -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))
}
}

+ 18
- 11
Shared/PostList/PostListView.swift View File

@@ -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)


+ 40
- 53
Shared/Preferences/PreferencesModel.swift View File

@@ -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
@Published var selectedColorScheme: ColorScheme?
@Published var appearance: Int = 0
@Published var font: Int = 0 {
didSet {
defaults.set(font, forKey: WFDefaults.defaultFontIntegerKey)
}
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
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
}

defaults.set(appearance, forKey: WFDefaults.colorSchemeIntegerKey)
return window
}
}
@Published var font: Int = 0 {
didSet {
defaults.set(font, forKey: WFDefaults.defaultFontIntegerKey)
#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
}
}
}

+ 26
- 0
Shared/Preferences/PreferencesView.swift View File

@@ -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)
}
}
}



+ 14
- 8
WriteFreely-MultiPlatform.xcodeproj/project.pbxproj View File

@@ -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 = "<group>"; };
17DFDE86251D309400A25F31 /* OpenSans-License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "OpenSans-License.txt"; sourceTree = "<group>"; };
17E5DF892543610700DCDC9B /* PostTextEditingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTextEditingView.swift; sourceTree = "<group>"; };
37113EF82A98C10A00B36B98 /* NoSelectedPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoSelectedPostView.swift; sourceTree = "<group>"; };
375A67E728FC555C007A1AC0 /* MultilineTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextView.swift; sourceTree = "<group>"; };
3779389629EC0C880032D6C1 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = "<group>"; };
37F749D029B4D3090087F0BF /* SearchablePostListFilteredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePostListFilteredView.swift; sourceTree = "<group>"; };
@@ -586,6 +589,7 @@
isa = PBXGroup;
children = (
17DF328224C87D3300BCE2E3 /* ContentView.swift */,
37113EF82A98C10A00B36B98 /* NoSelectedPostView.swift */,
);
path = Navigation;
sourceTree = "<group>";
@@ -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;


+ 4
- 1
iOS/PostEditor/PostEditorView.swift View File

@@ -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: {


+ 3
- 0
macOS/PostEditor/MacEditorTextView.swift View File

@@ -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
}()


+ 58
- 51
macOS/PostEditor/PostEditorView.swift View File

@@ -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()
}
})
.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()
VStack {
if !model.hasNetworkConnection {
Label("You are not connected to the internet", systemImage: "wifi.exclamationmark")
.font(.caption)
.foregroundColor(.secondary)
.padding(.top, 8)
}
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()
}
}
})
}
}
}



+ 2
- 1
macOS/PostEditor/PostTextEditingView.swift View File

@@ -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" {


Loading…
Cancel
Save