├── LICENSE.md ├── README.md ├── affix-completion.json ├── affix-parse.js ├── affix-parse2.js ├── affixes.json ├── alt-art.json ├── autocomplete.json ├── blacklist.js ├── blacklisted-entry.js ├── chunk.js ├── config.json ├── currency.js ├── filter-group.js ├── filter-groups.js ├── filter-groups.json ├── filter.js ├── filters.js ├── filters.json ├── font ├── Fontin-Regular.eot ├── Fontin-Regular.ttf ├── Fontin-Regular.woff └── stylesheet.css ├── index.html ├── item-blacklist.json ├── item.js ├── itemTypes.json ├── main.js ├── materialize ├── css │ ├── materialize.css │ └── materialize.min.css ├── fonts │ └── roboto │ │ ├── Roboto-Bold.eot │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-Bold.woff │ │ ├── Roboto-Bold.woff2 │ │ ├── Roboto-Light.eot │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-Light.woff │ │ ├── Roboto-Light.woff2 │ │ ├── Roboto-Medium.eot │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-Medium.woff │ │ ├── Roboto-Medium.woff2 │ │ ├── Roboto-Regular.eot │ │ ├── Roboto-Regular.ttf │ │ ├── Roboto-Regular.woff │ │ ├── Roboto-Regular.woff2 │ │ ├── Roboto-Thin.eot │ │ ├── Roboto-Thin.ttf │ │ ├── Roboto-Thin.woff │ │ └── Roboto-Thin.woff2 └── js │ ├── materialize.js │ └── materialize.min.js ├── media ├── alch.png ├── alt.png ├── aug.png ├── bauble.png ├── bless.png ├── chance.png ├── chaos.png ├── chisel.png ├── chrome.png ├── divine.png ├── eternal.png ├── exa.png ├── fuse.png ├── jew.png ├── loading.gif ├── mirror.png ├── portal.png ├── prism.png ├── regal.png ├── regret.png ├── scouring.png ├── scrap.png ├── socket_dex.png ├── socket_int.png ├── socket_link_horiz.png ├── socket_link_vert.png ├── socket_str.png ├── socket_white.png ├── stone.png ├── trans.png ├── vaal.png └── wisdom.png ├── misc.js ├── out.json ├── package.json ├── player-blacklist.json ├── renderer.js ├── reverseItemType.json ├── sniper.png ├── sound1.mp3 ├── sound2.mp3 ├── spectrum ├── LICENSE ├── spectrum.css └── spectrum.js ├── style.css ├── templates ├── affix-filter.html ├── affix.html ├── condition.html ├── entry.html ├── filter-group.html ├── filter.html └── poe-trade-form.html └── type-completion.json /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2017 Licoffe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | Notification sounds downloaded from https://notificationsounds.com and included with this software are licensed under the Creative Commons Attribution license. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Releases 2 | 3 | __IMPORTANT!__ poe-rates.com has been added to the whitelisted domains, I therefore have to stop serving whole API chunks through websockets since this goes against the whitelisting idea. Since this is the current way Poe-sniper is working, this also means that I have to retire poe-sniper, at least temporarly, until I use the same serving method used on poe-rates :: live. In the mean time, please use [https://poe-rates.com/live/](https://poe-rates.com/live/) instead. 4 | 5 | __NEW!__ Use the online version of the tool at [https://poe-rates.com/live/](https://poe-rates.com/live/) 6 | 7 | ![alt text](./sniper.png "Tool preview") 8 | 9 | ### Introduction 10 | This tool lets you run live searches by parsing the latest item data from the Path of Exile API. The main benefit of this tool compared to poe.trade live search is the ability to centralize and save searches within a single window. 11 | 12 | __Note for Linux users__: Make sure to install the [_xclip_](https://linux.die.net/man/1/xclip) clipboard manager to be able to copy whisper messages to the clipboard. 13 | 14 | ### Video 15 | [Here](https://youtu.be/-R8lXIVEd-k) is a short 5 min video showcasing some of the features of the tool :) 16 | 17 | ### Features 18 | Here is the set of features currently supported: 19 | 20 | - Find underpriced items for each leagues 21 | - Create item filters based on various criteria: 22 | - type 23 | - Armor/Shield/Evasion values 24 | - Affixes 25 | - Number of links 26 | - Sockets (total and R/G/B/W) 27 | - Item level 28 | - Item tier 29 | - Gem XP% 30 | - Quality 31 | - Corrupted/crafted/enchanted/identified 32 | - Rarity (including non-unique) 33 | - DPS (Total, Phys) 34 | - League 35 | - Price 36 | - Import your poe.trade searches 37 | - Show price stats of similar items appearing on poe.trade 38 | - Search on poe.trade using your filter criteria 39 | - Links to poe.trade, poe-rates.com search as well as the official wiki 40 | - Notification support (both visual and sound) 41 | - Contact sellers by clicking on an item entry or toggle automatic copy to the clipboard. 42 | - Item price recommendation based on poe.trade 43 | - Blacklist support (see [video](https://www.youtube.com/watch?v=ubZFsvfDSmE)) 44 | - Filter categories (see [video](https://www.youtube.com/watch?v=dPCcINACwrU)) 45 | - Mod group support (NOT, IF, SUM, COUNT, WEIGHT, AND) 46 | 47 | ### Technologies 48 | The app is written in [Node.js](https://nodejs.org/en/) and packaged as a native application using the [Electron](https://electron.atom.io/) framework. 49 | 50 | ### How it works 51 | The tool starts by fetching the last change_id using the [http://poe-rates.com](poe-rates) API. Chunks are then downloaded from the Path of Exile API with gzip compression into memory. Each item in the chunk is compared to the filters created by the user and, should it match the criteria, displayed in the tool. 52 | 53 | ### How fast is it? 54 | It depends on your connection speed. 55 | 56 | ### Is it faster than poe.trade? 57 | It is [slightly faster](https://www.youtube.com/watch?v=LvW7x6OCEJU) for me, but that may not be the case for everyone. 58 | 59 | ### Running the tool 60 | There are two ways to run the tool, either from the sources directly or by fetching one of the releases. 61 | #### From source 62 | Within a terminal: 63 | - Clone the code using `git clone https://github.com/licoffe/POE-sniper.git` 64 | - Change to the cloned location and run `npm install` to install all dependencies 65 | - Finally, run `npm start` to start the indexer 66 | 67 | ### Disclaimer 68 | Make sure to have an unlimited connection plan and a good bandwidth since the tool downloads currently around 1.5 MB of JSON data every second. 69 | 70 | ### Troubleshooting 71 | 72 | #### Error on startup 73 | ![alt text](https://user-images.githubusercontent.com/9851687/29394111-e6815842-82cd-11e7-8155-78f21215e25b.png "JS error on startup") 74 | 75 | If you run into this error on startup, it means that the config.json file holding your settings has been corrupted. The solution is to erase the config file, which will be rebuilt by the program on the next startup. Here are the different paths depending on your system: 76 | 77 | - On Windows: C:\Users\YourUser\AppData\Roaming\POE-Sniper 78 | - On MacOS: ~/Library/Application Support/POE-Sniper 79 | - On Linux: ~/.config/POE-Sniper 80 | 81 | #### Stuck on a change_id / No new items in a while 82 | The connection to the API is set to timeout after a minute. If it happens for some reason (loss of internet connection, realm restart, computer went to sleep, etc.), the program will fetch again the last known change_id from either poe.ninja/poe-rates.com and attempt to download it. 83 | 84 | If you did not receive item updates in a while, then this is most likely an error due to your current filter setup. Feel free to contact me on Reddit or Discord if it happens. 85 | 86 | #### Program freeze 87 | Make sure not to use filters which are too broad (ie. (any Gem) or (any Map)). A filter matching too many items is not useful, will greatly increase parsing time, consume additional memory and slow down the entire program. 88 | 89 | ### Contact 90 | 91 | You can contact me on reddit or Discord, same account. 92 | -------------------------------------------------------------------------------- /affix-parse.js: -------------------------------------------------------------------------------- 1 | var request = require( "request" ); 2 | var async = require( "async" ); 3 | var fs = require( "fs" ); 4 | 5 | const valueReg = /([0-9.]+)/g; 6 | 7 | // var typeLookup = { 8 | // "prefix" : 1, 9 | // "suffix" : 2, 10 | // "enchant" : 10, 11 | // "corrupted": 5 12 | // }; 13 | 14 | var buildURL = function( type, domain, filter ) { 15 | if ( !domain ) { 16 | return "https://pathofexile.gamepedia.com/Special:Ask/mainlabel%3D/format%3Djson/link%3Dnone/default%3D/template%3DSMW-20mod-20table/userparam%3Dextra-5Frows%3D2,-20effect-5Frowid%3D2,-20show-5Ftags%3Dno/introtemplate%3DSMW-20mod-20table-2Fshared-2Fintro-20without-20weight/outrotemplate%3DSMW-20mod-20table-2Foutro/order%3DASC/sort%3DIs-5Fmod/offset%3D0/limit%3D1000/-5B-5BIs-20mod::" + filter + "-5D-5D-20-5B-5BHas-20mod-20generation-20type::" + type + "-5D-5D-20-5B-5B-5D-5D/-3FHas-20name/-3FHas-20stat-20text/prettyprint%3Dtrue/unescape%3Dtrue/searchlabel%3DJSON"; 17 | } else { 18 | return "https://pathofexile.gamepedia.com/Special:Ask/mainlabel%3D/format%3Djson/link%3Dnone/default%3D/template%3DSMW-20mod-20table/userparam%3Dextra-5Frows%3D2,-20effect-5Frowid%3D2,-20show-5Ftags%3Dno/introtemplate%3DSMW-20mod-20table-2Fshared-2Fintro-20without-20weight/outrotemplate%3DSMW-20mod-20table-2Foutro/order%3DASC/sort%3DIs-5Fmod/offset%3D0/limit%3D1000/-5B-5BIs-20mod::" + filter + "-5D-5D-20-5B-5BHas-20mod-20generation-20type::" + type + "-5D-5D-20-5B-5BHas-20mod-20domain::" + domain + "-5D-5D/-3FHas-20name/-3FHas-20stat-20text/prettyprint%3Dtrue/unescape%3Dtrue/searchlabel%3DJSON"; 19 | } 20 | }; 21 | 22 | var urls = { 23 | "Body-armor" : { 24 | "prefix": buildURL( 1, 1, "!~*ssence*" ), 25 | "suffix": buildURL( 2, 1, "!~*ssence*" ), 26 | "crafted-prefix": buildURL( 1, 10, "!~*ssence*" ), 27 | "crafted-suffix": buildURL( 2, 10, "!~*ssence*" ), 28 | "essence-prefix": buildURL( 1, 1, "~*ssence*" ), 29 | "essence-suffix": buildURL( 2, 1, "~*ssence*" ), 30 | "enchant": buildURL( 10, null, "!~*ssence*" ), 31 | "corrupted": buildURL( 5, 1, "!~*ssence*" ), 32 | // "unique": buildURL( 3, 1, "!~*ssence*" ) 33 | }, 34 | "Flask": { 35 | "prefix": buildURL( 1, 2, "!~*ssence*" ), 36 | "suffix": buildURL( 2, 2, "!~*ssence*" ), 37 | // "unique": buildURL( 3, 1, "!~*lask*" ) 38 | }, 39 | "Jewel": { 40 | "prefix": buildURL( 1, 11, "!~*ssence*" ), 41 | "suffix": buildURL( 2, 11, "!~*ssence*" ), 42 | "corrupted": buildURL( 5, 11, "!~*ssence*" ), 43 | // "unique": buildURL( 3, 11, "!~*ewel*" ) 44 | }, 45 | "Map": { 46 | "prefix": buildURL( 1, 5, "!~*ssence*" ), 47 | "suffix": buildURL( 2, 5, "!~*ssence*" ), 48 | // "unique": buildURL( 3, 5, "!~*ap*" ) 49 | } 50 | }; 51 | 52 | var parseContent = function( url, itemType, affixType, callback ) { 53 | console.log( "Item-type: " + itemType + ", affix-type: " + affixType ); 54 | request({ "url": url, "gzip": true }, 55 | function( error, response, body ) { 56 | if ( error ) { 57 | console.log( "Error occured: " + error ); 58 | } 59 | var parsedJSON = JSON.parse( body ); 60 | async.eachLimit( Object.keys( parsedJSON.results ), 1, function( temp, cbAffix ) { 61 | var affix = parsedJSON.results[temp]; 62 | // console.log(affix); 63 | if ( affix.hasOwnProperty( "printouts" ) && affix["printouts"].hasOwnProperty( "Has stat text" )) { 64 | var rawAffix = affix["printouts"]["Has stat text"][0]; 65 | if ( rawAffix ) { 66 | var splitted = rawAffix.split( "
" ); 67 | var min = []; 68 | var max = []; 69 | var affixName = ""; 70 | async.each( splitted, function( split, cbSplit ) { 71 | affixName = split; 72 | if ( splitted.length > 1 ) { 73 | affixName += " - multi"; 74 | } 75 | var values = []; 76 | var matches = valueReg.exec( split ); 77 | while ( matches !== null ) { 78 | values.push( matches[1]); 79 | matches = valueReg.exec( split ); 80 | } 81 | if ( values.length === 4 ) { 82 | min = [parseFloat( values[0]), parseFloat( values[1])]; 83 | max = [parseFloat( values[2]), parseFloat( values[3])]; 84 | } else if ( values.length === 3 ) { 85 | min = [parseFloat( values[0])]; 86 | max = [parseFloat( values[1]), parseFloat( values[2])]; 87 | } else if ( values.length === 2 ) { 88 | min = [parseFloat( values[0])]; 89 | max = [parseFloat( values[1])]; 90 | } else if ( values.length === 1 ) { 91 | min = [parseFloat( values[0])]; 92 | max = [parseFloat( values[0])]; 93 | } else { 94 | console.log( "Could not match: " + split ); 95 | } 96 | //console.log(rawaffix); 97 | //still needs some work, tried to capture word differences, might be too hard - died to octal warnings anyhow 98 | // i.e. freeze/frozen in affix: (20-25)% chance to Avoid being [[Freeze|Frozen]] 99 | //rawaffix = rawaffix.replace(new RegExp('\[\[([A-Za-z\s]+)(?=\|)\|\1(.*)\]\]', 'g'), '$1\($2\)'); 100 | 101 | // Always favors second phrase (frozen over freeze) 102 | affixName = affixName.replace( /\[\[([A-Za-z\s0-9]+)(?=\|)\|([A-Za-z\s0-9]+)\]\]/g, '$2' ); 103 | affixName = affixName.replace( /\d+/g, '#'); 104 | affixName = affixName.replace( /[\[\]\(\)]/g, ''); 105 | affixName = affixName.replace( /#-#/g, '#'); 106 | if ( !affixes[itemType][affixType][affixName]) { 107 | affixes[itemType][affixType][affixName] = []; 108 | } 109 | affixes[itemType][affixType][affixName].push({ 110 | "original": affix["printouts"]["Has stat text"][0], 111 | "name": affix["printouts"]["Has name"][0], 112 | "min": min, 113 | "max": max, 114 | "lvl": parseFloat( 0 ) 115 | }); 116 | cbSplit(); 117 | }, function() { 118 | 119 | }); 120 | } 121 | cbAffix(); 122 | } else { 123 | console.log( "unhandled error" ); 124 | } 125 | }, function() { 126 | callback(); 127 | }); 128 | }); 129 | }; 130 | 131 | var affixes = {}; 132 | 133 | // For each item type 134 | async.eachLimit( Object.keys( urls ), 1, function( itemType, cbItem ) { 135 | affixes[itemType] = {}; 136 | // For each affix type 137 | async.eachLimit( Object.keys( urls[itemType]), 1, function( affixType, cbAffix ) { 138 | affixes[itemType][affixType] = {}; 139 | parseContent( urls[itemType][affixType], itemType, affixType, function() { 140 | // Switch affix type when content parsed 141 | cbAffix(); 142 | }); 143 | }, function() { 144 | // Switch item type 145 | cbItem(); 146 | }); 147 | }, function() { 148 | // console.log( JSON.stringify( affixes )); 149 | fs.writeFileSync( "out.json", JSON.stringify( affixes )); 150 | }); -------------------------------------------------------------------------------- /affix-parse2.js: -------------------------------------------------------------------------------- 1 | var jsdom = require("jsdom/lib/old-api.js"); 2 | var async = require( "async" ); 3 | var fs = require( "fs" ); 4 | 5 | var baseURL = "https://pathofexile.gamepedia.com/"; 6 | var reqLvlReg = /Req. Lv. (\d+)/; 7 | var valueReg = /([0-9.]+)/g; 8 | var itemTypes = { 9 | "One-handed axe": "List_of_one-handed_axe_modifiers", 10 | "Claw": "List_of_claw_modifiers", 11 | "Dagger": "List_of_dagger_modifiers", 12 | "One-handed mace": "List_of_one-handed_mace_modifiers", 13 | "Sceptre": "List_of_sceptre_modifiers", 14 | "One-handed sword": "List_of_one-handed_swords_modifiers", 15 | "Wand": "List_of_wand_modifiers", 16 | "Two-handed axe": "List_of_two-handed_axe_modifiers", 17 | "Bow": "List_of_bow_modifiers", 18 | "Fishing rod": "List_of_fishing_rod_modifiers", 19 | "Two-handed mace": "List_of_two-handed_mace_modifiers", 20 | "Staff": "List_of_staff_modifiers", 21 | "Two-handed sword": "List_of_two-handed_sword_modifiers", 22 | "Shield": { 23 | "AR": "List_of_str_shield_modifiers", 24 | "EV": "List_of_dex_shield_modifiers", 25 | "ES": "List_of_int_shield_modifiers", 26 | "AR/EV": "List_of_str_dex_shield_modifiers", 27 | "AR/ES": "List_of_str_int_shield_modifiers", 28 | "EV/ES": "List_of_dex_int_shield_modifiers" 29 | }, 30 | "Helmet": { 31 | "AR": "List_of_str_helmet_modifiers", 32 | "EV": "List_of_dex_helmet_modifiers", 33 | "ES": "List_of_int_helmet_modifiers", 34 | "AR/EV": "List_of_str_dex_helmet_modifiers", 35 | "AR/ES": "List_of_str_int_helmet_modifiers", 36 | "EV/ES": "List_of_dex_int_helmet_modifiers" 37 | }, 38 | "Boots": { 39 | "AR": "List_of_str_boot_modifiers", 40 | "EV": "List_of_dex_boot_modifiers", 41 | "ES": "List_of_int_boot_modifiers", 42 | "AR/EV": "List_of_str_dex_boot_modifiers", 43 | "AR/ES": "List_of_str_int_boot_modifiers", 44 | "EV/ES": "List_of_dex_int_boot_modifiers" 45 | }, 46 | "Gloves": { 47 | "AR": "List_of_str_glove_modifiers", 48 | "EV": "List_of_dex_glove_modifiers", 49 | "ES": "List_of_int_glove_modifiers", 50 | "AR/EV": "List_of_str_dex_glove_modifiers", 51 | "AR/ES": "List_of_str_int_glove_modifiers", 52 | "EV/ES": "List_of_dex_int_glove_modifiers" 53 | }, 54 | "Body-armor": { 55 | "AR": "List_of_str_body_armour_modifiers", 56 | "EV": "List_of_dex_body_armour_modifiers", 57 | "ES": "List_of_int_body_armour_modifiers", 58 | "AR/EV": "List_of_str_dex_body_armour_modifiers", 59 | "AR/ES": "List_of_str_int_body_armour_modifiers", 60 | "EV/ES": "List_of_dex_int_body_armour_modifiers" 61 | }, 62 | "Amulet": "List_of_amulet_modifiers", 63 | "Belt": "List_of_belt_modifiers", 64 | "Quiver": "List_of_quiver_modifiers", 65 | "Ring": "List_of_ring_modifiers", 66 | "Flask": { 67 | "Life": "List_of_life_flask_modifiers", 68 | "Mana": "List_of_mana_flask_modifiers", 69 | "Hybrid": "List_of_hybrid_flask_modifiers", 70 | "Utility": "List_of_utility_flask_modifiers", 71 | "Critical Utility": "List_of_critical_utility_flask_modifiers", 72 | "Silver": "List_of_silver_flask_modifiers" 73 | }, 74 | "Jewel": { 75 | "Cobalt": "List_of_cobalt_jewel_modifiers", 76 | "Crimson": "List_of_crimson_jewel_modifiers", 77 | "Viridian": "List_of_viridian_jewel_modifiers", 78 | "Prismatic": "List_of_prismatic_jewel_modifiers" 79 | } 80 | }; 81 | itemTypes = { 82 | "Gloves": { 83 | "AR": "List_of_str_glove_modifiers", 84 | "EV": "List_of_dex_glove_modifiers", 85 | "ES": "List_of_int_glove_modifiers", 86 | "AR/EV": "List_of_str_dex_glove_modifiers", 87 | "AR/ES": "List_of_str_int_glove_modifiers", 88 | "EV/ES": "List_of_dex_int_glove_modifiers" 89 | }, 90 | }; 91 | var parsedAffixes = {}; 92 | 93 | var headlines = ["prefix", "suffix", "corrupted"]; 94 | 95 | var parseAffixes = function( category, subcategory, window, callback ) { 96 | var maxValues = {}; 97 | var lastMaxes = []; 98 | var lastMod = ""; 99 | var $ = window.$; 100 | var entry = {}; 101 | var parsed = {}; 102 | $( "div" ).find( ".mw-headline" ).each( function() { 103 | var type = $( this ).text().toLowerCase(); 104 | if ( headlines.indexOf( type ) !== -1 ) { 105 | // console.log( type ); 106 | parsed[type] = {}; 107 | $( this ).parent().parent().find( "table th .-mod" ).each( function() { 108 | var counter = 0; 109 | $( this ).parent().parent().parent().find( "td" ).each( function() { 110 | var column = $( this ).text().trim(); 111 | if ( column !== "" ) { 112 | var match; 113 | switch ( counter ) { 114 | case 0: 115 | entry.name = column; 116 | break; 117 | case 1: 118 | match = reqLvlReg.exec( column ); 119 | if ( match && match.length > 1 ) { 120 | entry.lvl = parseInt( match[1]); 121 | } else { 122 | console.log( "Could not parse level: " + column ); 123 | } 124 | break; 125 | case 2: 126 | // Extract affix title 127 | var affixTitle = column; 128 | affixTitle = affixTitle.replace( valueReg, "#" ); 129 | affixTitle = affixTitle.replace( /\(|\)/g, "" ); 130 | affixTitle = affixTitle.replace( /#-#/g, "#" ); 131 | if ( affixTitle !== lastMod && lastMod !== "" ) { 132 | const splitted = lastMod.split( ", " ); 133 | if ( splitted.length > 1 ) { 134 | if ( !maxValues[splitted[0]]) { 135 | maxValues[splitted[0]] = 0; 136 | } 137 | if ( !maxValues[splitted[1]]) { 138 | maxValues[splitted[1]] = 0; 139 | } 140 | maxValues[splitted[0]] += parseFloat( lastMaxes[0]); 141 | maxValues[splitted[1]] += parseFloat( lastMaxes[1]); 142 | } else { 143 | if ( !maxValues[lastMod]) { 144 | maxValues[lastMod] = 0; 145 | } 146 | maxValues[lastMod] += parseFloat( lastMaxes[0]); 147 | } 148 | lastMaxes = []; 149 | } 150 | lastMod = affixTitle; 151 | // Check if the mod is hybrid 152 | const splitted = affixTitle.split( ", " ); 153 | const hybrid = splitted.length > 1 ? true : false; 154 | 155 | if ( !parsed[type][affixTitle] ) { 156 | parsed[type][affixTitle] = []; 157 | } 158 | match = valueReg.exec( column ); 159 | var values = []; 160 | while ( match !== null ) { 161 | values.push( match[1]); 162 | match = valueReg.exec( column ); 163 | } 164 | var min = []; 165 | var max = []; 166 | if ( values.length === 4 && !hybrid ) { 167 | min = [parseFloat( values[0]), parseFloat( values[1])]; 168 | max = [parseFloat( values[2]), parseFloat( values[3])]; 169 | } else if ( values.length === 3 ) { 170 | min = [parseFloat( values[0])]; 171 | max = [parseFloat( values[1]), parseFloat( values[2])]; 172 | } else if ( values.length === 2 ) { 173 | min = [parseFloat( values[0])]; 174 | max = [parseFloat( values[1])]; 175 | } else if ( values.length === 1 ) { 176 | min = [parseFloat( values[0])]; 177 | max = [parseFloat( values[0])]; 178 | } else { 179 | // console.log( "Could not match: " + column ); 180 | } 181 | if ( !hybrid ) { 182 | entry.min = min; 183 | entry.max = max; 184 | lastMaxes[0] = entry.max; 185 | } else { 186 | entry.hybrid = true; 187 | entry.mods = {}; 188 | entry.mods[splitted[0]] = { 189 | "min": [parseFloat( values[0])], 190 | "max": [parseFloat( values[1])] 191 | }; 192 | entry.mods[splitted[1]] = { 193 | "min": [parseFloat( values[2])], 194 | "max": [parseFloat( values[3])] 195 | }; 196 | lastMaxes = [ 197 | entry.mods[splitted[0]].max, 198 | entry.mods[splitted[1]].max 199 | ] 200 | } 201 | // console.log( lastMod ); 202 | // console.log( lastMaxes ); 203 | parsed[type][affixTitle].push( entry ); 204 | entry = {}; 205 | values = []; 206 | counter = -1; 207 | break; 208 | } 209 | counter++; 210 | // console.log( column ); 211 | } 212 | }); 213 | }); 214 | } 215 | }); 216 | parsed.maxValues = maxValues; 217 | callback( parsed ); 218 | }; 219 | 220 | var processItemCategory = function( category, subcategory, callback ) { 221 | var url; 222 | if ( !subcategory ) { 223 | url = baseURL + itemTypes[category]; 224 | } else { 225 | url = baseURL + itemTypes[category][subcategory]; 226 | } 227 | jsdom.env( 228 | url, 229 | ["http://code.jquery.com/jquery.js"], 230 | function ( err, window ) { 231 | parseAffixes( category, subcategory, window, callback ); 232 | } 233 | ); 234 | }; 235 | 236 | async.eachLimit( Object.keys( itemTypes ), 1, function( type, cbType ) { 237 | console.log( "Parsing " + type ); 238 | if ( typeof itemTypes[type] === "object" ) { 239 | async.eachLimit( Object.keys( itemTypes[type]), 1, function( subtype, cbSubtype ) { 240 | console.log( "Parsing subtype " + subtype ); 241 | if ( !parsedAffixes[type]) { 242 | parsedAffixes[type] = {}; 243 | } 244 | processItemCategory( type, subtype, function( parsed ) { 245 | parsedAffixes[type][subtype] = parsed; 246 | cbSubtype(); 247 | }); 248 | }, function() { 249 | fs.writeFile( "affix-parse/" + type + ".json", JSON.stringify( parsedAffixes[type], null, "\t" ), function() { 250 | parsedAffixes = {}; 251 | cbType(); 252 | }); 253 | }); 254 | } else { 255 | processItemCategory( type, null, function( parsed ) { 256 | fs.writeFile( "affix-parse/" + type + ".json", JSON.stringify( parsed, null, "\t" ), function() { 257 | cbType(); 258 | }); 259 | // parsedAffixes[type] = parsed; 260 | }); 261 | } 262 | }, function() { 263 | fs.writeFile( "out.json", JSON.stringify( parsedAffixes ), function() { 264 | console.log( "Wrote output to file" ); 265 | }); 266 | }); -------------------------------------------------------------------------------- /alt-art.json: -------------------------------------------------------------------------------- 1 | { 2 | "Alpha's Howl": "AlphasHowlAlt", 3 | "Andvarius": [ 4 | "andvariusAlt", 5 | "RingUnique1" 6 | ], 7 | "Asphyxia's Wrath": "AsphyxiasWrathRaceAlt", 8 | "Astramentis": "AstramentisAlt", 9 | "Atziri's Foible": [ 10 | "AtzirisFoibleAlt", 11 | "AtzirisFoibleAlt2" 12 | ], 13 | "Atziri's Mirror": "Atzirismirror2", 14 | "Aurumvorax": "AurumvoraxAlt", 15 | "Auxium": "AuxiumAlt", 16 | "Berek's Grip": "BereksGripAlt", 17 | "Berek's Pass": "BereksPassAlt", 18 | "Berek's Respite": "BereksRespiteAlt", 19 | "Bino's Kitchen Knife": "BinosKitchenKnifeAlt", 20 | "Blackgleam": "BlackgleamAlt", 21 | "Blackheart": "BlackheartAlt", 22 | "Blood of Corruption": "TearofPurityAltArtCorrupt", 23 | "Bloodgrip": "BloodAmuletALT", 24 | "Brightbeak": "BrightbeakAlt", 25 | "Call of the Brotherhood": "CallOfTheBrotherhoodRaceAlt", 26 | "Carcass Jack": "CarcassJackAlt", 27 | "Carnage Heart": "CarnageHeartAlt", 28 | "Cloak of Flame": "CloakofFlameAlt", 29 | "Crest of Perandus": "Perandus", 30 | "Cybil's Paw": "CybilsClawAlt", 31 | "Daresso's Salute": "DaressosSaluteAlt", 32 | "Death Rush": "DeathRushAlt", 33 | "Death's Harp": "DeathsharpAlt", 34 | "Demigod's Beacon": "DemigodsShieldAlt", 35 | "Demigod's Dominance": "DemiGodsArmourAlt", 36 | "Demigod's Eye": "DemigodsBand2", 37 | "Demigod's Stride": "DemigodsStrideAlt", 38 | "Demigod's Touch": "DemigodsTouchAlt", 39 | "Demigod's Triumph": "Demigodstriumph2", 40 | "Divinarius": "DivinariusAlt", 41 | "Divination Distillate": "DivinationDistillateAlt", 42 | "Doedre's Damning": "DoedresDamningAlt", 43 | "Dream Fragments": "DreamFragmentsAlt2", 44 | "Dyadian Dawn": "DyadianDawnRaceAlt", 45 | "Edge of Madness": "EdgeOfMadnessAlt", 46 | "Emberwake": "EmberwakeAlt", 47 | "Eye of Chayula": "EyeofChayulaAlt", 48 | "Facebreaker": "FaceBreaker2", 49 | "Fairgraves' Tricorne": "FairgravesTricorneAlt", 50 | "Fencoil": "Fencoil", 51 | "Geofri's Baptism": "GeofrisBaptismAlt", 52 | "Gifts from Above": "GiftsfromAboveAlt", 53 | "Goldwyrm": "GoldwyrmAlt", 54 | "Headhunter": "Headhunter2", 55 | "Heatshiver": "HeatShiverAlt", 56 | "Hrimnor's Resolve": "HrimnorsResolveAlt", 57 | "Immortal Flesh": "ImmortalFleshAlt", 58 | "Kaom's Sign": "KaomsSignAlt", 59 | "Karui Ward": "KaruiWardAlt", 60 | "Kikazaru": "KikazaruAlt", 61 | "Lavianga's Spirit": [ 62 | "http://web.poecdn.com/gen/image/YTo1OntzOjEwOiJsZWFn/dWVOYW1lIjtzOjg6IlN0/YW5kYXJkIjtzOjk6ImFj/Y291bnRJZCI7TzoxODoi/R3JpbmRiXERhdGFiYXNl/XElkIjoxOntzOjI6Imlk/IjtpOjA7fWk6MjthOjM6/e3M6MToiZiI7czozMjoi/QXJ0LzJESXRlbXMvRmxh/c2tzL0xhdmlhbmdhc09p/bDIiO3M6Mjoic3AiO2Q6/MC42MDg1MTkyNjk3NzY4/NzYzMTQ4MTQ3Mjg2MzAx/NTM2OTUxMjE0MDc1MDg4/NTAwOTc2NTYyNTtzOjU6/ImxldmVsIjtpOjA7fWk6/MTtpOjQ7aTowO2k6OTt9/773c280e9b/Item.png", 63 | "http://web.poecdn.com/gen/image/YTo2OntzOjEwOiJsZWFn/dWVOYW1lIjtzOjg6IlN0/YW5kYXJkIjtzOjk6ImFj/Y291bnRJZCI7TzoxODoi/R3JpbmRiXERhdGFiYXNl/XElkIjoxOntzOjI6Imlk/IjtpOjA7fXM6MTA6InNp/bXBsaWZpZWQiO2I6MTtp/OjI7YTozOntzOjE6ImYi/O3M6MzQ6IkFydC8yREl0/ZW1zL0ZsYXNrcy9MYXZp/YW5nYXNPaWxBbHQiO3M6/Mjoic3AiO2Q6MC42MDg1/MTkyNjk3NzY4NzYzO3M6/NToibGV2ZWwiO2k6MDt9/aToxO2k6NDtpOjA7aTo5/O30,/598bdcb0a0/Item.png" 64 | ], 65 | "Le Heup of All": "LeHeupofAllAlt", 66 | "Lifesprig": "LifesprigAlt", 67 | "Malachai's Artifice": "MalachaisArtificeAlt", 68 | "Maligaro's Virtuosity": "MaligarosVirtuosityAlt", 69 | "Meginord's Girdle": [ 70 | "MeginordsGirdleAlt", 71 | "MeginordsGirdleAlt2" 72 | ], 73 | "Mindspiral": "MindSpiralAlt", 74 | "Ming's Heart": "MingsHeartAlt", 75 | "Mokou's Embrace": "MokousEmbraceAlt", 76 | "Natural Hierarchy": "NaturalHierarchyAltArt2", 77 | "Ngamahu Tiki": "NgamahuTikiALTART", 78 | "Night's Hold": "NightsHoldAltArt2", 79 | "Perandus Blazon": "PerandusBlazonAlt", 80 | "Perandus Signet": "PerandusSignetAlt", 81 | "Pillar of the Caged God": "PillaroftheCagedGodAlt", 82 | "Prismatic Eclipse": "PrismaticEclipseAlt", 83 | "Pyre": "Cherufe2", 84 | "Queen's Decree": "QueensDecreeAlt", 85 | "Quill Rain": "QuillRainAlt", 86 | "Rainbowstride": "RainbowStrideAlt", 87 | "Rashkaldor's Patience": "RashkaldorsPatienceAlt", 88 | "Rathpith Globe": "RathpithGlobeAlt", 89 | "Reaper's Pursuit": "ReapersPursuitAlt", 90 | "Redbeak": "RedBeak2", 91 | "Rumi's Concoction": "http://web.poecdn.com/gen/image/YTo1OntzOjEwOiJsZWFn/dWVOYW1lIjtzOjg6IlN0/YW5kYXJkIjtzOjk6ImFj/Y291bnRJZCI7TzoxODoi/R3JpbmRiXERhdGFiYXNl/XElkIjoxOntzOjI6Imlk/IjtpOjA7fWk6MjthOjM6/e3M6MToiZiI7czozMDoi/QXJ0LzJESXRlbXMvRmxh/c2tzL0Jsb2NrRmxhc2sy/IjtzOjI6InNwIjtkOjAu/NjA4NTE5MjY5Nzc2ODc2/MzE0ODE0NzI4NjMwMTUz/Njk1MTIxNDA3NTA4ODUw/MDk3NjU2MjU7czo1OiJs/ZXZlbCI7aTowO31pOjE7/aTo0O2k6MDtpOjk7fQ,,/9e870a1952/Item.png", 92 | "Saffell's Frame": "SaffellsFrameAlt", 93 | "Shadows and Dust": "BloodGrip", 94 | "Shavronne's Pace": "ShavronnesPaceAlt", 95 | "Shiversting": "ShiverstingAlt", 96 | "Sidhebreath": "SidhebreathAlt", 97 | "Sin Trek": "SinTrekAlt", 98 | "Soul Taker": "SoultakerAlt", 99 | "Soulthirst": "SoulthirstALT", 100 | "Starkonja's Head": "FurryheadofstarkonjaAlt", 101 | "Stone of Lazhwar": "StoneofLazhwarAlt", 102 | "Storm Cloud": "StormcloudAlt", 103 | "Sundance": "SundanceAlt", 104 | "Tabula Rasa": "TabulaRasaAlt", 105 | "Talisman of the Victor": "TalismanoftheVictorAlt", 106 | "Taryn's Shiver": "TarynsShiverAlt", 107 | "Taste of Hate": "http://web.poecdn.com/gen/image/YTo2OntzOjEwOiJsZWFn/dWVOYW1lIjtzOjg6IlN0/YW5kYXJkIjtzOjk6ImFj/Y291bnRJZCI7TzoxODoi/R3JpbmRiXERhdGFiYXNl/XElkIjoxOntzOjI6Imlk/IjtpOjA7fXM6MTA6InNp/bXBsaWZpZWQiO2I6MTtp/OjI7YTozOntzOjE6ImYi/O3M6MzM6IkFydC8yREl0/ZW1zL0ZsYXNrcy9UYXN0/ZU9mSGF0ZUFsdCI7czoy/OiJzcCI7ZDowLjYwODUx/OTI2OTc3Njg3NjM7czo1/OiJsZXZlbCI7aTowO31p/OjE7aTo0O2k6MDtpOjk7/fQ,,/e539ca9da8/Item.png", 108 | "Tear of Purity": [ 109 | "TearofPurityAltArt", 110 | "TearofPurityAltArtCorrupt" 111 | ], 112 | "The Anvil": "TheAnvilAlt", 113 | "The Blood Dance": "GoreFrenzyAlt", 114 | "The Blood Thorn": "TheBloodThornALT", 115 | "The Bringer of Rain": "BringerOfRain", 116 | "The Ignomon": "TheIgnomonAlt", 117 | "The Princess": "ThePrincessAlt", 118 | "The Searing Touch": "TheSearingTouchAlt", 119 | "The Taming": "TheTamingAlt", 120 | "The Three Dragons": "TheThreeDragonsAlt", 121 | "The Whispering Ice": "TheWhisperingIceRaceAlt", 122 | "Thief's Torment": "ThiefsTorment2", 123 | "Thousand Ribbons": "ThousandribbonsAlt", 124 | "Ungil's Gauche": "UngilsGaucheAlt", 125 | "Ungil's Harmony": "UngilsHarmonyAlt", 126 | "Victario's Acuity": "VictariosAcuityAltArt", 127 | "Void Battery": "VoidBatteryAlt", 128 | "Voll's Devotion": [ 129 | "VollsDevotionAltArt2", 130 | "AgateAmuletUnique2" 131 | ], 132 | "Wanderlust": [ 133 | "WanderlustAlt3", 134 | "Wanderlust2" 135 | ], 136 | "Warped Timepiece": "WarpedTimepieceAlt", 137 | "Windscream": "WindscreamAlt", 138 | "Winterheart": "WinterHeart", 139 | "Wurm's Molt": "Belt6Unique2" 140 | } -------------------------------------------------------------------------------- /blacklist.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | /* jshint jquery: true */ 3 | /* jshint esversion: 6 */ 4 | "use strict"; 5 | 6 | /** 7 | * Blacklist class 8 | * 9 | * Manage a list of entries 10 | * @param Entry list 11 | * @return BlackList object 12 | */ 13 | 14 | var fs = require( "fs" ); 15 | const {app} = require( "electron" ).remote; 16 | const path = require( "path" ); 17 | var BlacklistedEntry = require( "./blacklisted-entry" ); 18 | 19 | class BlackList { 20 | constructor( blacklistName, entries ) { 21 | this.blacklistName = blacklistName; 22 | this.entries = entries; 23 | } 24 | 25 | add( entry ) { 26 | this.entries[entry.factor] = new BlacklistedEntry( entry ); 27 | } 28 | 29 | toggle( entry ) { 30 | this.entries[entry.factor].active = !this.entries[entry.factor].active; 31 | } 32 | 33 | revoke( entry ) { 34 | delete this.entries[entry.factor]; 35 | } 36 | 37 | check( factor ) { 38 | if ( this.entries[factor] && this.entries[factor].active ) { 39 | return true; 40 | } else { 41 | return false; 42 | } 43 | } 44 | 45 | /** 46 | * Write the blacklist to disk 47 | * 48 | * @param Nothing 49 | * @return Nothing 50 | */ 51 | save() { 52 | fs.writeFileSync( app.getPath( "userData" ) + path.sep + this.blacklistName + ".json", JSON.stringify( this )); 53 | } 54 | } 55 | 56 | module.exports = BlackList; -------------------------------------------------------------------------------- /blacklisted-entry.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | /* jshint jquery: true */ 3 | /* jshint esversion: 6 */ 4 | "use strict"; 5 | 6 | /** 7 | * BlacklistedEntry class 8 | * 9 | * Represent a blacklisted entry 10 | * @param entry content 11 | * @return BlackListEntry object 12 | */ 13 | 14 | class BlacklistedEntry { 15 | constructor( obj ) { 16 | this.date = Date.now(); 17 | this.factor = obj.factor; 18 | this.reason = obj.reason; 19 | this.active = obj.active; 20 | } 21 | } 22 | 23 | module.exports = BlacklistedEntry; -------------------------------------------------------------------------------- /chunk.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | /* jshint jquery: true */ 3 | /* jshint esversion: 6 */ 4 | "use strict"; 5 | 6 | /** 7 | * Chunk class 8 | * 9 | * Download, check last downloaded chunk 10 | * @params Nothing 11 | * @return Chunk object 12 | */ 13 | 14 | var fs = require( "fs" ); 15 | var request = require( "request" ); 16 | 17 | class Chunk { 18 | 19 | /** 20 | * Parse JSON and send it to callback 21 | * 22 | * @params Data to parse, callback 23 | * @return Send parsed JSON to callback 24 | */ 25 | static loadJSON( data, chunkID, callback ) { 26 | try { 27 | data = JSON.parse( data, 'utf8' ); 28 | // If we reached the top and next_change_id is null 29 | if ( !data.next_change_id ) { 30 | console.log( "Top reached, waiting" ); 31 | } else { 32 | callback( data ); 33 | } 34 | } catch ( e ) { 35 | console.log( "Error occured, retrying: " + e ); 36 | } 37 | } 38 | } 39 | 40 | module.exports = Chunk; -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | {"sound":"sound2.mp3","volume":0.5,"message":"@ Hi, I would like to buy your listed for in (stash tab \"\"; position: left , top )","barter":"@ Hi, I would like to buy your in (stash tab \"\"; position: left , top )","RATES_REFRESH_INTERVAL":10000,"POE_TRADE_STATS_INTERVAL":3600000,"CHUNK_RETRY_INTERVAL":1000,"CHUNK_DOWNLOAD_INTERVAL":750,"STREAM_TOP_WAIT_INTERVAL":2000,"SCROLL_BACK_TOP_SPEED":500,"NOTIFICATION_QUEUE_INTERVAL":5000,"writeChunkStats":false,"globalClipboard":false,"leagues":["Standard","Hardcore","Legacy","Hardcore Legacy","2 Week Mayhem (JRE092)", "2 Week Mayhem HC (JRE091)", "2 Week Mayhem HC Solo (JRE093)"],"defaultLeagueIndex":2,"showStatusBar":false,"windowWidth":1440,"windowHeight":900,"x":0,"y":0} -------------------------------------------------------------------------------- /currency.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | /* jshint jquery: true */ 3 | /* jshint esversion: 6 */ 4 | "use strict"; 5 | 6 | /** 7 | * Currency class 8 | * 9 | * Translate long currency names to trade symbol and look up last rates 10 | * @params Leagues 11 | * @return Currency object 12 | */ 13 | 14 | var async = require( "async" ); 15 | const {app} = require( "electron" ).remote; 16 | const path = require( "path" ); 17 | var config = require( app.getPath( "userData" ) + path.sep + "config.json" ); 18 | var Misc = require( "./misc.js" ); 19 | var leagues = config.leagues; 20 | 21 | class Currency { 22 | 23 | /** 24 | * Fetch last currency rates from poe-rates.com API 25 | * 26 | * Fetch currency rates in chaos for each leagues from 27 | * the poe-rates API. 28 | * @params callback 29 | * @return return rates through callback 30 | */ 31 | static getLastRates( callback ) { 32 | // console.log( "Downloading last rates from poe-rates.com" ); 33 | var shortRates = {}; 34 | // For each league 35 | try { 36 | async.each( config.leagues, function( league, cbLeague ) { 37 | $.get( "http://poe-rates.com/actions/getLastRates.php", { 38 | league: league 39 | }, function( data ) { 40 | shortRates[league] = {}; 41 | var parsed = $.parseJSON( data ); 42 | var rates = parsed.rates; 43 | // Change long rate name to short one using lookup table 44 | for ( var rate in rates ) { 45 | if ( rates.hasOwnProperty( rate )) { 46 | shortRates[league][Currency.shortToLongLookupTable[rate]] = parseFloat( rates[rate]); 47 | } 48 | } 49 | shortRates[league]["Chaos Orb"] = 1.0; 50 | cbLeague(); 51 | }); 52 | }, function( err ) { 53 | if ( err ) { 54 | console.log( err ); 55 | setTimeout( Currency.getLastRates, 1000, callback ); 56 | } 57 | callback( shortRates ); 58 | }); 59 | } catch ( e ) { 60 | console.log( "Error occured, retrying: " + e ); 61 | setTimeout( Currency.getLastRates, 1000, callback ); 62 | } 63 | } 64 | } 65 | 66 | Currency.currencyLookupTable = { 67 | "Exalted Orb": "exa", 68 | "Chaos Orb": "chaos", 69 | "Orb of Alchemy": "alch", 70 | "Orb of Alteration": "alt", 71 | "Orb of Fusing": "fuse", 72 | "Divine Orb": "divine", 73 | "Orb of Chance": "chance", 74 | "Jeweller's Orb": "jew", 75 | "Cartographer's Chisel": "chisel", 76 | "Vaal Orb": "vaal", 77 | "Orb of Regret": "regret", 78 | "Regal Orb": "regal", 79 | "Gemcutter's Prism": "gcp", 80 | "Chromatic Orb": "chrome", 81 | "Orb of Scouring": "scour", 82 | "Blessed Orb": "bless" 83 | }; 84 | 85 | Currency.shortToLongLookupTable = { 86 | "exa": "Exalted Orb", 87 | "chaos": "Chaos Orb", 88 | "alch": "Orb of Alchemy", 89 | "alt": "Orb of Alteration", 90 | "fuse": "Orb of Fusing", 91 | "divine": "Divine Orb", 92 | "chance": "Orb of Chance", 93 | "jew": "Jeweller's Orb", 94 | "chisel": "Cartographer's Chisel", 95 | "vaal": "Vaal Orb", 96 | "regret": "Orb of Regret", 97 | "regal": "Regal Orb", 98 | "gcp": "Gemcutter's Prism", 99 | "chrom": "Chromatic Orb", 100 | "scour": "Orb of Scouring", 101 | "bless": "Blessed Orb" 102 | }; 103 | 104 | module.exports = Currency; -------------------------------------------------------------------------------- /filter-group.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | /* jshint jquery: true */ 3 | /* jshint esversion: 6 */ 4 | "use strict"; 5 | 6 | /** 7 | * FilterGroup class 8 | * 9 | * Holds a group of filters 10 | * @params Nothing 11 | * @return Filter Group object 12 | */ 13 | 14 | var mu = require( "mu2" ); 15 | mu.root = __dirname + '/templates'; 16 | var Misc = require( "./misc.js" ); 17 | 18 | class FilterGroup { 19 | 20 | constructor( obj ) { 21 | this.clipboard = obj.clipboard; 22 | this.name = obj.name; 23 | this.checked = obj.checked ? "checked" : ""; 24 | this.color = obj.color; 25 | this.filters = {}; 26 | this.id = obj.id; 27 | this.folded = obj.folded; 28 | } 29 | 30 | add( filter ) { 31 | this.filters[filter.id] = filter; 32 | } 33 | 34 | remove( filter ) { 35 | delete this.filters[filter.id]; 36 | } 37 | 38 | getFilter( filterId ) { 39 | if ( this.filters[filterId]) { 40 | return this.filters[filterId]; 41 | } else { 42 | return null; 43 | } 44 | } 45 | 46 | /** 47 | * Render the filter group to html using a template 48 | * 49 | * @params Callback 50 | * @return Generated HTML through callback 51 | */ 52 | render( callback ) { 53 | var generated = ""; 54 | mu.compileAndRender( "filter-group.html", this ) 55 | .on( "data", function ( data ) { 56 | generated += data.toString(); 57 | }) 58 | .on( "end", function() { 59 | callback( generated ); 60 | }); 61 | } 62 | 63 | } 64 | 65 | module.exports = FilterGroup; -------------------------------------------------------------------------------- /filter-groups.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FilterGroups class 3 | * 4 | * List of Group representation 5 | * @params Nothing 6 | * @return FilterGroups object 7 | */ 8 | 9 | var async = require( "async" ); 10 | var fs = require( "fs" ); 11 | const {app} = require( "electron" ).remote; 12 | const path = require( "path" ); 13 | var config = {}; 14 | // Check if config.json exists in app data, otherwise create it from default 15 | // config file. 16 | console.log( "Loading config from " + app.getPath( "userData" ) + path.sep + "config.json" ); 17 | config = require( app.getPath( "userData" ) + path.sep + "config.json" ); 18 | 19 | 20 | class FilterGroups { 21 | 22 | constructor( groupList ) { 23 | this.groupList = groupList; 24 | this.length = groupList.length; 25 | this.sort(); 26 | } 27 | 28 | /** 29 | * Sort group list alphabetically 30 | * 31 | * @params Nothing 32 | * @return Groups in place 33 | */ 34 | sort() { 35 | this.groupList.sort( function( a, b ) { 36 | var alc = a.name.toLowerCase(); 37 | var blc = b.name.toLowerCase(); 38 | if ( alc < blc ) { 39 | return -1; 40 | } else if ( alc > blc ) { 41 | return 1; 42 | } else { 43 | return 0; 44 | } 45 | }); 46 | } 47 | 48 | /** 49 | * Activate/deactivate group 50 | * 51 | * @params Group id, callback 52 | * @return Nothing 53 | */ 54 | toggle( id, callback ) { 55 | var self = this; 56 | async.each( this.groupList, function( group, cbFilter ) { 57 | if ( group.id === id ) { 58 | group.checked = group.checked === "checked" ? "" : "checked" 59 | // console.log( "Toggled filter " + filter.item ); 60 | } 61 | cbFilter(); 62 | }, function( err ) { 63 | if ( err ) { 64 | console.log( err ); 65 | } 66 | callback(); 67 | }); 68 | } 69 | 70 | /** 71 | * Add a new group to the group list 72 | * 73 | * @params Group object to add 74 | * @return Nothing 75 | */ 76 | add( group ) { 77 | this.groupList.push( group ); 78 | this.length = this.groupList.length; 79 | this.sort(); 80 | } 81 | 82 | /** 83 | * Remove a group from the group list 84 | * 85 | * @params The group id corresponding to the group to be removed, callback 86 | * @return Nothing 87 | */ 88 | remove( groupId, callback ) { 89 | var self = this; 90 | var groups = []; 91 | async.each( this.groupList, function( group, cbGroup ) { 92 | if ( group.id !== groupId ) { 93 | groups.push( group ); 94 | cbGroup(); 95 | } else { 96 | cbGroup(); 97 | } 98 | }, function( err ) { 99 | if ( err ) { 100 | console.log( err ); 101 | } 102 | self.groupList = groups; 103 | self.length = self.groupList.length; 104 | self.sort(); 105 | callback(); 106 | }); 107 | } 108 | 109 | /** 110 | * Update group inside group list 111 | * 112 | * @params Group to find and replace 113 | * @return Callback 114 | */ 115 | update( groupToFind, callback ) { 116 | var self = this; 117 | var groups = []; 118 | async.each( this.groupList, function( group, cbGroup ) { 119 | if ( group.id === groupToFind.id ) { 120 | groups.push( groupToFind ); 121 | cbGroup(); 122 | } else { 123 | groups.push( group ); 124 | cbGroup(); 125 | } 126 | }, function( err ) { 127 | if ( err ) { 128 | console.log( err ); 129 | } 130 | self.groupList = groups; 131 | self.sort(); 132 | callback(); 133 | }); 134 | } 135 | 136 | /** 137 | * Return the group associated to input id 138 | * 139 | * @params Group ID 140 | * @return Group object through callback 141 | */ 142 | find( groupId, callback ) { 143 | var found = null; 144 | async.each( this.groupList, function( group, cbGroup ) { 145 | if ( group.id === groupId ) { 146 | found = group; 147 | } 148 | cbGroup(); 149 | }, function() { 150 | callback( found ); 151 | }); 152 | } 153 | 154 | /** 155 | * Write the group list to disk 156 | * 157 | * @params Nothing 158 | * @return Nothing 159 | */ 160 | save() { 161 | fs.writeFileSync( app.getPath( "userData" ) + path.sep + "filter-groups.json", JSON.stringify( this.groupList )); 162 | } 163 | } 164 | 165 | module.exports = FilterGroups; -------------------------------------------------------------------------------- /filter-groups.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /filters.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | /* jshint jquery: true */ 3 | /* jshint esversion: 6 */ 4 | "use strict"; 5 | 6 | /** 7 | * Filters class 8 | * 9 | * List of Filter representation 10 | * @param Nothing 11 | * @return Filters object 12 | */ 13 | 14 | var async = require( "async" ); 15 | var fs = require( "fs" ); 16 | const {app} = require( "electron" ).remote; 17 | const path = require( "path" ); 18 | var config = {}; 19 | // Check if config.json exists in app data, otherwise create it from default 20 | // config file. 21 | console.log( "Loading config from " + app.getPath( "userData" ) + path.sep + "config.json" ); 22 | config = require( app.getPath( "userData" ) + path.sep + "config.json" ); 23 | 24 | 25 | class Filters { 26 | 27 | constructor( filterList ) { 28 | this.filterList = filterList; 29 | this.length = filterList.length; 30 | this.sort(); 31 | } 32 | 33 | /** 34 | * Sort filter list alphabetically 35 | * 36 | * @param Nothing 37 | * @return Filters in place 38 | */ 39 | sort() { 40 | this.filterList.sort( function( a, b ) { 41 | var alc = a.item.toLowerCase(); 42 | var blc = b.item.toLowerCase(); 43 | // If both filters are not in groups or both filters are in group 44 | if ((( !alc.group || alc.group === "" ) && ( !blc.group || blc.group === "" )) || 45 | ( alc.group !== "" && blc.group !== "" )) { 46 | if ( alc < blc ) { 47 | return -1; 48 | } else if ( alc > blc ) { 49 | return 1; 50 | } else { 51 | return 0; 52 | } 53 | // If only filter a has a group, order a first 54 | } else if ( alc.group !== "" ) { 55 | return 1; 56 | // Otherwise if only b has a group, order b first 57 | } else { 58 | return -1; 59 | } 60 | }); 61 | } 62 | 63 | /** 64 | * Activate/deactivate filter 65 | * 66 | * @param Filter id, callback 67 | * @return Nothing 68 | */ 69 | toggle( id, callback ) { 70 | var self = this; 71 | async.each( this.filterList, function( filter, cbFilter ) { 72 | if ( filter.id === id ) { 73 | filter.active = !filter.active; 74 | // console.log( "Toggled filter " + filter.item ); 75 | } 76 | cbFilter(); 77 | }, function( err ) { 78 | if ( err ) { 79 | console.log( err ); 80 | } 81 | callback(); 82 | }); 83 | } 84 | 85 | /** 86 | * Find the sorted index of a specific filter in the list 87 | * 88 | * @param Filter to be found 89 | * @return Index through callback 90 | */ 91 | findFilterIndex( filter, callback ) { 92 | var self = this; 93 | var index = 0; 94 | var found = false; 95 | // If filter is not in a group we have to take into account 96 | // grouped filters which are ordered first 97 | if ( filter.group === "" ) { 98 | // Count all grouped filters 99 | async.each( this.filterList, function( f, cbFilter ) { 100 | if ( f.group !== "" ) { 101 | index++; 102 | } 103 | cbFilter(); 104 | }, function() { 105 | // console.log( "counted " + index + " filters in groups" ); 106 | async.eachLimit( self.filterList, 1, function( f, cbFilter ) { 107 | if ( f.group === "" && !found && JSON.stringify( filter ) === JSON.stringify( f )) { 108 | found = true; 109 | } 110 | if ( !found && f.group === "" ) { 111 | console.log( f.item ); 112 | index++; 113 | } 114 | cbFilter(); 115 | }, function() { 116 | callback({ found: found, index: index }); 117 | }); 118 | }); 119 | } else { 120 | async.each( this.filterList, function( f, cbFilter ) { 121 | if ( !found && JSON.stringify( filter ) === JSON.stringify( f )) { 122 | found = true; 123 | } 124 | if ( !found ) { 125 | index++; 126 | } 127 | cbFilter(); 128 | }, function( err ) { 129 | if ( err ) { 130 | console.log( err ); 131 | } 132 | callback({ found: found, index: index }); 133 | }); 134 | } 135 | } 136 | 137 | /** 138 | * Add a new filter to the filter list 139 | * 140 | * @param Filter object to add 141 | * @return Nothing 142 | */ 143 | add( filter ) { 144 | this.filterList.push( filter ); 145 | this.length = this.filterList.length; 146 | this.sort(); 147 | } 148 | 149 | /** 150 | * Remove a filter from the filter list 151 | * 152 | * @param The filter id corresponding to the filter to be removed, callback 153 | * @return Nothing 154 | */ 155 | remove( filterId, callback ) { 156 | var self = this; 157 | var filters = []; 158 | async.each( this.filterList, function( filter, cbFilter ) { 159 | if ( filter.id !== filterId ) { 160 | filters.push( filter ); 161 | cbFilter(); 162 | } else { 163 | cbFilter(); 164 | } 165 | }, function( err ) { 166 | if ( err ) { 167 | console.log( err ); 168 | } 169 | self.filterList = filters; 170 | self.length = filterList.length; 171 | self.sort(); 172 | callback(); 173 | }); 174 | } 175 | 176 | /** 177 | * Update filter inside filter list 178 | * 179 | * @param Filter to find and replace 180 | * @return Callback 181 | */ 182 | update( filterToFind, callback ) { 183 | var self = this; 184 | var filters = []; 185 | async.each( this.filterList, function( filter, cbFilter ) { 186 | if ( filter.id === filterToFind.id ) { 187 | filters.push( filterToFind ); 188 | cbFilter(); 189 | } else { 190 | filters.push( filter ); 191 | cbFilter(); 192 | } 193 | }, function( err ) { 194 | if ( err ) { 195 | console.log( err ); 196 | } 197 | self.filterList = filters; 198 | self.sort(); 199 | callback(); 200 | }); 201 | } 202 | 203 | /** 204 | * Write the filter list to disk 205 | * 206 | * @param Nothing 207 | * @return Nothing 208 | */ 209 | save() { 210 | fs.writeFileSync( app.getPath( "userData" ) + path.sep + "filters.json", JSON.stringify( this.filterList )); 211 | } 212 | } 213 | 214 | module.exports = Filters; -------------------------------------------------------------------------------- /filters.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /font/Fontin-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/licoffe/POE-sniper/e0370e7a0a5ca5db613482dcede15533657e1d8f/font/Fontin-Regular.eot -------------------------------------------------------------------------------- /font/Fontin-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/licoffe/POE-sniper/e0370e7a0a5ca5db613482dcede15533657e1d8f/font/Fontin-Regular.ttf -------------------------------------------------------------------------------- /font/Fontin-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/licoffe/POE-sniper/e0370e7a0a5ca5db613482dcede15533657e1d8f/font/Fontin-Regular.woff -------------------------------------------------------------------------------- /font/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* This stylesheet generated by Transfonter (http://transfonter.org) on September 20, 2016 4:39 PM */ 2 | 3 | @font-face { 4 | font-family: 'Fontin'; 5 | src: url('Fontin-Regular.eot'); 6 | src: url('Fontin-Regular.eot?#iefix') format('embedded-opentype'), 7 | url('Fontin-Regular.woff') format('woff'), 8 | url('Fontin-Regular.ttf') format('truetype'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PoE Sniper 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 | 27 | 28 |
29 | of 30 | 36 | in 37 | 39 | league ranging from 40 |
41 | 42 | 43 |
44 | to 45 |
46 | 47 | 48 |
49 | price 50 | 51 |
52 |
53 |
54 |
55 |
56 | 57 | 58 |
59 | 60 | 61 |
62 | 63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | 86 |
87 |
88 | 89 | 90 |
91 |
92 | 93 | 94 |
95 | 99 |
100 |
101 |
102 | 103 | 104 |
105 |
106 | 107 | 108 |
109 | 117 | Cancel 118 | 119 | Add 120 |
121 | 124 |
125 |
126 | 127 | 134 |
135 |
136 |
138 | 139 | 140 |
141 |
142 | 143 | 144 |
145 |
146 | 147 | 148 |
149 |
150 | 151 | 152 |
153 |
154 | 155 | 156 |
157 |
158 |
159 |
160 |
161 | 162 | 163 |
164 |
165 | 166 | 167 |
168 |
169 | 170 | 171 |
172 |
173 |
174 |
175 | 176 | 177 |
178 |
179 | 180 | 181 |
182 |
183 | 184 | 185 |
186 |
187 |
188 |
189 | 190 | 191 |
192 |
193 | 194 | 195 |
196 |
197 | 198 | 199 |
200 |
201 |
202 |
203 | 204 | 209 | 214 | 219 | 224 | 235 |
236 |
238 |
239 |
241 | 242 | 243 |
244 |
246 | 247 | 248 |
249 |
251 | 252 | 253 |
254 |
255 | 256 | 258 |
260 |
261 |
263 | 264 | 265 |
266 |
268 |

269 | 270 | 271 |

272 |
273 |
274 |
275 | 293 |
294 |

295 | 296 | 297 |

298 |

299 | 300 | 301 |

302 |
303 |
304 |
305 |
306 | 307 | 310 |
311 | info_outline... 312 | 313 | 314 |
315 | 321 |
322 | cloud_downloadImport filter 323 |

324 | 325 | 326 |

327 | 328 | deleteClear filter 329 | playlist_addAdd filter 330 | settingsSettings 331 | play_arrowSnipe 332 |
333 |
334 |
335 |
336 | 337 | Filters (0) 338 | keyboard_arrow_down 339 | 340 | 341 | Total processing time: 0ms 342 | tabAdd group 343 |
344 |
345 |
    346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 | Results (0) 354 | 355 | 356 | delete 357 | 358 |
359 |
360 |
    361 |
362 |
363 |
364 |
365 | 366 | 367 |
368 |
369 | 370 |
371 |
372 | 373 | 374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 | Dismiss 382 | Download 383 |
384 |
385 |
386 |
387 | 397 | 409 | 576 | 577 | 581 |