krz/libre-edit

A free and private text editor for iOS. ios privacy swift text-editor

Commit b5a5dc8963

b5a5dc8963dbd73d30c188eb9f3018add76a2c06

parent: 9e2c7e0954

Verified · cmc

cmc <hello@cleberg.net> · 2024-01-14 17:09 UTC

feat: update app to use native file manager as the base

Layout: unified · split

LibreEdit/ContentView.swift +54 −82
@@ -6,104 +6,81 @@
66//
77
88import SwiftUI
9import UniformTypeIdentifiers
910
10func getDatetime() -> String {
11 let dateFormatter = DateFormatter()
12 let enUSPosixLocale = Locale(identifier: "en_US_POSIX")
13 dateFormatter.locale = enUSPosixLocale
14 dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
15 dateFormatter.calendar = Calendar(identifier: .gregorian)
11struct TextFile: FileDocument {
12 // tell the system we support only plain text
13 static var readableContentTypes = [UTType.plainText]
1614
17 let iso8601String = dateFormatter.string(from: Date())
15 // by default our document is empty
16 var text = ""
1817
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 }
2037}
2138
22// Ensure that `Supports Document Browser` = YES in the `ios-edit.xcodeproj` file,
23// so that users can browse saved files in the Files app
24func getDocumentsDirectory() -> URL {
25 let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
26 let documentsDirectory = paths[0]
27 return documentsDirectory
39func countWords(text: String) -> Int {
40 let words = text.split { $0 == " " || $0.isNewline }
41 return words.count
2842}
2943
3044struct ContentView: View {
31 enum FocusedField {
32 case content
33 }
34
35 @State private var content = ""
36 @State private var wordCount: Int = 0
45 @Binding var document: TextFile
3746 @State private var showingPopover = false
38 @FocusState private var focusedField: FocusedField?
39
47 @State private var wordCount: Int = 0
48
4049 var body: some View {
41 NavigationView {
42 VStack(alignment: .leading) {
43 Text("\(wordCount) words")
44 .font(.headline)
45 .foregroundColor(.secondary)
46 .padding(.trailing)
47 TextEditor(text: $content)
48 .onChange(of: content) {
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 }
50 Text("\(wordCount) words")
51 .foregroundColor(Color.white)
52 .font(.headline)
53 .padding(.trailing)
54 .frame(maxWidth: .infinity, maxHeight: 50)
55 .background(Color.gray.opacity(0.3))
56 .padding(.top, 20)
57 TextEditor(text: $document.text)
5558 .onAppear {
56 focusedField = .content
59 self.wordCount = countWords(text: document.text)
60 }
61 .onChange(of: document.text) {
62 self.wordCount = countWords(text: document.text)
5763 }
58 .padding()
59 .navigationTitle("Libre Edit")
6064 .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 }
9665 ToolbarItemGroup(placement: .secondaryAction) {
9766 Button {
9867 showingPopover = true
9968 } label: {
100 Text("App Info")
69 Text("More Info")
10170 }.popover(isPresented: $showingPopover) {
10271 VStack(alignment: .leading) {
103 Text("App Info")
72 Text("More Info")
10473 .font(.largeTitle)
10574 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).")
10784 Text("")
10885 Text("Visit the [GitHub Repository](https://github.com/ccleberg/ios-edit) to view the source code.")
10986 Text("")
@@ -114,10 +91,5 @@ struct ContentView: View {
11491 }
11592 }
11693 }
117 }
11894 }
11995}
120
121#Preview {
122 ContentView()
123}
LibreEdit/LibreEditApp.swift +2 −2
@@ -10,8 +10,8 @@ import SwiftUI
1010@main
1111struct LibreEditApp: App {
1212 var body: some Scene {
13 WindowGroup {
14 ContentView()
13 DocumentGroup(newDocument: TextFile()) { file in
14 ContentView(document: file.$document)
1515 }
1616 }
1717}