Bugfixes, renames, create custom TextEditor, logic simplifications

This commit is contained in:
2026-05-14 20:21:41 +02:00
parent 23fd16f88e
commit 3e8d5eb025
8 changed files with 83 additions and 28 deletions
@@ -1,5 +1,5 @@
// //
// ContentView.swift // AllNotesScreen.swift
// Peered // Peered
// //
// Created by Oskar Chybowski on 11/05/2025. // Created by Oskar Chybowski on 11/05/2025.
@@ -7,18 +7,14 @@
import SwiftUI import SwiftUI
extension EnvironmentValues { struct AllNotesScreen: View {
@Entry var ownPeer: OwnPeer = .fallback @AppStorage("peered_username") private var username: String?
}
struct ContentView: View {
@AppStorage("peered_username") private var username: String = "fallback_user"
@State private var notes = [Note]() @State private var notes = [Note]()
@State private var notesClient: NoteEditingSessionClient? @State private var notesClient: NoteEditingSessionClient?
@State private var ownPeer: OwnPeer? @State private var ownPeer: OwnPeer?
var isUsernameValid: Bool { var isUsernameValid: Bool {
username != "fallback_user" && !username.isEmpty username.map(\.isEmpty) ?? false
} }
var body: some View { var body: some View {
@@ -44,7 +40,6 @@ struct ContentView: View {
} }
} }
} }
.environment(\.ownPeer, ownPeer ?? .fallback)
.navigationTitle("Peered") .navigationTitle("Peered")
.toolbar { .toolbar {
Button("Create note") { Button("Create note") {
@@ -55,20 +50,20 @@ struct ContentView: View {
} }
.onAppear { .onAppear {
notes = NotesStorage().loadNotes() notes = NotesStorage().loadNotes()
if isUsernameValid { if let username, isUsernameValid {
setupSession() setupSession(username: username)
} }
} }
.onChange(of: username) { _, newUsername in .onChange(of: username) { _, newUsername in
guard isUsernameValid else { return } guard let newUsername, isUsernameValid else { return }
setupSession() setupSession(username: newUsername)
} }
.sheet(isPresented: .constant(!isUsernameValid)) { .sheet(isPresented: .constant(!isUsernameValid)) {
SetUserNameBottomSheetView(username: $username) SetUserNameBottomSheetView(username: $username)
} }
} }
private func setupSession() { private func setupSession(username: String) {
notesClient?.stopBrowsingForNotes() notesClient?.stopBrowsingForNotes()
let peer = OwnPeer(peer: .init(displayName: username)) let peer = OwnPeer(peer: .init(displayName: username))
ownPeer = peer ownPeer = peer
@@ -78,5 +73,5 @@ struct ContentView: View {
} }
#Preview { #Preview {
ContentView() AllNotesScreen()
} }
@@ -6,7 +6,7 @@
// //
import SwiftUI import SwiftUI
struct ManageMembersView: View { struct ManageMembersScreen: View {
@Bindable var noteAdvertiser: NoteEditingSessionServer @Bindable var noteAdvertiser: NoteEditingSessionServer
let noteTitle: String let noteTitle: String
@Binding var noteContent: String @Binding var noteContent: String
@@ -16,9 +16,7 @@ struct Peer: Identifiable {
case invitationPending case invitationPending
} }
var id: String { var id: String { mcPeer.displayName }
mcPeer.displayName
}
let mcPeer: MCPeerID let mcPeer: MCPeerID
var state: ConnectionState var state: ConnectionState
} }
@@ -47,6 +45,7 @@ final class NoteEditingSessionServer: NSObject {
func stopServer() { func stopServer() {
browser.stopBrowsingForPeers() browser.stopBrowsingForPeers()
session.disconnect()
} }
func invite(peer: Peer, to note: NoteInvitation.NoteContent) { func invite(peer: Peer, to note: NoteInvitation.NoteContent) {
@@ -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..<newIndex)
}
}
private func commonPrefixUTF16Length(_ a: String, _ b: String) -> 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
}
}
@@ -0,0 +1,4 @@
struct NoteMessage: Codable {
let senderID: String
let content: String
}
@@ -36,8 +36,10 @@ struct NotesStorage {
func createNote(name: String) { func createNote(name: String) {
let currentNotes = loadNotes() let currentNotes = loadNotes()
var proposedName = name
var index: Int? = nil var index: Int? = nil
var proposedName: String {
index.map { name + " \($0)" } ?? name
}
while currentNotes.contains(where: { $0.name == proposedName }) { while currentNotes.contains(where: { $0.name == proposedName }) {
if let _index = index { if let _index = index {
@@ -46,12 +48,6 @@ struct NotesStorage {
index = 1 index = 1
} }
} }
proposedName = if let index {
"\(proposedName) \(index)"
} else {
proposedName
}
let pathToWrite = URL.documentsDirectory.appendingPathComponent(proposedName).appendingPathExtension(for: .text) let pathToWrite = URL.documentsDirectory.appendingPathComponent(proposedName).appendingPathExtension(for: .text)
try! String().write(to: pathToWrite, atomically: true, encoding: .utf8) try! String().write(to: pathToWrite, atomically: true, encoding: .utf8)
} }
+1 -1
View File
@@ -11,7 +11,7 @@ import SwiftUI
struct PeeredApp: App { struct PeeredApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() AllNotesScreen()
} }
} }
} }
@@ -2,7 +2,7 @@ import SwiftUI
struct SetUserNameBottomSheetView: View { struct SetUserNameBottomSheetView: View {
@State private var proposedUsername: String = "" @State private var proposedUsername: String = ""
@Binding var username: String @Binding var username: String?
var body: some View { var body: some View {
Form { Form {