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");
}