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/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..ea48eaf5 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, updateModified: false, completion: nil) + } + private func commitRename() { guard let note = noteToRename else { return } defer { noteToRename = nil }