diff --git a/TODO b/TODO new file mode 100644 index 0000000..740ca9f --- /dev/null +++ b/TODO @@ -0,0 +1,3 @@ +Config Page +Improve screenshot result +Add favicon support diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json new file mode 100644 index 0000000..12a7167 --- /dev/null +++ b/src/_locales/en/messages.json @@ -0,0 +1,52 @@ +{ + "extensionName": { + "message": "Quick Dial", + "description": "Name of the extension." + }, + + "extensionDescription": { + "message": "Quick Dial, new tab page.", + "description": "Description of the extension." + }, + + "menuAddToQuickDial": { + "message": "Add to Quick Dial", + "description": "Text of add bookmark menu item." + }, + + "menuAddBookmark": { + "message": "Add Bookmark", + "description": "Text of add bookmark menu item." + }, + + "AddBookmarkPrompt": { + "message": "Enter the new bookmark url :", + "description": "Text of the add bookmark prompt." + }, + + "menuAddFolder": { + "message": "Add Folder", + "description": "Text of add folder menu item." + }, + + "AddFolderPrompt": { + "message": "Enter the new folder name :", + "description": "Text of the add folder prompt." + }, + + "menuRefreshItem": { + "message": "Refresh", + "description": "Text of refresh menu item." + }, + + "menuDeleteItem": { + "message": "Delete", + "description": "Text of delete menu item." + }, + + "deleteItemConfimation": { + "message": "Delete $1 ?", + "description": "Text of delete confirmation." + } + +} diff --git a/src/_locales/fr/messages.json b/src/_locales/fr/messages.json new file mode 100644 index 0000000..4e7e134 --- /dev/null +++ b/src/_locales/fr/messages.json @@ -0,0 +1,52 @@ +{ + "extensionName": { + "message": "Quick Dial", + "description": "Name of the extension." + }, + + "extensionDescription": { + "message": "Quick Dial, page nouvel onglet.", + "description": "Description of the extension." + }, + + "menuAddToQuickDial": { + "message": "Ajouter à Quick Dial", + "description": "Text of add bookmark menu item." + }, + + "menuAddBookmark": { + "message": "Ajouter un marque-page", + "description": "Text of add bookmark menu item." + }, + + "AddBookmarkPrompt": { + "message": "Entrez l'url du nouveau marque-page :", + "description": "Text of the add bookmark prompt." + }, + + "menuAddFolder": { + "message": "Ajouter un dossier", + "description": "Text of add folder menu item." + }, + + "AddFolderPrompt": { + "message": "Entrez le nom du nouveau dossier :", + "description": "Text of the add folder prompt." + }, + + "menuRefreshItem": { + "message": "Actualiser", + "description": "Text of refresh menu item." + }, + + "menuDeleteItem": { + "message": "Supprimer", + "description": "Text of delete menu item." + }, + + "deleteItemConfimation": { + "message": "Supprimer $1 ?", + "description": "Text of delete confirmation." + } + +} diff --git a/src/dial b/src/dial new file mode 100644 index 0000000..92123f6 --- /dev/null +++ b/src/dial @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/html/options.html b/src/html/options.html new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/src/html/options.html @@ -0,0 +1 @@ + diff --git a/src/html/test.html b/src/html/test.html new file mode 100644 index 0000000..6906290 --- /dev/null +++ b/src/html/test.html @@ -0,0 +1,9 @@ + + + test + + + + + + \ No newline at end of file diff --git a/src/img/24.png b/src/img/24.png new file mode 100644 index 0000000..6f9210f Binary files /dev/null and b/src/img/24.png differ diff --git a/src/img/32.png b/src/img/32.png new file mode 100644 index 0000000..c497567 Binary files /dev/null and b/src/img/32.png differ diff --git a/src/img/README.md b/src/img/README.md new file mode 100644 index 0000000..dd8dc70 --- /dev/null +++ b/src/img/README.md @@ -0,0 +1,3 @@ +# About Image licence : +These images provide from fast dial extention but it's only temporary. +I would make my own to be free of all dependencies. diff --git a/src/img/back.png b/src/img/back.png new file mode 100644 index 0000000..c0095bf Binary files /dev/null and b/src/img/back.png differ diff --git a/src/img/folder.png b/src/img/folder.png new file mode 100644 index 0000000..f83bbec Binary files /dev/null and b/src/img/folder.png differ diff --git a/src/img/throbber.gif b/src/img/throbber.gif new file mode 100644 index 0000000..b8f5437 Binary files /dev/null and b/src/img/throbber.gif differ diff --git a/src/js/background.js b/src/js/background.js new file mode 100644 index 0000000..333de77 --- /dev/null +++ b/src/js/background.js @@ -0,0 +1,280 @@ +var core = {}; // Main app object in background.js +var app = {}; // Shared app object with pages + +core._init = function(){ // Called from core.Settings.load() + core.Bookmarks.initRoot(function(){ + core.GridNodes.sync(app.settings.grid.node, app.settings.grid.root); // Sync bookmarks with stored data + core.ContextMenus.initMenu(); + core.Bookmarks.initListener(); + }) +} + +core.Settings = {}; // Settings helper object +core.Settings.load = function(){ // Load settings and call core.init + browser.storage.local.get({ + background: '#3c4048', + grid: { + margin: 10, + rows: 4, + columns: 5, + cells: { + margin: 4, + ratioX: 4, + ratioY: 3, + borderColor: '#333333', + borderColorHover: '#a9a9a9', + borderRadius: 4, + title: true, + titleHeight: 18, + titleFontSize: 11, + titleFont: 'Arial, Verdana, Sans-serif', + titleColor: '#ffffff', + titleColorHover: '#33ccff', + backPanel: true, + backIcon: 'img/back.png', + folderIcon: 'img/folder.png', + loadingIcon: 'img/throbber.gif' + }, + root: 'Quick Dial', + node: {} + } + }).then(function(obj){ + app.settings = obj; + core._init(); + browser.runtime.sendMessage({ command: 'appReady'}).then(function(){}, function(){}); + },function(){}); +} +core.Settings.save = function(){ // Save settings + browser.storage.local.set(app.settings); +} +core.Settings.load(); // Need to be loaded first and call core.init when ready + +core.ContextMenus = {} // ContextMenu helper Object +core.ContextMenus.initMenu = function(){ // (Called from core._init) Init context menu in all pages + browser.contextMenus.create({ // Create Context menu + id: 'AddToQuickDial', + title: browser.i18n.getMessage("menuAddToQuickDial"), + contexts: ["all"], + documentUrlPatterns: [ 'http://*/*', 'https://*/*', 'file://*/*' ] + }, function(){}); + browser.contextMenus.onClicked.addListener(function(info, tab) { // Context menu click event + if (info.menuItemId == "AddToQuickDial") + core.GridNodes.createBookmark(app.settings.grid.node, info.pageUrl, tab.title, function(){}); + }); +} + +core.Bookmarks = {} // Bookmarks helper object +core.Bookmarks.initListener = function(){ // (Called from core._init) (/!\ Need filter to root tree only) Init listener of bookmarks + function notifyBookmarksChanged(){ core.GridNodes.sync(app.settings.grid.node, app.settings.grid.root); } + browser.bookmarks.onCreated.addListener(notifyBookmarksChanged); + browser.bookmarks.onChanged.addListener(notifyBookmarksChanged); + browser.bookmarks.onMoved.addListener(notifyBookmarksChanged); + browser.bookmarks.onRemoved.addListener(notifyBookmarksChanged); +} +core.Bookmarks.initRoot = function(callback){ // (Called from core._init) Create the root folder if not exist + browser.bookmarks.getSubTree('menu________').then(function(bookmarkItems){ + getChildItem = function(bookmarkItem, path, callback){ + if(path.length == 0){ + if(callback) callback(bookmarkItem); + return; + } + for(var child of bookmarkItem.children){ + if((path + '/').startsWith(child.title + '/')){ + getChildItem(child, path.substr(child.title.length + 1), callback); + return; + } + } + browser.bookmarks.create({ + parentId: bookmarkItem.id, + title: path.substr(0, (path + '/').indexOf('/')) + }).then(callback); + } + getChildItem(bookmarkItems[0], app.settings.grid.root, callback); + }, function(){ + console.log('Can not load bookmarks'); + if(callback) callback(null); + }); +} +core.Bookmarks.load = function(rootPath, callback){ // callback(bookmarkItem) Return BookmarkItem from rootPath + browser.bookmarks.getSubTree('menu________').then(function(bookmarkItems){ + if(callback) callback(core.Bookmarks.getItem(bookmarkItems[0], rootPath + '/')) + }, function(){ + console.log('Can not load bookmarks'); + if(callback) callback(null); + }); +} +core.Bookmarks.getItem = function(bookmarkItem, path){ // Return BookmarkItem from path from bookmarkItem as root + if(path.length == 0) return bookmarkItem; + for(var child of bookmarkItem.children) + if(path.startsWith(child.title + '/')) + return core.Bookmarks.getItem(child, path.substr(child.title.length + 1)); + return null; +} + +core.SiteInfos = {} // Siteinfos helper object +core.SiteInfos.loadInfos = function(url, args, callback){ // args: { icon: false; screenshot: false }, callback( { url, title, (/!\ Not handled now)icon, screenshot } || error: {} ) + function pageLoaded(){ + var docTitle = iframe.contentWindow.document.title; + var docIcon = null; + var docScreenshot = null; + if(args && args.icon){ + // + } + if(args && args.screenshot){ + var canvas = document.createElement('canvas'); + canvas.style.width = previewWidth.toString() + 'px'; + canvas.style.height = previewHeight.toString() + 'px'; + canvas.width = previewWidth / 2; + canvas.height = previewHeight / 2; + var ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, previewWidth, previewHeight); + ctx.save(); + ctx.scale(0.5, 0.5); + ctx.drawWindow(iframe.contentWindow, 0, 0, previewWidth, previewHeight, 'rgb(255, 255, 255)'); + ctx.restore(); + docScreenshot = canvas.toDataURL(); + } + + document.body.removeChild(iframe); + if(callback) callback({ url: url, title: docTitle, icon: docIcon, screenshot:docScreenshot }); + } + + var previewWidth = 1200; // Need to be linked to settings + var previewHeight = 710; // Need to be linked to settings + var iframe = document.createElement('iframe'); + iframe.width = previewWidth + iframe.height = previewHeight + iframe.style.position = 'absolute'; + iframe.style.visibility = 'hidden'; + var xmlHttp = new XMLHttpRequest(); + xmlHttp.timeout = 2000 + xmlHttp.open('GET', url, true); + xmlHttp.onload = function(){ + document.body.appendChild(iframe); + iframe.contentWindow.document.write(xmlHttp.responseText.replace('', '')); + //iframe.contentWindow.document.write(xmlHttp.responseText.replace('', '')); + setTimeout(function(){ pageLoaded(); }, 2000); // /!\ Caution function can be shortcuted and sendtimeout is not the best way + } + xmlHttp.onabort = function(){ if(callback) callback(); } + xmlHttp.onerror = function(){ if(callback) callback(); } + xmlHttp.ontimeout = function(){ if(callback) callback(); } + xmlHttp.send(null); +} + +core.GridNodes = {}; // GridNodes helper object +core.GridNodes.sync = function(gridNode, rootPath){ // Sync GridNodes with Bookmarks + core.Bookmarks.load(rootPath, function(bookmarkItem){ + core.GridNodes.syncItem(gridNode, bookmarkItem); + browser.runtime.sendMessage({ command: 'gridNodesSynced'}).then(function(){}, function(){}); + core.Settings.save(); + }); +} +core.GridNodes.syncItem = function(gridNode, bookmarkItem){ // Sync GridNode with BookmarkItem + gridNode.id = bookmarkItem.id; + gridNode.title = bookmarkItem.title; // /!\ Need check last update + if(bookmarkItem.url){ + gridNode.type = 'bookmark'; + gridNode.url = bookmarkItem.url; // /!\ Need check last update + } else if(bookmarkItem.children){ + gridNode.type = 'folder'; + var EmptyItems = []; + if(! gridNode.children) gridNode.children = []; + else { + for(var i=gridNode.children.length-1; i>=0; i--){ + if(gridNode.children[i].type!='empty'){ + var found = false; + for(var child of bookmarkItem.children){ + if(child.id==gridNode.children[i].id){ + found = true; + break; + } + } + if(! found){ + if(i0){ + childGridNode = EmptyItems[0]; + EmptyItems.shift(); + }else { + childGridNode = {}; + gridNode.children.push(childGridNode) + } + } + core.GridNodes.syncItem(childGridNode, child); + } + EmptyItems.length = 0; + } else node.type = 'empty'; +} +core.GridNodes.getChild = function(gridNode, id){ // Return child node by ID + for(var child of gridNode.children) if(child.id == id) return child; + return null; +} +core.GridNodes.getNode = function(gridNode, path){ // Return GridNode from RootGridNode path + if(path.length == 0 || path == '/') return gridNode; + for(var child of gridNode.children) + if(path.startsWith(child.title + '/')) + return core.GridNodes.getNode(child, path.substr(child.title.length + 1)); + return null; +} +core.GridNodes.refreshNode = function(gridNode, callback){ // Refresh content of a GridNode + core.SiteInfos.loadInfos(gridNode.url, { screenshot: true }, function(infos){ + if(infos){ + gridNode.title = infos.title; + gridNode.image = infos.screenshot; + browser.bookmarks.update(gridNode.id, { + title: infos.title + }).then(function(bookmarkItem){}, function(){}); + core.Settings.save(); + } + if(callback) callback(infos); + }); +} +core.GridNodes.createFolder = function(gridNode, name, callback){ // Create a new folder in a GridNode + browser.bookmarks.create({ + parentId: gridNode.id, + title: name + }).then(callback); +} +core.GridNodes.createBookmark = function(gridNode, url, title, callback){ // Create a new Bookmark in a GridNode + browser.bookmarks.create({ + parentId: gridNode.id, + title: title || url, + url: url + }).then(callback); +} +core.GridNodes.delete = function(id, callback){ // Delete a GridNode + browser.bookmarks.removeTree(id).then(callback); +} +core.GridNodes.setNodeIndex = function(gridNode, index, newIndex, callback){ // Set Child GridNodeIndex + while(newIndex>=gridNode.children.length){ + gridNode.children.push({ type: 'empty' }); + } + var node1 = gridNode.children[index]; + var node2 = gridNode.children[newIndex]; + gridNode.children[index] = node2; + gridNode.children[newIndex] = node1; + core.Settings.save(); + if(callback) callback(); + browser.runtime.sendMessage({ command: 'gridNodesSynced'}).then(function(){}, function(){}); +} + +// Public functions +app.refreshNode = core.GridNodes.refreshNode; +app.getNode = core.GridNodes.getNode; +app.createFolder = core.GridNodes.createFolder; +app.createBookmark = core.GridNodes.createBookmark; +app.deleteNode = core.GridNodes.delete; +app.setNodeIndex = core.GridNodes.setNodeIndex; diff --git a/src/js/dial.js b/src/js/dial.js new file mode 100644 index 0000000..a2a026d --- /dev/null +++ b/src/js/dial.js @@ -0,0 +1,305 @@ +var app = {} +var dial = { + styles: {}, + page: 1, + maxpage: 1 +}; + +browser.runtime.getBackgroundPage().then(function(page){ app = page.app; }, function(){}); +window.onload = function(){ if(app.settings) dial.initUI(); } +window.onresize = function(){ + if(app.settings) dial.updateGridLayout(dial.Grid, app.settings.grid, dial.styles.grid); +} +window.onwheel = function(ev){ + if(app.settings){ + if(ev.deltaY > 0){ + if(dial.page < dial.maxpage){ + dial.page += 1; + dial.populateGrid(dial.Grid, app.settings.grid, dial.Node); + } + } else if(ev.deltaY < 0){ + if(dial.page > 1){ + dial.page -= 1; + dial.populateGrid(dial.Grid, app.settings.grid, dial.Node); + } + } + } +} +browser.runtime.onMessage.addListener(function(request, sender, sendResponse){ + switch(request.command){ + case 'gridNodesSynced': + if(app.settings) dial.populateGrid(dial.Grid, app.settings.grid, dial.Node); + break; + case 'appReady': + browser.runtime.getBackgroundPage().then(function(page){ app = page.app; }, function(){}); + dial.initUI(); + break; + } +}); + + +dial.initUI = function(){ + dial.Head = document.getElementById('head'); + dial.Body = document.getElementById('body'); + dial.Body.setAttribute('contextmenu', 'page'); + dial.Body.setAttribute('contextmenu', 'page'); + dial.initStyles(); + dial.initMenus(); + dial.Grid = dial.initGrid('Grid', app.settings.grid, dial.Body); + var url = new URL(window.location); + dial.path = url.searchParams.get('path'); + /* + if(url.searchParams.get('path')) dial.Node = app.getNode(app.settings.grid.node, dial.path + '/'); + else dial.Node = app.getNode(app.settings.grid.node, '/'); + */ + if(url.searchParams.get('path')) { + dial.Node = app.getNode(app.settings.grid.node, dial.path + '/'); + } else { + dial.Node = app.getNode(app.settings.grid.node, '/'); + } + dial.populateGrid(dial.Grid, app.settings.grid, dial.Node); +} + +dial.initStyles = function(){ + dial.Style = document.createElement('style'), StyleSheet; + document.head.appendChild(dial.Style); + dial.styles.html = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('html { height: 100%; }')].style; + dial.styles.body = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('body { user-select: none; -moz-user-select: none; display: flex; width: 100%; height: 100%; margin: 0px; padding: 0px; background: ' + app.settings.background + '; }')].style; + dial.styles.grid = {}; + dial.styles.grid.grid = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid { border-collapse: collapse; margin: auto auto; }')].style; + dial.styles.grid.cell = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid td { margin: 0px; padding: 0px; }')].style; + dial.styles.grid.link = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid td>a { display: block; outline: none; text-decoration: none; margin: ' + app.settings.grid.cells.margin + 'px; border: 1px solid ' + app.settings.grid.cells.borderColor + '; border-radius: ' + app.settings.grid.cells.borderRadius + 'px; }')].style; + dial.styles.grid.linkHover = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid td>a:hover { border-color: ' + app.settings.grid.cells.borderColorHover + '; }')].style; + dial.styles.grid.linkPanel = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid td>a>div:first-child { background-repeat: no-repeat; }')].style; + dial.styles.grid.linkTitle = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid td>a>div:last-child { height: ' + app.settings.grid.cells.titleHeight + 'px; font-size: ' + app.settings.grid.cells.titleFontSize + 'pt; font-family: ' + app.settings.grid.cells.titleFont + 'pt; text-align: center; overflow: hidden; color: ' + app.settings.grid.cells.titleColor + '; border-top: 1px solid ' + app.settings.grid.cells.borderColor + '; }')].style; + dial.styles.grid.linkTitleHover = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid td>a:hover>div:last-child { color: ' + app.settings.grid.cells.titleColorHover + '; border-top-color: ' + app.settings.grid.cells.borderColorHover + ' }')].style; + dial.styles.grid.linkEmpty = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid td>a.Empty { display: none; }')].style; + dial.styles.grid.linkBack = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid td>a.Back :first-child { background-image: url("' + app.settings.grid.cells.backIcon + '"); background-repeat: no-repeat; background-position: center center; }')].style; + dial.styles.grid.linkFolder = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid td>a.Folder :first-child { background-image: url("' + app.settings.grid.cells.folderIcon + '"); background-repeat: no-repeat; background-size: 100% 100%; }')].style; + dial.styles.grid.linkBookmark = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid td>a.Bookmark :first-child { background-repeat: no-repeat; background-size: 100% 100%; }')].style; + dial.styles.grid.linkBookmarkLoading = dial.Style.sheet.cssRules[dial.Style.sheet.insertRule('.Grid td>a.BookmarkLoading :first-child { background-image: url("' + app.settings.grid.cells.loadingIcon + '"); background-repeat: no-repeat; background-position: center center; }')].style; +} + +dial.initMenus = function(){ + dial.PageMenu = document.createElement('menu'); + dial.PageMenu.type = 'context'; + dial.PageMenu.id = 'page' + dial.PageMenuCreateBookmark = document.createElement('menuitem'); + dial.PageMenuCreateBookmark.label = 'Add bookmark'; + dial.PageMenuCreateBookmark.onclick = dial.createBookmark; + dial.PageMenuCreateFolder = document.createElement('menuitem'); + dial.PageMenuCreateFolder.label = 'Add folder'; + dial.PageMenuCreateFolder.onclick = dial.createFolder; + dial.PageMenu.appendChild(dial.PageMenuCreateBookmark); + dial.PageMenu.appendChild(dial.PageMenuCreateFolder); + dial.Body.appendChild(dial.PageMenu); + + dial.ItemMenu = document.createElement('menu'); + dial.ItemMenu.type = 'context'; + dial.ItemMenu.id = 'item' + dial.ItemMenuCreateBookmark = document.createElement('menuitem'); + dial.ItemMenuCreateBookmark.label = browser.i18n.getMessage("menuAddBookmark"); + dial.ItemMenuCreateBookmark.onclick = dial.createBookmark; + dial.ItemMenuCreateFolder = document.createElement('menuitem'); + dial.ItemMenuCreateFolder.label = browser.i18n.getMessage("menuAddFolder"); + dial.ItemMenuCreateFolder.onclick = dial.createFolder; + dial.ItemMenuEdit = document.createElement('menuitem'); + dial.ItemMenuEdit.label = 'Edit'; + //dial.ItemMenuEdit.onclick = dial.test; + dial.ItemMenuRefresh = document.createElement('menuitem'); + dial.ItemMenuRefresh.label = 'Refresh'; + dial.ItemMenuRefresh.label = browser.i18n.getMessage("menuRefreshItem"); + dial.ItemMenuRefresh.onclick = dial.refreshNode; + dial.ItemMenuDelete = document.createElement('menuitem'); + dial.ItemMenuDelete.label = browser.i18n.getMessage("menuDeleteItem"); + dial.ItemMenuDelete.onclick = dial.deleteNode; + dial.ItemMenu.appendChild(dial.ItemMenuCreateBookmark); + dial.ItemMenu.appendChild(dial.ItemMenuCreateFolder); + dial.ItemMenu.appendChild(document.createElement('hr')); + //dial.ItemMenu.appendChild(dial.ItemMenuEdit); + dial.ItemMenu.appendChild(dial.ItemMenuRefresh); + dial.ItemMenu.appendChild(dial.ItemMenuDelete); + dial.Body.appendChild(dial.ItemMenu); +} + +dial.initGrid = function(name, settings, container){ + var grid = document.createElement('table'); + grid.className = name; + grid.getLink = function(index){ + var num_columns = grid.rows[0].cells.length; + return grid.rows[Math.floor(index/num_columns)].cells[index % num_columns].childNodes[0]; + } + for(var i=0; i dial.maxpage) dial.page = dial.maxpage; + if(dial.page > 1) iBase = (dial.page -1) * maxCells; + for(var i = iBase; i', '')); + } + xmlHttp.send(null); +} diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..fbdec02 --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,42 @@ +{ + + "manifest_version": 2, + "name": "__MSG_extensionName__", + "version": "0.0.2", + + "description": "__MSG_extensionDescription__", + + "icons": { + "24": "img/24.png", + "32": "img/32.png" + }, + + "permissions": [ + "storage", + "bookmarks", + "contextMenus", + "tabs", + "" + ], + + "background": { + "scripts": ["js/background.js"] + }, + + "chrome_url_overrides" : { + "newtab": "dial" + }, + + "options_ui": { + "page": "options.html" + }, + + "applications": { + "gecko": { + "id": "mat@matmoul.com_quickdial_test_3", + "strict_min_version": "52.0" + } + }, + + "default_locale": "en" +}