pdf-tei-editor / app /src /plugins /start.js
cmboulanger's picture
disaster-recovery deploy of pdf-tei-editor
6a49f21 verified
Raw
History Blame Contribute Delete
7.24 kB
/**
* 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<import('./config.js').default['getApi']>} */
#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