Skip to content

Commit 7605f11

Browse files
committed
#351 add: allow notes to be marked as favorite
Signed-off-by: Patrizio Bekerle <patrizio@bekerle.com>
1 parent 782485b commit 7605f11

File tree

6 files changed

+240
-4
lines changed

6 files changed

+240
-4
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# QOwnNotes Changelog
22

3+
## 25.12.5
4+
5+
- Added ability to **mark notes as favorite** (for [#351](https://github.com/pbek/QOwnNotes/issues/351))
6+
- Right-click on a note and select "Mark as favorite" or "Unmark as favorite"
7+
- Favorite notes are displayed with a star icon in the note tree widget
8+
- Favorite notes are automatically positioned at the top of the note list
9+
- Favorite status is stored per note folder in local settings
10+
- The feature uses subfolder path and filename to identify notes (persistent across restarts)
11+
- Works with all sorting modes (alphabetical, by date)
12+
- **Automatic migration**: When a note is moved, renamed, or its headline changes
13+
(triggering auto-rename), favorite status is preserved
14+
- **Automatic cleanup**: When a note is deleted, it's removed from the favorites list
15+
- **Periodic cleanup**: Non-existent notes are automatically removed from favorites on note list refresh
16+
317
## 25.12.4
418

519
- Fixed an issue where external changes to recently edited notes were silently

src/entities/note.cpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,119 @@ QString Note::getNextcloudNotesLink() const {
212212
return link;
213213
}
214214

215+
/**
216+
* @brief Get a unique identifier for this note (subfolder path + filename)
217+
* @return Unique identifier string
218+
*/
219+
QString Note::getFavoriteIdentifier() const {
220+
// Combine subfolder path and filename to create a unique identifier
221+
QString subFolderPath = relativeNoteSubFolderPath();
222+
if (subFolderPath.isEmpty()) {
223+
return _fileName;
224+
}
225+
return subFolderPath + QStringLiteral("/") + _fileName;
226+
}
227+
228+
/**
229+
* @brief Check if this note is marked as favorite
230+
* @return true if the note is favorite, false otherwise
231+
*/
232+
bool Note::isFavorite() const {
233+
const QStringList favoriteIdentifiers = getFavoriteNoteIdentifiers();
234+
return favoriteIdentifiers.contains(getFavoriteIdentifier());
235+
}
236+
237+
/**
238+
* @brief Toggle the favorite status of this note
239+
* @return true if the note is now favorite, false otherwise
240+
*/
241+
bool Note::toggleFavorite() {
242+
NoteFolder noteFolder = NoteFolder::currentNoteFolder();
243+
QStringList favoriteIdentifiers = getFavoriteNoteIdentifiers();
244+
QString identifier = getFavoriteIdentifier();
245+
246+
bool wasFavorite = favoriteIdentifiers.contains(identifier);
247+
248+
if (wasFavorite) {
249+
// Remove from favorites
250+
favoriteIdentifiers.removeAll(identifier);
251+
} else {
252+
// Add to favorites
253+
favoriteIdentifiers.append(identifier);
254+
}
255+
256+
// Store the updated list
257+
noteFolder.setSettingsValue(QStringLiteral("favoriteNoteIdentifiers"), favoriteIdentifiers);
258+
259+
return !wasFavorite;
260+
}
261+
262+
/**
263+
* @brief Get the list of favorite note identifiers for the current note folder
264+
* @return List of favorite note identifiers (subfolderPath/filename)
265+
*/
266+
QStringList Note::getFavoriteNoteIdentifiers() {
267+
NoteFolder noteFolder = NoteFolder::currentNoteFolder();
268+
return noteFolder.settingsValue(QStringLiteral("favoriteNoteIdentifiers")).toStringList();
269+
}
270+
271+
/**
272+
* @brief Migrate a favorite note identifier when a note is moved
273+
* @param oldIdentifier The old identifier (before the move)
274+
*/
275+
void Note::migrateFavoriteIdentifier(const QString &oldIdentifier) {
276+
QStringList favoriteIdentifiers = getFavoriteNoteIdentifiers();
277+
278+
// Check if the old identifier was in the favorites list
279+
if (favoriteIdentifiers.contains(oldIdentifier)) {
280+
// Remove the old identifier
281+
favoriteIdentifiers.removeAll(oldIdentifier);
282+
283+
// Add the new identifier
284+
QString newIdentifier = getFavoriteIdentifier();
285+
if (!favoriteIdentifiers.contains(newIdentifier)) {
286+
favoriteIdentifiers.append(newIdentifier);
287+
}
288+
289+
// Store the updated list
290+
NoteFolder noteFolder = NoteFolder::currentNoteFolder();
291+
noteFolder.setSettingsValue(QStringLiteral("favoriteNoteIdentifiers"), favoriteIdentifiers);
292+
293+
qDebug() << __func__ << "Migrated favorite from" << oldIdentifier << "to" << newIdentifier;
294+
}
295+
}
296+
297+
/**
298+
* @brief Clean up favorite notes list by removing entries for notes that no longer exist
299+
*/
300+
void Note::cleanupFavoriteNotes() {
301+
NoteFolder noteFolder = NoteFolder::currentNoteFolder();
302+
QStringList favoriteIdentifiers = getFavoriteNoteIdentifiers();
303+
QStringList validIdentifiers;
304+
305+
QString noteFolderPath = noteFolder.getLocalPath();
306+
307+
for (const QString &identifier : favoriteIdentifiers) {
308+
// Build the full file path
309+
QString filePath = noteFolderPath + QDir::separator() + identifier;
310+
311+
// Check if the file exists
312+
QFile file(filePath);
313+
if (file.exists()) {
314+
validIdentifiers.append(identifier);
315+
} else {
316+
qDebug() << __func__ << "Removing non-existent favorite:" << identifier;
317+
}
318+
}
319+
320+
// Only update if something changed
321+
if (validIdentifiers.count() != favoriteIdentifiers.count()) {
322+
noteFolder.setSettingsValue(QStringLiteral("favoriteNoteIdentifiers"), validIdentifiers);
323+
qDebug() << __func__ << "Cleaned up"
324+
<< (favoriteIdentifiers.count() - validIdentifiers.count()) << "favorite entries";
325+
}
326+
}
327+
215328
void Note::setCryptoKey(const qint64 cryptoKey) { this->_cryptoKey = cryptoKey; }
216329

217330
void Note::setNoteText(QString text) { this->_noteText = std::move(text); }
@@ -363,6 +476,17 @@ bool Note::remove(bool withFile) {
363476
return false;
364477
} else {
365478
if (withFile) {
479+
// Remove from favorites if it was marked as favorite
480+
QString identifier = getFavoriteIdentifier();
481+
QStringList favoriteIdentifiers = getFavoriteNoteIdentifiers();
482+
if (favoriteIdentifiers.contains(identifier)) {
483+
favoriteIdentifiers.removeAll(identifier);
484+
NoteFolder noteFolder = NoteFolder::currentNoteFolder();
485+
noteFolder.setSettingsValue(QStringLiteral("favoriteNoteIdentifiers"),
486+
favoriteIdentifiers);
487+
qDebug() << __func__ << "Removed favorite:" << identifier;
488+
}
489+
366490
this->removeNoteFile();
367491

368492
// remove all links to tags
@@ -3876,6 +4000,13 @@ bool Note::handleNoteMoving(Note oldNote) {
38764000
const int noteCount = noteIdList.count();
38774001
bool result = false;
38784002

4003+
// Migrate favorite status if the note was moved to a different subfolder or renamed
4004+
QString oldIdentifier = oldNote.getFavoriteIdentifier();
4005+
QString newIdentifier = getFavoriteIdentifier();
4006+
if (oldIdentifier != newIdentifier) {
4007+
migrateFavoriteIdentifier(oldIdentifier);
4008+
}
4009+
38794010
// Handle incoming note links
38804011
if (noteCount > 0) {
38814012
result = handleBacklinkedNotesAfterMoving(oldNote, noteIdList);

src/entities/note.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,18 @@ class Note {
410410

411411
QString getNextcloudNotesLink() const;
412412

413+
bool isFavorite() const;
414+
415+
bool toggleFavorite();
416+
417+
static QStringList getFavoriteNoteIdentifiers();
418+
419+
QString getFavoriteIdentifier() const;
420+
421+
void migrateFavoriteIdentifier(const QString &oldIdentifier);
422+
423+
static void cleanupFavoriteNotes();
424+
413425
protected:
414426
int _id;
415427
int _noteSubFolderId;

src/mainwindow.cpp

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2190,6 +2190,9 @@ QIcon MainWindow::getSystemTrayIcon() {
21902190
void MainWindow::loadNoteDirectoryList() {
21912191
qDebug() << __func__;
21922192

2193+
// Clean up favorite notes list (remove entries for deleted notes)
2194+
Note::cleanupFavoriteNotes();
2195+
21932196
const QSignalBlocker blocker(ui->noteTextEdit);
21942197
Q_UNUSED(blocker)
21952198

@@ -2280,7 +2283,17 @@ bool MainWindow::addNoteToNoteTreeWidget(const Note &note, QTreeWidgetItem *pare
22802283
noteItem->setText(0, name);
22812284
noteItem->setData(0, Qt::UserRole, note.getId());
22822285
noteItem->setData(0, Qt::UserRole + 1, NoteType);
2283-
noteItem->setIcon(0, Utils::Gui::noteIcon());
2286+
2287+
// Store favorite status for sorting (UserRole + 2)
2288+
const bool isFavorite = note.isFavorite();
2289+
noteItem->setData(0, Qt::UserRole + 2, isFavorite);
2290+
2291+
// Set the icon based on favorite status
2292+
if (isFavorite) {
2293+
noteItem->setIcon(0, Utils::Gui::favoriteNoteIcon());
2294+
} else {
2295+
noteItem->setIcon(0, Utils::Gui::noteIcon());
2296+
}
22842297

22852298
const Tag tag = Tag::fetchOneOfNoteWithColor(note);
22862299
if (tag.isFetched()) {
@@ -2297,8 +2310,26 @@ bool MainWindow::addNoteToNoteTreeWidget(const Note &note, QTreeWidgetItem *pare
22972310
Q_UNUSED(blocker)
22982311

22992312
if (parent == nullptr) {
2300-
// strange things happen if we insert with insertTopLevelItem
2301-
ui->noteTreeWidget->addTopLevelItem(noteItem);
2313+
// Insert notes with favorites at the top
2314+
// Find the insertion position based on favorite status
2315+
if (isFavorite) {
2316+
// For favorite notes, find the last position of favorites
2317+
int insertPos = 0;
2318+
const int count = ui->noteTreeWidget->topLevelItemCount();
2319+
for (int i = 0; i < count; ++i) {
2320+
QTreeWidgetItem *existingItem = ui->noteTreeWidget->topLevelItem(i);
2321+
if (existingItem->data(0, Qt::UserRole + 1) == NoteType &&
2322+
existingItem->data(0, Qt::UserRole + 2).toBool()) {
2323+
insertPos = i + 1;
2324+
} else {
2325+
break;
2326+
}
2327+
}
2328+
ui->noteTreeWidget->insertTopLevelItem(insertPos, noteItem);
2329+
} else {
2330+
// Non-favorite notes go after all favorites
2331+
ui->noteTreeWidget->addTopLevelItem(noteItem);
2332+
}
23022333
} else {
23032334
parent->addChild(noteItem);
23042335
}
@@ -2355,7 +2386,27 @@ void MainWindow::makeCurrentNoteFirstInNoteList() {
23552386
Q_UNUSED(blocker)
23562387

23572388
ui->noteTreeWidget->takeTopLevelItem(ui->noteTreeWidget->indexOfTopLevelItem(item));
2358-
ui->noteTreeWidget->insertTopLevelItem(0, item);
2389+
2390+
// Determine insertion position based on favorite status
2391+
const bool isFavorite = currentNote.isFavorite();
2392+
int insertPos = 0;
2393+
2394+
if (!isFavorite) {
2395+
// Non-favorite note: insert after all favorites
2396+
const int count = ui->noteTreeWidget->topLevelItemCount();
2397+
for (int i = 0; i < count; ++i) {
2398+
QTreeWidgetItem *existingItem = ui->noteTreeWidget->topLevelItem(i);
2399+
if (existingItem->data(0, Qt::UserRole + 1) == NoteType &&
2400+
existingItem->data(0, Qt::UserRole + 2).toBool()) {
2401+
insertPos = i + 1;
2402+
} else {
2403+
break;
2404+
}
2405+
}
2406+
}
2407+
// For favorite notes, insertPos stays at 0 (first position)
2408+
2409+
ui->noteTreeWidget->insertTopLevelItem(insertPos, item);
23592410

23602411
// set the item as current item if it is visible
23612412
if (!item->isHidden()) {
@@ -9889,6 +9940,7 @@ void MainWindow::openNotesContextMenu(const QPoint globalPos, bool multiNoteMenu
98899940
QAction *showInFileManagerAction = nullptr;
98909941
QAction *showNoteGitLogAction = nullptr;
98919942
QAction *copyNotePathToClipboardAction = nullptr;
9943+
QAction *toggleFavoriteAction = nullptr;
98929944

98939945
if (!multiNoteMenuEntriesOnly) {
98949946
noteMenu.addSeparator();
@@ -9914,6 +9966,17 @@ void MainWindow::openNotesContextMenu(const QPoint globalPos, bool multiNoteMenu
99149966
if (Utils::Git::isCurrentNoteFolderUseGit() && Utils::Git::hasLogCommand()) {
99159967
showNoteGitLogAction = noteMenu.addAction(tr("Show note git versions"));
99169968
}
9969+
9970+
// Add favorite toggle action for single note
9971+
noteMenu.addSeparator();
9972+
const Note note = getCurrentNote();
9973+
if (note.isFetched()) {
9974+
if (note.isFavorite()) {
9975+
toggleFavoriteAction = noteMenu.addAction(tr("Unmark as favorite"));
9976+
} else {
9977+
toggleFavoriteAction = noteMenu.addAction(tr("Mark as favorite"));
9978+
}
9979+
}
99179980
}
99189981

99199982
// add the custom actions to the context menu
@@ -9981,6 +10044,11 @@ void MainWindow::openNotesContextMenu(const QPoint globalPos, bool multiNoteMenu
998110044
} else if (selectedItem == showNoteGitLogAction) {
998210045
// show the git log of the current note
998310046
on_actionShow_note_git_versions_triggered();
10047+
} else if (selectedItem == toggleFavoriteAction) {
10048+
// toggle favorite status of the current note
10049+
currentNote.toggleFavorite();
10050+
// Reload the note list to update the icon and sorting
10051+
loadNoteDirectoryList();
998410052
} else if (selectedItem == renameAction) {
998510053
QTreeWidgetItem *item = ui->noteTreeWidget->currentItem();
998610054

src/utils/gui.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,16 @@ QIcon Utils::Gui::noteIcon() {
11601160
return s_noteIcon;
11611161
}
11621162

1163+
QIcon Utils::Gui::favoriteNoteIcon() {
1164+
// Try different common star icon names
1165+
static const QIcon s_favoriteNoteIcon = QIcon::fromTheme(
1166+
QStringLiteral("starred-symbolic"),
1167+
QIcon::fromTheme(QStringLiteral("emblem-favorite"),
1168+
QIcon::fromTheme(QStringLiteral("star"),
1169+
noteIcon()))); // Fallback to regular note icon
1170+
return s_favoriteNoteIcon;
1171+
}
1172+
11631173
QIcon Utils::Gui::tagIcon() {
11641174
static const QIcon s_tagIcon = QIcon::fromTheme(
11651175
QStringLiteral("tag"), QIcon(QStringLiteral(":/icons/breeze-qownnotes/16x16/tag.svg")));

src/utils/gui.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ bool doLinuxDarkModeCheck();
141141

142142
QIcon folderIcon();
143143
QIcon noteIcon();
144+
QIcon favoriteNoteIcon();
144145
QIcon tagIcon();
145146

146147
/**

0 commit comments

Comments
 (0)