Loading...
@@ -49,9 +49,51 @@
-
<none>
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ or...
+
+
+
@@ -68,6 +110,21 @@
+
+
@@ -91,11 +148,14 @@
You must have the XML backup
available on your device to view
it.
-
Long usernames are truncated.
- Click on such a username to see a
- popup with the full username.
- Click in the same spot to hide the
- popup.
+
Long usernames are shortened.
+ Click or tap on such a username to
+ see a popup with the full
+ username. Click or tap again to
+ hide the popup.
+
When you have opened a backup for
+ viewing, you can easily re-open it
+ later by selecting "Load Previous".
@@ -109,16 +169,13 @@
- Tap on the pound sign to see the
date and time of the message.
- - Tap again in the same place to
- hide the popup.
- - Usernames are similarly shortened.
- Tap on a username to see the full
- name.
+ - Tap again to hide the popup.
- Long chat messages are scrollable.
A small icon in the lower right
part of the message indicates
this.
+
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index 20529b0..0000000
--- a/src/index.js
+++ /dev/null
@@ -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));
-});
diff --git a/src/index.ts b/src/index.ts
index 4367991..82b5756 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,9 @@
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 { db } from './db';
+
+const xsltProcessor = createXSLTProcessor(xslContent)!;
function showApplication(toggle: boolean) {
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) {
console.error(ex);
const errorHeading = errorDiv.querySelector('.alert-heading') as HTMLElement | null;
@@ -55,37 +64,18 @@ function hideErrors() {
.forEach(el => el.classList.add('d-none'));
}
-function importStylesheet(xsl: Node) {
- //Firefox does not seem to report XML parsing errors into the
- //exception tree. So we catch here, log, and return some friendly
- //error.
+function createXSLTProcessor(xmlStylesheet: string): XSLTProcessor | undefined {
+ const doc = new DOMParser().parseFromString(xmlStylesheet, "text/xml");
try {
const xsltProcessor = new XSLTProcessor();
- xsltProcessor.importStylesheet(xsl);
+ xsltProcessor.importStylesheet(doc);
return xsltProcessor;
} catch (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) {
while (element.hasChildNodes() && element.lastChild != null) {
element.removeChild(element.lastChild);
@@ -101,7 +91,7 @@ function checkOverflow(elem: HTMLElement) {
function processFragment() {
document
.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 => {
if (checkOverflow(div as HTMLElement)) {
@@ -110,48 +100,95 @@ function processFragment() {
});
}
-function displayBackup(xsltProcessor: XSLTProcessor, file: File) {
- hideErrors();
-
- removeChildren(ui.chatDisplay);
-
- file.text().then(xmlText => {
- ui.currentlyViewing.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);
- ui.chatDisplay.appendChild(fragment);
- processFragment();
- }
- catch (e) {
- if (e instanceof Error) {
- displayNonFatalError('Could not load backup', e);
- }
- }
- } else {
- const err = new Error(errorNode.firstChild?.nodeValue || '');
- displayNonFatalError('Could not load backup', err);
- }
-
- });
+async function saveToDatabase(filename: string, xml: string) {
+ db.entries
+ .put({ filename, backupData: xml })
+ .catch(err => displayFatalError('Saving Failed', err));
}
-function initEvents(xsltProcessor: XSLTProcessor) {
+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) {
- displayBackup(xsltProcessor, 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) {
@@ -161,11 +198,7 @@ window.addEventListener('DOMContentLoaded', async () => {
}
});
- createXSLTProcessor()
- .then(initEvents)
- .then(_ => {
- showLoadingIndicator(false);
- showApplication(true);
- })
- .catch(ex => displayFatalError('Initialiation Failed', ex));
+ initEvents();
+ showLoadingIndicator(false);
+ showApplication(true);
});
diff --git a/src/scss/index.scss b/src/scss/index.scss
index aa366b2..a41e7f5 100644
--- a/src/scss/index.scss
+++ b/src/scss/index.scss
@@ -1,4 +1,5 @@
-@import "~bootstrap/scss/bootstrap";
+@import "npm:bootstrap/scss/bootstrap";
+@import "npm:bootstrap-icons/font/bootstrap-icons.css";
table {
/* table-layout: fixed; */
@@ -32,23 +33,31 @@ th, td {
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
navbar or chat messages.
*/
- .container-md {
+ .container-lg {
padding-left: 0px;
padding-right: 0px;
}
- #backup-info {
- padding-left: 1rem;
- padding-right: 1rem;
+ #file-loaded-controls {
+ padding: 0.25rem;
}
- /* Table stuff */
+ #no-file-loaded-controls {
+ max-width: 200px;
+ }
+
+ /* Chat Display table stuff */
#chat-display table {
table-layout: fixed;
}
diff --git a/src/ui.ts b/src/ui.ts
index 3c5aca6..629da25 100644
--- a/src/ui.ts
+++ b/src/ui.ts
@@ -6,4 +6,12 @@ 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 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;
diff --git a/yarn.lock b/yarn.lock
index 6e4f603..fe871a4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -824,6 +824,11 @@ boolbase@^1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
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:
version "5.2.3"
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"
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:
version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"