156 lines
4.3 KiB
TypeScript
156 lines
4.3 KiB
TypeScript
import { db } from './db.js'
|
|
import { getActiveMasterKey } from './auth.js'
|
|
import { getLogbookKey } from './logbookKeys.js'
|
|
import { encryptJson, decryptJson } from './crypto.js'
|
|
import { syncLogbook } from './sync.js'
|
|
import { PlausibleEvents, trackPlausibleEvent } from './analytics.js'
|
|
|
|
async function getEncryptionKey(logbookId: string): Promise<ArrayBuffer> {
|
|
const key = await getLogbookKey(logbookId) || getActiveMasterKey()
|
|
if (!key) throw new Error('Encryption key not found. Please log in.')
|
|
return key
|
|
}
|
|
|
|
export async function saveEntryVoiceMemo(options: {
|
|
logbookId: string
|
|
entryId: string
|
|
audioDataUrl: string
|
|
mimeType: string
|
|
durationSec: number
|
|
caption?: string
|
|
transcribed?: boolean
|
|
analyticsContext?: string
|
|
}): Promise<string> {
|
|
const {
|
|
logbookId,
|
|
entryId,
|
|
audioDataUrl,
|
|
mimeType,
|
|
durationSec,
|
|
caption = '',
|
|
transcribed = true,
|
|
analyticsContext = 'logbook'
|
|
} = options
|
|
const masterKey = await getEncryptionKey(logbookId)
|
|
const voiceId = window.crypto.randomUUID()
|
|
const voicePayload = {
|
|
audio: audioDataUrl,
|
|
mimeType,
|
|
durationSec,
|
|
caption: caption.trim(),
|
|
transcribed: !!transcribed
|
|
}
|
|
|
|
const encrypted = await encryptJson(voicePayload, masterKey)
|
|
const now = new Date().toISOString()
|
|
|
|
await db.voiceMemos.put({
|
|
payloadId: voiceId,
|
|
entryId,
|
|
logbookId,
|
|
encryptedData: encrypted.ciphertext,
|
|
iv: encrypted.iv,
|
|
tag: encrypted.tag,
|
|
updatedAt: now
|
|
})
|
|
|
|
await db.syncQueue.put({
|
|
action: 'create',
|
|
type: 'voiceMemo',
|
|
payloadId: voiceId,
|
|
logbookId,
|
|
data: JSON.stringify({
|
|
encryptedData: encrypted.ciphertext,
|
|
iv: encrypted.iv,
|
|
tag: encrypted.tag,
|
|
entryId
|
|
}),
|
|
updatedAt: now
|
|
})
|
|
|
|
trackPlausibleEvent(PlausibleEvents.VOICE_MEMO_UPLOADED, { context: analyticsContext })
|
|
syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err))
|
|
return voiceId
|
|
}
|
|
|
|
export async function deleteEntryVoiceMemo(logbookId: string, voiceId: string): Promise<void> {
|
|
const now = new Date().toISOString()
|
|
await db.voiceMemos.delete(voiceId)
|
|
await db.syncQueue.put({
|
|
action: 'delete',
|
|
type: 'voiceMemo',
|
|
payloadId: voiceId,
|
|
logbookId,
|
|
data: '',
|
|
updatedAt: now
|
|
})
|
|
syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err))
|
|
}
|
|
|
|
/** Deletes the newest voice memo for an entry; returns its id or null. */
|
|
export async function removeLastVoiceMemoForEntry(
|
|
logbookId: string,
|
|
entryId: string
|
|
): Promise<string | null> {
|
|
const memos = await db.voiceMemos.where({ entryId }).toArray()
|
|
if (memos.length === 0) return null
|
|
memos.sort(
|
|
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
)
|
|
const lastId = memos[0].payloadId
|
|
await deleteEntryVoiceMemo(logbookId, lastId)
|
|
return lastId
|
|
}
|
|
|
|
/** Updates an existing voice memo payload with a new transcript and sets transcribed: true. */
|
|
export async function updateVoiceMemoTranscript(
|
|
logbookId: string,
|
|
voiceId: string,
|
|
transcript: string
|
|
): Promise<void> {
|
|
const masterKey = await getEncryptionKey(logbookId)
|
|
const record = await db.voiceMemos.get(voiceId)
|
|
if (!record) throw new Error('Voice memo not found')
|
|
|
|
const decrypted = await decryptJson(record.encryptedData, record.iv, record.tag, masterKey)
|
|
if (!decrypted) throw new Error('Failed to decrypt voice memo')
|
|
|
|
const manualCaption = decrypted.caption ? String(decrypted.caption).trim() : ''
|
|
const finalCaption = manualCaption
|
|
? `${manualCaption}\n(Transkript: ${transcript.trim()})`
|
|
: transcript.trim()
|
|
|
|
const updatedPayload = {
|
|
...decrypted,
|
|
caption: finalCaption,
|
|
transcribed: true
|
|
}
|
|
|
|
const encrypted = await encryptJson(updatedPayload, masterKey)
|
|
const now = new Date().toISOString()
|
|
|
|
await db.voiceMemos.put({
|
|
...record,
|
|
encryptedData: encrypted.ciphertext,
|
|
iv: encrypted.iv,
|
|
tag: encrypted.tag,
|
|
updatedAt: now
|
|
})
|
|
|
|
await db.syncQueue.put({
|
|
action: 'update',
|
|
type: 'voiceMemo',
|
|
payloadId: voiceId,
|
|
logbookId,
|
|
data: JSON.stringify({
|
|
encryptedData: encrypted.ciphertext,
|
|
iv: encrypted.iv,
|
|
tag: encrypted.tag,
|
|
entryId: record.entryId
|
|
}),
|
|
updatedAt: now
|
|
})
|
|
|
|
syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err))
|
|
}
|