├── .gitignore ├── MMM-AnyList.css ├── MMM-AnyList.js ├── README.md ├── anylist.png ├── node_helper.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | -------------------------------------------------------------------------------- /MMM-AnyList.css: -------------------------------------------------------------------------------- 1 | .anylist ul { 2 | margin: 0; 3 | font-size: 20px; 4 | padding: 0px; 5 | line-height: 25px; 6 | color: lightgray; 7 | } 8 | 9 | .anylist .leftAligned td, 10 | .anylist .leftAligned th { 11 | text-align: left; 12 | } 13 | 14 | .anylist .centerAligned td, 15 | .anylist .centerAligned th { 16 | text-align: center; 17 | } 18 | -------------------------------------------------------------------------------- /MMM-AnyList.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | Module.register('MMM-AnyList', { 3 | defaults: { 4 | onlyShowUnchecked: true, 5 | maxItemsInList: 10, 6 | fade: true, 7 | fadePoint: 0.5, 8 | animationSpeed: 2000, 9 | highlightAlternateRows: false, 10 | highlightColor: 'darkslategrey', 11 | trimText: true, 12 | showCategories: true, 13 | showQuantities: true, 14 | textAlign: 'center' 15 | }, 16 | 17 | start() { 18 | // Send user config to backend 19 | // Backend will reply with socket notification when ready 20 | this.sendSocketNotification('INIT', this.config); 21 | }, 22 | 23 | getHeader() { 24 | return this.data.header; 25 | }, 26 | 27 | getDom() { 28 | const wrapper = document.createElement('div'); 29 | wrapper.className = 'anylist'; 30 | 31 | if (this.error) { 32 | // If an error was sent from the node_helper, display it in the UI 33 | wrapper.innerHTML = `${this.error}`; 34 | wrapper.className = 'small'; 35 | return wrapper; 36 | } 37 | 38 | if (!this.list) { 39 | // Data hasn't been loaded yet, return 40 | wrapper.innerHTML = 'Loading …'; 41 | wrapper.className = 'small dimmed'; 42 | return wrapper; 43 | } 44 | 45 | // Create table container 46 | const tableContainer = document.createElement('table'); 47 | tableContainer.className = 'small'; 48 | 49 | if (this.config.textAlign === 'left') { 50 | tableContainer.className += ' leftAligned'; 51 | } 52 | 53 | if (this.config.textAlign === 'center') { 54 | tableContainer.className += ' centerAligned'; 55 | } 56 | 57 | let category = ''; 58 | 59 | // Add items to container 60 | this.list.items.forEach((item, i) => { 61 | // Create header cell if current item is in a different category than the previous item 62 | if (item.categoryMatchId !== category && this.config.showCategories) { 63 | const headerRow = document.createElement('tr'); 64 | 65 | const header = document.createElement('th'); 66 | headerRow.append(header); 67 | 68 | const toUpper = item.categoryMatchId[0].toUpperCase() + item.categoryMatchId.slice(1); // Make first letter upper case 69 | const format = toUpper.replace(/-/g, ' '); // Replace hyphens with spaces 70 | 71 | if (i > 0) { 72 | header.style.paddingTop = '12px'; 73 | } // Add padding if not the first category 74 | 75 | header.style.opacity = this.config.fade ? 76 | this._getFadedOpacity(this.list.items.length, i) : 77 | 1; 78 | 79 | header.innerHTML = format; 80 | 81 | tableContainer.append(headerRow); 82 | 83 | category = item.categoryMatchId; 84 | } 85 | 86 | const itemRow = document.createElement('tr'); 87 | 88 | const itemCellName = document.createElement('td'); 89 | if (item.name.length > 25 && this.config.trimText) { 90 | itemCellName.innerHTML = item.name.slice(0, 24) + '…'; 91 | } else { 92 | itemCellName.innerHTML = item.name; 93 | } 94 | 95 | itemRow.append(itemCellName); 96 | 97 | if (this.config.showQuantities) { 98 | const itemCellQuantity = document.createElement('td'); 99 | itemCellQuantity.innerHTML = item.quantity || 1; 100 | itemCellQuantity.style.width = '50px'; 101 | itemCellQuantity.style.textAlign = 'right'; 102 | itemRow.append(itemCellQuantity); 103 | } 104 | 105 | if ( 106 | i % 2 === 0 && 107 | this.config.highlightAlternateRows && 108 | !this.config.showCategories 109 | ) { 110 | itemRow.style.backgroundColor = this.config.highlightColor; 111 | } 112 | 113 | itemRow.style.opacity = this.config.fade ? 114 | this._getFadedOpacity(this.list.items.length, i) : 115 | 1; 116 | 117 | tableContainer.append(itemRow); 118 | }); 119 | 120 | wrapper.append(tableContainer); 121 | 122 | return wrapper; 123 | }, 124 | 125 | socketNotificationReceived(notification, payload) { 126 | if (notification === 'LIST_DATA' && payload.name === this.config.list) { 127 | // Update local data 128 | this.error = null; 129 | 130 | let items = payload.items; 131 | 132 | if (this.config.onlyShowUnchecked) { 133 | items = items.filter(i => !i.checked); 134 | } 135 | 136 | if (this.config.maxItemsInList > 0) { 137 | items = items.slice(0, this.config.maxItemsInList); 138 | } 139 | 140 | const list = {...payload, items}; 141 | 142 | // Set list header to module header 143 | this.data.header = list.name; 144 | 145 | const itemsByCategory = {}; 146 | 147 | for (const item of list.items) { 148 | if (itemsByCategory[item.categoryMatchId]) { 149 | itemsByCategory[item.categoryMatchId].push(item); 150 | } else { 151 | itemsByCategory[item.categoryMatchId] = [item]; 152 | } 153 | } 154 | 155 | list.items = Object.keys(itemsByCategory).reduce((accum, category) => [...accum, ...itemsByCategory[category]], []); 156 | 157 | this.list = list; 158 | 159 | // Update display 160 | this.updateDom(this.config.animationSpeed); 161 | } else if (notification === 'ANYLIST_ERROR') { 162 | this.error = payload; 163 | this.updateDom(this.config.animationSpeed); 164 | } 165 | }, 166 | 167 | getStyles() { 168 | return ['MMM-AnyList.css']; 169 | }, 170 | 171 | _getFadedOpacity(length_, i, startPercentage = this.config.fadePoint) { 172 | // Calculates the opacity of an item in a list 173 | // given a percentage at which to start fading out. 174 | const startIndex = length_ * startPercentage; 175 | const fadeSteps = length_ - startIndex; 176 | 177 | if (i >= startIndex) { 178 | const currentFadeStep = i - startIndex; 179 | return 1 - ((1 / fadeSteps) * currentFadeStep); 180 | } 181 | 182 | return 1; 183 | } 184 | }); 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MMM-AnyList 2 | 3 | MMM-AnyList is a Magic Mirror Module for [AnyList](https://www.anylist.com/). 4 | 5 | 6 | ## Screenshot 7 | ![screenshot](/anylist.png) 8 | 9 | 10 | ## Installation 11 | Open a terminal session, navigate to your MagicMirror's `modules` folder and execute: 12 | ```` 13 | git clone https://github.com/codetheweb/MMM-AnyList.git 14 | cd MMM-AnyList 15 | npm install 16 | ```` 17 | 18 | Activate the module by adding it to the config.js file as shown below. 19 | 20 | 21 | ## Using the module 22 | ```javascript 23 | { 24 | module: 'MMM-AnyList', 25 | position: 'top_left', 26 | config: { 27 | email: 'your-email', 28 | password: 'your-password', 29 | list: 'your-list-name', 30 | // Optional, values listed are the defaults 31 | onlyShowUnchecked: true, 32 | maxItemsInList: 10, 33 | fade: true, 34 | fadePoint: 0.5, 35 | animationSpeed: 2000, 36 | trimText: true, 37 | showCategories: true, 38 | showQuantities: true, 39 | textAlign: 'center' 40 | } 41 | } 42 | ``` 43 | 44 | ## Config 45 | The entry in `config.js` can include the following options: 46 | 47 | |Option|Description|Default Value|Accepted Values| 48 | |---|---|---|---| 49 | |`email`|*Required* Your AnyList email address|-|`Your email address`| 50 | |`password`|*Required* Your AnyList password|-|`Your password`| 51 | |`list`|*Required* The name of the list you want to display|-|`Your list name`| 52 | |`onlyShowUnchecked`|Only show items not crossed off your list|`true`|`true / false`| 53 | |`maxItemsInList`|The maximum number of items to display|`10`|`Any number (0 for unlimited)`| 54 | |`fade`|Toggle to fade out list towards the end|`true`|`true / false`| 55 | |`fadePoint`|Rate at which to fade out|`0.5`|`A number between 0 and 1`| 56 | |`animationSpeed`|Speed for refresh animation|`2000`|`Any number (in milliseconds)`| 57 | |`highlightAlternateRows`|Highlights every second row. Not available when showing categories|`false`|`true / false`| 58 | |`highlightColor`|The color to highlight|`darkslategrey`|`Any color value`| 59 | |`trimText`|Trim any items more than 25 characters long to save space|`true`|`true / false`| 60 | |`showCategories`|Use item categories as table headers|`true`|`true / false`| 61 | |`showQuantities`|Show the quantity of each item from your list|`true`|`true / false`| 62 | |`textAlign`|Choose whether the items in the list are left-aligned or centered|`'center'`|`'center' / 'left'` 63 | 64 | 65 | -------------------------------------------------------------------------------- /anylist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetheweb/MMM-AnyList/6f7acf59230adda1bfe0df6192e0566374f0ba97/anylist.png -------------------------------------------------------------------------------- /node_helper.js: -------------------------------------------------------------------------------- 1 | const NodeHelper = require('node_helper'); 2 | const AnyList = require('anylist'); 3 | const queue = require('async/queue'); 4 | 5 | module.exports = NodeHelper.create({ 6 | start() { 7 | const self = this; 8 | 9 | console.log(`Starting node helper: ${this.name}`); 10 | 11 | // Limit concurrency, otherwise we get errors when 12 | // logging in. 13 | this.queue = queue(async task => { 14 | const {notification, payload} = task; 15 | 16 | if (notification === 'INIT') { 17 | const {email, password, list} = payload; 18 | 19 | this.anylist = new AnyList({email, password}); 20 | 21 | // Send update when list is updated 22 | this.anylist.on('lists-update', () => { 23 | this.sendSocketNotification('LIST_DATA', this.anylist.getListByName(list)); 24 | }); 25 | 26 | await this.anylist.login(); 27 | await this.anylist.getLists(); 28 | 29 | this.sendSocketNotification('LIST_DATA', this.anylist.getListByName(list)); 30 | } 31 | }, 1); 32 | 33 | this.queue.error((err, task) => { 34 | const message = `AnyList module experienced an error while processing a ${task.notification} notification: ${err}`; 35 | console.error(message); 36 | self.sendSocketNotification('ANYLIST_ERROR', message); 37 | }); 38 | }, 39 | 40 | async socketNotificationReceived(notification, payload) { 41 | this.queue.push({notification, payload}); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MMM-AnyList", 3 | "description": "A Magic Mirror Module for AnyList", 4 | "keywords": [], 5 | "version": "0.1.0", 6 | "main": "lib/index.js", 7 | "license": "MIT", 8 | "scripts": { 9 | "test": "xo", 10 | "lint-fix": "xo --fix" 11 | }, 12 | "dependencies": { 13 | "anylist": "^0.5.2", 14 | "async": "^3.2.0", 15 | "xo": "^0.28.1" 16 | }, 17 | "devDependencies": { 18 | "husky": "^4.2.3" 19 | }, 20 | "xo": { 21 | "env": [ 22 | "browser", 23 | "node" 24 | ], 25 | "rules": { 26 | "unicorn/filename-case": "off" 27 | } 28 | }, 29 | "husky": { 30 | "hooks": { 31 | "pre-commit": "npm test" 32 | } 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/dwburger/AnyListModule.git" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/dwburger/AnyListModule/issues" 40 | }, 41 | "homepage": "https://github.com/dwburger/AnyListModule" 42 | } 43 | --------------------------------------------------------------------------------