Extract shared workspace core
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
export const timelineLimit = 200
|
||||
export const defaultTranslationId = 'kjv-eng-kjv2006'
|
||||
|
||||
const sessionStorageKey = 'libre-study.sessions.v1'
|
||||
const activeSessionStorageKey = 'libre-study.active-session.v1'
|
||||
const studySettingsStorageKey = 'libre-study.study-settings.v1'
|
||||
const remoteWorkspaceEnabled = import.meta.env.VITE_REMOTE_WORKSPACE === 'true'
|
||||
const remoteWorkspaceApiBase = (import.meta.env.VITE_API_BASE_URL ?? '/api').replace(/\/$/, '')
|
||||
|
||||
export type ViewMode = 'page' | 'study' | 'notes' | 'compare'
|
||||
export type StrongDisplayMode = 'off' | 'highlight' | 'numbers'
|
||||
export type ThemeMode = 'light' | 'dark'
|
||||
export type TimelineEntryType = 'verse' | 'strong' | 'footnote' | 'search' | 'navigation' | 'view' | 'setting'
|
||||
|
||||
export type TimelineEntry = {
|
||||
id: string
|
||||
at: string
|
||||
type: TimelineEntryType
|
||||
label: string
|
||||
reference?: string
|
||||
detail?: string
|
||||
}
|
||||
|
||||
export type StudySession = {
|
||||
id: string
|
||||
title: string
|
||||
baseTranslation: string
|
||||
footnotesEnabled: boolean
|
||||
strongsEnabled: boolean
|
||||
commentaryEnabled: boolean
|
||||
dictionaryEnabled: boolean
|
||||
timelineEnabled: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
selectedId: number | null
|
||||
pageStartId: number | null
|
||||
viewMode: ViewMode
|
||||
strongDisplay: StrongDisplayMode
|
||||
noteDraft: string
|
||||
timeline: TimelineEntry[]
|
||||
}
|
||||
|
||||
export type StudySettings = {
|
||||
lemmaFontSize: number
|
||||
linkedFontSize: number
|
||||
originalCollapsed: boolean
|
||||
linkedCollapsed: boolean
|
||||
definitionCollapsed: boolean
|
||||
studyNotesCollapsed: boolean
|
||||
commentaryCollapsed: boolean
|
||||
dictionaryCollapsed: boolean
|
||||
showOriginalLine: boolean
|
||||
showLinkedLine: boolean
|
||||
showDefinitionPanel: boolean
|
||||
showCommentaryPanel: boolean
|
||||
showDictionaryPanel: boolean
|
||||
splitPercent: number
|
||||
notesPinned: boolean
|
||||
themeMode: ThemeMode
|
||||
}
|
||||
|
||||
export type WorkspaceSnapshot = {
|
||||
sessions: StudySession[]
|
||||
activeSessionId: string
|
||||
studySettings: StudySettings
|
||||
}
|
||||
|
||||
export function hostedWorkspaceEnabled() {
|
||||
return remoteWorkspaceEnabled
|
||||
}
|
||||
|
||||
export function defaultStudySettings(): StudySettings {
|
||||
return {
|
||||
lemmaFontSize: 17,
|
||||
linkedFontSize: 15,
|
||||
originalCollapsed: false,
|
||||
linkedCollapsed: false,
|
||||
definitionCollapsed: false,
|
||||
studyNotesCollapsed: false,
|
||||
commentaryCollapsed: false,
|
||||
dictionaryCollapsed: false,
|
||||
showOriginalLine: true,
|
||||
showLinkedLine: true,
|
||||
showDefinitionPanel: true,
|
||||
showCommentaryPanel: true,
|
||||
showDictionaryPanel: true,
|
||||
splitPercent: 50,
|
||||
notesPinned: false,
|
||||
themeMode: 'light',
|
||||
}
|
||||
}
|
||||
|
||||
export function createSession(title = 'Study Session', baseTranslation = defaultTranslationId): StudySession {
|
||||
const now = new Date().toISOString()
|
||||
const id =
|
||||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
baseTranslation,
|
||||
footnotesEnabled: true,
|
||||
strongsEnabled: true,
|
||||
commentaryEnabled: true,
|
||||
dictionaryEnabled: true,
|
||||
timelineEnabled: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
selectedId: null,
|
||||
pageStartId: null,
|
||||
viewMode: 'notes',
|
||||
strongDisplay: 'highlight',
|
||||
noteDraft: '',
|
||||
timeline: [],
|
||||
}
|
||||
}
|
||||
|
||||
export function timelineId() {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
}
|
||||
|
||||
export function normalizeStudySettings(settings?: Partial<StudySettings> | null): StudySettings {
|
||||
return {
|
||||
...defaultStudySettings(),
|
||||
...(settings ?? {}),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSessions(sessions?: StudySession[] | null): StudySession[] {
|
||||
const source = Array.isArray(sessions) && sessions.length ? sessions : [createSession('KJV Study Session')]
|
||||
|
||||
return source.map((session) => ({
|
||||
...createSession(session.title),
|
||||
...session,
|
||||
baseTranslation: session.baseTranslation ?? defaultTranslationId,
|
||||
footnotesEnabled: session.footnotesEnabled ?? true,
|
||||
strongsEnabled: session.strongsEnabled ?? true,
|
||||
commentaryEnabled: session.commentaryEnabled ?? true,
|
||||
dictionaryEnabled: session.dictionaryEnabled ?? true,
|
||||
timelineEnabled: session.timelineEnabled ?? true,
|
||||
}))
|
||||
}
|
||||
|
||||
export function selectActiveSessionId(sessions: StudySession[], requestedId?: string | null): string {
|
||||
if (!sessions.length) return ''
|
||||
return sessions.some((session) => session.id === requestedId) ? requestedId ?? sessions[0].id : sessions[0].id
|
||||
}
|
||||
|
||||
export async function loadWorkspaceSnapshot(): Promise<WorkspaceSnapshot> {
|
||||
const remoteSnapshot = remoteWorkspaceEnabled ? await loadRemoteWorkspaceSnapshot() : null
|
||||
const localSnapshot = remoteSnapshot ? null : loadLocalWorkspaceSnapshot()
|
||||
const source = remoteSnapshot ?? localSnapshot
|
||||
const sessions = normalizeSessions(source?.sessions)
|
||||
const activeSessionId = selectActiveSessionId(sessions, source?.activeSessionId)
|
||||
|
||||
return {
|
||||
sessions,
|
||||
activeSessionId,
|
||||
studySettings: normalizeStudySettings(source?.studySettings),
|
||||
}
|
||||
}
|
||||
|
||||
export function saveWorkspaceSnapshot(snapshot: WorkspaceSnapshot): Promise<void> {
|
||||
saveLocalWorkspaceSnapshot(snapshot)
|
||||
if (!remoteWorkspaceEnabled) return Promise.resolve()
|
||||
return saveRemoteWorkspaceSnapshot(snapshot)
|
||||
}
|
||||
|
||||
function loadLocalWorkspaceSnapshot(): Partial<WorkspaceSnapshot> {
|
||||
return {
|
||||
sessions: readJson<StudySession[]>(sessionStorageKey) ?? undefined,
|
||||
activeSessionId: localStorage.getItem(activeSessionStorageKey) ?? undefined,
|
||||
studySettings: readJson<StudySettings>(studySettingsStorageKey) ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function saveLocalWorkspaceSnapshot(snapshot: WorkspaceSnapshot) {
|
||||
localStorage.setItem(sessionStorageKey, JSON.stringify(snapshot.sessions))
|
||||
localStorage.setItem(activeSessionStorageKey, snapshot.activeSessionId)
|
||||
localStorage.setItem(studySettingsStorageKey, JSON.stringify(snapshot.studySettings))
|
||||
}
|
||||
|
||||
function readJson<T>(key: string): T | null {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (!raw) return null
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as T
|
||||
} catch {
|
||||
localStorage.removeItem(key)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRemoteWorkspaceSnapshot(): Promise<Partial<WorkspaceSnapshot> | null> {
|
||||
const response = await fetch(`${remoteWorkspaceApiBase}/workspace`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
if (response.status === 204 || response.status === 404) return null
|
||||
if (response.status === 401) {
|
||||
throw new Error('Login is required for the hosted workspace.')
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to load hosted workspace: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json() as Promise<Partial<WorkspaceSnapshot>>
|
||||
}
|
||||
|
||||
async function saveRemoteWorkspaceSnapshot(snapshot: WorkspaceSnapshot) {
|
||||
const response = await fetch(`${remoteWorkspaceApiBase}/workspace`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(snapshot),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to save hosted workspace: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user