UNPKG

ebt-vue3

Version:

Vue3 Library for SuttaCentral Voice EBT-Sites

772 lines (743 loc) • 23.6 kB
import { defineStore } from 'pinia' import { SuttaRef } from "scv-esm/main.mjs"; import { default as EbtCard } from "../ebt-card.mjs"; import { default as CardFactory } from "../card-factory.mjs"; import { default as EbtSettings } from "../ebt-settings.mjs"; import { Dictionary } from "@sc-voice/ms-dpd/main.mjs"; import { logger } from "log-instance/index.mjs"; import { ref, nextTick } from "vue"; import { useSettingsStore } from "./settings.mjs"; import { useAudioStore } from "./audio.mjs"; import { useSuttasStore } from './suttas.mjs'; import { default as IdbSutta } from '../idb-sutta.mjs'; import { DBG, DBG_CLICK, DBG_HOME, DBG_WIKI, DBG_FETCH, } from "../defines.mjs"; import Utils from "../utils.mjs"; import * as Idb from "idb-keyval"; const suttas = new Map(); const displayBox = ref({ w: 375, h: 667, initialized: false, }); const showSettings = ref(false); const homeHtml = ref('loading...'); const SAMPLE_RATE = 48000; const ICON_DOWNLOAD = 'mdi-wan'; const ICON_PROCESSING = 'mdi-factory'; const showLegacyDialog = ref(false); const logHtml = ref([]); const console_log = ref(null); const routeCard = ref(null); const appFocus = ref(null); // because document.activeElement is flaky const transientMsg = ref(null); const showTransientMsg = ref(false); const showHtmlLog = ref(false); const waitingContext = ref('...'); const searchResultMap = ref({}); const config = ref(undefined); const paliSearchCard = ref(undefined); const dictionary = ref(undefined); const INITIAL_STATE = { $t: t=>t, alertHtml: ref("hello<br>there"), alertMsg: ref(null), appFocus, btnSettings: ref(undefined), collapseAppBar: ref(false), config, debugText: ref('debugText:'), delayedWaiting: 0, dictionary, ebtChips: ref(undefined), homeHtml, logHtml, paliSearchCard, routeCard, searchResultMap, showAlertMsg: ref(false), showHtmlLog, showLegacyDialog, showSettings, showTransientMsg, showWaiting: ref(false), suttas, touchSwipe: ref('waiting...'), transientMsg, trilingual: ref(true), waiting: 0, waitingDelay: ref(500), waitingIcon: ref(ICON_DOWNLOAD), waitingMsg: ref('...'), waitingContext, }; export const useVolatileStore = defineStore('volatile', { state: () => { return INITIAL_STATE; }, getters: { iconProcessing() { return ICON_PROCESSING; }, iconLoading() { return ICON_LOADING; }, audioCard() { switch (routeCard.value?.context) { case EbtCard.CONTEXT_PLAY: case EbtCard.CONTEXT_SUTTA: return true; } return null; }, displayBox() { let root = document?.documentElement; if (root) { let onresize = ()=>{ displayBox.value = { w: root.clientWidth, h: root.clientHeight, initialized: true, } } if (!displayBox.value?.initialized) { document.defaultView.onresize = onresize; nextTick(()=>onresize()); } } return displayBox; }, }, actions: { async verifyState() { const msg = "Volatile.verifyState"; let dbg = DBG.DICTIONARY; let settings = useSettingsStore(); let { docLang:lang } = settings; let curLang = dictionary?.lang; if (dictionary==null || dictionary.lang!==lang) { await Dictionary.create({lang}).then(aDict=>{ dictionary.value = aDict; dbg && console.log(msg, '[1]dictionary', curLang, lang, this.dictionary.lang); }); } return this; }, showTutorials(show) { const msg = "volatile.showTutorials()"; const dbg = DBG.TUTORIAL; let settings = useSettingsStore(); let { tutorialPath, homePath } = config; settings.tutorClose = show; settings.tutorPlay = show; settings.tutorSearch = show; settings.tutorSettings = show; settings.tutorWiki = show; if (show) { tutorialPath = tutorialPath || homePath; dbg && console.log(msg, "[1]show", tutorialPath); this.setRoute(tutorialPath); } else { dbg && console.log(msg, "[2]hide", homePath); this.setRoute(homePath); } }, async searchResults(pattern, opts={}) { let { cached=false, } = opts; let searchKey = this.trilingualPattern(pattern); let searchResult = searchResultMap.value[searchKey]; if (cached && searchResult) { return searchResult; } const suttas = useSuttasStore(); let url = this.searchUrl(pattern); let res = await this.fetchJson(url); let resJson = res.ok ? await res.json() : res; let { results, mlDocs=[] } = resJson; let cardData = results.map((r,i)=>{ let mld = mlDocs[i]; let scids = Object.keys(mld.segMap); let segments = []; for (let i=0; i<scids.length; i++) { let scid = scids[i]; let seg = mld.segMap[scid]; if (seg?.matched) { segments.push(seg); break; } } let title = mld.title.split('\n').slice(1).join('\n'); return { uid: r.uid, lang: mld.docLang, author_uid: mld.docAuthor, blurb: r.blurb, title, segments, stats: r.stats, suttaplex: r.suttaplex, } }); mlDocs.forEach(mlDoc=>this.addMlDoc(mlDoc)); for (let i = 0; i < mlDocs.length; i++) { try { let mlDoc = mlDocs[i]; let { sutta_uid, lang, author_uid } = mlDoc; this.waitBegin('ebt.processing', this.ICON_PROCESSING, sutta_uid); let idbKey = IdbSutta.idbKey({ sutta_uid, lang, author:author_uid}); let idbData = await Idb.get(idbKey); let idbSutta; let msStart2 = Date.now(); if (idbData) { idbSutta = IdbSutta.create(idbData); idbSutta.merge({mlDoc}); } else { idbSutta = IdbSutta.create(mlDoc); } suttas.saveIdbSutta(idbSutta); let result = cardData[i]; result.segsMatched = idbSutta.segments.reduce((a,v)=>{ return a + (v.matched ? 1 : 0); }, 0); result.showMatched = Math.min(3, result.segsMatched); delete result.sections; result.segments = idbSutta.segments; } finally { this.waitEnd(); } } let suttaRefs = cardData.map(d=>{ return SuttaRef.create({ sutta_uid:d.uid, lang:d.lang, author: d.author_uid, }); }); searchResult = { cardData, docLang: res.lang, docAuthor: res.author, pattern, suttaRefs, } searchResultMap.value[searchKey] = searchResult; return searchResult; }, setTransientMessage(msg) { transientMsg.value = msg; showTransientMsg.value = true; }, focusCardElementId(card, eltId=card.autofocusId) { const msg = 'volatile.focusCardElementId()'; const dbg = DBG.FOCUS; const dbgv = DBG.VERBOSE && dbg; let { firstTabId } = card; let elt = document.getElementById(eltId); let ae = document.activeElement; let aeId = ae?.id; if (elt) { if (ae !== elt) { dbg && console.log(msg, `[1]focus ${aeId}=>${eltId}`); this.focusElement(elt); } else { dbgv && console.log(msg, '[2]nochange', aeId); } } else if ((elt = document.getElementById(firstTabId))) { if (ae !== elt) { dbg && console.log(msg, '[3]focus alt', eltId); this.focusElement(elt); } else { dbgv && console.log(msg, '[4]nochange', aeId); } } else { dbgv && console.log(msg, '[5] element not found', { eltId, firstTabId, elt}); } return elt; }, enableLog(on) { const msg = "volatile.enableLog()"; const dbg = DBG.LOG_HTML; if (on) { if (console_log.value == null) { console_log.value = console.log; // save true console.log let conLog = function(...args) { let line = Utils.logLine(...args); console_log.value(line); let lines = logHtml.value; let lastLine = lines[lines.length-1]; if (lines.length && lastLine.line === line) { lastLine.count++; } else { logHtml.value.push({count:1, line}); } } dbg && conLog(msg, 'enabled'); console.log = conLog; } } }, focusElement(elt) { const msg = 'volatile.focusElement()'; const dbg = DBG.FOCUS || DBG.FOCUS_ELT; const dbgv = DBG.VERBOSE && dbg; let ae = document?.activeElement; let af = appFocus.value; if (af === elt && ae === elt) { dbgv && console.log(msg, "[1]n/a", elt.id || elt); return false; } if (ae !== elt) { if (af !== elt) { dbg && console.log(msg, "[1]focus", elt.id || elt); appFocus.value = elt; } else { dbg && console.log(msg, "[2]focus", elt.id || elt); } elt.focus(); } else { // ae === elt && af !== elt (UNEXPECTED) console.warn(msg, "[3]focus", ae.id || ae, elt.id || elt); appFocus.value = elt; } return true; }, setRouteCard(card) { const msg = 'volatile.setRouteCard()'; const dbg = DBG.ROUTE; let settings = useSettingsStore(); dbg && console.log(msg, card?.debugString); routeCard.value = card; if (card) { card.open(); } }, trilingualPattern(search) { const msg = 'volatile.trilingualPattern() '; let settings = useSettingsStore(); let { trilingual } = this; return EbtSettings.trilingualPattern(settings, search, trilingual); }, searchUrl(search) { let settings = useSettingsStore(); let pattern = this.trilingualPattern(search); let { langTrans, maxResults, } = settings; let searchPath = [ settings.serverUrl, 'search', encodeURIComponent(pattern), ].join('/'); let query=[ `maxResults=${maxResults}`, ].join('&'); return `${searchPath}/${langTrans}?${query}` }, setRoute(cardOrRoute, keepFocus, caller) { const msg = 'volatile.setRoute()'; const dbg = DBG.SET_ROUTE; let { config, } = this; let settings = useSettingsStore(); if (!cardOrRoute) { let homePath = settings.homePath(config); dbg && console.log(msg, `[1]`, {homePath}); cardOrRoute = homePath; } if (!cardOrRoute) { let emsg = `${msg} [2]ERROR: cardOrRoute is required`; console.log(emsg); throw new Error(emsg); } let isCard = !(typeof cardOrRoute === 'string'); let route = isCard ? cardOrRoute.routeHash() : cardOrRoute; let cardFactory = CardFactory.singleton; let addCard = (opts=>cardFactory.addCard(opts)); let card = isCard ? cardOrRoute : cardFactory.pathToCard({path:route, addCard}); if (card == null) { dbg && console.log(msg, '[1]no card', {route}); return; } let { visible } = card; const { window } = globalThis; if (window == null) { let emsg = `${msg} [3]window?`; console.log(emsg); throw new Error(emsg); } if (window.location.hash === route) { if (card.isOpen) { switch (card.context) { case EbtCard.CONTEXT_WIKI: // dbg && console.log(msg, "[4]n/a", card.debugString); break; case EbtCard.CONTEXT_SEARCH: case EbtCard.CONTEXT_PLAY: case EbtCard.CONTEXT_SUTTA: dbg && console.log(msg, "[5]scrollToCard", card.debugString); /* await */ this.scrollToCard(card); break; } } } else if (window.location.hash !== route) { let { document } = globalThis; let activeElement = document?.activeElement; this.debugText += `${msg}-${caller}-${route}`; dbg && console.log(msg, "[6]route", {route}); window.location.hash = route; let expected = activeElement; let actual = document?.activeElement; if (expected !== actual) { if (keepFocus) { this.focusElement(activeElement); // why? } else { dbg && console.log(msg, `[7]activeElement`, {expected, actual, route}); } } } if (routeCard.value !== card) { dbg && console.log(msg, '[8]setRouteCard', card.debugString, {visible}); this.setRouteCard(card); if (!visible) { /* await */ this.scrollToCard(card); } } return card; }, async fetchText(href) { const msg = "volatile.fetchText() "; const dbg = DBG_FETCH; let res; let text; try { res = await this.fetch(href); if (res.ok) { text = await res.text(); dbg && console.log(msg, '[1]OK', {href, text}); } else { dbg && console.warn(msg, '[2]FAIL', {href, res}); } } catch (e) { dbg && console.warn(msg, '[3]ERROR', {href, e}); } return text; }, contentPath(wikiPath) { let { config={} } = this; wikiPath = wikiPath.replace(/\/?#?\/?wiki\//, ''); wikiPath = wikiPath.replace(/\/-.*/, ''); return `${config.basePath}content/${wikiPath}.html`; }, async updateWikiRoute(opts={}) { const msg = 'volatile.updateWikiRoute()'; const dbg = DBG.ROUTE; let { card, path } = opts; let settings = useSettingsStore(); try { let html = await this.fetchWikiHtml(card); if (html && settings.tutorialState(false) && !card.isOpen) { card.open(true); dbg && console.log(msg, `[1]opened card`, card.debugString); } } catch(e) { dbg && console.log(msg, `[5]invalid`, card.location.join('/'), e); card.location = settings.homePath(); } }, async fetchWikiHtml(card) { const msg = 'volatile.fetchWikiHtml() '; const dbg = DBG_WIKI || DBG_HOME; let { location } = card; let { config } = this; let settings = useSettingsStore(); let homePath = settings.homePath(config); let windowPath = window?.location?.hash; let loc = location.join('/'); dbg && console.log(msg, '[1]', { homePath, windowPath, loc}); let html = ''; let locs = [loc]; if (windowPath.match(`#/${EbtCard.CONTEXT_WIKI}`)) { let winLoc = windowPath.split('/').slice(2).join('/'); if (winLoc !== loc) { locs = [winLoc, loc]; } } let hrefs = locs.map(p => this.contentPath(p)); dbg && console.log(msg, '[2]hrefs', hrefs); let hrefMap = hrefs.reduce((a,hr,i) => { a[hr] = i; return a; }, {}); hrefs = Object.keys(hrefMap); // unique hrefs dbg && console.log(msg, '[3]', {hrefs}); for (let i=0; !html && i < hrefs.length; i++) { let href = hrefs[i]; html = await this.fetchText(href); } if (!html) { // Can't load wiki html let homeLocation = homePath.split('/').slice(2); let homeLoc = homeLocation.join('/'); if (homeLoc !== loc) { // Retry with a known location card.location = homeLocation; dbg && console.log(msg, '[4]retry', homeLoc); return this.fetchWikiHtml(card); } // Give up let { $t } = this; let alertMsg = $t('ebt.cannotLoadWikiHtml'); console.warn(msg, '[4]', alertMsg, hrefs); html = [ `<h2>${alertMsg}</h2>`, '<pre>', ...hrefs, '</pre>', ].join('\n'); } homeHtml.value = html; return html; }, alert(eOrMsg, context, alertHtml="") { let msg = eOrMsg; if (msg instanceof Error) { msg = eOrMsg.message; console.warn('volatile.alert()', eOrMsg); } msg && console.warn(`volatile.alert() ${msg} ${context}`); this.alertMsg = msg && { msg, context }; this.alertHtml = alertHtml; this.showAlertMsg = !!msg; }, waitBegin(msgKey, icon=ICON_DOWNLOAD, context=waitingContext.value || '') { const msg = "volatile.waitBegin()"; const dbg = 0; let { $t } = this; msgKey && (this.waitingMsg = $t(msgKey)); dbg && console.log(msg, {msgKey, context}, this.waitingMsg); this.waitingIcon = icon; waitingContext.value = context; if (this.waiting === 0) { setTimeout(()=>{ if (this.waiting > 0) { this.showWaiting = true; } }, this.waitingDelay); } this.waiting++; }, waitEnd() { this.waiting--; if (this.waiting <= 0) { this.showWaiting = false; } }, addMlDoc(mld) { let { sutta_uid, lang, author_uid:author } = mld || {}; let suttaRef = SuttaRef.create({sutta_uid, lang, author}); let key = suttaRef.toString(); logger.debug("volatile.addMlDoc", {key, mld}); suttas[key] = mld; }, mlDocFromSuttaRef(suttaRefArg) { let suttaRef = SuttaRef.create(suttaRefArg); let key = suttaRef.toString(); return suttas[key]; }, async fetch(url, options={}) { const msg = `volatile.fetch()`; const dbg = DBG_FETCH; let res; try { this.waitBegin(); let fetchOpts = Object.assign({ // mode: 'no-cors', }, options); dbg && console.log(msg, '[1]', decodeURI(url), fetchOpts); res = await fetch(url, fetchOpts); } catch(e) { console.warn(msg, '[2]error', decodeURI(url), res, e); res = { error: `ERROR: ${url} ${e.message}` }; } finally { this.waitEnd(); } return res; }, async fetchJson(url, options) { try { let res = await this.fetch(url, options);; return res.ok ? await res.json() : res; } catch(e) { logger.error("volatile.fetchJson() ERROR:", res, e); res = { error: `ERROR: ${url.value} ${e.message}` }; } return res; }, onClickCard(evt, card) { const msg = "volatile.onClickCard() "; const dbg = DBG_CLICK || DBG.FOCUS; let { appFocus} = this; let { target } = evt || {}; let { localName, href, hash } = target; dbg && console.log(msg, '[1]setRoute', card.debugString, evt); this.setRoute(card, undefined, msg); if (!card.hasFocus(appFocus)) { let elt = document.getElementById(card.firstTabId); dbg && console.log(msg, '[2]focusElement', elt); this.focusElement(elt); } }, clearLog() { const msg = "volatile.clearLog()"; const dbg = DBG.LOG_HTML; logHtml.value = []; dbg && console.log(msg, logHtml.value.length); }, async scrollToCard(card) { const msg = 'volatile.scrollToCard()'; const dbg = DBG.SCROLL; const dbgv = dbg && DBG.VERBOSE; let { appFocus } = this; let settings = useSettingsStore(); let { deleteId } = card; let afId = appFocus?.id; let viewportElt = card.viewportElement(appFocus); let eltInViewport = viewportElt && Utils.elementInViewport(viewportElt, {zone:80}); if (eltInViewport && card.hasFocus(appFocus)) { DBG.FOCUS_ELT && console.log(msg, '[1]focus', appFocus); appFocus.focus(); return; } if (settings.openCard(card)) { await new Promise(resolve => setTimeout(()=>resolve(), 200)); } let curId = card.currentElementId; let topId = card.topAnchor; let scrolled = false; if (curId === card.titleAnchor) { let eltScroll = settings.scrollableElement(curId, topId); if (eltScroll) { dbgv && console.log(msg, "[1]scrollToElement", eltScroll.id); await settings.scrollToElementId(curId, topId); } return !!eltScroll; } scrolled = await settings.scrollToElementId(curId); if (!scrolled) { dbgv && console.log(msg, "[2]n/a", curId); } return scrolled; }, copySegment(opts={}) { const msg = "volatile.copySegment()"; const dbg = DBG.COPY_SEG; let audio = useAudioStore(); let settings = useSettingsStore(); let { docLang, showTrans, showPali } = settings; let { segment, href=window.location.href, lang, author, } = opts; let { $t, } = this; if (segment == null) { let { audioSutta, audioIndex } = audio; let segments = audioSutta?.segments || []; segment = segments[audioIndex] || {}; } let { scid } = segment; let langText = segment[lang]; let paliText = showPali && segment.pli; let mdList = []; showPali && paliText && mdList.push(`> [${scid}](${href}) <i>${paliText}</i> \n`); showTrans && langText && mdList.push(`> [${scid}](${href}) ${langText} \n`); let clip = mdList.join(''); dbg && console.log(msg, '[1]clip', clip); Utils.updateClipboard(clip); let tm = `${scid}: ${$t('ebt.copiedToClipboard')}`; this.setTransientMessage(tm); return segment; }, dpdLink(paliWord) { let { url } = Dictionary.dpdLink(paliWord); return url; }, dpdCartoucheHtml(def, iList, opts={}) { let { showLemma=false } = opts; let { lemma_1, word } = def; let [ lemmaHead, ...lemmaTail ] = lemma_1.split(' '); let prefix = showLemma ? lemmaHead+' ' : ''; if (lemmaTail && lemmaTail.length) { prefix += lemmaTail.join('&nbsp;'); } else { prefix += iList+1; } let grammar = this.dpdGrammarHtml(def); return [ `<div class="dpd-cartouche">`, prefix, '<div class="dpd-grammar">', grammar, '</div>', '</div>', ].join(''); }, dpdBullet(i) { return String.fromCharCode(0x2460+i); }, dpdCartoucheTitle(def) { const msg = "volatile.dpdCartoucheTitle"; let { dictionary } = this; let { pos } = def; let info = dictionary.abbreviationInfo(pos) || {}; let { meaning=pos, explanation='' } = info; return explanation.length ? `${meaning}: ${explanation}` : meaning; }, dpdGrammarHtml(def) { let { dictionary }= this; if (dictionary == null) { return "(loading)"; } let { pos, } = def; // part of speech let info = dictionary.abbreviationInfo(pos); return info && info.abbreviation || pos || "abbr/pos?"; }, dpdLemmaHtml(def, i) { let { dictionary }= this; if (dictionary == null) { return "(loading)"; } let { lemma_1 } = def; let lemma = lemma_1 ? lemma_1.split(' ') : '<span style="opacity:0.5">lemma_1?</span>' let result = lemma_1.split(' '); let parts = dictionary.hyphenate(result[0]); if (parts) { result[0] = parts.join('-'); } return result.join(' '); }, }, })