Commit b5a5dc8963
Verified · cmc
Layout: unified · split
LibreEdit/ContentView.swift +54 −82
| @@ -6,104 +6,81 @@ | |||
| 6 | // | 6 | // |
| 7 | 7 | ||
| 8 | import SwiftUI | 8 | import SwiftUI |
| 9 | import UniformTypeIdentifiers | ||
| 9 | 10 | ||
| 10 | func getDatetime() -> String { | 11 | struct TextFile: FileDocument { |
| 11 | let dateFormatter = DateFormatter() | 12 | // tell the system we support only plain text |
| 12 | let enUSPosixLocale = Locale(identifier: "en_US_POSIX") | 13 | static var readableContentTypes = [UTType.plainText] |
| 13 | dateFormatter.locale = enUSPosixLocale | ||
| 14 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" | ||
| 15 | dateFormatter.calendar = Calendar(identifier: .gregorian) | ||
| 16 | 14 | ||
| 17 | let iso8601String = dateFormatter.string(from: Date()) | 15 | // by default our document is empty |
| 16 | var text = "" | ||
| 18 | 17 | ||
| 19 | return iso8601String | 18 | // a simple initializer that creates new, empty documents |
| 19 | init(initialText: String = "") { | ||
| 20 | text = initialText | ||
| 21 | } | ||
| 22 | |||
| 23 | // this initializer loads data that has been saved previously | ||
| 24 | init(configuration: ReadConfiguration) throws { | ||
| 25 | if let data = configuration.file.regularFileContents { | ||
| 26 | text = String(decoding: data, as: UTF8.self) | ||
| 27 | } else { | ||
| 28 | throw CocoaError(.fileReadCorruptFile) | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | // this will be called when the system wants to write our data to disk | ||
| 33 | func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { | ||
| 34 | let data = Data(text.utf8) | ||
| 35 | return FileWrapper(regularFileWithContents: data) | ||
| 36 | } | ||
| 20 | } | 37 | } |
| 21 | 38 | ||
| 22 | // Ensure that `Supports Document Browser` = YES in the `ios-edit.xcodeproj` file, | 39 | func countWords(text: String) -> Int { |
| 23 | // so that users can browse saved files in the Files app | 40 | let words = text.split { $0 == " " || $0.isNewline } |
| 24 | func getDocumentsDirectory() -> URL { | 41 | return words.count |
| 25 | let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) | ||
| 26 | let documentsDirectory = paths[0] | ||
| 27 | return documentsDirectory | ||
| 28 | } | 42 | } |
| 29 | 43 | ||
| 30 | struct ContentView: View { | 44 | struct ContentView: View { |
| 31 | enum FocusedField { | 45 | @Binding var document: TextFile |
| 32 | case content | ||
| 33 | } | ||
| 34 | |||
| 35 | @State private var content = "" | ||
| 36 | @State private var wordCount: Int = 0 | ||
| 37 | @State private var showingPopover = false | 46 | @State private var showingPopover = false |
| 38 | @FocusState private var focusedField: FocusedField? | 47 | @State private var wordCount: Int = 0 |
| 39 | 48 | ||
| 40 | var body: some View { | 49 | var body: some View { |
| 41 | NavigationView { | 50 | Text("\(wordCount) words") |
| 42 | VStack(alignment: .leading) { | 51 | .foregroundColor(Color.white) |
| 43 | Text("\(wordCount) words") | 52 | .font(.headline) |
| 44 | .font(.headline) | 53 | .padding(.trailing) |
| 45 | .foregroundColor(.secondary) | 54 | .frame(maxWidth: .infinity, maxHeight: 50) |
| 46 | .padding(.trailing) | 55 | .background(Color.gray.opacity(0.3)) |
| 47 | TextEditor(text: $content) | 56 | .padding(.top, 20) |
| 48 | .onChange(of: content) { | 57 | TextEditor(text: $document.text) |
| 49 | let words = content.split { $0 == " " || $0.isNewline } | ||
| 50 | self.wordCount = words.count | ||
| 51 | } | ||
| 52 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) | ||
| 53 | .focused($focusedField, equals: .content) | ||
| 54 | } | ||
| 55 | .onAppear { | 58 | .onAppear { |
| 56 | focusedField = .content | 59 | self.wordCount = countWords(text: document.text) |
| 60 | } | ||
| 61 | .onChange(of: document.text) { | ||
| 62 | self.wordCount = countWords(text: document.text) | ||
| 57 | } | 63 | } |
| 58 | .padding() | ||
| 59 | .navigationTitle("Libre Edit") | ||
| 60 | .toolbar { | 64 | .toolbar { |
| 61 | ToolbarItemGroup(placement: .primaryAction) { | ||
| 62 | Button(action: { | ||
| 63 | do { | ||
| 64 | let data = Data(self.content.utf8) | ||
| 65 | let url = URL.documentsDirectory.appending(path: (getDatetime() + ".txt")) | ||
| 66 | try data.write(to: url, options: [.atomic, .completeFileProtection]) | ||
| 67 | |||
| 68 | // TODO: Alerts currently aren't working/aren't visible | ||
| 69 | Text("File saved successfully!") | ||
| 70 | .foregroundColor(.white) | ||
| 71 | .bold() | ||
| 72 | .font(.footnote) | ||
| 73 | .frame(width: 140, height: 30) | ||
| 74 | .background(Color.indigo.cornerRadius(7)) | ||
| 75 | print("Saved content to file: \(url)") | ||
| 76 | } catch { | ||
| 77 | // TODO: Alerts currently aren't working/aren't visible | ||
| 78 | Text("Failed to save file.") | ||
| 79 | .foregroundColor(.white) | ||
| 80 | .bold() | ||
| 81 | .font(.footnote) | ||
| 82 | .frame(width: 140, height: 30) | ||
| 83 | .background(Color.red.cornerRadius(7)) | ||
| 84 | print("Error saving content to file: \(error)") | ||
| 85 | } | ||
| 86 | }) { | ||
| 87 | Text("Save") | ||
| 88 | } | ||
| 89 | // TODO: Confirm (possibly save) & then clear TextEditor | ||
| 90 | Button(action: { | ||
| 91 | print("TODO: This will create a new note.") | ||
| 92 | }) { | ||
| 93 | Text("New") | ||
| 94 | } | ||
| 95 | } | ||
| 96 | ToolbarItemGroup(placement: .secondaryAction) { | 65 | ToolbarItemGroup(placement: .secondaryAction) { |
| 97 | Button { | 66 | Button { |
| 98 | showingPopover = true | 67 | showingPopover = true |
| 99 | } label: { | 68 | } label: { |
| 100 | Text("App Info") | 69 | Text("More Info") |
| 101 | }.popover(isPresented: $showingPopover) { | 70 | }.popover(isPresented: $showingPopover) { |
| 102 | VStack(alignment: .leading) { | 71 | VStack(alignment: .leading) { |
| 103 | Text("App Info") | 72 | Text("More Info") |
| 104 | .font(.largeTitle) | 73 | .font(.largeTitle) |
| 105 | Text("") | 74 | Text("") |
| 106 | Text("Libre Edit is a free and open source text editor for iOS built by [Christian Cleberg](https://cleberg.net).") | 75 | Text("Instructions") |
| 76 | .font(.title) | ||
| 77 | Text("") | ||
| 78 | Text("LibreEdit provides a direct interface with the Apple Files app on your iPhone. Simply navigate to your preferred directory, edit existing files, or create new files!") | ||
| 79 | Text("") | ||
| 80 | Text("Developer") | ||
| 81 | .font(.title) | ||
| 82 | Text("") | ||
| 83 | Text("LibreEdit is a free and open source text editor for iOS built by [Christian Cleberg](https://cleberg.net).") | ||
| 107 | Text("") | 84 | Text("") |
| 108 | Text("Visit the [GitHub Repository](https://github.com/ccleberg/ios-edit) to view the source code.") | 85 | Text("Visit the [GitHub Repository](https://github.com/ccleberg/ios-edit) to view the source code.") |
| 109 | Text("") | 86 | Text("") |
| @@ -114,10 +91,5 @@ struct ContentView: View { | |||
| 114 | } | 91 | } |
| 115 | } | 92 | } |
| 116 | } | 93 | } |
| 117 | } | ||
| 118 | } | 94 | } |
| 119 | } | 95 | } |
| 120 | |||
| 121 | #Preview { | ||
| 122 | ContentView() | ||
| 123 | } | ||
LibreEdit/LibreEditApp.swift +2 −2
| @@ -10,8 +10,8 @@ import SwiftUI | |||
| 10 | @main | 10 | @main |
| 11 | struct LibreEditApp: App { | 11 | struct LibreEditApp: App { |
| 12 | var body: some Scene { | 12 | var body: some Scene { |
| 13 | WindowGroup { | 13 | DocumentGroup(newDocument: TextFile()) { file in |
| 14 | ContentView() | 14 | ContentView(document: file.$document) |
| 15 | } | 15 | } |
| 16 | } | 16 | } |
| 17 | } | 17 | } |