From 96f877c10e6835aa2311b6d0e1c2ad8b607b7195 Mon Sep 17 00:00:00 2001 From: Bracken Mosbacker Date: Fri, 30 Jan 2026 12:58:19 -0800 Subject: [PATCH] Adds a "Content Type Browser" to the server Can easily browse installed types and their library and semantics If example xAPI events are stored they can also be previewed --- assets/languages/de.json | 3 +- assets/languages/en.json | 3 +- assets/languages/es.json | 3 +- assets/languages/fr.json | 3 +- assets/styles/style.css | 3 + assets/templates/dashboard.html | 3 + .../components/h5pcli-content-type.js | 281 ++++++++++++++ .../components/h5pcli-library-overview.js | 176 +++++++++ .../components/h5pcli-semantics-docs.js | 342 ++++++++++++++++++ .../components/h5pcli-semantics.js | 125 +++++++ .../components/h5pcli-type-browser.js | 216 +++++++++++ .../components/h5pcli-xapi-examples.js | 112 ++++++ .../components/h5pcli-xapi-viewer.js | 210 +++++++++++ assets/type_browser/components/index.js | 7 + assets/type_browser/components/tabs.css | 35 ++ assets/type_browser/content_type.html | 38 ++ assets/type_browser/index.html | 36 ++ src/server/apis/typeBrowser.js | 182 ++++++++++ src/server/server.js | 23 +- 19 files changed, 1796 insertions(+), 5 deletions(-) create mode 100644 assets/type_browser/components/h5pcli-content-type.js create mode 100644 assets/type_browser/components/h5pcli-library-overview.js create mode 100644 assets/type_browser/components/h5pcli-semantics-docs.js create mode 100644 assets/type_browser/components/h5pcli-semantics.js create mode 100644 assets/type_browser/components/h5pcli-type-browser.js create mode 100644 assets/type_browser/components/h5pcli-xapi-examples.js create mode 100644 assets/type_browser/components/h5pcli-xapi-viewer.js create mode 100644 assets/type_browser/components/index.js create mode 100644 assets/type_browser/components/tabs.css create mode 100644 assets/type_browser/content_type.html create mode 100644 assets/type_browser/index.html create mode 100644 src/server/apis/typeBrowser.js diff --git a/assets/languages/de.json b/assets/languages/de.json index bd37dcfa..f2612da2 100644 --- a/assets/languages/de.json +++ b/assets/languages/de.json @@ -18,5 +18,6 @@ "lang_resetSession": "Sitzung zurücksetzen", "lang_sessionName": "Sitzungsname", "lang_createSession": "Sitzung erstellen", - "lang_saveAllChanges": "Alle Änderungen speichern" + "lang_saveAllChanges": "Alle Änderungen speichern", + "lang_installed_libraries": "Inhaltstypen" } diff --git a/assets/languages/en.json b/assets/languages/en.json index 1b3789c3..db0d30f7 100644 --- a/assets/languages/en.json +++ b/assets/languages/en.json @@ -18,5 +18,6 @@ "lang_resetSession": "reset session", "lang_sessionName": "session name", "lang_createSession": "create session", - "lang_saveAllChanges": "save all changes" + "lang_saveAllChanges": "save all changes", + "lang_installed_libraries": "Content Types" } diff --git a/assets/languages/es.json b/assets/languages/es.json index aaea71b2..710833d9 100644 --- a/assets/languages/es.json +++ b/assets/languages/es.json @@ -18,5 +18,6 @@ "lang_resetSession": "restablecer sesión", "lang_sessionName": "nombre de la sesión", "lang_createSession": "crear sesión", - "lang_saveAllChanges": "guardar todos los cambios" + "lang_saveAllChanges": "guardar todos los cambios", + "lang_installed_libraries": "Tipo de contenido" } diff --git a/assets/languages/fr.json b/assets/languages/fr.json index f1a1d24e..4fd18859 100644 --- a/assets/languages/fr.json +++ b/assets/languages/fr.json @@ -18,5 +18,6 @@ "lang_sessionName": "nom de la session", "lang_resetSession": "réinitialiser session", "lang_createSession": "créer une session", - "lang_saveAllChanges": "enregistrer toutes les modifications" + "lang_saveAllChanges": "enregistrer toutes les modifications", + "lang_installed_libraries": "Type de contenu" } diff --git a/assets/styles/style.css b/assets/styles/style.css index 8c99cb23..b2ae8d9d 100644 --- a/assets/styles/style.css +++ b/assets/styles/style.css @@ -110,6 +110,9 @@ body { .dashboard:before { content: "\e903"; } +.type-browser:before { + content: "\1F3EA"; +} .save-button { background-color: #388E3C; border-color: #388E3C; diff --git a/assets/templates/dashboard.html b/assets/templates/dashboard.html index 2f8ed723..988e5974 100644 --- a/assets/templates/dashboard.html +++ b/assets/templates/dashboard.html @@ -44,6 +44,9 @@
{lang_import}
+ + {lang_installed_libraries} +
{lang_language} + +
+
+ + +
+ +
+ +
+ + +
+
+ + +
+
+
+
+ + + Loading... + `; + return tmpl; + } + + connectedCallback() { + this.restoreFilterState(); + [this._runnableToggle, this._notrunnableToggle, this._dependencyToggle, this._searchInput].forEach((elem) =>{ + elem.addEventListener(elem.type === 'text' ? 'input' : 'change', (e)=>{ + this.saveFilterState(); + this.applyFilters(); + }); + }); + } + disconnectedCallback(){ + [this._runnableToggle, this._notrunnableToggle, this._dependencyToggle, this._searchInput].forEach((elem) =>{ + elem.removeAllListeners(); + }); + } + + /* + Set visible libraries for current search and filter options + */ + async applyFilters(){ + let searchVal = this._searchInput.value; + + this._slottedLibraries.forEach((lib) => { + let visible = false; + + if(lib.isRunnable()){ + visible = this._runnableToggle.checked; + } else { + visible = this._notrunnableToggle.checked; + } + + // it wasn't filtered out, do search string + if(visible && searchVal.length > 0){ + visible = lib.matchesSearch(searchVal, this._dependencyToggle.checked); + } + + if(visible){ + lib.classList.remove(H5pcliTypeBrowser.filteredClassName); + } else { + lib.classList.add(H5pcliTypeBrowser.filteredClassName); + } + }); + } + + saveFilterState(){ + localStorage.setItem(H5pcliTypeBrowser.runnableFilterState, this._runnableToggle.checked); + localStorage.setItem(H5pcliTypeBrowser.notrunnableFilterState, this._notrunnableToggle.checked); + localStorage.setItem(H5pcliTypeBrowser.showDependencyState, this._dependencyToggle.checked); + localStorage.setItem(H5pcliTypeBrowser.searchState, this._searchInput.value); + } + + restoreFilterState(){ + this._runnableToggle.checked = JSON.parse(localStorage.getItem(H5pcliTypeBrowser.runnableFilterState)) ?? true; + this._notrunnableToggle.checked = JSON.parse(localStorage.getItem(H5pcliTypeBrowser.notrunnableFilterState)) ?? false; + this._dependencyToggle.checked = JSON.parse(localStorage.getItem(H5pcliTypeBrowser.showDependencyState)) ?? false; + this._searchInput.value = localStorage.getItem(H5pcliTypeBrowser.searchState) ?? ""; + } + + // Invoked when component attribute changes + attributeChangedCallback(attrName, oldVal, newVal) { + if (attrName === 'src') { + this.fetchAndListLibraries(); + } + } + + static get observedAttributes() { + return ['src']; + } + + get src() { + return this.getAttribute('src'); + } + set src(val) { + this.setAttribute('src', val); + } + + + /* + Add a collapsed h5pcli-content-type for each library to the library slot + */ + async fetchAndListLibraries() { + if(!customElements.get('h5pcli-content-type')) throw("Required component h5pcli-content-type not loaded"); + + let res = await this.fetchLibraries(); + + for (const [name, lib] of Object.entries(res)) { + let type = document.createElement('h5pcli-content-type'); + type.setAttribute("class", "item"); + type.setAttribute("collapsed", ""); + type.slot = "library"; + type.renderFromAugmentedLibrary(lib); + this.append(type); + } + + this._slottedLibraries = this.shadowRoot.querySelector('slot[name="library"]').assignedElements({ flatten: true }); + this.applyFilters(); + } + + async fetchLibraries() { + try { + const response = await fetch(this.src); + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + + const json = await response.json(); + return json; + } catch (error) { + console.error(error.message); + } + } + +} + +if( !customElements.get('h5pcli-type-browser')){ + customElements.define('h5pcli-type-browser', H5pcliTypeBrowser); +} diff --git a/assets/type_browser/components/h5pcli-xapi-examples.js b/assets/type_browser/components/h5pcli-xapi-examples.js new file mode 100644 index 00000000..3647164b --- /dev/null +++ b/assets/type_browser/components/h5pcli-xapi-examples.js @@ -0,0 +1,112 @@ +/** + * A wrapper component for h5pcli-xapi-viewer to populate it pre-saved xAPI examples + * It will defer fetching the events until visible, and disables the h5pcli-xapi-viewer instance from listening for new events + * + * @attribute {string} src - The optional API endpoint that returns an array of example xAPI events. Fetching events is deferred until the element is visible. + * + * @slot viewer - The slot for the h5pcli-xapi-viewer which will be added with the attributes: open, nolisten + * + * @tag h5pcli-xapi-examples + * @tagname h5pcli-xapi-examples + * + * @example HTML Usage + * + * + * @example Javascript Instantiation + * let examples = document.createElement('h5pcli-xapi-examples'); + * examples.src = "./examples.json"; + * this.append(examples); + */ +export default class H5pcliXapiExamples extends HTMLElement { + constructor() { + super(); + + let shadowRoot = this.attachShadow({ mode: 'open' }); + shadowRoot.appendChild(H5pcliXapiExamples.template.content.cloneNode(true)); + + this._loaded = false; + } + + static get template() { + let tmpl = document.createElement('template'); + tmpl.innerHTML = ` + + Loading... + `; + return tmpl; + } + + /* + * Use an IntersectionObserver to detect when the element is visible + * once visible, it will stop observing and asynchronously fetch and load the src data + */ + connectedCallback() { + // delay fetching the xapi events until visible + const observer = new IntersectionObserver( + (entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + if(this.src){ + this.fetchAndListEvents(); + } + observer.unobserve(entry.target); + } + }); + }, + { + root: this.parentNode + } + ); + + observer.observe(this); + } + + get src() { + return this.getAttribute('src'); + } + set src(val) { + this.setAttribute('src', val); + } + + /* + * If the h5pcli-xapi-viewer element exists, fetch the src data and populate that component + * with all the xAPI events from the src call. + * The h5pcli-xapi-viewer element is set to nolisten mode. + */ + async fetchAndListEvents() { + if(!customElements.get('h5pcli-xapi-viewer')) throw("Required component h5pcli-xapi-viewer not loaded"); + if(this._loaded){return}; + this._loaded = true; + + let res = await this.fetchEvents(); + let viewer = document.createElement('h5pcli-xapi-viewer'); + viewer.slot = "viewer"; + viewer.setAttribute("nolisten", "nolisten"); + viewer.setAttribute("open", "open"); + this.append(viewer); + + for (const event of res['xapiExamples']) { + viewer.createSummaryAndAdd(event); + } + } + + async fetchEvents() { + try { + const response = await fetch(this.src); + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + + const json = await response.json(); + return json; + } catch (error) { + console.error(error.message); + } + } + +} + +if( !customElements.get('h5pcli-xapi-examples')){ + customElements.define('h5pcli-xapi-examples', H5pcliXapiExamples); +} diff --git a/assets/type_browser/components/h5pcli-xapi-viewer.js b/assets/type_browser/components/h5pcli-xapi-viewer.js new file mode 100644 index 00000000..c3805912 --- /dev/null +++ b/assets/type_browser/components/h5pcli-xapi-viewer.js @@ -0,0 +1,210 @@ +/** + * Display xAPI events in collapsed details nodes + * Each event is displayed with a human-readable summary for {actor} {action} {object} + * + * If in listen mode (default): Listen for xAPI events from the H5P.externalDispatcher and render on page + * If in nolisten mode: Only display xAPI events + * + * @attribute {bool} open - The list of captured events is closed by default, adding this attribute will start it open + * @attribute {bool} float - Enable the list renders _over_ the surrounding content + * @attribute {bool} nolisten - Don't listen for H5P events, they can still be added manually + * + * @slot events - Slot for added events + * + * @example HTML Usage - Capture events on an H5P View page and when expanded, float content over page + * + * + * @example Javascript Instantiation with existing list of events + * let viewer = document.createElement('h5pcli-xapi-viewer'); + * viewer.setAttribute("nolisten", "nolisten"); + * viewer.setAttribute("open", "open"); + * this.append(viewer); + * + * for (const event of xAPIJsonExamplesArray) { + * viewer.createSummaryAndAdd(event); + * } + * + * @tag h5pcli-xapi-viewer + * @tagname h5pcli-xapi-viewer + */ +export default class H5pcliXapiViewer extends HTMLElement { + static get observedAttributes() { + return ["open", "float", "nolisten"]; + } + + constructor() { + super(); + + let shadowRoot = this.attachShadow({ mode: 'open' }); + shadowRoot.appendChild(H5pcliXapiViewer.template.content.cloneNode(true)); + + this._summary = this.shadowRoot.querySelector('summary'); + this._list = this.shadowRoot.querySelector('.list'); + this._content = this.shadowRoot.querySelector('.content'); + + this._listening = false; + + this.preProcessEvent = this.preProcessEvent.bind(this) + } + + static get template() { + let tmpl = document.createElement('template'); + tmpl.innerHTML = ` + +
+ xAPI Events +
+ +
+
+ + `; + return tmpl; + } + + connectedCallback() { + this.setXapiListener(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if(name === "open"){ + if(newValue === null){ + this._list.removeAttribute("open"); + } else { + this._list.setAttribute("open", 'open'); + } + } + if(name === "float"){ + if(newValue === null){ + this._content.classList.remove("float"); + } else { + this._content.classList.add("float"); + } + } + if(name === "nolisten"){ + this.setXapiListener(); + } + + } + + setXapiListener(){ + if(!this.hasAttribute("nolisten") && typeof H5P !== 'undefined'){ + H5P.externalDispatcher.on('xAPI', this.preProcessEvent); + } else if (typeof H5P !== 'undefined') { + H5P.externalDispatcher.off('xAPI', this.preProcessEvent); + } + } + + preProcessEvent(event){ + this.createSummaryAndAdd(event.data.statement); + } + + /** + * Create a human-readable summary for {actor} {action} {object} {score/result summary} + * @param statement + */ + createSummaryAndAdd(statement) { + let summary = "Example Event"; + try { + let user = statement.actor.name; + let verb = statement.verb.id.split('/').pop(); + let object = statement.object.id.split('/').pop(); + let result = ''; + + if (statement.object.definition?.name?.['en-US']) { + object = statement.object.definition.name['en-US']; + } + + if (statement.result) { + let res = statement.result; + if("success" in res){ + result = res.success ? "correct" : "incorrect"; + } + if(res.score){ + result += ` (${res.score.raw}/${res.score.max})`; + } + if(result){ + result = ` - (${result})`; + } + } + + summary = `${user} "${verb}" ${object} ${result}`; + } catch (e) { + console.log("Error creating summary for event. ", e, statement); + } + this.addEvent(summary, statement); + } + + /** + * Add the given short summary with a collapsed details node of the actual xAPI JSON object + * + * @param shortStatement + * @param jsonStringOrObj + */ + addEvent(shortStatement, jsonStringOrObj){ + if(!jsonStringOrObj){return;} + let json_parsed; + + try{ + json_parsed = (typeof jsonStringOrObj === 'string') ? JSON.parse(jsonStringOrObj) : jsonStringOrObj; + } catch(err) { + console.log("Error parsing json", err, json_parsed) + return; + } + + let details = document.createElement('details') + details.slot = "events"; + details.setAttribute("class", "slotted-details") + let summary = document.createElement('summary') + summary.textContent = shortStatement; + details.append(summary); + let pre = document.createElement('pre'); + let code = document.createElement('code') + code.setAttribute("class", "language-json") + code.textContent = JSON.stringify(json_parsed, undefined, 2) + pre.append(code); + + details.append(pre) + this.append(details); + + if(typeof hljs !== "undefined"){ + try{hljs.highlightElement(code);} catch {console.log("hljs loaded, but failed to highlight xapi event.")} + } + + let count = this.querySelectorAll("details").length; + this._summary.textContent = `xAPI Events (${count})` + } +} + +if( !customElements.get("h5pcli-xapi-viewer")){ + customElements.define('h5pcli-xapi-viewer', H5pcliXapiViewer); +} diff --git a/assets/type_browser/components/index.js b/assets/type_browser/components/index.js new file mode 100644 index 00000000..0dfae8b7 --- /dev/null +++ b/assets/type_browser/components/index.js @@ -0,0 +1,7 @@ +import "./h5pcli-content-type.js"; +import "./h5pcli-xapi-viewer.js"; +import "./h5pcli-xapi-examples.js"; +import "./h5pcli-semantics.js"; +import "./h5pcli-semantics-docs.js"; +import "./h5pcli-library-overview.js"; +import "./h5pcli-type-browser.js"; diff --git a/assets/type_browser/components/tabs.css b/assets/type_browser/components/tabs.css new file mode 100644 index 00000000..19f2a996 --- /dev/null +++ b/assets/type_browser/components/tabs.css @@ -0,0 +1,35 @@ +h5pcli-content-type > details { + display: contents; +} + +h5pcli-content-type > details > summary { + display: flex; + justify-content: center; + grid-row: 1; + cursor: pointer; + border: 1px solid #ccc; + /*border-bottom: none;*/ + padding: 4px; +} + +h5pcli-content-type > details > div { + border: 1px solid #ccc; + padding: 1rem; + grid-column: 1 / -1; + position: sticky; + left: 0; + border-radius: 0 0 4px 4px; +} + +h5pcli-content-type > details[open]::details-content { + display: contents; +} + +h5pcli-content-type > details[open] > summary { + font-weight: bold; +} + +h5pcli-content-type[collapsed] > details > div { + /*max-height: 500px;*/ + overflow: auto; +} diff --git a/assets/type_browser/content_type.html b/assets/type_browser/content_type.html new file mode 100644 index 00000000..5f65e3c2 --- /dev/null +++ b/assets/type_browser/content_type.html @@ -0,0 +1,38 @@ + + + + {lang_installed_libraries} + + + + + + + + + + + + +
+ +
+ +
+ +
+ + diff --git a/assets/type_browser/index.html b/assets/type_browser/index.html new file mode 100644 index 00000000..c70fee3d --- /dev/null +++ b/assets/type_browser/index.html @@ -0,0 +1,36 @@ + + + + {lang_installed_libraries} + + + + + + + + + + + + +
+ +
+ + +
+ +
+ + diff --git a/src/server/apis/typeBrowser.js b/src/server/apis/typeBrowser.js new file mode 100644 index 00000000..5f3db639 --- /dev/null +++ b/src/server/apis/typeBrowser.js @@ -0,0 +1,182 @@ +const fs = require('fs'); +const path = require('path'); +const logic = require('../../../logic.js'); +const config = require('../../../configLoader.js'); +const userSession = require('../user_session.js'); + +const xapiExamplesDir = "events/xapi/examples"; +// Default data links to locate +const linksData = {semantics: "semantics.json", }; + +/** + * Render the /type_browser/index.html page with translations + * + * @route GET /type_browser + */ +async function typeBrowserIndex(request, response, next) { + try { + userSession.updateFromQuery(request); + const html = fs.readFileSync(`${require.main.path}/${config.folders.assets}/type_browser/index.html`, 'utf-8'); + const labels = await userSession.getLangLabels(); + response.set('Content-Type', 'text/html'); + response.end(logic.fromTemplate(html, labels)); + } catch (error) { + handleError(error, response); + } +} + +/** + * Return list of Augmented H5P Library objects that include links + * + * @route GET /type_browser/installed_libraries + */ +async function installedLibraries(request, response, next) { + try { + const out = await fullLibraries(); + + response.set('Content-Type', 'application/json'); + response.end(JSON.stringify(out)); + } catch (error) { + handleError(error, response); + } +} + +/** + * Renders a content page for displaying details about a Library + * + * @route GET /type_browser/:library + * @param request.library + */ +async function contentType(request, response, next) { + try { + userSession.updateFromQuery(request); + const html = fs.readFileSync(`${require.main.path}/${config.folders.assets}/type_browser/content_type.html`, 'utf-8'); + const labels = await userSession.getLangLabels(); + response.set('Content-Type', 'text/html'); + response.end(logic.fromTemplate(html, labels)); + } catch (error) { + handleError(error, response); + } +} + +/** + * Returns the Augmented Library JSON object for the specified :library + * + * @route /type_browser/:library/library.json + * @param request.library + */ +async function contentTypeLibrary(request, response, next){ + const out = await singleLibrary(request.params.library); + + response.set('Content-Type', 'application/json'); + response.end(JSON.stringify(out)); +} + +/** + * If the specified :library has example xAPI events in events/xapi/examples return them in a JSON array + * + * @route /type_browser/:library/xapi_examples + * @param request.library + */ +async function xapiEventsForType(request, response, next){ + const out = {xapiExamples: []}; + + let xapiDir = path.join(config.folders.libraries, request.params.library, xapiExamplesDir) + if(fs.existsSync(xapiDir)){ + const files = fs.readdirSync(xapiDir); + const jsonFiles = files.filter(el => path.extname(el) === '.json') + for (let file of jsonFiles) { + out['xapiExamples'].push(await logic.getFile(path.join(xapiDir, file), true)); + } + } + + response.set('Content-Type', 'application/json'); + response.end(JSON.stringify(out)); +} + +module.exports = {contentType, contentTypeLibrary, installedLibraries, typeBrowserIndex, xapiEventsForType} + + +/* +Helper functions + */ + +/** + * Using the registry list, iterate all local libraries and generate a list of Augmented Library JSON + */ +async function fullLibraries() { + let registry = await logic.getRegistry(); + registry = registry.regular; + const output = {}; + const dirs = fs.readdirSync(config.folders.libraries); + for (let folder of dirs) { + let lib = await fullLibrary(folder, registry); + if(lib) output[lib.library.machineName] = lib; + } + return output; +} + +/** + * Generate an Augmented Library JSON for the specified Library + * @param libraryFolder + * @returns {Promise} + */ +async function singleLibrary(libraryFolder){ + let registry = await logic.getRegistry(); + registry = registry.regular; + + return await fullLibrary(libraryFolder, registry); +} + +/** + * Generate an Augmented Library JSON for the specified Library + * @param folderName + * @param registry + * @returns {Promise<{links_data: {library: string}, icon_url: null, links_web: {docPage: string}, library: null}>} + */ +async function fullLibrary(folderName, registry) { + let folder = path.join(config.folders.libraries, folderName); + const libraryFile = path.join(folder, "library.json"); + if (!fs.existsSync(libraryFile)) { + return; + } + + let out = { + links_data: {library: path.join("/", libraryFile)}, + links_web: {docPage: `/type_browser/${folderName}/`}, + icon_url: null, + library: null, + } + + out.library = await logic.getFile(libraryFile, true); + + const id = out.library.machineName; + if (registry[id]) { + out.library["repoName"] = registry[id]["repoName"]; + out.library["org"] = registry[id]["org"]; + out.library["shortName"] = registry[id]["shortName"]; + } + + let iconPath = path.join(folder, "icon.svg") + if (fs.existsSync(iconPath)) out.icon_url = path.join("/", iconPath); + + let xapiPath = path.join(folder, xapiExamplesDir) + if(fs.existsSync(xapiPath)){ + out.links_data['xapi_examples'] = `/type_browser/${folderName}/xapi_examples`; + } + + for (const [key, file] of Object.entries(linksData)) { + const localPath = path.join(folder, file); + if (fs.existsSync(localPath)) out.links_data[key] = path.join("/", localPath); + } + + return out; +} + + +// Helpers copied from ../api.js, find way to reuse? +const handleError = (error, response) => { + console.log(error); + response.set('Content-Type', 'application/json'); + response.end(JSON.stringify({ error: error.toString() })); +} diff --git a/src/server/server.js b/src/server/server.js index cd9c035b..7ed8048b 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -2,11 +2,14 @@ const express = require('express'); const config = require('../../configLoader.js'); const multer = require('multer')({ dest: `./${config.folders.temp}` }); const api = require('./api.js'); +const typeBrowser = require('./apis/typeBrowser.js'); let app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); + +// Dashboard & CMS app.get('/', (req, res) => res.redirect('/dashboard')); app.get('/favicon.ico', api.favicon); app.get('/dashboard', api.dashboard); @@ -16,18 +19,36 @@ app.get('/export/:library/:folder', api.export); app.post('/import/:folder', multer.single('file'), api.import); app.post('/create/:type/:folder', api.create); app.post('/remove/:folder', api.remove); -app.get('/split/:library/:folder', api.splitView); + +// View app.get('/view/:library/:folder', api.view); +app.get('/split/:library/:folder', api.splitView); + +// Editing app.get('/edit/:library/:folder/libraries', api.ajaxLibraries); app.post('/edit/:library/:folder/translations', api.ajaxTranslations); app.get('/edit/:library/:folder', api.edit); app.post('/edit/:library/:folder/libraries', api.ajaxLibraries); app.post('/edit/:library/:folder/files', multer.single('file'), api.uploadFile); app.post('/edit/:library/:folder', multer.none(), api.saveContent); + +// Content Sessions app.get('/content-user-data/:folder/:type/:id', api.getUserData); app.post('/content-user-data/:folder/:type/:id', api.setUserData); app.delete('/content-user-data/:folder', api.resetUserData); + +// Serve assets dir app.use(`/${config.folders.assets}`, express.static(`${require.main.path}/${config.folders.assets}`)) + +// Content Type Browser +app.get('/type_browser/installed_libraries', typeBrowser.installedLibraries); +app.get('/type_browser/:library/xapi_examples', typeBrowser.xapiEventsForType); +app.get('/type_browser/:library/library.json', typeBrowser.contentTypeLibrary); +app.get('/type_browser/:library', typeBrowser.contentType); +app.get('/type_browser', typeBrowser.typeBrowserIndex); +app.use(`/type_browser`, express.static(`${require.main.path}/${config.folders.assets}/type_browser/`)) + +// Everything else will serve statically from the working directory where `h5p server` is running: app.use(express.static('./')); let port = config.port;