├── README.md ├── img ├── scr_dev1.png ├── scr_dev2.png ├── scr_edu1.png └── scr_edu2.png └── src ├── app ├── bg.html ├── css │ ├── normalize.css │ ├── options.css │ └── style.css ├── index.html ├── js │ ├── index.js │ ├── lib.js │ └── options.js └── options.html └── manifest.json /README.md: -------------------------------------------------------------------------------- 1 | # landing 2 | 3 | A Chrome extension that provides a minimal New Tab page. 4 | 5 | ## Screenshots 6 | 7 | ![](/img/scr_dev1.png?raw=true) 8 | ![](/img/scr_edu1.png?raw=true) 9 | ![](/img/scr_dev2.png?raw=true) 10 | ![](/img/scr_edu2.png?raw=true) 11 | 12 | ## Usage 13 | 14 | After installing (either from the [Chrome Web Store](https://chrome.google.com/webstore/detail/landing/ejfjhmoplgdbebjlgllbkjgmjfakjjfd) or by loading the unpacked extension from `chrome://extensions`), open a new tab. A new bookmarks folder named **landing** will be created in your **Other bookmarks** folder. To add items to your New Tab page, place bookmarks in the **landing** folder in the following structure: 15 | 16 | - Other bookmarks (folder) 17 | - landing (folder) 18 | - category 1 (folder) 19 | - item A (bookmark) 20 | - item B (bookmark) 21 | - category 2 (folder) 22 | - item X (bookmark) 23 | - item Y (bookmark) 24 | 25 | etc. 26 | 27 | -------------------------------------------------------------------------------- /img/scr_dev1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-chew/landing/037cfe8789c5eceb2ccabb0bdef5a22219b2140e/img/scr_dev1.png -------------------------------------------------------------------------------- /img/scr_dev2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-chew/landing/037cfe8789c5eceb2ccabb0bdef5a22219b2140e/img/scr_dev2.png -------------------------------------------------------------------------------- /img/scr_edu1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-chew/landing/037cfe8789c5eceb2ccabb0bdef5a22219b2140e/img/scr_edu1.png -------------------------------------------------------------------------------- /img/scr_edu2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-chew/landing/037cfe8789c5eceb2ccabb0bdef5a22219b2140e/img/scr_edu2.png -------------------------------------------------------------------------------- /src/app/bg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} 2 | -------------------------------------------------------------------------------- /src/app/css/options.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 8px; 3 | } 4 | 5 | .option { 6 | margin-bottom: 8px; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/app/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #333; 3 | font-family: "Roboto Mono", monospace; 4 | 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | height: 100vh; 10 | } 11 | 12 | .container { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | 17 | width: 80%; 18 | } 19 | 20 | .table { 21 | margin: 2.4em 0; 22 | } 23 | 24 | .quote { 25 | margin-bottom: 2.4em; 26 | text-align: center; 27 | } 28 | 29 | .animated .row, .animated .quote { 30 | opacity: 0; 31 | transform: translateY(1em); 32 | 33 | animation-duration: 0.5s; 34 | animation-fill-mode: forwards; 35 | animation-name: enter; 36 | } 37 | 38 | @keyframes enter { 39 | to { 40 | opacity: 1; 41 | transform: translateY(0); 42 | } 43 | } 44 | 45 | .row:not(:last-child) .list { 46 | margin-bottom: 1.6em; 47 | } 48 | 49 | .category { 50 | box-sizing: border-box; 51 | padding-right: 0.8em; 52 | 53 | text-align: right; 54 | vertical-align: top; 55 | } 56 | 57 | .list { 58 | box-sizing: border-box; 59 | margin: 0; 60 | padding-left: 0.8em; 61 | 62 | border-left-style: solid; 63 | } 64 | 65 | .item { 66 | margin-bottom: 0.4em; 67 | 68 | list-style: none; 69 | vertical-align: top; 70 | } 71 | 72 | .link { 73 | text-decoration: none; 74 | color: inherit; 75 | } 76 | 77 | .errMsg { 78 | width: 300px; 79 | height: auto; 80 | 81 | font-size: 24px; 82 | text-align: center; 83 | } 84 | 85 | .text-custom { 86 | color: #ccc; 87 | transition: color 0.25s ease-out 0s; 88 | } 89 | 90 | .line-custom { 91 | border-left-color: #ccc; 92 | transition: border-left-color 0.25s ease-out 0s; 93 | } 94 | -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | New Tab 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/js/index.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | var createDom = function(categories, options) { 3 | // Animate body if enabled 4 | if (options.animate) document.body.setAttribute("class", "animated"); 5 | 6 | // Create rows with title and links 7 | var bookmarks = document.getElementById("bookmarks"); 8 | bookmarks.innerHTML = categories.map((category, index) => { 9 | var items = category.children 10 | .map(bookmark => `
  • 11 | ${bookmark.title} 12 |
  • `) 13 | .join(""); 14 | return ` 15 | ${category.title} 16 | 17 | `; 18 | }).join(""); 19 | 20 | // Add quote 21 | var quote = document.getElementById("quote"); 22 | quote.innerHTML = options.quote; 23 | quote.setAttribute("style", 24 | `animation-delay: ${categories.length * 50}ms`); 25 | }; 26 | 27 | var setStyles = function(options) { 28 | var style = document.createElement("style"); 29 | style.type = "text/css"; 30 | style.innerHTML += `body {background: ${options.bgcolor};}\n`; 31 | style.innerHTML += 32 | `.line-custom {border-left-color: ${options.lncolor}; 33 | border-left-width: ${options.lnsize}px;}\n`; 34 | style.innerHTML += 35 | `.text-custom {color: ${options.txcolor}; 36 | font-family: ${options.font}; 37 | font-size: ${options.txsize}px;}\n 38 | .link:hover, .link:focus {color: ${options.txcolorhv}}\n`; 39 | 40 | document.head.appendChild(style); 41 | }; 42 | 43 | document.body.onload = function() { 44 | Promise.all([Landing.getCategories(), Landing.getOptions()]) 45 | .then(results => { 46 | setStyles(results[1]); 47 | createDom(results[0], results[1]); 48 | }); 49 | }; 50 | })(); 51 | -------------------------------------------------------------------------------- /src/app/js/lib.js: -------------------------------------------------------------------------------- 1 | // Functions/constants used across multiple pages 2 | var Landing = (() => { 3 | var defaultOptions = { 4 | animate: true, 5 | bgcolor: "#cccccc", 6 | lncolor: "#333333", 7 | lnsize: 2, 8 | font: "sans-serif", 9 | txcolor: "#333333", 10 | txcolorhv: "#0000FF", 11 | txsize: 20, 12 | quote: "", 13 | }; 14 | 15 | var getCategories = async function() { 16 | var otherBookmarks = await (new Promise(resolve => { 17 | chrome.bookmarks.getChildren("2", resolve); 18 | })); 19 | 20 | var baseFolder = 21 | otherBookmarks.find(node => node.title == "landing") || 22 | await sampleBaseFolder(); 23 | 24 | return (await (new Promise(resolve => { 25 | chrome.bookmarks.getSubTree(baseFolder.id, resolve); 26 | })))[0].children; 27 | }; 28 | 29 | var getOptions = async function() { 30 | var options = (await (new Promise(resolve => { 31 | chrome.storage.sync.get("landing_options", resolve); 32 | }))).landing_options; 33 | 34 | if (options) { 35 | // Ensure that all option values have set, even if the options from 36 | // storage have missing values 37 | for (var o in defaultOptions) { 38 | if (options[o] == undefined) options[o] = defaultOptions[o]; 39 | } 40 | } else { 41 | options = sampleOptions(); 42 | } 43 | 44 | return options; 45 | }; 46 | 47 | var sampleBaseFolder = async function() { 48 | var landing = await (new Promise(resolve => { 49 | chrome.bookmarks.create({parentId: "2", title: "landing"}, resolve); 50 | })); 51 | 52 | var welcome = await (new Promise(resolve => { 53 | chrome.bookmarks.create({ 54 | parentId: landing.id, 55 | title: "Welcome!" 56 | }, resolve); 57 | })); 58 | 59 | var instructions = await (new Promise(resolve => { 60 | chrome.bookmarks.create({ 61 | parentId: welcome.id, 62 | title: "Click here for instructions on how to get started.", 63 | url: "https://github.com/alex-chew/landing#usage" 64 | }, resolve); 65 | })); 66 | 67 | var optionsPage = await (new Promise(resolve => { 68 | chrome.bookmarks.create({ 69 | parentId: welcome.id, 70 | title: "Or click here to customize the theme!", 71 | url: chrome.runtime.getURL("app/options.html") 72 | }, resolve); 73 | })); 74 | 75 | return landing; 76 | }; 77 | 78 | var sampleOptions = function() { 79 | chrome.storage.sync.set({ 80 | landing_options: defaultOptions 81 | }); 82 | return defaultOptions; 83 | }; 84 | 85 | return { 86 | getCategories: getCategories, 87 | getOptions: getOptions, 88 | sampleOptions: sampleOptions, 89 | }; 90 | })(); 91 | -------------------------------------------------------------------------------- /src/app/js/options.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | var setOptionValues = function(options) { 3 | for (var o in options) { 4 | var input = document.getElementsByName(o)[0]; 5 | if (input.type == "checkbox") input.checked = options[o]; 6 | else input.value = options[o]; 7 | } 8 | }; 9 | 10 | var getOptionValues = function(options) { 11 | for (var o in options) { 12 | var input = document.getElementsByName(o)[0]; 13 | if (input.type == "checkbox") options[o] = input.checked; 14 | else options[o] = input.value; 15 | } 16 | return options; 17 | }; 18 | 19 | document.body.onload = function() { 20 | var options = {}; 21 | Landing.getOptions().then(o => { 22 | options = o; 23 | setOptionValues(options); 24 | }); 25 | 26 | var saveButton = document.getElementsByName("save")[0]; 27 | saveButton.addEventListener("click", () => { 28 | chrome.storage.sync.set({ 29 | landing_options: getOptionValues(options) 30 | }); 31 | }); 32 | 33 | var resetButton = document.getElementsByName("reset")[0]; 34 | resetButton.addEventListener("click", () => { 35 | if (chrome.extension.getBackgroundPage() 36 | .confirm("Reset to default settings? This cannot be undone.")) { 37 | options = Landing.sampleOptions(); 38 | setOptionValues(options); 39 | } 40 | }); 41 | }; 42 | })(); 43 | -------------------------------------------------------------------------------- /src/app/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Landing options 5 | 6 | 7 | 8 | 9 |
    10 |
    11 | 12 | 13 |
    14 |
    15 | 16 | 17 |
    18 |
    19 | 20 | 21 |
    22 |
    23 | 24 | 25 |
    26 |
    27 | 28 | 29 |
    30 |
    31 | 32 | 33 |
    34 |
    35 | 36 | 37 |
    38 |
    39 | 40 | 41 |
    42 |
    43 | 44 | 45 |
    46 | 47 | 48 |
    49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Landing", 3 | "version": "0.7", 4 | "manifest_version": 2, 5 | "background": { 6 | "page": "app/bg.html" 7 | }, 8 | "options_ui": { 9 | "page": "app/options.html", 10 | "chrome_style": true 11 | }, 12 | "chrome_url_overrides": { 13 | "newtab": "app/index.html" 14 | }, 15 | "permissions": [ 16 | "bookmarks", 17 | "storage" 18 | ] 19 | } 20 | --------------------------------------------------------------------------------