275 lines
8.8 KiB
TypeScript
275 lines
8.8 KiB
TypeScript
import shortid from 'shortid'
|
|
import { useTitle } from '@vueuse/core'
|
|
import { doc, getDoc, setDoc } from 'firebase/firestore'
|
|
import { viewModes, activeViewMode } from '@/composables/useViewMode'
|
|
import { initialized, user, db } from '@/composables/useFirebase'
|
|
import { decryptNotes, encryptNotes, encryptionKey } from '@/composables/useEncryption'
|
|
import { defaultNotes } from '@/utils/defaultNotes'
|
|
import { mdToHtml } from '@/utils/markdown'
|
|
import { getAllMatches } from '@/utils/helpers'
|
|
import { preferredNotesSource } from '@/composables/useSettings'
|
|
import { Haptics, ImpactStyle } from '@capacitor/haptics'
|
|
|
|
export const notesSources = computed(() => ({
|
|
local: true,
|
|
firebase: initialized.value && user.value
|
|
}))
|
|
|
|
export const availableNotesSources = computed<notesSourceValues[]>(() =>
|
|
Object.entries(notesSources.value)
|
|
.filter(([, enabled]) => enabled)
|
|
.map(([source]) => source as notesSourceValues)
|
|
)
|
|
|
|
export type notesSourceValues = keyof typeof notesSources.value
|
|
|
|
export const activeNotesSource = ref<notesSourceValues | null>(null)
|
|
|
|
watchEffect(() => {
|
|
const getSource = (): notesSourceValues | null => {
|
|
if (!initialized.value) return null
|
|
if (
|
|
preferredNotesSource.value &&
|
|
availableNotesSources.value.includes(preferredNotesSource.value)
|
|
) {
|
|
return preferredNotesSource.value
|
|
} else {
|
|
return user.value ? 'firebase' : 'local'
|
|
}
|
|
}
|
|
activeNotesSource.value = getSource()
|
|
})
|
|
|
|
export const baseNotes = ref<BaseNotes>({})
|
|
|
|
const syncNotesLocal = (notes: BaseNotes) => {
|
|
localStorage.setItem('notes', JSON.stringify(notes))
|
|
}
|
|
|
|
// export const syncNotesToFirebase = async (newNotes: BaseNotes, oldNotes?: BaseNotes) => {
|
|
export const syncNotesToFirebase = async (baseNotes: BaseNotes) => {
|
|
if (!db.value) throw Error("Database undefined, can't sync to Firebase")
|
|
if (!user.value) throw Error("User undefined, can't sync to Firebase")
|
|
const notes = encryptionKey.value ? encryptNotes(baseNotes, encryptionKey.value) : baseNotes
|
|
try {
|
|
const docRef = doc(db.value, 'pages', user.value.uid)
|
|
// if (oldNotes) {
|
|
// const notesToDelete = Object.keys(oldNotes).filter((x) => !Object.keys(newNotes).includes(x))
|
|
// await Promise.all(
|
|
// notesToDelete.map((noteId: string) => {
|
|
// return updateDoc(docRef, { [noteId]: deleteField() })
|
|
// })
|
|
// )
|
|
// }
|
|
await setDoc(docRef, notes)
|
|
} catch (error: any) {
|
|
console.error(error)
|
|
}
|
|
}
|
|
|
|
watch(
|
|
baseNotes,
|
|
async () => {
|
|
if (!activeNotesSource.value || Object.keys(baseNotes.value).length === 0) return
|
|
if (activeNotesSource.value === 'local') {
|
|
syncNotesLocal(baseNotes.value)
|
|
} else if (activeNotesSource.value === 'firebase') {
|
|
syncNotesToFirebase(baseNotes.value)
|
|
}
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
export const notes = computed<Note[]>(() => {
|
|
return Object.entries(baseNotes.value)
|
|
.map(([, note]) => ({
|
|
...note,
|
|
wordCount: note.content.split(' ').filter((word) => word.length > 0).length
|
|
}))
|
|
.sort((a, b) => b.modified - a.modified) as Note[]
|
|
})
|
|
watch(notes, () => {
|
|
if (notes.value.length > 0 && !activeNote.value && activeViewMode.value.name === 'Note')
|
|
setActiveNote(rootNote.value?.id, false)
|
|
})
|
|
|
|
const activeNoteId = ref<string>()
|
|
export const activeNote = computed(() => notes.value.find((note) => note.id === activeNoteId.value))
|
|
watch(activeNote, () => {
|
|
if (activeNote.value) {
|
|
useTitle(`${activeNote.value.title} | Contexted`)
|
|
}
|
|
})
|
|
export const setActiveNote = (noteId: string | undefined, haptic: boolean = true) => {
|
|
if (noteId) {
|
|
activeNoteId.value = noteId
|
|
activeViewMode.value = viewModes.find((mode) => mode.name === 'Note') || viewModes[0]
|
|
if (haptic) Haptics.impact({ style: ImpactStyle.Light })
|
|
}
|
|
}
|
|
|
|
export const rootNote = computed<Note | undefined>(() => {
|
|
const rootNote = notes.value.find((note: Note) => note.isRoot)
|
|
return rootNote
|
|
})
|
|
|
|
export const setRootNote = (noteId: string) => {
|
|
if (rootNote.value) {
|
|
const updatedRootNote = { ...baseNotes.value[rootNote.value.id], isRoot: false }
|
|
updateNote(updatedRootNote.id, updatedRootNote)
|
|
}
|
|
const note = { ...baseNotes.value[noteId], isRoot: true }
|
|
updateNote(noteId, note)
|
|
setActiveNote(noteId, false)
|
|
}
|
|
|
|
export const insertDefaultNotes = (defaultNotes: BaseNote[]) => {
|
|
defaultNotes.forEach((defaultNote) => {
|
|
baseNotes.value[defaultNote.id] = defaultNote
|
|
})
|
|
}
|
|
|
|
export const getNoteById = (noteId: string) => {
|
|
return notes.value.find((note) => note.id === noteId)
|
|
}
|
|
|
|
export const getNoteByTitle = (title: string) => {
|
|
return notes.value.find((note) => note.title === title)
|
|
}
|
|
|
|
export const findNotesByByTitle = (title: string) => {
|
|
const titleLowerCase = title.toLowerCase()
|
|
return notes.value.filter((note) => note.title.toLowerCase().includes(titleLowerCase))
|
|
}
|
|
|
|
export const findNotes = (query: string): Note[] => {
|
|
const removeMdFromText = (mdText: string): string => {
|
|
const div = document.createElement('div')
|
|
div.innerHTML = mdToHtml(mdText)
|
|
const textWithoutMd = div.textContent || div.innerText || ''
|
|
return textWithoutMd
|
|
}
|
|
return notes.value.filter((note) => {
|
|
const matchTitle = note.title.toLowerCase().includes(query.toLowerCase())
|
|
const matchContent = removeMdFromText(note.content).toLowerCase().includes(query.toLowerCase())
|
|
return matchTitle || matchContent
|
|
})
|
|
}
|
|
|
|
export const updateNote = (noteId: string, note: BaseNote) => {
|
|
const updatedNote: BaseNote = {
|
|
...note,
|
|
modified: new Date().getTime()
|
|
}
|
|
baseNotes.value[noteId] = updatedNote
|
|
}
|
|
|
|
export const addNote = (title: string, content: string, goToNote: boolean = false) => {
|
|
const id = shortid.generate()
|
|
const newNote: BaseNote = {
|
|
id,
|
|
title,
|
|
content,
|
|
isRoot: false,
|
|
created: new Date().getTime(),
|
|
modified: new Date().getTime()
|
|
}
|
|
baseNotes.value[id] = newNote
|
|
if (goToNote) setActiveNote(id)
|
|
return newNote
|
|
}
|
|
|
|
export const deleteNote = (noteId: string) => {
|
|
const baseNotesClone: BaseNotes = structuredClone(toRaw(baseNotes.value))
|
|
delete baseNotesClone[noteId]
|
|
baseNotes.value = baseNotesClone
|
|
}
|
|
|
|
const getNoteLinksByNoteId = (noteId: string): string[] => {
|
|
const note = baseNotes.value[noteId]
|
|
const regex = /\[\[(.*?)\]\]/g
|
|
const links = getAllMatches(regex, note.content || '')
|
|
.map((to) => notes.value.find((note) => note.title === to[1])?.id || '')
|
|
.filter((noteId) => Object.keys(baseNotes.value).includes(noteId))
|
|
return [...links]
|
|
}
|
|
|
|
export const notesRelations = computed(() => {
|
|
const noteIds = Object.keys(baseNotes.value)
|
|
const relations = noteIds
|
|
.filter((id) => id !== undefined)
|
|
.map((id) => {
|
|
const to = getNoteLinksByNoteId(id)
|
|
return { id, to }
|
|
})
|
|
.map((noteRelations, _, notesRelations): NoteRelations => {
|
|
const from = [...notesRelations]
|
|
.map((noteRelation) =>
|
|
noteRelation.to.filter((toId) => toId === noteRelations.id).map(() => noteRelation.id)
|
|
)
|
|
.reduce((arr, elem) => arr.concat(elem), [])
|
|
.filter((value, index, self) => self.indexOf(value) === index)
|
|
return {
|
|
id: noteRelations.id,
|
|
to: noteRelations.to,
|
|
from
|
|
}
|
|
})
|
|
.reduce((notes, { id, to, from }) => {
|
|
notes[id] = { to, from }
|
|
return notes
|
|
}, {} as NotesRelations)
|
|
return relations
|
|
})
|
|
|
|
export function getNoteReferences(note: Note) {
|
|
const relations = notesRelations.value[note.id]
|
|
return relations
|
|
? (relations.from || [])
|
|
.map((noteId) => {
|
|
return notes.value.find((note) => note.id === noteId)
|
|
})
|
|
.filter((note): note is Note => note !== undefined)
|
|
: []
|
|
}
|
|
|
|
const parseBaseNotes = (notes: BaseNotes): BaseNotes => {
|
|
return Object.fromEntries(
|
|
Object.entries(notes).map(([noteId, note]) => {
|
|
return [
|
|
noteId,
|
|
{
|
|
id: noteId,
|
|
title: note.title,
|
|
content: note.content,
|
|
isRoot: Boolean(note.isRoot),
|
|
created: note.created || note.modified || new Date().getTime(),
|
|
modified: note.modified || note.created || new Date().getTime()
|
|
}
|
|
]
|
|
})
|
|
)
|
|
}
|
|
|
|
export const getNotes = async () => {
|
|
baseNotes.value = {}
|
|
let notes: BaseNotes = {}
|
|
if (activeNotesSource.value === 'local') {
|
|
try {
|
|
notes = JSON.parse(localStorage.getItem('notes') || '{}')
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
} else if (activeNotesSource.value === 'firebase') {
|
|
if (encryptionKey.value === undefined || !user.value || !db.value) return
|
|
const firebaseNotes = (await getDoc(doc(db.value, 'pages', user.value.uid))).data() as BaseNotes
|
|
notes = encryptionKey.value
|
|
? decryptNotes(firebaseNotes, encryptionKey.value)
|
|
: firebaseNotes || {}
|
|
console.log('get notes from firebase', notes)
|
|
}
|
|
baseNotes.value = parseBaseNotes(notes)
|
|
if (!rootNote.value) insertDefaultNotes(defaultNotes)
|
|
setActiveNote(rootNote.value?.id, false)
|
|
}
|