From 3e8d5eb025752039643f940c09e7ffb745dc3f9d Mon Sep 17 00:00:00 2001 From: Oschly Date: Thu, 14 May 2026 20:21:41 +0200 Subject: [PATCH] Bugfixes, renames, create custom TextEditor, logic simplifications --- ...ContentView.swift => AllNotesScreen.swift} | 25 +++----- ...rsView.swift => ManageMembersScreen.swift} | 2 +- .../NoteEditor/NoteEditingSessionServer.swift | 5 +- .../Peered/NoteEditor/NoteTextEditor.swift | 61 +++++++++++++++++++ .../Peered/NotesList/NoteMessage.swift | 4 ++ .../Peered/NotesList/NotesStorage.swift | 10 +-- Implementation/Peered/PeeredApp.swift | 2 +- .../SetUserNameBottomSheetView.swift | 2 +- 8 files changed, 83 insertions(+), 28 deletions(-) rename Implementation/Peered/{ContentView.swift => AllNotesScreen.swift} (75%) rename Implementation/Peered/NoteEditor/{ManageMembersView.swift => ManageMembersScreen.swift} (93%) create mode 100644 Implementation/Peered/NoteEditor/NoteTextEditor.swift create mode 100644 Implementation/Peered/NotesList/NoteMessage.swift diff --git a/Implementation/Peered/ContentView.swift b/Implementation/Peered/AllNotesScreen.swift similarity index 75% rename from Implementation/Peered/ContentView.swift rename to Implementation/Peered/AllNotesScreen.swift index 8f702bc..f044ea8 100644 --- a/Implementation/Peered/ContentView.swift +++ b/Implementation/Peered/AllNotesScreen.swift @@ -1,5 +1,5 @@ // -// ContentView.swift +// AllNotesScreen.swift // Peered // // Created by Oskar Chybowski on 11/05/2025. @@ -7,18 +7,14 @@ import SwiftUI -extension EnvironmentValues { - @Entry var ownPeer: OwnPeer = .fallback -} - -struct ContentView: View { - @AppStorage("peered_username") private var username: String = "fallback_user" +struct AllNotesScreen: View { + @AppStorage("peered_username") private var username: String? @State private var notes = [Note]() @State private var notesClient: NoteEditingSessionClient? @State private var ownPeer: OwnPeer? var isUsernameValid: Bool { - username != "fallback_user" && !username.isEmpty + username.map(\.isEmpty) ?? false } var body: some View { @@ -44,7 +40,6 @@ struct ContentView: View { } } } - .environment(\.ownPeer, ownPeer ?? .fallback) .navigationTitle("Peered") .toolbar { Button("Create note") { @@ -55,20 +50,20 @@ struct ContentView: View { } .onAppear { notes = NotesStorage().loadNotes() - if isUsernameValid { - setupSession() + if let username, isUsernameValid { + setupSession(username: username) } } .onChange(of: username) { _, newUsername in - guard isUsernameValid else { return } - setupSession() + guard let newUsername, isUsernameValid else { return } + setupSession(username: newUsername) } .sheet(isPresented: .constant(!isUsernameValid)) { SetUserNameBottomSheetView(username: $username) } } - private func setupSession() { + private func setupSession(username: String) { notesClient?.stopBrowsingForNotes() let peer = OwnPeer(peer: .init(displayName: username)) ownPeer = peer @@ -78,5 +73,5 @@ struct ContentView: View { } #Preview { - ContentView() + AllNotesScreen() } diff --git a/Implementation/Peered/NoteEditor/ManageMembersView.swift b/Implementation/Peered/NoteEditor/ManageMembersScreen.swift similarity index 93% rename from Implementation/Peered/NoteEditor/ManageMembersView.swift rename to Implementation/Peered/NoteEditor/ManageMembersScreen.swift index 8b703e0..907e669 100644 --- a/Implementation/Peered/NoteEditor/ManageMembersView.swift +++ b/Implementation/Peered/NoteEditor/ManageMembersScreen.swift @@ -6,7 +6,7 @@ // import SwiftUI -struct ManageMembersView: View { +struct ManageMembersScreen: View { @Bindable var noteAdvertiser: NoteEditingSessionServer let noteTitle: String @Binding var noteContent: String diff --git a/Implementation/Peered/NoteEditor/NoteEditingSessionServer.swift b/Implementation/Peered/NoteEditor/NoteEditingSessionServer.swift index d577c38..4c4e988 100644 --- a/Implementation/Peered/NoteEditor/NoteEditingSessionServer.swift +++ b/Implementation/Peered/NoteEditor/NoteEditingSessionServer.swift @@ -16,9 +16,7 @@ struct Peer: Identifiable { case invitationPending } - var id: String { - mcPeer.displayName - } + var id: String { mcPeer.displayName } let mcPeer: MCPeerID var state: ConnectionState } @@ -47,6 +45,7 @@ final class NoteEditingSessionServer: NSObject { func stopServer() { browser.stopBrowsingForPeers() + session.disconnect() } func invite(peer: Peer, to note: NoteInvitation.NoteContent) { diff --git a/Implementation/Peered/NoteEditor/NoteTextEditor.swift b/Implementation/Peered/NoteEditor/NoteTextEditor.swift new file mode 100644 index 0000000..0d32aa8 --- /dev/null +++ b/Implementation/Peered/NoteEditor/NoteTextEditor.swift @@ -0,0 +1,61 @@ +import SwiftUI + +struct NoteTextEditor: View { + @Binding var text: String + let remoteText: String? + @State private var selection: TextSelection? = nil + + var body: some View { + TextEditor(text: $text, selection: $selection) + .onChange(of: remoteText) { oldValue, newValue in + guard let newValue, newValue != text else { return } + + let previousRemote = oldValue ?? text + + // Where the remote edit begins + let changeStart = commonPrefixUTF16Length(previousRemote, newValue) + + // How many UTF-16 units were inserted (positive) or removed (negative) + let delta = newValue.utf16.count - previousRemote.utf16.count + + // Apply remote text first + text = newValue + + // Get current cursor offset + guard + let selection, + case .selection(let range) = selection.indices, + let cursorPos = range.lowerBound.samePosition(in: text.utf16) + else { return } + + let cursorUTF16 = text.utf16.distance( + from: text.utf16.startIndex, + to: cursorPos + ) + + // Only shift if the change happened strictly before the cursor + guard changeStart < cursorUTF16 else { return } + + let newCursorUTF16 = max(0, min(cursorUTF16 + delta, newValue.utf16.count)) + let newUTF16 = newValue.utf16 + let newUTF16Index = newUTF16.index(newUTF16.startIndex, offsetBy: newCursorUTF16) + guard let newIndex = newUTF16Index.samePosition(in: newValue) else { return } + + self.selection = TextSelection(range: newIndex.. Int { + var count = 0 + let aUTF16 = a.utf16 + let bUTF16 = b.utf16 + let minLen = min(aUTF16.count, bUTF16.count) + while count < minLen { + let aIdx = aUTF16.index(aUTF16.startIndex, offsetBy: count) + let bIdx = bUTF16.index(bUTF16.startIndex, offsetBy: count) + guard aUTF16[aIdx] == bUTF16[bIdx] else { break } + count += 1 + } + return count + } +} diff --git a/Implementation/Peered/NotesList/NoteMessage.swift b/Implementation/Peered/NotesList/NoteMessage.swift new file mode 100644 index 0000000..b0e6625 --- /dev/null +++ b/Implementation/Peered/NotesList/NoteMessage.swift @@ -0,0 +1,4 @@ +struct NoteMessage: Codable { + let senderID: String + let content: String +} diff --git a/Implementation/Peered/NotesList/NotesStorage.swift b/Implementation/Peered/NotesList/NotesStorage.swift index 60a7f29..f6702b3 100644 --- a/Implementation/Peered/NotesList/NotesStorage.swift +++ b/Implementation/Peered/NotesList/NotesStorage.swift @@ -36,8 +36,10 @@ struct NotesStorage { func createNote(name: String) { let currentNotes = loadNotes() - var proposedName = name var index: Int? = nil + var proposedName: String { + index.map { name + " \($0)" } ?? name + } while currentNotes.contains(where: { $0.name == proposedName }) { if let _index = index { @@ -46,12 +48,6 @@ struct NotesStorage { index = 1 } } - - proposedName = if let index { - "\(proposedName) \(index)" - } else { - proposedName - } let pathToWrite = URL.documentsDirectory.appendingPathComponent(proposedName).appendingPathExtension(for: .text) try! String().write(to: pathToWrite, atomically: true, encoding: .utf8) } diff --git a/Implementation/Peered/PeeredApp.swift b/Implementation/Peered/PeeredApp.swift index 75e18d3..b38214d 100644 --- a/Implementation/Peered/PeeredApp.swift +++ b/Implementation/Peered/PeeredApp.swift @@ -11,7 +11,7 @@ import SwiftUI struct PeeredApp: App { var body: some Scene { WindowGroup { - ContentView() + AllNotesScreen() } } } diff --git a/Implementation/Peered/SetUsername/SetUserNameBottomSheetView.swift b/Implementation/Peered/SetUsername/SetUserNameBottomSheetView.swift index ab07a57..c3b25d6 100644 --- a/Implementation/Peered/SetUsername/SetUserNameBottomSheetView.swift +++ b/Implementation/Peered/SetUsername/SetUserNameBottomSheetView.swift @@ -2,7 +2,7 @@ import SwiftUI struct SetUserNameBottomSheetView: View { @State private var proposedUsername: String = "" - @Binding var username: String + @Binding var username: String? var body: some View { Form {