diff --git a/application/assets/images/link.svg b/application/assets/images/link.svg new file mode 100644 index 0000000..11a4482 --- /dev/null +++ b/application/assets/images/link.svg @@ -0,0 +1,49 @@ + + + + + + + diff --git a/application/assets/js/helper.js b/application/assets/js/helper.js index 6151b59..2de285b 100644 --- a/application/assets/js/helper.js +++ b/application/assets/js/helper.js @@ -260,6 +260,41 @@ const helper = (() => { } }; + let readFile = (filepath, callback) => { + let sdcard = ""; + + try { + sdcard = navigator.getDeviceStorage("sdcard"); + } catch (e) {} + + if ("b2g" in navigator) { + try { + sdcard = navigator.b2g.getDeviceStorage("sdcard"); + } catch (e) {} + } + + let request = sdcard.get(filepath); + console.log(filepath); + + request.onsuccess = function (e) { + const file = e.target.result; // The retrieved file + console.log(file); + + const reader = new FileReader(); + + reader.addEventListener("load", function (event) { + callback(event.target.result); + console.log(event.target.result); + }); + + reader.readAsText(file); + }; + request.onerror = function (e) { + // Handle errors + console.error("Error reading file:", e); + }; + }; + //delete file let renameFile = function (filename, new_filename, callback) { let sdcard = ""; @@ -450,6 +485,7 @@ const helper = (() => { toaster, add_script, deleteFile, + readFile, isOnline, side_toaster, renameFile, diff --git a/application/assets/js/vcard-parser.js b/application/assets/js/vcard-parser.js new file mode 100644 index 0000000..b262879 --- /dev/null +++ b/application/assets/js/vcard-parser.js @@ -0,0 +1,230 @@ +var vCardParser = (function () { + var fieldPropertyMapping = { + "TITLE": "title", + "TEL": "telephone", + "FN": "displayName", + "N": "name", + "EMAIL": "email", + "CATEGORIES": "categories", + "ADR": "address", + "URL": "url", + "NOTE": "notes", + "ORG": "organization", + "BDAY": "birthday", + "PHOTO": "photo", + }; + + function lookupField(context, fieldName) { + var propertyName = fieldPropertyMapping[fieldName]; + + if (!propertyName && fieldName !== "BEGIN" && fieldName !== "END") { + context.info("define property name for " + fieldName); + propertyName = fieldName; + } + + return propertyName; + } + + function removeWeirdItemPrefix(line) { + // sometimes lines are prefixed by "item" keyword like "item1.ADR;type=WORK:....." + return line.substring(0, 4) === "item" + ? line.match(/item\d\.(.*)/)[1] + : line; + } + + function singleLine(context, fieldValue, fieldName) { + // convert escaped new lines to real new lines. + fieldValue = fieldValue.replace("\\n", "\n"); + + // append value if previously specified + if (context.currentCard[fieldName]) { + context.currentCard[fieldName] += "\n" + fieldValue; + } else { + context.currentCard[fieldName] = fieldValue; + } + } + + function typedLine(context, fieldValue, fieldName, typeInfo, valueFormatter) { + var isDefault = false; + + // strip type info and find out is that preferred value + typeInfo = typeInfo.filter(function (type) { + isDefault = isDefault || type.name === "PREF"; + return type.name !== "PREF"; + }); + + typeInfo = typeInfo.reduce(function (p, c) { + p[c.name] = c.value; + return p; + }, {}); + + context.currentCard[fieldName] = context.currentCard[fieldName] || []; + + context.currentCard[fieldName].push({ + isDefault: isDefault, + valueInfo: typeInfo, + value: valueFormatter ? valueFormatter(fieldValue) : fieldValue, + }); + } + + function commaSeparatedLine(context, fieldValue, fieldName) { + context.currentCard[fieldName] = fieldValue.split(","); + } + + function dateLine(context, fieldValue, fieldName) { + // if value is in "19531015T231000Z" format strip time field and use date value. + fieldValue = + fieldValue.length === 16 ? fieldValue.substr(0, 8) : fieldValue; + + var dateValue; + + if (fieldValue.length === 8) { + // "19960415" format ? + dateValue = new Date( + fieldValue.substr(0, 4), + fieldValue.substr(4, 2), + fieldValue.substr(6, 2) + ); + } else { + // last chance to try as date. + dateValue = new Date(fieldValue); + } + + if (!dateValue || isNaN(dateValue.getDate())) { + dateValue = null; + context.error("invalid date format " + fieldValue); + } + + context.currentCard[fieldName] = dateValue && dateValue.toJSON(); // always return the ISO date format + } + + function structured(fields) { + return function (context, fieldValue, fieldName) { + var values = fieldValue.split(";"); + + context.currentCard[fieldName] = fields.reduce(function (p, c, i) { + p[c] = values[i] || ""; + return p; + }, {}); + }; + } + + function addressLine(context, fieldValue, fieldName, typeInfo) { + typedLine(context, fieldValue, fieldName, typeInfo, function (value) { + var names = value.split(";"); + + return { + // ADR field sequence + postOfficeBox: names[0], + number: names[1], + street: names[2] || "", + city: names[3] || "", + region: names[4] || "", + postalCode: names[5] || "", + country: names[6] || "", + }; + }); + } + + function noop() {} + + function endCard(context) { + // store card in context and create a new card. + context.cards.push(context.currentCard); + context.currentCard = {}; + } + + var fieldParsers = { + "BEGIN": noop, + "VERSION": noop, + "N": structured(["surname", "name", "additionalName", "prefix", "suffix"]), + "TITLE": singleLine, + "TEL": typedLine, + "EMAIL": typedLine, + "ADR": addressLine, + "NOTE": singleLine, + "NICKNAME": commaSeparatedLine, + "BDAY": dateLine, + "URL": singleLine, + "CATEGORIES": commaSeparatedLine, + "END": endCard, + "FN": singleLine, + "ORG": singleLine, + "UID": singleLine, + "PHOTO": singleLine, + }; + + function feedData(context) { + for (var i = 0; i < context.data.length; i++) { + var line = removeWeirdItemPrefix(context.data[i]); + + var pairs = line.split(":"), + fieldName = pairs[0], + fieldTypeInfo, + fieldValue = pairs.slice(1).join(":"); + + // is additional type info provided ? + if ( + fieldName.indexOf(";") >= 0 && + line.indexOf(";") < line.indexOf(":") + ) { + var typeInfo = fieldName.split(";"); + fieldName = typeInfo[0]; + fieldTypeInfo = typeInfo.slice(1).map(function (type) { + var info = type.split("="); + return { + name: info[0].toLowerCase(), + value: info[1].replace(/"(.*)"/, "$1"), + }; + }); + } + + // ensure fieldType is in upper case + fieldName = fieldName.toUpperCase(); + + var fieldHandler = fieldParsers[fieldName]; + + if (fieldHandler) { + fieldHandler( + context, + fieldValue, + lookupField(context, fieldName), + fieldTypeInfo + ); + } else if (fieldName.substring(0, 2) != "X-") { + // ignore X- prefixed extension fields. + context.info( + "unknown field " + fieldName + " with value " + fieldValue + ); + } + } + } + + function parse(data) { + var lines = data + // replace escaped new lines + .replace(/\n\s{1}/g, "") + // split if a character is directly after a newline + .split(/\r\n(?=\S)|\r(?=\S)|\n(?=\S)/); + + var context = { + info: function (desc) { + console.info(desc); + }, + error: function (err) { + console.error(err); + }, + data: lines, + currentCard: {}, + cards: [], + }; + + feedData(context); + + return context.cards; + } + + return { + parse: parse, + }; +})(); diff --git a/application/index.html b/application/index.html index 8ee4551..df4306d 100644 --- a/application/index.html +++ b/application/index.html @@ -54,6 +54,7 @@ + diff --git a/application/index.js b/application/index.js index b74b9cd..dba1ef0 100644 --- a/application/index.js +++ b/application/index.js @@ -3,8 +3,11 @@ let debug = false; let filter_query; let file_content = []; +let content = ""; let current_file; -let files = []; +let files = JSON.parse(localStorage.getItem("files")) || []; +console.log("start:", files); + let action = null; let action_element = null; let selected_image; @@ -27,6 +30,30 @@ if (debug) { }; } +let save_files = () => { + localStorage.setItem("files", JSON.stringify(files)); +}; + +let url_test = (string) => { + // Regular expression for a basic URL validation + const urlRegex = /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i; + + return urlRegex.test(string); +}; + +//https://github.com/ertant/vCard + +let vcard_parser = (string) => { + console.log(vCardParser.parse(string)); +}; + +function formatVCardContent(content) { + // Replace LF with CRLF and add CRLF at the end if not present + return ( + content.replace(/\r?\n/g, "\r\n") + (content.endsWith("\r\n") ? "" : "\r\n") + ); +} + let set_tabindex = () => { document .querySelectorAll('.item:not([style*="display: none"]') @@ -112,9 +139,10 @@ try { }); } catch (e) {} +let filename_list = []; //list files let read_files = (callback) => { - files = []; + //files = []; try { var d = navigator.getDeviceStorage("sdcard"); @@ -122,7 +150,17 @@ let read_files = (callback) => { cursor.onsuccess = function () { if (!this.result) { + // Remove element from files array + + for (let i = 0; i < files.length; i++) { + if (filename_list.indexOf(files[i].path) == -1) { + files.splice(i, 1); + } + } + save_files(); + m.route.set("/start"); + callback(); } if (cursor.result.name !== null) { @@ -136,12 +174,25 @@ let read_files = (callback) => { file.name.includes("/passport/") && !file.name.includes("/sdcard/.") ) { - files.push({ - "path": file.name, - "name": file_name, - "file": f, - "type": type[type.length - 1], + //update files array if exist + filename_list.push(file.name); + let fileExists = false; + files.forEach((e) => { + if (e.path == file.name) { + fileExists = true; + e.file = f; + } }); + + if (!fileExists) { + files.push({ + "path": file.name, + "name": file_name, + "file": f, + "type": type[type.length - 1], + "qr": true, + }); + } } this.continue(); } @@ -176,12 +227,23 @@ let read_files = (callback) => { file.value.name.includes("/passport/") && !file.value.name.includes("/sdcard/.") ) { - files.push({ - path: file.value.name, - file: f, - type: file_name.split(".").pop(), - name: file_name, + let fileExists = false; + files.forEach((e) => { + if (e.path == file.name) { + fileExists = true; + e.file = f; + } }); + + if (!fileExists) { + files.push({ + "path": file.name, + "name": file_name, + "file": f, + "type": type[type.length - 1], + "qr": true, + }); + } } next(_files); @@ -228,6 +290,14 @@ let load_qrcode_content = (blobUrl) => { qrcode_content = ""; helper.bottom_bar("", "", ""); document.querySelector(".loading-spinner").style.display = "none"; + + files.forEach((e) => { + if (e.path == selected_image_url) { + e.qr = false; + } + }); + + save_files(); } }; @@ -339,7 +409,7 @@ function write_file(data, filename, filetype) { var request = sdcard.addNamed(file, filename); request.onsuccess = function () { - files = []; + //files = []; read_files(); startup = false; m.route.set("/start?focus=" + filename); @@ -523,12 +593,25 @@ document.addEventListener("DOMContentLoaded", function () { m("img", { src: selected_image, id: "image", - oninit: () => {}, + oninit: () => { + console.log(selected_image_url); + }, oncreate: () => { qrcode_content = ""; helper.bottom_bar("", "", ""); try { - load_qrcode_content(selected_image); + files.forEach((e) => { + if (e.path == selected_image_url) { + if (e.qr) { + load_qrcode_content(selected_image); + } else { + qrcode_content = ""; + helper.bottom_bar("", "", ""); + document.querySelector(".loading-spinner").style.display = + "none"; + } + } + }); } catch (e) { document.querySelector(".loading-spinner").style.display = "none"; } @@ -556,6 +639,28 @@ document.addEventListener("DOMContentLoaded", function () { }, }; + let read_file_callback = (e) => { + content = e; + document.querySelector("#qr-content").textContent = e; + }; + var show_vcf = { + view: function () { + return m("div", {}, [ + m( + "div", + { + id: "qr-content", + oninit: () => { + helper.bottom_bar("", "", ""); + helper.readFile(selected_image_url, read_file_callback); + }, + }, + "" + ), + ]); + }, + }; + var show_qr_content = { view: function () { return m( @@ -567,6 +672,24 @@ document.addEventListener("DOMContentLoaded", function () { if (status == "after_scan") { helper.bottom_bar("", "", ""); } + + if (url_test(qrcode_content)) { + helper.bottom_bar("", "", ""); + } + + if (qrcode_content.startsWith("BEGIN:VCARD")) { + let a = confirm( + "Looks like it's a vCard, do you want to save it as a vcard file." + ); + if (a) { + let name = new Date() / 1000 + ".vcf"; + write_file( + formatVCardContent(qrcode_content), + "passport/" + name, + "text/vcard" + ); + } + } }, }, qrcode_content @@ -601,6 +724,8 @@ document.addEventListener("DOMContentLoaded", function () { "/show_qr_content": show_qr_content, "/show_image": show_image, "/show_pdf": show_pdf, + "/show_vcf": show_vcf, + "/start": start, "/options": options, "/scan": scan, @@ -632,7 +757,7 @@ document.addEventListener("DOMContentLoaded", function () { let cb = () => { m.route.set("/start?focus=+/sdcard/passport/" + filename); }; - + files = []; read_files(cb); }; @@ -641,8 +766,6 @@ document.addEventListener("DOMContentLoaded", function () { let cb = () => { if (files.length == 0) { - console.log("why"); - setTimeout(() => { m.route.set("/start"); }, 1000); @@ -671,6 +794,7 @@ document.addEventListener("DOMContentLoaded", function () { } } }; + files = []; read_files(cb); }; @@ -730,6 +854,8 @@ document.addEventListener("DOMContentLoaded", function () { m.route.set("/start?focus=" + selected_image_url); } else if (m.route.get().includes("/show_pdf")) { m.route.set("/start?focus=" + selected_image_url); + } else if (m.route.get().includes("/show_vcf")) { + m.route.set("/start?focus=" + selected_image_url); } else if (m.route.get().includes("/options")) { m.route.set("/start"); } else if (m.route.get().includes("/scan")) { @@ -757,8 +883,8 @@ document.addEventListener("DOMContentLoaded", function () { m.route.set("/start?focus=" + selected_image_url); } else if (m.route.get().includes("/show_pdf")) { m.route.set("/start?focus=" + selected_image_url); - - break; + } else if (m.route.get().includes("/show_vcf")) { + m.route.set("/start?focus=" + selected_image_url); } else { window.close(); } @@ -776,6 +902,10 @@ document.addEventListener("DOMContentLoaded", function () { break; } + if (m.route.get().includes("/show_qr_content")) { + if (url_test(qrcode_content)) window.open(qrcode_content); + } + if (m.route.get().includes("/start")) { if (general.fileAction) { general.blocker = true; @@ -858,6 +988,10 @@ document.addEventListener("DOMContentLoaded", function () { if (document.activeElement.getAttribute("data-type") == "pdf") { m.route.set("/show_pdf"); + } else if ( + document.activeElement.getAttribute("data-type") == "vcf" + ) { + m.route.set("/show_vcf"); } else { m.route.set("/show_image"); }