All kinds of improvements:

- New UX for picking backups
 - Save backups in indexed DB for later viewing
 - Better display of currently viewed file.
 - Updated help text
This commit is contained in:
projectmoon 2023-01-03 20:16:31 +01:00
parent 4a761fb031
commit c66bd1909d
10 changed files with 235 additions and 442 deletions

185
index.js
View File

@ -1,185 +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(new URL("/MessageLog.xsl"))
.then(resp => {
if (resp.ok)
return resp.text();
else
throw new Error(resp.status + ' ' + resp.statusText);
})
.then(str => {
console.log(str);
return 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));
});

View File

@ -17,6 +17,8 @@
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.6", "@popperjs/core": "^2.11.6",
"@types/bootstrap": "^5.2.3", "@types/bootstrap": "^5.2.3",
"bootstrap": "^5.2" "bootstrap": "^5.2",
"bootstrap-icons": "^1.10.3",
"dexie": "^3.2.2"
} }
} }

View File

@ -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-trigger="focus"
data-bs-container="body" data-bs-toggle="popover" data-bs-container="body" data-bs-toggle="popover"
data-bs-placement="bottom" data-bs-title="Username"> 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-trigger="focus"
data-bs-container="body" data-bs-toggle="popover" data-bs-container="body" data-bs-toggle="popover"
data-bs-placement="right" data-bs-title="Date/Time"> 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"/>&#160;<xsl:value-of select="@Time"/> <xsl:value-of select="@Date"/>&#160;<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>

20
src/db.ts Normal file
View File

@ -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();

View File

@ -8,7 +8,7 @@
</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>
@ -49,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">&lt;none&gt;</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">
@ -68,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">
@ -91,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>
@ -109,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>

View File

@ -1,163 +0,0 @@
import * as bootstrap from 'bootstrap';
import xsl from 'bundle-text:./MessgeLog.xsl';
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')) {
// @ts-ignore
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() {
const doc = new DOMParser().parseFromString(xsl, "text/xml");
return Promise.resolve(importStylesheet(doc));
// return fetch(new URL("./MessageLog.xsl", import.meta.url))
// .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 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 target = e.currentTarget;
const file = target.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));
});

View File

