PageRenderTime 51ms CodeModel.GetById 10ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/portals-list.user.js

https://github.com/mdunc/ingress-intel-total-conversion
JavaScript | 611 lines | 468 code | 55 blank | 88 comment | 71 complexity | 69650910ffb87dbc507e0e85ae63c4ff MD5 | raw file
Possible License(s): 0BSD
  1. // ==UserScript==
  2. // @id iitc-plugin-portals-list@teo96
  3. // @name IITC plugin: show list of portals
  4. // @category Info
  5. // @version 0.1.1.@@DATETIMEVERSION@@
  6. // @namespace https://github.com/jonatkins/ingress-intel-total-conversion
  7. // @updateURL @@UPDATEURL@@
  8. // @downloadURL @@DOWNLOADURL@@
  9. // @description [@@BUILDNAME@@-@@BUILDDATE@@] Display a sortable list of all visible portals with full details about the team, resonators, links, etc.
  10. // @include https://www.ingress.com/intel*
  11. // @include http://www.ingress.com/intel*
  12. // @match https://www.ingress.com/intel*
  13. // @match http://www.ingress.com/intel*
  14. // @grant none
  15. // ==/UserScript==
  16. @@PLUGINSTART@@
  17. // PLUGIN START ////////////////////////////////////////////////////////
  18. /* whatsnew
  19. * 0.1.0 : Using the new data format
  20. * 0.0.15: Add 'age' column to display how long each portal has been controlled by its current owner.
  21. * 0.0.14: Add support to new mods (S:Shield - T:Turret - LA:Link Amp - H:Heat-sink - M:Multi-hack - FA:Force Amp)
  22. * 0.0.12: Use dialog() instead of alert so the user can drag the box around
  23. * 0.0.11: Add nominal energy column and # links, fix sort bug when opened even amounts of times, nits
  24. * 0.0.10: Fixed persistent css problem with alert
  25. * 0.0.9 : bugs hunt
  26. * 0.0.8 : Aborted to avoid problems with Niantic (export portals informations as csv or kml file)
  27. * 0.0.7 : more informations available via tooltips (who deployed, energy, ...), new E/AP column
  28. * 0.0.6 : Add power charge information into a new column + bugfix
  29. * 0.0.5 : Filter portals by clicking on 'All portals', 'Res Portals' or 'Enl Portals'
  30. * 0.0.4 : Add link to portals name, one click to display full information in portal panel, double click to zoom on portal, hover to show address
  31. * 0.0.3 : sorting ascending/descending and add numbers of portals by faction on top on table
  32. * 0.0.2 : add sorting feature when click on header column
  33. * 0.0.1 : initial release, show list of portals with level, team, resonators and shield information
  34. *
  35. * Display code inspired from @vita10gy's scoreboard plugin : iitc-plugin-scoreboard@vita10gy - https://github.com/breunigs/ingress-intel-total-conversion
  36. * Portal link code from xelio - iitc: AP List - https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/ap-list.user.js
  37. *
  38. * todo : export as GPX, Open in Google Maps, more statistics in the header, what else ?
  39. */
  40. // use own namespace for plugin
  41. window.plugin.portalslist = function() {};
  42. window.plugin.portalslist.listPortals = [];
  43. window.plugin.portalslist.sortBy = 'level';
  44. window.plugin.portalslist.sortOrder = -1;
  45. window.plugin.portalslist.enlP = 0;
  46. window.plugin.portalslist.resP = 0;
  47. window.plugin.portalslist.filter = 0;
  48. //fill the listPortals array with portals avaliable on the map (level filtered portals will not appear in the table)
  49. window.plugin.portalslist.getPortals = function() {
  50. //filter : 0 = All, 1 = Res, 2 = Enl
  51. var retval=false;
  52. var displayBounds = map.getBounds();
  53. window.plugin.portalslist.listPortals = [];
  54. $.each(window.portals, function(i, portal) {
  55. // eliminate offscreen portals (selected, and in padding)
  56. if(!displayBounds.contains(portal.getLatLng())) return true;
  57. retval=true;
  58. var d = portal.options.data;
  59. var teamN = portal.options.team;
  60. switch (teamN) {
  61. case TEAM_RES:
  62. window.plugin.portalslist.resP++;
  63. break;
  64. case TEAM_ENL:
  65. window.plugin.portalslist.enlP++;
  66. break;
  67. }
  68. var l = window.getPortalLinks(i);
  69. var f = window.getPortalFields(i);
  70. var ap = portalApGainMaths(d.resCount, l.in.length+l.out.length, f.length);
  71. var thisPortal = {
  72. 'portal': portal,
  73. 'guid': i,
  74. 'teamN': teamN, // TEAM_NONE, TEAM_RES or TEAM_ENL
  75. 'team': d.team, // "NEUTRAL", "RESISTANCE" or "ENLIGHTENED"
  76. 'name': d.title || '(untitled)',
  77. 'nameLower': d.title && d.title.toLowerCase(),
  78. 'level': portal.options.level,
  79. 'health': d.health,
  80. 'resCount': d.resCount,
  81. 'img': d.img,
  82. 'linkCount': l.in.length + l.out.length,
  83. 'link' : l,
  84. 'fieldCount': f.length,
  85. 'field' : f,
  86. 'enemyAp': ap.enemyAp,
  87. 'ap': ap,
  88. };
  89. window.plugin.portalslist.listPortals.push(thisPortal);
  90. });
  91. return retval;
  92. }
  93. window.plugin.portalslist.displayPL = function() {
  94. var html = '';
  95. window.plugin.portalslist.sortBy = 'level';
  96. window.plugin.portalslist.sortOrder = -1;
  97. window.plugin.portalslist.enlP = 0;
  98. window.plugin.portalslist.resP = 0;
  99. window.plugin.portalslist.filter = 0;
  100. if (window.plugin.portalslist.getPortals()) {
  101. html += window.plugin.portalslist.portalTable(window.plugin.portalslist.sortBy, window.plugin.portalslist.sortOrder,window.plugin.portalslist.filter);
  102. } else {
  103. html = '<table class="noPortals"><tr><td>Nothing to show!</td></tr></table>';
  104. };
  105. if(window.useAndroidPanes()) {
  106. $('<div id="portalslist" class="mobile">' + html + '</div>').appendTo(document.body);
  107. } else {
  108. dialog({
  109. html: '<div id="portalslist">' + html + '</div>',
  110. dialogClass: 'ui-dialog-portalslist',
  111. title: 'Portal list: ' + window.plugin.portalslist.listPortals.length + ' ' + (window.plugin.portalslist.listPortals.length == 1 ? 'portal' : 'portals'),
  112. id: 'portal-list',
  113. width: 700,
  114. buttons: {'Download KML': function() {window.plugin.portalslist.downloadKML();}}
  115. });
  116. }
  117. }
  118. window.plugin.portalslist.downloadKML = function() {
  119. var portals = window.plugin.portalslist.listPortals;
  120. var kmlData = '<?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://www.opengis.net/kml/2.2"><Document>';
  121. $.each(portals, function(ind, portal) {
  122. var coords = portal.portal.getLatLng();
  123. var descData = "<strong>Level:</strong>" + portal.level + "<br>"
  124. + "<strong>Team:</strong>" + portal.team + "<br>"
  125. + "<strong>Link:</strong>" + window.plugin.portalslist.getPortalLink(portal, portal.guid) + "<br>"
  126. + "<img src='" + portal.portal.options.data.image + "' /><br>";
  127. var labelColor = null;
  128. var icon = null;
  129. switch (portal.teamN) {
  130. case TEAM_NONE:
  131. labelColor = "cc333333";
  132. icon = "http://maps.google.com/mapfiles/kml/paddle/wht-blank.png";
  133. break;
  134. case TEAM_ENL:
  135. labelColor = "cc0C9119";
  136. icon = "http://maps.google.com/mapfiles/kml/paddle/grn-blank.png";
  137. break;
  138. case TEAM_RES:
  139. labelColor = "ccAD5910";
  140. icon = "http://maps.google.com/mapfiles/kml/paddle/blu-blank.png";
  141. break;
  142. }
  143. kmlData += '<Placemark>'
  144. + '<Style><IconStyle><Icon><href>' + icon + '</href></Icon></IconStyle>'
  145. + '<LabelStyle><color>' + labelColor + '</color></LabelStyle></Style>'
  146. + '<name>' + portal.name + '</name>'
  147. + '<description><![CDATA[' + descData + ']]></description>'
  148. + '<Point><coordinates>' + coords.lng + ',' + coords.lat + '</coordinates></Point>'
  149. + '</Placemark>';
  150. });
  151. kmlData += '</Document></kml>';
  152. saveAs(new Blob([kmlData], {type: 'text/plain;charset=' + document.characterSet}), "portals.kml");
  153. }
  154. window.plugin.portalslist.portalTable = function(sortBy, sortOrder, filter) {
  155. // save the sortBy/sortOrder/filter
  156. window.plugin.portalslist.sortBy = sortBy;
  157. window.plugin.portalslist.sortOrder = sortOrder;
  158. window.plugin.portalslist.filter = filter;
  159. var portals=window.plugin.portalslist.listPortals;
  160. //Array sort
  161. window.plugin.portalslist.listPortals.sort(function(a, b) {
  162. var retVal = 0;
  163. var aComp = a[sortBy];
  164. var bComp = b[sortBy];
  165. if (aComp < bComp) {
  166. retVal = -1;
  167. } else if (aComp > bComp) {
  168. retVal = 1;
  169. } else {
  170. // equal - compare GUIDs to ensure consistent (but arbitrary) order
  171. retVal = a.guid < b.guid ? -1 : 1;
  172. }
  173. // sortOrder is 1 (normal) or -1 (reversed)
  174. retVal = retVal * sortOrder;
  175. return retVal;
  176. });
  177. var sortAttr = window.plugin.portalslist.portalTableHeaderSortAttr;
  178. var html = window.plugin.portalslist.stats();
  179. html += '<table class="portals">'
  180. + '<tr class="header">'
  181. + '<th>#</th>'
  182. + '<th ' + sortAttr('nameLower', sortBy, 1, 'portalTitle') + '>Portal Name</th>'
  183. + '<th ' + sortAttr('level', sortBy, -1) + '>Level</th>'
  184. + '<th ' + sortAttr('teamN', sortBy, 1) + '>Team</th>'
  185. + '<th ' + sortAttr('health', sortBy, -1) + '>Health</th>'
  186. + '<th ' + sortAttr('resCount', sortBy, -1) + '>Res</th>'
  187. + '<th ' + sortAttr('linkCount', sortBy, -1) + '>Links</th>'
  188. + '<th ' + sortAttr('fieldCount', sortBy, -1) + '>Fields</th>'
  189. + '<th ' + sortAttr('enemyAp', sortBy, -1) + '>AP</th>'
  190. + '</tr>\n';
  191. var rowNum = 1;
  192. $.each(portals, function(ind, portal) {
  193. if (filter === TEAM_NONE || filter === portal.teamN) {
  194. html += '<tr class="' + (portal.teamN === window.TEAM_RES ? 'res' : (portal.teamN === window.TEAM_ENL ? 'enl' : 'neutral')) + '">'
  195. + '<td>'+rowNum+'</td>'
  196. + '<td class="portalTitle" style="">' + window.plugin.portalslist.getPortalLink(portal, portal.guid) + '</td>'
  197. + '<td class="L' + portal.level +'" style="background-color: '+COLORS_LVL[portal.level]+'">' + portal.level + '</td>'
  198. + '<td style="text-align:center;">' + portal.team.substr(0,3) + '</td>';
  199. html += '<td>' + (portal.teamN!=TEAM_NONE?portal.health+'%':'-') + '</td>'
  200. + '<td>' + portal.resCount + '</td>'
  201. + '<td class="help" title="In: ' + portal.link.in.length + ' Out: ' + portal.link.out.length + '">' + (portal.linkCount?portal.linkCount:'-') + '</td>'
  202. + '<td>' + (portal.fieldCount?portal.fieldCount:'-') + '</td>';
  203. var apTitle = '';
  204. if (PLAYER.team == portal.team) {
  205. apTitle += 'Friendly AP:\t'+portal.ap.friendlyAp+'\n'
  206. + '- deploy '+(8-portal.resCount)+' resonator(s)\n'
  207. + '- upgrades/mods unknown\n';
  208. }
  209. apTitle += 'Enemy AP:\t'+portal.ap.enemyAp+'\n'
  210. + '- Destroy AP:\t'+portal.ap.destroyAp+'\n'
  211. + '- Capture AP:\t'+portal.ap.captureAp;
  212. html += '<td class="help apGain" title="' + apTitle + '">' + digits(portal.ap.enemyAp) + '</td>';
  213. html+= '</tr>';
  214. rowNum++;
  215. }
  216. });
  217. html += '</table>';
  218. html += '<div class="disclaimer">Click on portals table headers to sort by that column. '
  219. + 'Click on <b>All Portals, Resistance Portals, Enlightened Portals</b> to filter</div>';
  220. return html;
  221. }
  222. window.plugin.portalslist.stats = function(sortBy) {
  223. var html = '<table class="teamFilter"><tr>'
  224. + '<td class="filterAll" style="cursor:pointer"><a href=""></a>All Portals : (click to filter)</td><td class="filterAll">' + window.plugin.portalslist.listPortals.length + '</td>'
  225. + '<td class="filterRes" style="cursor:pointer" class="sorted">Resistance Portals : </td><td class="filterRes">' + window.plugin.portalslist.resP +' (' + Math.floor(window.plugin.portalslist.resP/window.plugin.portalslist.listPortals.length*100) + '%)</td>'
  226. + '<td class="filterEnl" style="cursor:pointer" class="sorted">Enlightened Portals : </td><td class="filterEnl">'+ window.plugin.portalslist.enlP +' (' + Math.floor(window.plugin.portalslist.enlP/window.plugin.portalslist.listPortals.length*100) + '%)</td>'
  227. + '</tr>'
  228. + '</table>';
  229. return html;
  230. }
  231. // A little helper function so the above isn't so messy
  232. window.plugin.portalslist.portalTableHeaderSortAttr = function(name, by, defOrder, extraClass) {
  233. // data-sort attr: used by jquery .data('sort') below
  234. var retVal = 'data-sort="'+name+'" data-defaultorder="'+defOrder+'" class="'+(extraClass?extraClass+' ':'')+'sortable'+(name==by?' sorted':'')+'"';
  235. return retVal;
  236. };
  237. // portal link - single click: select portal
  238. // double click: zoom to and select portal
  239. // hover: show address
  240. // code from getPortalLink function by xelio from iitc: AP List - https://raw.github.com/breunigs/ingress-intel-total-conversion/gh-pages/plugins/ap-list.user.js
  241. window.plugin.portalslist.getPortalLink = function(portal,guid) {
  242. var coord = portal.portal.getLatLng();
  243. var latlng = [coord.lat, coord.lng].join();
  244. var jsSingleClick = 'window.renderPortalDetails(\''+guid+'\');return false';
  245. var jsDoubleClick = 'window.zoomToAndShowPortal(\''+guid+'\', ['+latlng+']);return false';
  246. var perma = 'https://www.ingress.com/intel?ll='+coord.lat+','+coord.lng+'&z=17&pll='+coord.lat+','+coord.lng;
  247. //Use Jquery to create the link, which escape characters in TITLE and ADDRESS of portal
  248. var a = $('<a>',{
  249. text: portal.name,
  250. title: portal.name,
  251. href: perma,
  252. onClick: jsSingleClick,
  253. onDblClick: jsDoubleClick
  254. })[0].outerHTML;
  255. return a;
  256. }
  257. window.plugin.portalslist.onPaneChanged = function(pane) {
  258. if(pane == "plugin-portalslist")
  259. window.plugin.portalslist.displayPL();
  260. else
  261. $("#portalslist").remove()
  262. };
  263. var setup = function() {
  264. if(window.useAndroidPanes()) {
  265. android.addPane("plugin-portalslist", "Portals list", "ic_action_paste");
  266. addHook("paneChanged", window.plugin.portalslist.onPaneChanged);
  267. } else {
  268. $('#toolbox').append(' <a onclick="window.plugin.portalslist.displayPL()" title="Display a list of portals in the current view">Portals list</a>');
  269. }
  270. $('head').append('<style>' +
  271. '#portalslist.mobile {background: transparent; border: 0 none !important; height: 100% !important; width: 100% !important; left: 0 !important; top: 0 !important; position: absolute; overflow: auto; }' +
  272. '#portalslist table { margin-top:5px; border-collapse: collapse; empty-cells: show; width: 100%; clear: both; }' +
  273. '#portalslist table td, #portalslist table th {border-bottom: 1px solid #0b314e; padding:3px; color:white; background-color:#1b415e}' +
  274. '#portalslist table tr.res td { background-color: #005684; }' +
  275. '#portalslist table tr.enl td { background-color: #017f01; }' +
  276. '#portalslist table tr.neutral td { background-color: #000000; }' +
  277. '#portalslist table th { text-align: center; }' +
  278. '#portalslist table td { text-align: center; }' +
  279. '#portalslist table.portals td { white-space: nowrap; }' +
  280. '#portalslist table td.portalTitle { text-align: left;}' +
  281. '#portalslist table th.sortable { cursor:pointer;}' +
  282. '#portalslist table th.portalTitle { text-align: left;}' +
  283. '#portalslist table .portalTitle { min-width: 120px !important; max-width: 240px !important; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }' +
  284. '#portalslist table .apGain { text-align: right !important; }' +
  285. '#portalslist .sorted { color:#FFCE00; }' +
  286. '#portalslist .filterAll { margin-top: 10px;}' +
  287. '#portalslist .filterRes { margin-top: 10px; background-color: #005684 }' +
  288. '#portalslist .filterEnl { margin-top: 10px; background-color: #017f01 }' +
  289. '#portalslist .disclaimer { margin-top: 10px; font-size:10px; }' +
  290. '</style>');
  291. // Setup sorting
  292. $(document).on('click.portalslist', '#portalslist table th.sortable', function() {
  293. var sortBy = $(this).data('sort');
  294. // if this is the currently selected column, toggle the sort order - otherwise use the columns default sort order
  295. var sortOrder = sortBy == window.plugin.portalslist.sortBy ? window.plugin.portalslist.sortOrder*-1 : parseInt($(this).data('defaultorder'));
  296. $('#portalslist').html(window.plugin.portalslist.portalTable(sortBy,sortOrder,window.plugin.portalslist.filter));
  297. });
  298. $(document).on('click.portalslist', '#portalslist .filterAll', function() {
  299. $('#portalslist').html(window.plugin.portalslist.portalTable(window.plugin.portalslist.sortBy,window.plugin.portalslist.sortOrder,0));
  300. });
  301. $(document).on('click.portalslist', '#portalslist .filterRes', function() {
  302. $('#portalslist').html(window.plugin.portalslist.portalTable(window.plugin.portalslist.sortBy,window.plugin.portalslist.sortOrder,1));
  303. });
  304. $(document).on('click.portalslist', '#portalslist .filterEnl', function() {
  305. $('#portalslist').html(window.plugin.portalslist.portalTable(window.plugin.portalslist.sortBy,window.plugin.portalslist.sortOrder,2));
  306. });
  307. }
  308. /*! FileSaver.js
  309. * A saveAs() FileSaver implementation.
  310. * 2014-01-24
  311. *
  312. * By Eli Grey, http://eligrey.com
  313. * License: X11/MIT
  314. * See LICENSE.md
  315. */
  316. /*global self */
  317. /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
  318. /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
  319. var saveAs = saveAs
  320. // IE 10+ (native saveAs)
  321. || (typeof navigator !== "undefined" &&
  322. navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator))
  323. // Everyone else
  324. || (function(view) {
  325. "use strict";
  326. // IE <10 is explicitly unsupported
  327. if (typeof navigator !== "undefined" &&
  328. /MSIE [1-9]\./.test(navigator.userAgent)) {
  329. return;
  330. }
  331. var
  332. doc = view.document
  333. // only get URL when necessary in case BlobBuilder.js hasn't overridden it yet
  334. , get_URL = function() {
  335. return view.URL || view.webkitURL || view;
  336. }
  337. , URL = view.URL || view.webkitURL || view
  338. , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
  339. , can_use_save_link = !view.externalHost && "download" in save_link
  340. , click = function(node) {
  341. var event = doc.createEvent("MouseEvents");
  342. event.initMouseEvent(
  343. "click", true, false, view, 0, 0, 0, 0, 0
  344. , false, false, false, false, 0, null
  345. );
  346. node.dispatchEvent(event);
  347. }
  348. , webkit_req_fs = view.webkitRequestFileSystem
  349. , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
  350. , throw_outside = function(ex) {
  351. (view.setImmediate || view.setTimeout)(function() {
  352. throw ex;
  353. }, 0);
  354. }
  355. , force_saveable_type = "application/octet-stream"
  356. , fs_min_size = 0
  357. , deletion_queue = []
  358. , process_deletion_queue = function() {
  359. var i = deletion_queue.length;
  360. while (i--) {
  361. var file = deletion_queue[i];
  362. if (typeof file === "string") { // file is an object URL
  363. URL.revokeObjectURL(file);
  364. } else { // file is a File
  365. file.remove();
  366. }
  367. }
  368. deletion_queue.length = 0; // clear queue
  369. }
  370. , dispatch = function(filesaver, event_types, event) {
  371. event_types = [].concat(event_types);
  372. var i = event_types.length;
  373. while (i--) {
  374. var listener = filesaver["on" + event_types[i]];
  375. if (typeof listener === "function") {
  376. try {
  377. listener.call(filesaver, event || filesaver);
  378. } catch (ex) {
  379. throw_outside(ex);
  380. }
  381. }
  382. }
  383. }
  384. , FileSaver = function(blob, name) {
  385. // First try a.download, then web filesystem, then object URLs
  386. var
  387. filesaver = this
  388. , type = blob.type
  389. , blob_changed = false
  390. , object_url
  391. , target_view
  392. , get_object_url = function() {
  393. var object_url = get_URL().createObjectURL(blob);
  394. deletion_queue.push(object_url);
  395. return object_url;
  396. }
  397. , dispatch_all = function() {
  398. dispatch(filesaver, "writestart progress write writeend".split(" "));
  399. }
  400. // on any filesys errors revert to saving with object URLs
  401. , fs_error = function() {
  402. // don't create more object URLs than needed
  403. if (blob_changed || !object_url) {
  404. object_url = get_object_url(blob);
  405. }
  406. if (target_view) {
  407. target_view.location.href = object_url;
  408. } else {
  409. window.open(object_url, "_blank");
  410. }
  411. filesaver.readyState = filesaver.DONE;
  412. dispatch_all();
  413. }
  414. , abortable = function(func) {
  415. return function() {
  416. if (filesaver.readyState !== filesaver.DONE) {
  417. return func.apply(this, arguments);
  418. }
  419. };
  420. }
  421. , create_if_not_found = {create: true, exclusive: false}
  422. , slice
  423. ;
  424. filesaver.readyState = filesaver.INIT;
  425. if (!name) {
  426. name = "download";
  427. }
  428. if (can_use_save_link) {
  429. object_url = get_object_url(blob);
  430. // FF for Android has a nasty garbage collection mechanism
  431. // that turns all objects that are not pure javascript into 'deadObject'
  432. // this means `doc` and `save_link` are unusable and need to be recreated
  433. // `view` is usable though:
  434. doc = view.document;
  435. save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a");
  436. save_link.href = object_url;
  437. save_link.download = name;
  438. var event = doc.createEvent("MouseEvents");
  439. event.initMouseEvent(
  440. "click", true, false, view, 0, 0, 0, 0, 0
  441. , false, false, false, false, 0, null
  442. );
  443. save_link.dispatchEvent(event);
  444. filesaver.readyState = filesaver.DONE;
  445. dispatch_all();
  446. return;
  447. }
  448. // Object and web filesystem URLs have a problem saving in Google Chrome when
  449. // viewed in a tab, so I force save with application/octet-stream
  450. // http://code.google.com/p/chromium/issues/detail?id=91158
  451. if (view.chrome && type && type !== force_saveable_type) {
  452. slice = blob.slice || blob.webkitSlice;
  453. blob = slice.call(blob, 0, blob.size, force_saveable_type);
  454. blob_changed = true;
  455. }
  456. // Since I can't be sure that the guessed media type will trigger a download
  457. // in WebKit, I append .download to the filename.
  458. // https://bugs.webkit.org/show_bug.cgi?id=65440
  459. if (webkit_req_fs && name !== "download") {
  460. name += ".download";
  461. }
  462. if (type === force_saveable_type || webkit_req_fs) {
  463. target_view = view;
  464. }
  465. if (!req_fs) {
  466. fs_error();
  467. return;
  468. }
  469. fs_min_size += blob.size;
  470. req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
  471. fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
  472. var save = function() {
  473. dir.getFile(name, create_if_not_found, abortable(function(file) {
  474. file.createWriter(abortable(function(writer) {
  475. writer.onwriteend = function(event) {
  476. target_view.location.href = file.toURL();
  477. deletion_queue.push(file);
  478. filesaver.readyState = filesaver.DONE;
  479. dispatch(filesaver, "writeend", event);
  480. };
  481. writer.onerror = function() {
  482. var error = writer.error;
  483. if (error.code !== error.ABORT_ERR) {
  484. fs_error();
  485. }
  486. };
  487. "writestart progress write abort".split(" ").forEach(function(event) {
  488. writer["on" + event] = filesaver["on" + event];
  489. });
  490. writer.write(blob);
  491. filesaver.abort = function() {
  492. writer.abort();
  493. filesaver.readyState = filesaver.DONE;
  494. };
  495. filesaver.readyState = filesaver.WRITING;
  496. }), fs_error);
  497. }), fs_error);
  498. };
  499. dir.getFile(name, {create: false}, abortable(function(file) {
  500. // delete file if it already exists
  501. file.remove();
  502. save();
  503. }), abortable(function(ex) {
  504. if (ex.code === ex.NOT_FOUND_ERR) {
  505. save();
  506. } else {
  507. fs_error();
  508. }
  509. }));
  510. }), fs_error);
  511. }), fs_error);
  512. }
  513. , FS_proto = FileSaver.prototype
  514. , saveAs = function(blob, name) {
  515. return new FileSaver(blob, name);
  516. }
  517. ;
  518. FS_proto.abort = function() {
  519. var filesaver = this;
  520. filesaver.readyState = filesaver.DONE;
  521. dispatch(filesaver, "abort");
  522. };
  523. FS_proto.readyState = FS_proto.INIT = 0;
  524. FS_proto.WRITING = 1;
  525. FS_proto.DONE = 2;
  526. FS_proto.error =
  527. FS_proto.onwritestart =
  528. FS_proto.onprogress =
  529. FS_proto.onwrite =
  530. FS_proto.onabort =
  531. FS_proto.onerror =
  532. FS_proto.onwriteend =
  533. null;
  534. view.addEventListener("unload", process_deletion_queue, false);
  535. saveAs.unload = function() {
  536. process_deletion_queue();
  537. view.removeEventListener("unload", process_deletion_queue, false);
  538. };
  539. return saveAs;
  540. }(
  541. typeof self !== "undefined" && self
  542. || typeof window !== "undefined" && window
  543. || this.content
  544. ));
  545. // `self` is undefined in Firefox for Android content script context
  546. // while `this` is nsIContentFrameMessageManager
  547. // with an attribute `content` that corresponds to the window
  548. if (typeof module !== "undefined" && module !== null) {
  549. module.exports = saveAs;
  550. } else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) {
  551. define([], function() {
  552. return saveAs;
  553. });
  554. }
  555. // PLUGIN END //////////////////////////////////////////////////////////
  556. @@PLUGINEND@@