Compare commits
2 Commits
10c31561fd
...
c66bd1909d
Author | SHA1 | Date |
---|---|---|
|
c66bd1909d | |
|
4a761fb031 |
|
@ -0,0 +1,4 @@
|
||||||
|
.parcel-cache/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "@parcel/config-default",
|
||||||
|
"transformers": {
|
||||||
|
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
|
||||||
|
},
|
||||||
|
"validators": {
|
||||||
|
"*.{ts,tsx}": ["@parcel/validator-typescript"]
|
||||||
|
}
|
||||||
|
}
|
182
index.js
182
index.js
|
@ -1,182 +0,0 @@
|
||||||
function showApplication(toggle) {
|
|
||||||
if (toggle) {
|
|
||||||
document.getElementById('real-body').classList.remove('d-none');
|
|
||||||
} else {
|
|
||||||
document.getElementById('real-body').classList.add('d-none');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLoadingIndicator(toggle) {
|
|
||||||
if (toggle) {
|
|
||||||
document.getElementById('loading').classList.remove('d-none');
|
|
||||||
} else {
|
|
||||||
document.getElementById('loading').classList.add('d-none');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayError(querySelector, category, ex) {
|
|
||||||
console.error(ex);
|
|
||||||
const errorDiv = document.querySelector(querySelector);
|
|
||||||
const errorHeading = errorDiv.querySelector('.alert-heading');
|
|
||||||
|
|
||||||
let message = ex.message;
|
|
||||||
|
|
||||||
if (ex.hasOwnProperty('cause')) {
|
|
||||||
if (ex.cause.hasOwnProperty('message')) {
|
|
||||||
message += ': ' + ex.cause.message;
|
|
||||||
} else {
|
|
||||||
message += ex.cause;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//clear all error text, but not heading
|
|
||||||
const errorMessages = errorDiv.querySelectorAll('span');
|
|
||||||
errorMessages.forEach(message => errorDiv.removeChild(message));
|
|
||||||
|
|
||||||
//add new message and show the error alert
|
|
||||||
const errorSpan = document.createElement('span');
|
|
||||||
errorSpan.innerText = message;
|
|
||||||
errorDiv.appendChild(errorSpan);
|
|
||||||
|
|
||||||
errorHeading.innerText = category;
|
|
||||||
errorDiv.classList.remove('d-none');
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayFatalError(category, ex) {
|
|
||||||
showApplication(false);
|
|
||||||
showLoadingIndicator(false);
|
|
||||||
displayError('#fatal-error', category, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayNonFatalError(category, ex) {
|
|
||||||
showApplication(true);
|
|
||||||
showLoadingIndicator(false);
|
|
||||||
displayError('#display-backup-error', category, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideErrors() {
|
|
||||||
document
|
|
||||||
.querySelectorAll('#fatal-error, #display-backup-error')
|
|
||||||
.forEach(el => el.classList.add('d-none'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function importStylesheet(xsl) {
|
|
||||||
//Firefox does not seem to report XML parsing errors into the
|
|
||||||
//exception tree. So we catch here, log, and return some friendly
|
|
||||||
//error.
|
|
||||||
try {
|
|
||||||
const xsltProcessor = new XSLTProcessor();
|
|
||||||
xsltProcessor.importStylesheet(xsl);
|
|
||||||
return xsltProcessor;
|
|
||||||
} catch (cause) {
|
|
||||||
console.error(cause);
|
|
||||||
throw new Error('Stylesheet parsing failed', { cause });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createXSLTProcessor() {
|
|
||||||
return fetch("/MessageLog.xsl")
|
|
||||||
.then(resp => {
|
|
||||||
if (resp.ok)
|
|
||||||
return resp.text();
|
|
||||||
else
|
|
||||||
throw new Error(resp.status + ' ' + resp.statusText);
|
|
||||||
})
|
|
||||||
.then(str => new DOMParser().parseFromString(str, "text/xml"))
|
|
||||||
.then(importStylesheet)
|
|
||||||
.catch(cause => {
|
|
||||||
throw new Error('Could not load XSL stylesheet', { cause });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeChildren(element) {
|
|
||||||
while (element.hasChildNodes()) {
|
|
||||||
element.removeChild(element.lastChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkOverflow(elem) {
|
|
||||||
const elemHeight = elem.scrollHeight;
|
|
||||||
const parentHeight = elem.offsetHeight;
|
|
||||||
return elemHeight > parentHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function processFragment() {
|
|
||||||
document
|
|
||||||
.querySelectorAll('[data-bs-toggle="popover"]')
|
|
||||||
.forEach(popover => new bootstrap.Popover(popover));
|
|
||||||
|
|
||||||
document.querySelectorAll('.message-content div').forEach(div => {
|
|
||||||
if (checkOverflow(div)) {
|
|
||||||
div.parentElement.classList.add('overflow-icon');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseXML(xmlText) {
|
|
||||||
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayBackup(xsltProcessor, file) {
|
|
||||||
hideErrors();
|
|
||||||
|
|
||||||
//document.getElementById('intro-card').classList.add('d-none');
|
|
||||||
|
|
||||||
const chatDisplay = document.getElementById("chat-display");
|
|
||||||
removeChildren(chatDisplay);
|
|
||||||
|
|
||||||
file.text().then(xmlText => {
|
|
||||||
document.getElementById('currently-viewing').innerText = file.name;
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
|
|
||||||
const errorNode = xmlDoc.querySelector('parsererror');
|
|
||||||
|
|
||||||
if (!errorNode) {
|
|
||||||
try {
|
|
||||||
const fragment = xsltProcessor.transformToFragment(xmlDoc, document);
|
|
||||||
chatDisplay.appendChild(fragment);
|
|
||||||
processFragment();
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
displayNonFatalError('Could not load backup', e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const err = new Error(errorNode.firstChild.nodeValue);
|
|
||||||
displayNonFatalError('Could not load backup', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initEvents(xsltProcessor) {
|
|
||||||
const selectFileButtons = document.querySelectorAll("#backup-file-button, #backup-file-button-card");
|
|
||||||
const fileSelector = document.getElementById("backup-file");
|
|
||||||
|
|
||||||
selectFileButtons.forEach(button => button.addEventListener("click", e => {
|
|
||||||
fileSelector.click();
|
|
||||||
e.preventDefault();
|
|
||||||
}));
|
|
||||||
|
|
||||||
fileSelector.addEventListener("change", async (e) => {
|
|
||||||
const file = e.currentTarget.files[0];
|
|
||||||
displayBackup(xsltProcessor, file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
if (window.innerWidth <= 500) {
|
|
||||||
document.querySelector('#chat-display table').classList.add('table-sm');
|
|
||||||
} else {
|
|
||||||
document.querySelector('#chat-display table').classList.remove('table-sm');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
createXSLTProcessor()
|
|
||||||
.then(initEvents)
|
|
||||||
.then(_ => {
|
|
||||||
showLoadingIndicator(false);
|
|
||||||
showApplication(true);
|
|
||||||
})
|
|
||||||
.catch(ex => displayFatalError('Initialiation Failed', ex));
|
|
||||||
});
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2017",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"jsx": "react",
|
|
||||||
"lib": [ "dom", "es2017" ]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "msn-viewer",
|
||||||
|
"source": "src/index.html",
|
||||||
|
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||||
|
"scripts": {
|
||||||
|
"start": "parcel",
|
||||||
|
"build": "parcel build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@parcel/transformer-inline-string": "2.8.2",
|
||||||
|
"@parcel/transformer-sass": "2.8.2",
|
||||||
|
"@parcel/transformer-typescript-tsc": "^2.8.2",
|
||||||
|
"@parcel/validator-typescript": "^2.8.2",
|
||||||
|
"parcel": "^2.8.2",
|
||||||
|
"typescript": ">=3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@popperjs/core": "^2.11.6",
|
||||||
|
"@types/bootstrap": "^5.2.3",
|
||||||
|
"bootstrap": "^5.2",
|
||||||
|
"bootstrap-icons": "^1.10.3",
|
||||||
|
"dexie": "^3.2.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,7 +75,7 @@ https://github.com/jerecui/MSNChatHistoryCombiner
|
||||||
|
|
||||||
<td class="message-from-user"><xsl:apply-templates select="From/User"/> </td>
|
<td class="message-from-user"><xsl:apply-templates select="From/User"/> </td>
|
||||||
<td class="message-content">
|
<td class="message-content">
|
||||||
<div class="overflow-y-auto overflow-x-hidden">
|
<div class="overflow-auto">
|
||||||
<xsl:value-of select="Text"/>
|
<xsl:value-of select="Text"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -96,14 +96,15 @@ https://github.com/jerecui/MSNChatHistoryCombiner
|
||||||
</xsl:template>
|
</xsl:template>
|
||||||
|
|
||||||
<xsl:template match="From/User">
|
<xsl:template match="From/User">
|
||||||
<span
|
<a class="info-popover" href="#" tabindex="0"
|
||||||
data-bs-container="body" data-bs-toggle="popover"
|
data-bs-trigger="focus"
|
||||||
data-bs-placement="bottom" data-bs-title="Username">
|
data-bs-container="body" data-bs-toggle="popover"
|
||||||
|
data-bs-placement="bottom" data-bs-title="Username">
|
||||||
<xsl:attribute name="data-bs-content">
|
<xsl:attribute name="data-bs-content">
|
||||||
<xsl:value-of select="@FriendlyName"/>
|
<xsl:value-of select="@FriendlyName"/>
|
||||||
</xsl:attribute>
|
</xsl:attribute>
|
||||||
<xsl:value-of select="@FriendlyName"/>
|
<xsl:value-of select="@FriendlyName"/>
|
||||||
</span>
|
</a>
|
||||||
</xsl:template>
|
</xsl:template>
|
||||||
|
|
||||||
<xsl:template name="CommonMessageProcessing">
|
<xsl:template name="CommonMessageProcessing">
|
||||||
|
@ -118,14 +119,15 @@ https://github.com/jerecui/MSNChatHistoryCombiner
|
||||||
|
|
||||||
<!-- mobile shows a popover span for date/time -->
|
<!-- mobile shows a popover span for date/time -->
|
||||||
<td class="d-lg-none message-mobile-date-time">
|
<td class="d-lg-none message-mobile-date-time">
|
||||||
<span
|
<a class="info-popover" href="#" tabindex="0"
|
||||||
data-bs-container="body" data-bs-toggle="popover"
|
data-bs-trigger="focus"
|
||||||
data-bs-placement="right" data-bs-title="Date/Time">
|
data-bs-container="body" data-bs-toggle="popover"
|
||||||
|
data-bs-placement="right" data-bs-title="Date/Time">
|
||||||
<xsl:attribute name="data-bs-content">
|
<xsl:attribute name="data-bs-content">
|
||||||
<xsl:value-of select="@Date"/> <xsl:value-of select="@Time"/>
|
<xsl:value-of select="@Date"/> <xsl:value-of select="@Time"/>
|
||||||
</xsl:attribute>
|
</xsl:attribute>
|
||||||
#
|
#
|
||||||
</span>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-lg-table-cell message-date"> <xsl:value-of select="@Date"/> </td>
|
<td class="d-none d-lg-table-cell message-date"> <xsl:value-of select="@Date"/> </td>
|
||||||
<td class="d-none d-lg-table-cell message-time"> <xsl:value-of select="@Time"/> </td>
|
<td class="d-none d-lg-table-cell message-time"> <xsl:value-of select="@Time"/> </td>
|
|
@ -0,0 +1,20 @@
|
||||||
|
import Dexie from 'dexie';
|
||||||
|
|
||||||
|
export class MsnDatabase extends Dexie {
|
||||||
|
entries!: Dexie.Table<BackupEntry, string>; // number = type of the primkey
|
||||||
|
//...other tables goes here...
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super("MsnDatabase");
|
||||||
|
this.version(1).stores({
|
||||||
|
entries: '&filename, backupData',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupEntry {
|
||||||
|
filename: string,
|
||||||
|
backupData: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const db = new MsnDatabase();
|
|
@ -4,15 +4,11 @@
|
||||||
<title>🔥 MSN Viewer</title>
|
<title>🔥 MSN Viewer</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link rel="stylesheet" href="./scss/index.scss" />
|
||||||
integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css">
|
|
||||||
<link rel="stylesheet" href="index.css" />
|
|
||||||
<script type="text/javascript" src="index.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container-md mx-auto">
|
<div class="container-lg mx-auto">
|
||||||
<div id="loading" class="d-flex justify-content-center">
|
<div id="loading" class="d-flex justify-content-center">
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
@ -27,7 +23,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="real-body" class="d-none">
|
<div id="real-body" class="d-none">
|
||||||
<nav id="main-navbar" class="navbar px-0 sticky-top navbar-expand-lg navbar-light bg-body-tertiary">
|
<nav id="main-navbar" class="navbar px-0 sticky-top navbar-expand-lg bg-light">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#">
|
<a class="navbar-brand" href="#">
|
||||||
🔥 MSN Viewer
|
🔥 MSN Viewer
|
||||||
|
@ -53,9 +49,51 @@
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="backup-info" class="mt-3">
|
<div id="backup-info" class="mt-3">
|
||||||
<div id="currently-viewing" class="alert alert-secondary text-center d-none" role="alert"><none></div>
|
<input class="form-control" id="backup-file" type="file" style="display: none;">
|
||||||
<label for="backup-file" class="form-label">Choose an MSN Messenger XML Backup File.</label>
|
|
||||||
<input class="form-control" id="backup-file" type="file">
|
<!-- displayed once a file has been opened -->
|
||||||
|
<div id="file-loaded-controls" class="d-none">
|
||||||
|
<div class="input-group">
|
||||||
|
<button id="loaded-view-new-button" type="button" class="btn btn-primary">
|
||||||
|
<i class="bi bi-file-earmark-plus"></i> View New
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary dropdown-toggle dropdown-toggle-split"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<span class="visually-hidden">Toggle Dropdown</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a id="loaded-load-previous-button" class="dropdown-item" href="#">
|
||||||
|
<i class="text-start bi bi-files"></i> Load Previous
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<input id="currently-viewing" type="text" class="form-control bg-transparent" disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- displayed initially -->
|
||||||
|
<div id="no-file-loaded-controls" class="d-grid gap-2 text-center d-md-block col-8 mx-auto">
|
||||||
|
<button id="unloaded-view-new-button" type="button" class="btn btn-primary btn-lg">
|
||||||
|
<span class="float-start">
|
||||||
|
<i class="bi bi-file-earmark-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
View New
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span class="d-none d-lg-inline fs-4">or...</span>
|
||||||
|
|
||||||
|
<button id="unloaded-load-previous-button" type="button"
|
||||||
|
class="btn btn-primary btn-lg">
|
||||||
|
<span class="float-start">
|
||||||
|
<i class="text-start bi bi-files"></i>
|
||||||
|
</span>
|
||||||
|
<span class="text-end">Load Previous</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="backup-display" class="mt-3">
|
<div id="backup-display" class="mt-3">
|
||||||
|
@ -72,6 +110,21 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- modals -->
|
<!-- modals -->
|
||||||
|
<div class="modal fade" id="previous-backups-modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-fullscreen-lg-down">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="exampleModalLabel">Previously Viewed</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="previous-backups-list" class="list-group">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="help" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
<div class="modal fade" id="help" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-fullscreen-lg-down">
|
<div class="modal-dialog modal-fullscreen-lg-down">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -95,11 +148,14 @@
|
||||||
<li>You must have the XML backup
|
<li>You must have the XML backup
|
||||||
available on your device to view
|
available on your device to view
|
||||||
it.</li>
|
it.</li>
|
||||||
<li>Long usernames are truncated.
|
<li>Long usernames are shortened.
|
||||||
Click on such a username to see a
|
Click or tap on such a username to
|
||||||
popup with the full username.
|
see a popup with the full
|
||||||
Click in the same spot to hide the
|
username. Click or tap again to
|
||||||
popup.</li>
|
hide the popup.</li>
|
||||||
|
<li>When you have opened a backup for
|
||||||
|
viewing, you can easily re-open it
|
||||||
|
later by selecting "Load Previous".</li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -113,16 +169,13 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>Tap on the pound sign to see the
|
<li>Tap on the pound sign to see the
|
||||||
date and time of the message.</li>
|
date and time of the message.</li>
|
||||||
<li>Tap again in the same place to
|
<li>Tap again to hide the popup.</li>
|
||||||
hide the popup.</li>
|
|
||||||
<li>Usernames are similarly shortened.
|
|
||||||
Tap on a username to see the full
|
|
||||||
name.</li>
|
|
||||||
<li>Long chat messages are scrollable.
|
<li>Long chat messages are scrollable.
|
||||||
A small icon in the lower right
|
A small icon in the lower right
|
||||||
part of the message indicates
|
part of the message indicates
|
||||||
this.</li>
|
this.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -159,10 +212,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="module" src="index.ts"></script>
|
||||||
<script
|
|
||||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"
|
|
||||||
integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -0,0 +1,204 @@
|
||||||
|
import * as bootstrap from 'bootstrap';
|
||||||
|
import xslContent from 'bundle-text:./MessageLog.xsl';
|
||||||
|
import * as ui from './ui';
|
||||||
|
import { db } from './db';
|
||||||
|
|
||||||
|
const xsltProcessor = createXSLTProcessor(xslContent)!;
|
||||||
|
|
||||||
|
function showApplication(toggle: boolean) {
|
||||||
|
if (toggle) {
|
||||||
|
ui.realBody.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
ui.realBody.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoadingIndicator(toggle: boolean) {
|
||||||
|
if (toggle) {
|
||||||
|
ui.loadingIndicator.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
ui.loadingIndicator.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoadedControls() {
|
||||||
|
ui.noFileLoadedControls.classList.add('d-none');
|
||||||
|
ui.noFileLoadedControls.classList.remove('d-md-block');
|
||||||
|
ui.fileLoadedControls.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayError(errorDiv: HTMLElement, category: string, ex: Error) {
|
||||||
|
console.error(ex);
|
||||||
|
const errorHeading = errorDiv.querySelector('.alert-heading') as HTMLElement | null;
|
||||||
|
|
||||||
|
//clear all error text, but not heading
|
||||||
|
const errorMessages = errorDiv.querySelectorAll('span');
|
||||||
|
errorMessages.forEach(message => errorDiv.removeChild(message));
|
||||||
|
|
||||||
|
//add new message and show the error alert
|
||||||
|
const errorSpan = document.createElement('span');
|
||||||
|
errorSpan.innerText = ex.message;
|
||||||
|
errorDiv.appendChild(errorSpan);
|
||||||
|
|
||||||
|
if (errorHeading) {
|
||||||
|
errorHeading.innerText = category;
|
||||||
|
}
|
||||||
|
errorDiv.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayFatalError(category: string, ex: Error) {
|
||||||
|
showApplication(false);
|
||||||
|
showLoadingIndicator(false);
|
||||||
|
displayError(ui.fatalError, category, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayNonFatalError(category: string, ex: Error) {
|
||||||
|
showApplication(true);
|
||||||
|
showLoadingIndicator(false);
|
||||||
|
displayError(ui.nonFatalError, category, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideErrors() {
|
||||||
|
document
|
||||||
|
.querySelectorAll('#fatal-error, #display-backup-error')
|
||||||
|
.forEach(el => el.classList.add('d-none'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createXSLTProcessor(xmlStylesheet: string): XSLTProcessor | undefined {
|
||||||
|
const doc = new DOMParser().parseFromString(xmlStylesheet, "text/xml");
|
||||||
|
try {
|
||||||
|
const xsltProcessor = new XSLTProcessor();
|
||||||
|
xsltProcessor.importStylesheet(doc);
|
||||||
|
return xsltProcessor;
|
||||||
|
} catch (cause) {
|
||||||
|
console.error(cause);
|
||||||
|
displayFatalError('Initialiation Failed', new Error('Stylesheet parsing failed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeChildren(element: HTMLElement) {
|
||||||
|
while (element.hasChildNodes() && element.lastChild != null) {
|
||||||
|
element.removeChild(element.lastChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkOverflow(elem: HTMLElement) {
|
||||||
|
const elemHeight = elem.scrollHeight;
|
||||||
|
const parentHeight = elem.offsetHeight;
|
||||||
|
return elemHeight > parentHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processFragment() {
|
||||||
|
document
|
||||||
|
.querySelectorAll('[data-bs-toggle="popover"]')
|
||||||
|
.forEach(popover => new bootstrap.Popover(popover, { trigger: 'focus' }));
|
||||||
|
|
||||||
|
document.querySelectorAll('.message-content div').forEach(div => {
|
||||||
|
if (checkOverflow(div as HTMLElement)) {
|
||||||
|
div.parentElement?.classList.add('overflow-icon');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveToDatabase(filename: string, xml: string) {
|
||||||
|
db.entries
|
||||||
|
.put({ filename, backupData: xml })
|
||||||
|
.catch(err => displayFatalError('Saving Failed', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayBackup(xsltProcessor: XSLTProcessor, filename: string, xmlText: string): boolean {
|
||||||
|
hideErrors();
|
||||||
|
removeChildren(ui.chatDisplay);
|
||||||
|
showLoadedControls();
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
|
||||||
|
const errorNode = xmlDoc.querySelector('parsererror');
|
||||||
|
|
||||||
|
if (!errorNode) {
|
||||||
|
try {
|
||||||
|
const fragment = xsltProcessor.transformToFragment(xmlDoc, document);
|
||||||
|
ui.chatDisplay.appendChild(fragment);
|
||||||
|
ui.currentlyViewing.value = filename;
|
||||||
|
processFragment();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
displayNonFatalError('Could not load backup', e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const err = new Error(errorNode.firstChild?.nodeValue || '');
|
||||||
|
displayNonFatalError('Could not load backup', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initEvents() {
|
||||||
|
ui.loadPreviousButtons.forEach(button => button.addEventListener('click', async () => {
|
||||||
|
await populateSavedBackups();
|
||||||
|
}));
|
||||||
|
|
||||||
|
ui.viewNewButtons.forEach(button => button.addEventListener('click', e => {
|
||||||
|
ui.fileSelector.click();
|
||||||
|
e.preventDefault();
|
||||||
|
}));
|
||||||
|
|
||||||
|
ui.fileSelector.addEventListener("change", async (e) => {
|
||||||
|
const target = e.currentTarget as HTMLInputElement | null;
|
||||||
|
const file = target?.files?.[0];
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
const xmlText = await file.text();
|
||||||
|
if (displayBackup(xsltProcessor, file.name, xmlText)) {
|
||||||
|
await saveToDatabase(file.name, xmlText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayNonFatalError('Could not load file', new Error('No file found.'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function populateSavedBackups() {
|
||||||
|
removeChildren(ui.previousBackupsList);
|
||||||
|
const modal = new bootstrap.Modal('#previous-backups-modal');
|
||||||
|
const entries = await db.entries.orderBy('filename').toArray();
|
||||||
|
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const listItem = document.createElement('a');
|
||||||
|
listItem.href = '#';
|
||||||
|
listItem.dataset['filename'] = entry.filename;
|
||||||
|
listItem.innerText = entry.filename;
|
||||||
|
listItem.classList.add('list-group-item', 'list-group-item-action');
|
||||||
|
|
||||||
|
listItem.addEventListener('click', async () => {
|
||||||
|
const reloaded = await db.entries.get(entry.filename);
|
||||||
|
if (reloaded) {
|
||||||
|
displayBackup(xsltProcessor, reloaded.filename, reloaded.backupData);
|
||||||
|
} else {
|
||||||
|
displayNonFatalError('Internal Error', new Error('Backup data disappeared?!'));
|
||||||
|
}
|
||||||
|
modal.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.previousBackupsList.appendChild(listItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (window.innerWidth <= 500) {
|
||||||
|
ui.chatDisplay.querySelector('table')?.classList.add('table-sm');
|
||||||
|
} else {
|
||||||
|
ui.chatDisplay.querySelector('table')?.classList.remove('table-sm');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
initEvents();
|
||||||
|
showLoadingIndicator(false);
|
||||||
|
showApplication(true);
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Enable typescript completion for bundle-text inlining.
|
||||||
|
declare module 'bundle-text:*' {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
|
@import "npm:bootstrap/scss/bootstrap";
|
||||||
|
@import "npm:bootstrap-icons/font/bootstrap-icons.css";
|
||||||
|
|
||||||
table {
|
table {
|
||||||
/* table-layout: fixed; */
|
/* table-layout: fixed; */
|
||||||
}
|
}
|
||||||
|
@ -30,23 +33,31 @@ th, td {
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-popover, .info-popover:link, .info-popover:visited, .info-popover:hover, .info-popover:active {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 500px) {
|
@include media-breakpoint-down(lg) {
|
||||||
/*
|
/*
|
||||||
Remove padding from container, but keep it on stuff that isn't
|
Remove padding from container, but keep it on stuff that isn't
|
||||||
navbar or chat messages.
|
navbar or chat messages.
|
||||||
*/
|
*/
|
||||||
.container-md {
|
.container-lg {
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#backup-info {
|
#file-loaded-controls {
|
||||||
padding-left: 1rem;
|
padding: 0.25rem;
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Table stuff */
|
#no-file-loaded-controls {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Display table stuff */
|
||||||
#chat-display table {
|
#chat-display table {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"target": "es6",
|
||||||
|
"module": "es6",
|
||||||
|
"lib": [ "dom", "es6" ],
|
||||||
|
"jsxImportSource": "preact",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~*": ["./*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
//All of these can ignore maybe-null errors, as the script runs at the bottom of the page.
|
||||||
|
//So DOM is guaranteed to exist.
|
||||||
|
export const realBody: HTMLElement = document.getElementById('real-body')!;
|
||||||
|
export const loadingIndicator: HTMLElement = document.getElementById('loading')!;
|
||||||
|
export const fatalError: HTMLElement = document.getElementById('fatal-error')!;
|
||||||
|
export const nonFatalError: HTMLElement = document.getElementById('display-backup-error')!;
|
||||||
|
export const chatDisplay: HTMLElement = document.getElementById('chat-display')!;
|
||||||
|
export const fileSelector: HTMLInputElement = document.getElementById('backup-file')! as HTMLInputElement;
|
||||||
|
export const savedBackups: HTMLSelectElement = document.getElementById('saved-backups')! as HTMLSelectElement;
|
||||||
|
export const viewNewButtons =
|
||||||
|
document.querySelectorAll('#unloaded-view-new-button, #loaded-view-new-button');
|
||||||
|
export const loadPreviousButtons =
|
||||||
|
document.querySelectorAll('#unloaded-load-previous-button, #loaded-load-previous-button');
|
||||||
|
export const previousBackupsList: HTMLDivElement = document.getElementById('previous-backups-list')! as HTMLDivElement;
|
||||||
|
export const noFileLoadedControls: HTMLDivElement = document.getElementById('no-file-loaded-controls')! as HTMLDivElement;
|
||||||
|
export const fileLoadedControls: HTMLDivElement = document.getElementById('file-loaded-controls')! as HTMLDivElement;
|
||||||
|
export const currentlyViewing: HTMLInputElement = document.getElementById('currently-viewing')! as HTMLInputElement;
|
Loading…
Reference in New Issue