@ -1,6 +1,9 @@
import * as bootstrap from 'bootstrap'; import * as bootstrap from 'bootstrap';
import xsl from 'bundle-text:./MessageLog.xsl'; import xslContent from 'bundle-text:./MessageLog.xsl';
import * as ui from './ui'; import * as ui from './ui';
import { db } from './db';
const xsltProcessor = createXSLTProcessor(xslContent)!;
function showApplication(toggle: boolean) { function showApplication(toggle: boolean) {
if (toggle) { if (toggle) {
@ -18,6 +21,12 @@ function showLoadingIndicator(toggle: boolean) {
} }
} }
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) { function displayError(errorDiv: HTMLElement, category: string, ex: Error) {
console.error(ex); console.error(ex);
const errorHeading = errorDiv.querySelector('.alert-heading') as HTMLElement | null; const errorHeading = errorDiv.querySelector('.alert-heading') as HTMLElement | null;
@ -55,37 +64,18 @@ function hideErrors() {
.forEach(el => el.classList.add('d-none')); .forEach(el => el.classList.add('d-none'));
} }
function importStylesheet(xsl: Node) { function createXSLTProcessor(xmlStylesheet: string): XSLTProcessor | undefined {
//Firefox does not seem to report XML parsing errors into the const doc = new DOMParser().parseFromString(xmlStylesheet, "text/xml");
//exception tree. So we catch here, log, and return some friendly
//error.
try { try {
const xsltProcessor = new XSLTProcessor(); const xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(xsl); xsltProcessor.importStylesheet(doc);
return xsltProcessor; return xsltProcessor;
} catch (cause) { } catch (cause) {
console.error(cause); console.error(cause);
throw new Error('Stylesheet parsing failed'); displayFatalError('Initialiation Failed', new Error('Stylesheet parsing failed'));
} }
} }
async function createXSLTProcessor() {
const doc = new DOMParser().parseFromString(xsl, "text/xml");
return Promise.resolve(importStylesheet(doc));
// return fetch(new URL("./MessageLog.xsl", import.meta.url))
// .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: HTMLElement) { function removeChildren(element: HTMLElement) {
while (element.hasChildNodes() && element.lastChild != null) { while (element.hasChildNodes() && element.lastChild != null) {
element.removeChild(element.lastChild); element.removeChild(element.lastChild);
@ -101,7 +91,7 @@ function checkOverflow(elem: HTMLElement) {
function processFragment() { function processFragment() {
document document
.querySelectorAll('[data-bs-toggle="popover"]') .querySelectorAll('[data-bs-toggle="popover"]')
.forEach(popover => new bootstrap.Popover(popover)); .forEach(popover => new bootstrap.Popover(popover, { trigger: 'focus' }));
document.querySelectorAll('.message-content div').forEach(div => { document.querySelectorAll('.message-content div').forEach(div => {
if (checkOverflow(div as HTMLElement)) { if (checkOverflow(div as HTMLElement)) {
@ -110,13 +100,17 @@ function processFragment() {
}); });
} }
function displayBackup(xsltProcessor: XSLTProcessor, file: File) { 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(); hideErrors();
removeChildren(ui.chatDisplay); removeChildren(ui.chatDisplay);
showLoadedControls();
file.text().then(xmlText => {
ui.currentlyViewing.innerText = file.name;
const parser = new DOMParser(); const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, "text/xml"); const xmlDoc = parser.parseFromString(xmlText, "text/xml");
const errorNode = xmlDoc.querySelector('parsererror'); const errorNode = xmlDoc.querySelector('parsererror');
@ -125,33 +119,76 @@ function displayBackup(xsltProcessor: XSLTProcessor, file: File) {
try { try {
const fragment = xsltProcessor.transformToFragment(xmlDoc, document); const fragment = xsltProcessor.transformToFragment(xmlDoc, document);
ui.chatDisplay.appendChild(fragment); ui.chatDisplay.appendChild(fragment);
ui.currentlyViewing.value = filename;
processFragment(); processFragment();
return true;
} }
catch (e) { catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
displayNonFatalError('Could not load backup', e); displayNonFatalError('Could not load backup', e);
} }
return false;
} }
} else { } else {
const err = new Error(errorNode.firstChild?.nodeValue || ''); const err = new Error(errorNode.firstChild?.nodeValue || '');
displayNonFatalError('Could not load backup', err); 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();
}));
function initEvents(xsltProcessor: XSLTProcessor) {
ui.fileSelector.addEventListener("change", async (e) => { ui.fileSelector.addEventListener("change", async (e) => {
const target = e.currentTarget as HTMLInputElement | null; const target = e.currentTarget as HTMLInputElement | null;
const file = target?.files?.[0]; const file = target?.files?.[0];
if (file) { if (file) {
displayBackup(xsltProcessor, file); const xmlText = await file.text();
if (displayBackup(xsltProcessor, file.name, xmlText)) {
await saveToDatabase(file.name, xmlText);
}
} else { } else {
displayNonFatalError('Could not load file', new Error('No file found.')); 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('DOMContentLoaded', async () => {
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
if (window.innerWidth <= 500) { if (window.innerWidth <= 500) {
@ -161,11 +198,7 @@ window.addEventListener('DOMContentLoaded', async () => {
} }
}); });
createXSLTProcessor() initEvents();
.then(initEvents)
.then(_ => {
showLoadingIndicator(false); showLoadingIndicator(false);
showApplication(true); showApplication(true);
})
.catch(ex => displayFatalError('Initialiation Failed', ex));
}); });

View File

@ -1,4 +1,5 @@
@import "~bootstrap/scss/bootstrap"; @import "npm:bootstrap/scss/bootstrap";
@import "npm:bootstrap-icons/font/bootstrap-icons.css";
table { table {
/* table-layout: fixed; */ /* table-layout: fixed; */
@ -32,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;
} }

View File

@ -6,4 +6,12 @@ export const fatalError: HTMLElement = document.getElementById('fatal-error')!;
export const nonFatalError: HTMLElement = document.getElementById('display-backup-error')!; export const nonFatalError: HTMLElement = document.getElementById('display-backup-error')!;
export const chatDisplay: HTMLElement = document.getElementById('chat-display')!; export const chatDisplay: HTMLElement = document.getElementById('chat-display')!;
export const fileSelector: HTMLInputElement = document.getElementById('backup-file')! as HTMLInputElement; export const fileSelector: HTMLInputElement = document.getElementById('backup-file')! as HTMLInputElement;
export const currentlyViewing: HTMLElement = document.getElementById('currently-viewing')!; 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;

View File

@ -824,6 +824,11 @@ boolbase@^1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
bootstrap-icons@^1.10.3:
version "1.10.3"
resolved "https://registry.yarnpkg.com/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz#c587b078ca6743bef4653fe90434b4aebfba53b2"
integrity sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw==
bootstrap@^5.2: bootstrap@^5.2:
version "5.2.3" version "5.2.3"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.3.tgz#54739f4414de121b9785c5da3c87b37ff008322b" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.3.tgz#54739f4414de121b9785c5da3c87b37ff008322b"
@ -984,6 +989,11 @@ detect-libc@^1.0.3:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
dexie@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.2.tgz#fa6f2a3c0d6ed0766f8d97a03720056f88fe0e01"
integrity sha512-q5dC3HPmir2DERlX+toCBbHQXW5MsyrFqPFcovkH9N2S/UW/H3H5AWAB6iEOExeraAu+j+zRDG+zg/D7YhH0qg==
dom-serializer@^1.0.1: dom-serializer@^1.0.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"