191 lines
5.2 KiB
TypeScript
191 lines
5.2 KiB
TypeScript
import { atom } from 'nanostores'
|
|
import { db } from './db'
|
|
|
|
type Note = {
|
|
id: string
|
|
x: number
|
|
y: number
|
|
content: string
|
|
}
|
|
|
|
type Space = {
|
|
id: string
|
|
notes: Note[]
|
|
}
|
|
|
|
export const currentSpaceId = atom<string>('home')
|
|
export const currentSpace = atom<Space | null>(null)
|
|
export const navigationHistory = atom<string[]>(['home'])
|
|
export const editingNoteId = atom<string | null>(null)
|
|
export const selectedNoteIds = atom<string[]>([])
|
|
export const existingSpaceIds = atom<string[]>([])
|
|
|
|
export const refreshExistingSpaces = async () => {
|
|
const spaces = await db.getAllSpaceIds()
|
|
existingSpaceIds.set(spaces)
|
|
}
|
|
|
|
export const loadSpace = async (spaceId: string) => {
|
|
// Check if current space is empty and delete it (except 'home')
|
|
const currentSpace_value = currentSpace.get()
|
|
const currentSpaceId_value = currentSpaceId.get()
|
|
if (currentSpace_value && currentSpaceId_value !== 'home' && currentSpace_value.notes.length === 0) {
|
|
await db.deleteSpace(currentSpaceId_value)
|
|
}
|
|
|
|
currentSpace.set(null)
|
|
let space = await db.getSpace(spaceId)
|
|
if (!space) {
|
|
space = await db.createSpace(spaceId)
|
|
}
|
|
currentSpace.set(space)
|
|
currentSpaceId.set(spaceId)
|
|
window.history.pushState({}, '', `/#${spaceId}`)
|
|
|
|
// Refresh list of existing spaces
|
|
await refreshExistingSpaces()
|
|
|
|
return space
|
|
}
|
|
|
|
export const navigateToSpace = async (spaceId: string) => {
|
|
const history = navigationHistory.get()
|
|
if (history[history.length - 1] !== spaceId) {
|
|
navigationHistory.set([...history, spaceId])
|
|
}
|
|
await loadSpace(spaceId)
|
|
}
|
|
|
|
export const goBack = async () => {
|
|
const history = navigationHistory.get()
|
|
if (history.length > 1) {
|
|
const newHistory = history.slice(0, -1)
|
|
navigationHistory.set(newHistory)
|
|
const previousSpaceId = newHistory[newHistory.length - 1]
|
|
await loadSpace(previousSpaceId)
|
|
}
|
|
}
|
|
|
|
export const createNote = async (x: number, y: number) => {
|
|
const space = currentSpace.get()
|
|
if (!space) return null
|
|
|
|
const note: Note = {
|
|
id: `note-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
x,
|
|
y,
|
|
content: ''
|
|
}
|
|
|
|
await db.addNoteToSpace(space.id, note)
|
|
const updatedSpace = await db.getSpace(space.id)
|
|
currentSpace.set(updatedSpace!)
|
|
|
|
return note.id
|
|
}
|
|
|
|
export const updateNote = async (noteId: string, updates: Partial<Note>) => {
|
|
const space = currentSpace.get()
|
|
if (!space) return
|
|
|
|
await db.updateNoteInSpace(space.id, noteId, updates)
|
|
const updatedSpace = await db.getSpace(space.id)
|
|
currentSpace.set(updatedSpace!)
|
|
}
|
|
|
|
export const deleteNote = async (noteId: string) => {
|
|
const space = currentSpace.get()
|
|
if (!space) return
|
|
|
|
await db.deleteNoteFromSpace(space.id, noteId)
|
|
const updatedSpace = await db.getSpace(space.id)
|
|
currentSpace.set(updatedSpace!)
|
|
}
|
|
|
|
export const getSpaceFromUrl = () => {
|
|
const hash = window.location.hash.slice(1)
|
|
return hash || 'home'
|
|
}
|
|
|
|
// Selection management
|
|
export const selectNote = (noteId: string, multiSelect: boolean = false) => {
|
|
const currentSelection = selectedNoteIds.get()
|
|
|
|
if (multiSelect) {
|
|
if (!currentSelection.includes(noteId)) {
|
|
// Add to selection only
|
|
const newSelection = [...currentSelection, noteId]
|
|
selectedNoteIds.set(newSelection)
|
|
}
|
|
// If already selected and multiSelect, do nothing (don't remove)
|
|
} else {
|
|
// Single selection
|
|
selectedNoteIds.set([noteId])
|
|
}
|
|
}
|
|
|
|
export const selectNotesInRect = (minX: number, minY: number, maxX: number, maxY: number) => {
|
|
const space = currentSpace.get()
|
|
if (!space) return
|
|
|
|
const selectedIds = space.notes
|
|
.filter(note => {
|
|
// Check if note overlaps with rectangle (considering note has some width/height)
|
|
const noteRight = note.x + 50 // Approximate note width
|
|
const noteBottom = note.y + 20 // Approximate note height
|
|
|
|
const inRect = !(note.x > maxX || noteRight < minX || note.y > maxY || noteBottom < minY)
|
|
return inRect
|
|
})
|
|
.map(note => note.id)
|
|
|
|
selectedNoteIds.set(selectedIds)
|
|
}
|
|
|
|
export const clearSelection = () => {
|
|
selectedNoteIds.set([])
|
|
}
|
|
|
|
export const deleteSelectedNotes = async () => {
|
|
const selectedIds = selectedNoteIds.get()
|
|
const space = currentSpace.get()
|
|
if (!space || selectedIds.length === 0) return
|
|
|
|
for (const noteId of selectedIds) {
|
|
await db.deleteNoteFromSpace(space.id, noteId)
|
|
}
|
|
|
|
const updatedSpace = await db.getSpace(space.id)
|
|
currentSpace.set(updatedSpace!)
|
|
clearSelection()
|
|
}
|
|
|
|
export const moveSelectedNotesLocal = (deltaX: number, deltaY: number) => {
|
|
const selectedIds = selectedNoteIds.get()
|
|
const space = currentSpace.get()
|
|
if (!space || selectedIds.length === 0) return
|
|
|
|
// Update positions locally without DB calls
|
|
const updatedNotes = space.notes.map(note => {
|
|
if (selectedIds.includes(note.id)) {
|
|
return { ...note, x: note.x + deltaX, y: note.y + deltaY }
|
|
}
|
|
return note
|
|
})
|
|
|
|
currentSpace.set({ ...space, notes: updatedNotes })
|
|
}
|
|
|
|
export const saveSelectedNotesToDB = async () => {
|
|
const selectedIds = selectedNoteIds.get()
|
|
const space = currentSpace.get()
|
|
if (!space || selectedIds.length === 0) return
|
|
|
|
// Save to DB
|
|
for (const noteId of selectedIds) {
|
|
const note = space.notes.find(n => n.id === noteId)
|
|
if (note) {
|
|
await db.updateNoteInSpace(space.id, noteId, { x: note.x, y: note.y })
|
|
}
|
|
}
|
|
} |