Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F10669138
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Subscribers
None
View Options
diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift
index f5b1011..d392e02 100644
--- a/Shared/Models/WriteFreelyModel.swift
+++ b/Shared/Models/WriteFreelyModel.swift
@@ -1,74 +1,76 @@
import Foundation
import WriteFreely
import Security
import Network
// MARK: - WriteFreelyModel
final 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
+ static var shared = WriteFreelyModel()
+
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 reviewURL = URL(string: "https://apps.apple.com/app/id1531530896?action=write-review")!
let licensesURL = URL(string: "https://github.com/writeas/writefreely-swiftui-multiplatform/tree/main/Shared/Resources/Licenses")!
// swiftlint:enable line_length
internal var client: WFClient?
private let defaults = UserDefaults.standard
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")
internal var postToUpdate: WFAPost?
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)
}
}
diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift
index 09d389f..a60322f 100644
--- a/Shared/Navigation/ContentView.swift
+++ b/Shared/Navigation/ContentView.swift
@@ -1,98 +1,94 @@
import SwiftUI
struct ContentView: View {
@EnvironmentObject var model: WriteFreelyModel
var body: some View {
NavigationView {
#if os(macOS)
CollectionListView()
.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
-
- // Navigate to the Drafts list
- self.model.showAllPosts = false
- self.model.selectedCollection = nil
}
// Create the new-post managed object
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
DispatchQueue.main.asyncAfter(deadline: .now()) {
// Load the new post in the editor
self.model.selectedPost = managedPost
}
}
}, label: { Image(systemName: "square.and.pencil") })
.help("Create a new local draft.")
}
#else
CollectionListView()
#endif
#if os(macOS)
ZStack {
PostListView()
if model.isProcessingRequest {
ZStack {
Color(NSColor.controlBackgroundColor).opacity(0.75)
ProgressView()
}
}
}
#else
PostListView()
#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/PostEditor/PostEditorModel.swift b/Shared/PostEditor/PostEditorModel.swift
index 014681f..8219ead 100644
--- a/Shared/PostEditor/PostEditorModel.swift
+++ b/Shared/PostEditor/PostEditorModel.swift
@@ -1,63 +1,63 @@
import SwiftUI
import CoreData
enum PostAppearance: String {
case sans = "OpenSans-Regular"
case mono = "Hack-Regular"
case serif = "Lora-Regular"
}
struct PostEditorModel {
@AppStorage("showAllPostsFlag") var showAllPostsFlag: Bool = false
@AppStorage("selectedCollectionURL") var selectedCollectionURL: URL?
@AppStorage("lastDraftURL") var lastDraftURL: URL?
func saveLastDraft(_ post: WFAPost) {
self.lastDraftURL = post.status != PostStatus.published.rawValue ? post.objectID.uriRepresentation() : nil
}
func clearLastDraft() {
self.lastDraftURL = nil
}
func fetchLastDraftFromAppStorage() -> WFAPost? {
guard let postURL = lastDraftURL else { return nil }
guard let post = fetchManagedObject(from: postURL) as? WFAPost else { return nil }
return post
}
func generateNewLocalPost(withFont appearance: Int) -> WFAPost {
let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext)
managedPost.createdDate = Date()
managedPost.title = ""
managedPost.body = ""
managedPost.status = PostStatus.local.rawValue
- managedPost.collectionAlias = nil
+ managedPost.collectionAlias = WriteFreelyModel.shared.selectedCollection?.alias
switch appearance {
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
}
return managedPost
}
func fetchSelectedCollectionFromAppStorage() -> WFACollection? {
guard let collectionURL = selectedCollectionURL else { return nil }
guard let collection = fetchManagedObject(from: collectionURL) as? WFACollection else { return nil }
return collection
}
private func fetchManagedObject(from objectURL: URL) -> NSManagedObject? {
let coordinator = LocalStorageManager.persistentContainer.persistentStoreCoordinator
guard let managedObjectID = coordinator.managedObjectID(forURIRepresentation: objectURL) else { return nil }
let object = LocalStorageManager.persistentContainer.viewContext.object(with: managedObjectID)
return object
}
}
diff --git a/Shared/WriteFreely_MultiPlatformApp.swift b/Shared/WriteFreely_MultiPlatformApp.swift
index bca2dd9..900a267 100644
--- a/Shared/WriteFreely_MultiPlatformApp.swift
+++ b/Shared/WriteFreely_MultiPlatformApp.swift
@@ -1,149 +1,145 @@
import SwiftUI
#if os(macOS)
import Sparkle
#endif
@main
struct CheckForDebugModifier {
static func main() {
#if os(macOS)
if NSEvent.modifierFlags.contains(.shift) {
// Clear the launch-to-last-draft values to load a new draft.
UserDefaults.standard.setValue(false, forKey: "showAllPostsFlag")
UserDefaults.standard.setValue(nil, forKey: "selectedCollectionURL")
UserDefaults.standard.setValue(nil, forKey: "lastDraftURL")
} else {
// No-op
}
#endif
WriteFreely_MultiPlatformApp.main()
}
}
struct WriteFreely_MultiPlatformApp: App {
- @StateObject private var model = WriteFreelyModel()
+ @StateObject private var model = WriteFreelyModel.shared
#if os(macOS)
// swiftlint:disable:next weak_delegate
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@State private var selectedTab = 0
#endif
var body: some Scene {
WindowGroup {
ContentView()
.onAppear(perform: {
if model.editor.showAllPostsFlag {
DispatchQueue.main.async {
self.model.selectedCollection = nil
self.model.showAllPosts = true
}
} else {
DispatchQueue.main.async {
self.model.selectedCollection = model.editor.fetchSelectedCollectionFromAppStorage()
self.model.showAllPosts = false
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if model.editor.lastDraftURL != nil {
self.model.selectedPost = model.editor.fetchLastDraftFromAppStorage()
} else {
createNewLocalPost()
}
}
})
.environmentObject(model)
.environment(\.managedObjectContext, LocalStorageManager.persistentContainer.viewContext)
// .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info.
}
.commands {
#if os(macOS)
CommandGroup(after: .appInfo, addition: {
Button("Check For Updates") {
SUUpdater.shared()?.checkForUpdates(self)
}
})
#endif
CommandGroup(replacing: .newItem, addition: {
Button("New Post") {
createNewLocalPost()
}
.keyboardShortcut("n", modifiers: [.command])
})
CommandGroup(after: .newItem) {
Button("Refresh Posts") {
DispatchQueue.main.async {
model.fetchUserCollections()
model.fetchUserPosts()
}
}
.disabled(!model.account.isLoggedIn)
.keyboardShortcut("r", modifiers: [.command])
}
SidebarCommands()
#if os(macOS)
PostCommands(model: model)
#endif
CommandGroup(after: .help) {
Button("Visit Support Forum") {
#if os(macOS)
NSWorkspace().open(model.helpURL)
#else
UIApplication.shared.open(model.helpURL)
#endif
}
}
ToolbarCommands()
TextEditingCommands()
}
#if os(macOS)
Settings {
TabView(selection: $selectedTab) {
MacAccountView()
.environmentObject(model)
.tabItem {
Image(systemName: "person.crop.circle")
Text("Account")
}
.tag(0)
MacPreferencesView(preferences: model.preferences)
.tabItem {
Image(systemName: "gear")
Text("Preferences")
}
.tag(1)
MacUpdatesView()
.tabItem {
Image(systemName: "arrow.down.circle")
Text("Updates")
}
.tag(2)
}
.frame(minWidth: 500, maxWidth: 500, minHeight: 200)
.padding()
// .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info.
}
#endif
}
private func createNewLocalPost() {
withAnimation {
// Un-set the currently selected post
self.model.selectedPost = nil
-
- // Navigate to the Drafts list
- self.model.showAllPosts = false
- self.model.selectedCollection = nil
}
// Create the new-post managed object
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
// Set it as the selectedPost
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.model.selectedPost = managedPost
}
}
}
}
diff --git a/macOS/PostEditor/PostTextEditingView.swift b/macOS/PostEditor/PostTextEditingView.swift
index feb6e5c..fabae24 100644
--- a/macOS/PostEditor/PostTextEditingView.swift
+++ b/macOS/PostEditor/PostTextEditingView.swift
@@ -1,99 +1,115 @@
import SwiftUI
struct PostTextEditingView: View {
@ObservedObject var post: WFAPost
@Binding var updatingFromServer: Bool
@State private var appearance: PostAppearance = .serif
@State private var combinedText = ""
+ @State private var hasBeenEdited: Bool = false
+
+ let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ZStack(alignment: .topLeading) {
if combinedText.count == 0 {
Text("Write…")
.foregroundColor(Color(NSColor.placeholderTextColor))
.padding(.horizontal, 5)
.font(.custom(appearance.rawValue, size: 17, relativeTo: .body))
}
if post.appearance == "sans" {
MacEditorTextView(
text: $combinedText,
isFirstResponder: combinedText.isEmpty,
isEditable: true,
font: NSFont(name: PostAppearance.sans.rawValue, size: 17),
onEditingChanged: onEditingChanged,
onCommit: onCommit,
onTextChange: onTextChange
)
} else if post.appearance == "wrap" || post.appearance == "mono" || post.appearance == "code" {
MacEditorTextView(
text: $combinedText,
isFirstResponder: combinedText.isEmpty,
isEditable: true,
font: NSFont(name: PostAppearance.mono.rawValue, size: 17),
onEditingChanged: onEditingChanged,
onCommit: onCommit,
onTextChange: onTextChange
)
} else {
MacEditorTextView(
text: $combinedText,
isFirstResponder: combinedText.isEmpty,
isEditable: true,
font: NSFont(name: PostAppearance.serif.rawValue, size: 17),
onEditingChanged: onEditingChanged,
onCommit: onCommit,
onTextChange: onTextChange
)
}
}
.background(Color(NSColor.controlBackgroundColor))
.onAppear(perform: {
if post.title.isEmpty {
self.combinedText = post.body
} else {
self.combinedText = "# \(post.title)\n\n\(post.body)"
}
})
+ .onReceive(timer) { _ in
+ if !post.body.isEmpty && hasBeenEdited {
+ DispatchQueue.main.async {
+ LocalStorageManager().saveContext()
+ hasBeenEdited = false
+ }
+ }
+ }
}
private func onEditingChanged() {
- // Add code here to take action when the user first starts typing.
+ hasBeenEdited = true
}
private func onTextChange(_ text: String) {
extractTitle(text)
if post.status == PostStatus.published.rawValue && !updatingFromServer {
post.status = PostStatus.edited.rawValue
}
if updatingFromServer {
self.updatingFromServer = false
}
+ hasBeenEdited = true
}
private func onCommit() {
- // Add code here to take action when the user navigates away from the post.
+ if !post.body.isEmpty && hasBeenEdited {
+ DispatchQueue.main.async {
+ LocalStorageManager().saveContext()
+ }
+ }
+ hasBeenEdited = false
}
private func extractTitle(_ text: String) {
var detectedTitle: String
if text.hasPrefix("# ") {
let endOfTitleIndex = text.firstIndex(of: "\n") ?? text.endIndex
detectedTitle = String(text[..<endOfTitleIndex])
self.post.title = String(detectedTitle.dropFirst("# ".count))
let remainingText = String(text.dropFirst(detectedTitle.count).dropFirst(1))
if remainingText.hasPrefix("\n") {
self.post.body = String(remainingText.dropFirst(1))
} else {
self.post.body = remainingText
}
} else {
self.post.title = ""
self.post.body = text
}
}
-
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, May 15, 4:30 PM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3239827
Attached To
rWFSUI WriteFreely SwiftUI
Event Timeline
Log In to Comment