mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
448 lines
11 KiB
JavaScript
448 lines
11 KiB
JavaScript
/*
|
|
This file is part of the Notesnook project (https://notesnook.com/)
|
|
|
|
Copyright (C) 2022 Streetwriters (Private) Limited
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import Collection from "./collection";
|
|
import Note from "../models/note";
|
|
import getId from "../utils/id";
|
|
import { getContentFromData } from "../content-types";
|
|
import qclone from "qclone";
|
|
import { deleteItem, findById } from "../utils/array";
|
|
|
|
/**
|
|
* @typedef {{ id: string, topic: string, rebuildCache?: boolean }} NotebookReference
|
|
*/
|
|
|
|
export default class Notes extends Collection {
|
|
constructor(db, name, cached) {
|
|
super(db, name, cached);
|
|
this.topicReferences = new NoteIdCache(this._db);
|
|
}
|
|
|
|
async init() {
|
|
await super.init();
|
|
this.topicReferences.rebuild();
|
|
}
|
|
|
|
async merge(remoteNote) {
|
|
if (!remoteNote) return;
|
|
|
|
const id = remoteNote.id;
|
|
const localNote = this._collection.getItem(id);
|
|
if (localNote) {
|
|
if (localNote.color) await this._db.colors.untag(localNote.color, id);
|
|
|
|
for (let tag of localNote.tags || []) {
|
|
await this._db.tags.untag(tag, id);
|
|
}
|
|
}
|
|
|
|
if (remoteNote.deleted) return await this._collection.addItem(remoteNote);
|
|
|
|
await this._resolveColorAndTags(remoteNote);
|
|
|
|
return await this._collection.addItem(remoteNote);
|
|
}
|
|
|
|
async add(noteArg) {
|
|
if (!noteArg) return;
|
|
if (noteArg.remote)
|
|
throw new Error("Please use db.notes.merge to merge remote notes.");
|
|
|
|
let id = noteArg.id || getId();
|
|
let oldNote = this._collection.getItem(id);
|
|
|
|
let note = {
|
|
...oldNote,
|
|
...noteArg
|
|
};
|
|
|
|
if (oldNote) note.contentId = oldNote.contentId;
|
|
|
|
if (!oldNote && !noteArg.content && !noteArg.contentId && !noteArg.title)
|
|
return;
|
|
|
|
if (noteArg.content && noteArg.content.data && noteArg.content.type) {
|
|
const { type, data } = noteArg.content;
|
|
|
|
let content = getContentFromData(type, data);
|
|
if (!content) throw new Error("Invalid content type.");
|
|
|
|
note.contentId = await this._db.content.add({
|
|
noteId: id,
|
|
sessionId: note.sessionId,
|
|
id: note.contentId,
|
|
type,
|
|
data,
|
|
localOnly: !!note.localOnly
|
|
});
|
|
|
|
note.headline = getNoteHeadline(note, content);
|
|
if (oldNote) note.dateEdited = Date.now();
|
|
}
|
|
|
|
if (noteArg.localOnly !== undefined) {
|
|
await this._db.content.add({
|
|
id: note.contentId,
|
|
localOnly: !!noteArg.localOnly
|
|
});
|
|
}
|
|
|
|
const noteTitle = getNoteTitle(note, oldNote);
|
|
if (oldNote && oldNote.title !== noteTitle) note.dateEdited = Date.now();
|
|
|
|
note = {
|
|
id,
|
|
contentId: note.contentId,
|
|
type: "note",
|
|
|
|
title: noteTitle,
|
|
headline: note.headline,
|
|
|
|
tags: note.tags || [],
|
|
notebooks: note.notebooks || undefined,
|
|
color: note.color,
|
|
|
|
pinned: !!note.pinned,
|
|
locked: !!note.locked,
|
|
favorite: !!note.favorite,
|
|
localOnly: !!note.localOnly,
|
|
conflicted: !!note.conflicted,
|
|
readonly: !!note.readonly,
|
|
|
|
dateCreated: note.dateCreated,
|
|
dateEdited: note.dateEdited || note.dateCreated || Date.now(),
|
|
dateModified: note.dateModified
|
|
};
|
|
|
|
await this._collection.addItem(note);
|
|
|
|
await this._resolveColorAndTags(note);
|
|
return note.id;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} id The id of note
|
|
* @returns {Note} The note of the given id
|
|
*/
|
|
note(id) {
|
|
if (!id) return;
|
|
let note = id.type ? id : this._collection.getItem(id);
|
|
if (!note || note.deleted) return;
|
|
return new Note(note, this._db);
|
|
}
|
|
|
|
get raw() {
|
|
return this._collection.getRaw();
|
|
}
|
|
|
|
/**
|
|
* @returns {any[]}
|
|
*/
|
|
get all() {
|
|
const items = this._collection.getItems();
|
|
return items;
|
|
}
|
|
|
|
get pinned() {
|
|
return this.all.filter((item) => item.pinned === true);
|
|
}
|
|
|
|
get conflicted() {
|
|
return this.all.filter((item) => item.conflicted === true);
|
|
}
|
|
|
|
get favorites() {
|
|
return this.all.filter((item) => item.favorite === true);
|
|
}
|
|
|
|
get deleted() {
|
|
return this.raw.filter((item) => item.dateDeleted > 0);
|
|
}
|
|
|
|
get locked() {
|
|
return this.all.filter((item) => item.locked === true);
|
|
}
|
|
|
|
tagged(tagId) {
|
|
return this._getTagItems(tagId, "tags");
|
|
}
|
|
|
|
colored(colorId) {
|
|
return this._getTagItems(colorId, "colors");
|
|
}
|
|
|
|
exists(id) {
|
|
return this._collection.exists(id);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getTagItems(tagId, collection) {
|
|
const tag = this._db[collection].tag(tagId);
|
|
if (!tag || tag.noteIds.length <= 0) return [];
|
|
const array = tag.noteIds.reduce((arr, id) => {
|
|
const item = this.note(id);
|
|
if (item) arr.push(item.data);
|
|
return arr;
|
|
}, []);
|
|
return array.sort((a, b) => b.dateCreated - a.dateCreated);
|
|
}
|
|
|
|
delete(...ids) {
|
|
return this._delete(true, ...ids);
|
|
}
|
|
|
|
remove(...ids) {
|
|
return this._delete(false, ...ids);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
async _delete(moveToTrash = true, ...ids) {
|
|
for (let id of ids) {
|
|
let item = this.note(id);
|
|
if (!item) continue;
|
|
const itemData = qclone(item.data);
|
|
|
|
if (itemData.notebooks && !moveToTrash) {
|
|
for (let notebook of itemData.notebooks) {
|
|
for (let topicId of notebook.topics) {
|
|
await this.removeFromNotebook(
|
|
{ id: notebook.id, topic: topicId, rebuildCache: false },
|
|
id
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let tag of itemData.tags) {
|
|
await this._db.tags.untag(tag, id);
|
|
}
|
|
|
|
if (itemData.color) {
|
|
await this._db.colors.untag(itemData.color, id);
|
|
}
|
|
|
|
const attachments = this._db.attachments.ofNote(itemData.id, "all");
|
|
for (let attachment of attachments) {
|
|
await this._db.attachments.delete(
|
|
attachment.metadata.hash,
|
|
itemData.id
|
|
);
|
|
}
|
|
|
|
// await this._collection.removeItem(id);
|
|
if (moveToTrash) await this._db.trash.add(itemData);
|
|
else {
|
|
await this._collection.removeItem(id);
|
|
await this._db.content.remove(itemData.contentId);
|
|
}
|
|
}
|
|
this.topicReferences.rebuild();
|
|
}
|
|
|
|
async _resolveColorAndTags(note) {
|
|
const { color, tags, id } = note;
|
|
|
|
if (color) {
|
|
const addedColor = await this._db.colors.add(color, id);
|
|
if (addedColor) note.color = addedColor.title;
|
|
}
|
|
|
|
if (tags && tags.length) {
|
|
for (let i = 0; i < tags.length; ++i) {
|
|
const tag = tags[i];
|
|
const addedTag = await this._db.tags.add(tag, id).catch(() => void 0);
|
|
if (!addedTag) {
|
|
tags.splice(i, 1);
|
|
continue;
|
|
}
|
|
if (addedTag.title !== tag) tags[i] = addedTag.title;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {NotebookReference} to
|
|
*/
|
|
async addToNotebook(to, ...noteIds) {
|
|
if (!to) throw new Error("The destination notebook cannot be undefined.");
|
|
if (!to.id || !to.topic)
|
|
throw new Error(
|
|
"The destination notebook must contain notebookId and topic."
|
|
);
|
|
|
|
const { id: notebookId, topic: topicId } = to;
|
|
|
|
for (let noteId of noteIds) {
|
|
let note = this._db.notes.note(noteId);
|
|
if (!note || note.data.deleted) continue;
|
|
|
|
const notebooks = note.notebooks || [];
|
|
|
|
const noteNotebook = notebooks.find((nb) => nb.id === notebookId);
|
|
const noteHasNotebook = !!noteNotebook;
|
|
const noteHasTopic =
|
|
noteHasNotebook && noteNotebook.topics.indexOf(topicId) > -1;
|
|
if (noteHasNotebook && !noteHasTopic) {
|
|
// 1 note can be inside multiple topics
|
|
noteNotebook.topics.push(topicId);
|
|
} else if (!noteHasNotebook) {
|
|
notebooks.push({
|
|
id: notebookId,
|
|
topics: [topicId]
|
|
});
|
|
}
|
|
|
|
if (!noteHasNotebook || !noteHasTopic) {
|
|
await this._db.notes.add({
|
|
id: noteId,
|
|
notebooks
|
|
});
|
|
this.topicReferences.add(topicId, noteId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {NotebookReference} to
|
|
*/
|
|
async removeFromNotebook(to, ...noteIds) {
|
|
if (!to) throw new Error("The destination notebook cannot be undefined.");
|
|
if (!to.id || !to.topic)
|
|
throw new Error(
|
|
"The destination notebook must contain notebookId and topic."
|
|
);
|
|
|
|
const { id: notebookId, topic: topicId, rebuildCache = true } = to;
|
|
|
|
for (const noteId of noteIds) {
|
|
const note = this.note(noteId);
|
|
if (!note || note.deleted || !note.notebooks) {
|
|
continue;
|
|
}
|
|
|
|
const { notebooks } = note;
|
|
|
|
const notebook = findById(notebooks, notebookId);
|
|
if (!notebook) continue;
|
|
|
|
const { topics } = notebook;
|
|
if (!deleteItem(topics, topicId)) continue;
|
|
|
|
if (topics.length <= 0) deleteItem(notebooks, notebook);
|
|
|
|
await this._db.notes.add({
|
|
id: noteId,
|
|
notebooks
|
|
});
|
|
}
|
|
if (rebuildCache) this.topicReferences.rebuild();
|
|
}
|
|
|
|
async _clearAllNotebookReferences(notebookId) {
|
|
const notes = this._db.notes.all;
|
|
|
|
for (const note of notes) {
|
|
const { notebooks } = note;
|
|
if (!notebooks) continue;
|
|
|
|
for (let notebook of notebooks) {
|
|
if (notebook.id !== notebookId) continue;
|
|
if (!deleteItem(notebooks, notebook)) continue;
|
|
}
|
|
|
|
await this._collection.updateItem(note);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getNoteHeadline(note, content) {
|
|
if (note.locked) return "";
|
|
return content.toHeadline();
|
|
}
|
|
|
|
const NEWLINE_STRIP_REGEX = /[\r\n\t\v]+/gm;
|
|
function getNoteTitle(note, oldNote) {
|
|
if (note.title && note.title.trim().length > 0) {
|
|
return note.title.replace(NEWLINE_STRIP_REGEX, " ");
|
|
} else if (oldNote && oldNote.title && oldNote.title.trim().length > 0) {
|
|
return oldNote.title.replace(NEWLINE_STRIP_REGEX, " ");
|
|
}
|
|
|
|
return `Note ${new Date().toLocaleString(undefined, {
|
|
dateStyle: "short",
|
|
timeStyle: "short"
|
|
})}`;
|
|
}
|
|
|
|
class NoteIdCache {
|
|
/**
|
|
*
|
|
* @param {import("../api/index").default} db
|
|
*/
|
|
constructor(db) {
|
|
this._db = db;
|
|
this.cache = new Map();
|
|
}
|
|
|
|
rebuild() {
|
|
this.cache = new Map();
|
|
const notes = this._db.notes.all;
|
|
|
|
for (const note of notes) {
|
|
const { notebooks } = note;
|
|
if (!notebooks) continue;
|
|
|
|
for (let notebook of notebooks) {
|
|
for (let topic of notebook.topics) {
|
|
this.add(topic, note.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
add(topicId, noteId) {
|
|
let noteIds = this.cache.get(topicId);
|
|
if (!noteIds) noteIds = [];
|
|
if (noteIds.includes(noteId)) return;
|
|
noteIds.push(noteId);
|
|
this.cache.set(topicId, noteIds);
|
|
}
|
|
|
|
has(topicId, noteId) {
|
|
let noteIds = this.cache.get(topicId);
|
|
if (!noteIds) return false;
|
|
return noteIds.includes(noteId);
|
|
}
|
|
|
|
count(topicId) {
|
|
let noteIds = this.cache.get(topicId);
|
|
if (!noteIds) return 0;
|
|
return noteIds.length;
|
|
}
|
|
|
|
get(topicId) {
|
|
return this.cache.get(topicId) || [];
|
|
}
|
|
}
|