From 9f6a9251af64161d1b49f39a9e9d04814ad26b53 Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Wed, 8 Jul 2026 17:08:01 +0200 Subject: [PATCH 1/2] feat: add favorites management to the notes list Add a favorite toggle via leading swipe action and context menu, and sort favorites to the top within the current sort mode. Closes #52 Signed-off-by: Julius Knorr Assisted-by: ClaudeCode:claude-opus-4-8 --- iOCNotes/Views/NotesListModel.swift | 3 +++ iOCNotes/Views/NotesListView.swift | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/iOCNotes/Views/NotesListModel.swift b/iOCNotes/Views/NotesListModel.swift index f1d331c7..26c3351d 100644 --- a/iOCNotes/Views/NotesListModel.swift +++ b/iOCNotes/Views/NotesListModel.swift @@ -91,12 +91,15 @@ final class NotesListModel: NSObject, NSFetchedResultsControllerDelegate { request.predicate = predicate(for: searchText) if groupByCategory { + // "category" must stay first so it matches the section key path; favorites float to the top within each category. request.sortDescriptors = [ NSSortDescriptor(key: "category", ascending: true), + NSSortDescriptor(key: "favorite", ascending: false), NSSortDescriptor(key: "modified", ascending: false) ] } else { request.sortDescriptors = [ + NSSortDescriptor(key: "favorite", ascending: false), NSSortDescriptor(key: "modified", ascending: false) ] } diff --git a/iOCNotes/Views/NotesListView.swift b/iOCNotes/Views/NotesListView.swift index 91a2c192..8cea9145 100644 --- a/iOCNotes/Views/NotesListView.swift +++ b/iOCNotes/Views/NotesListView.swift @@ -82,6 +82,14 @@ struct NotesListView: View { .contextMenu { contextMenu(for: note) } + .swipeActions(edge: .leading) { + Button { + toggleFavorite(note) + } label: { + favoriteLabel(for: note) + } + .tint(.yellow) + } .swipeActions(edge: .trailing) { Button(role: .destructive) { delete(note) @@ -111,6 +119,12 @@ struct NotesListView: View { } } + Button { + toggleFavorite(note) + } label: { + favoriteLabel(for: note) + } + Button { NotesPresenter.share(note: note, exporter: &exporter) } label: { @@ -131,10 +145,24 @@ struct NotesListView: View { ) } + @ViewBuilder + private func favoriteLabel(for note: Note) -> some View { + if note.favorite { + Label(String(localized: "Remove from Favorites", comment: "Action to unmark a note as favorite"), systemImage: "star.slash") + } else { + Label(String(localized: "Favorite", comment: "Action to mark a note as favorite"), systemImage: "star") + } + } + private func delete(_ note: Note) { NoteSessionManager.shared.delete(note: note) } + private func toggleFavorite(_ note: Note) { + note.favorite.toggle() + NoteSessionManager.shared.update(note: note, completion: nil) + } + private func commitRename() { guard let note = noteToRename else { return } defer { noteToRename = nil } From fc88cbfa8311afdcf79b04074d679888ad63612d Mon Sep 17 00:00:00 2001 From: Julius Knorr Date: Thu, 9 Jul 2026 22:41:05 +0200 Subject: [PATCH 2/2] perf: keep modification date when toggling favorite Toggling favorite went through the generic update path, which stamped the note with the current time. In a date-sorted list that made the row jump and re-render a second time after the sync round-trip. Pass updateModified: false so a favorite toggle preserves the existing modification date. Signed-off-by: Julius Knorr Assisted-by: ClaudeCode:claude-opus-4-8 --- Networking/NoteSessionManager.swift | 11 +++++++---- iOCNotes/Views/NotesListView.swift | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Networking/NoteSessionManager.swift b/Networking/NoteSessionManager.swift index 60ba6e6f..5878c194 100644 --- a/Networking/NoteSessionManager.swift +++ b/Networking/NoteSessionManager.swift @@ -515,13 +515,13 @@ class NoteSessionManager { } } - func update(note: NoteProtocol, completion: SyncCompletionBlock? = nil) { + func update(note: NoteProtocol, updateModified: Bool = true, completion: SyncCompletionBlock? = nil) { logger.notice("Updating note...") var incoming = note incoming.updateNeeded = true if NoteSessionManager.isOnline { - updateOnServer(incoming) { [weak self] result in + updateOnServer(incoming, updateModified: updateModified) { [weak self] result in switch result { case .success( _): completion?() @@ -538,11 +538,14 @@ class NoteSessionManager { } } - fileprivate func updateOnServer(_ note: NoteProtocol, handler: @escaping SyncHandler) { + fileprivate func updateOnServer(_ note: NoteProtocol, updateModified: Bool = true, handler: @escaping SyncHandler) { + // Metadata-only changes (e.g. toggling favorite) keep the existing modification date so the note does + // not jump to the top of a date-sorted list and the row is not re-sorted a second time after the sync. + let modified = updateModified ? Date().timeIntervalSince1970 : note.modified let parameters: Parameters = ["title": note.title as Any, "content": note.content as Any, "category": note.category as Any, - "modified": Date().timeIntervalSince1970 as Any, + "modified": modified as Any, "favorite": note.favorite] let router = Router.updateNote(id: Int(note.id), paramters: parameters) session diff --git a/iOCNotes/Views/NotesListView.swift b/iOCNotes/Views/NotesListView.swift index 8cea9145..ea48eaf5 100644 --- a/iOCNotes/Views/NotesListView.swift +++ b/iOCNotes/Views/NotesListView.swift @@ -160,7 +160,7 @@ struct NotesListView: View { private func toggleFavorite(_ note: Note) { note.favorite.toggle() - NoteSessionManager.shared.update(note: note, completion: nil) + NoteSessionManager.shared.update(note: note, updateModified: false, completion: nil) } private func commitRename() {