/** * Plugin which hosts the start function, which is responsible for loading the documents at startup * and configures the general behavior of the application. General rule: behavior that depends solely * on a particular plugin should be configured in the plugin, behavior that depends on the interplay * of several plugins should be configured here. */ /** * @import { ApplicationState } from '../state.js' * @import { PluginContext } from '../modules/plugin-context.js' */ import { Plugin } from '../modules/plugin-base.js' import ep from '../extension-points.js' import ui, { Spinner, updateUi } from '../ui.js' import { testLog } from '../modules/test-log.js' import { HeartbeatPlugin } from '../plugin-registry.js' import { UrlHash } from '../modules/browser-utils.js' import { notify } from '../modules/sl-utils.js' class StartPlugin extends Plugin { /** @type {import('./logger.js').default} */ #logger /** @type {ReturnType} */ #config /** @type {import('./dialog.js').default} */ #dialog /** @type {import('./services.js').default} */ #services /** @type {import('./tei-validation.js').default} */ #validation /** @type {import('./xmleditor.js').default} */ #xmlEditor /** @type {import('./authentication.js').default} */ #authentication /** @param {PluginContext} context */ constructor(context) { super(context, { name: 'start', deps: ['logger', 'config', 'dialog', 'services', 'tei-validation', 'xmleditor', 'authentication', 'heartbeat'] }) } /** * @param {ApplicationState} state */ async install(state) { await super.install(state) this.#logger = this.getDependency('logger') this.#config = this.getDependency('config') this.#dialog = this.getDependency('dialog') this.#services = this.getDependency('services') this.#validation = this.getDependency('tei-validation') this.#xmlEditor = this.getDependency('xmleditor') this.#authentication = this.getDependency('authentication') // spinner/blocker const spinner = new Spinner // @ts-ignore spinner.setAttribute('name', "spinner") document.body.appendChild(spinner) updateUi() // Note: validation status widget creation moved to xmleditor plugin's start() function } /** * Starts the application, configures plugins and the UI */ async start() { // async operations try { testLog('APP_START_INITIATED', { timestamp: new Date().toISOString() }) // Authenticate user, otherwise we don't proceed further const userData = await this.#authentication.ensureAuthenticated() const name = userData.fullname || userData.username this.#logger.info(`${name} has logged in.`) notify(`Welcome back, ${name}!`) testLog('USER_AUTHENTICATED', { username: userData.username, fullname: userData.fullname }) // load config data await this.#config.load() ui.spinner.show('Loading documents, please wait...') // update the file data (use cache on startup, only refresh on explicit user action) await this.context.invokePluginEndpoint(ep.filedata.reload, { refresh: false }) // disable regular validation so that we have more control over it this.#validation.configure({ mode: "off" }) // get document paths from URL hash // @ts-ignore const pdf = this.state.pdf || null const xml = this.state.xml || null const diff = this.state.diff if (pdf !== null || xml !== null) { // load the documents (PDF-XML pairs or XML-only files) try { const filesToLoad = {} if (pdf) filesToLoad.pdf = pdf if (xml) filesToLoad.xml = xml await this.#services.load(filesToLoad) } catch (error) { this.#dialog.error(String(error)) this.#logger.critical(String(error)) } } // two alternative initial states: // a) if the diff param was given and is different from the xml param, show a diff/merge view // b) if no diff, try to validate the document and select first match of xpath expression if (diff && diff !== xml) { // a) load the diff view try { await this.#services.showMergeView(diff) } catch (error) { this.#logger.warn("Error loading diff view: " + String(error)) } } else { // b) validation & xpath selection // measure how long it takes to validate the document const startTime = new Date().getTime() this.#services.validateXml().then(() => { const endTime = new Date().getTime() const seconds = Math.round((endTime - startTime) / 1000) // disable validation if it took longer than 3 seconds on slow servers this.#logger.info(`Validation took ${seconds} seconds${seconds > 3 ? ", disabling it." : "."}`) this.#validation.configure({ mode: seconds > 3 ? "off" : "auto" }) }) // the xpath of the (to be) selected node in the xml editor, setting the state triggers the selection const xpath = UrlHash.get("xpath") || '' // update the UI await this.dispatchStateChange({ xpath }) // synchronize in the background this.context.invokePluginEndpoint(ep.sync.syncFiles, this.state).then(async (summary) => { if (summary && !summary.skipped) { await this.context.invokePluginEndpoint(ep.filedata.reload, { refresh: true }) } }) } // configure the xml editor events this.#configureFindNodeInPdf() // Heartbeat mechanism for file locking and offline detection HeartbeatPlugin.getInstance().start(await this.#config.get('heartbeat.interval', 10)) // finish initialization ui.spinner.hide() this.#xmlEditor.setLineWrapping(true) this.#logger.info("Application ready.") testLog('APP_START_COMPLETED', { pdf: this.state.pdf, xml: this.state.xml, diff: this.state.diff }) // Notify all plugins that app startup is complete await this.context.invokePluginEndpoint(ep.ready) } catch (error) { ui.spinner.hide() this.#dialog.error(String(error)) throw error } } /** * Add behavior that looks up the content of the current node in the PDF */ #configureFindNodeInPdf() { /** @type {Node | null} */ let lastNode = null // Cross-plugin coordination: Find the currently selected node's contents in the PDF this.#xmlEditor.on("selectionChanged", async () => { // workaround for the node selection not being updated immediately await new Promise(resolve => setTimeout(resolve, 100)) // wait for the next tick // trigger auto-search if enabled and if a new node has been selected const node = this.#xmlEditor.selectedNode if (this.getDependency('pdfviewer').isAutoSearchEnabled() && node && node !== lastNode) { await this.#services.searchNodeContentsInPdf(node) lastNode = node } }) } } export default StartPlugin /** @deprecated Use StartPlugin class directly */ export const plugin = StartPlugin