From c66bd1909d9dcf3b9d1ac2b0cedb06ae6f59f451 Mon Sep 17 00:00:00 2001 From: projectmoon Date: Tue, 3 Jan 2023 20:16:31 +0100 Subject: [PATCH] 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 --- index.js | 185 -------------------------------------------- package.json | 4 +- src/MessageLog.xsl | 20 ++--- src/db.ts | 20 +++++ src/index.html | 85 ++++++++++++++++---- src/index.js | 163 -------------------------------------- src/index.ts | 157 ++++++++++++++++++++++--------------- src/scss/index.scss | 23 ++++-- src/ui.ts | 10 ++- yarn.lock | 10 +++ 10 files changed, 235 insertions(+), 442 deletions(-) delete mode 100644 index.js create mode 100644 src/db.ts delete mode 100644 src/index.js diff --git a/index.js b/index.js deleted file mode 100644 index c5690b6..0000000 --- a/index.js +++ /dev/null @@ -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)); -}); diff --git a/package.json b/package.json index 0912dce..1e30271 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "dependencies": { "@popperjs/core": "^2.11.6", "@types/bootstrap": "^5.2.3", - "bootstrap": "^5.2" + "bootstrap": "^5.2", + "bootstrap-icons": "^1.10.3", + "dexie": "^3.2.2" } } diff --git a/src/MessageLog.xsl b/src/MessageLog.xsl index f0a1ac9..6660c21 100644 --- a/src/MessageLog.xsl +++ b/src/MessageLog.xsl @@ -75,7 +75,7 @@ https://github.com/jerecui/MSNChatHistoryCombiner -
+
@@ -96,14 +96,15 @@ https://github.com/jerecui/MSNChatHistoryCombiner - + - + @@ -118,14 +119,15 @@ https://github.com/jerecui/MSNChatHistoryCombiner - +   # - + diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 0000000..1ea0421 --- /dev/null +++ b/src/db.ts @@ -0,0 +1,20 @@ +import Dexie from 'dexie'; + +export class MsnDatabase extends Dexie { + entries!: Dexie.Table; // 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(); diff --git a/src/index.html b/src/index.html index cb07fb3..14f00f3 100644 --- a/src/index.html +++ b/src/index.html @@ -8,7 +8,7 @@ -
+
Loading... @@ -49,9 +49,51 @@
- - - + + + +
+
+ + + + +
+
+ + +
+ + + or... + + +
@@ -68,6 +110,21 @@
+ + 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"