Feedly NG Filter

ルールにマッチするアイテムを既読にして取り除きます。ルールは正規表現で記述でき、複数のルールをツリー状に組み合わせることができます。

当前为 2015-04-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Feedly NG Filter
  3. // @id feedlyngfilter
  4. // @description ルールにマッチするアイテムを既読にして取り除きます。ルールは正規表現で記述でき、複数のルールをツリー状に組み合わせることができます。
  5. // @include http://feedly.com/*
  6. // @include https://feedly.com/*
  7. // @grant GM_getValue
  8. // @grant GM_setValue
  9. // @grant GM_addStyle
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_unregisterMenuCommand
  13. // @grant GM_notification
  14. // @grant GM_log
  15. // @charset utf-8
  16. // @compatibility Firefox
  17. // @run-at document-start
  18. // @jsversion 1.8
  19. // @priority 1
  20. // @homepage https://greasyfork.org/ja/scripts/9030-feedly-ng-filter
  21. // @supportURL http://twitter.com/?status=%40xulapp+
  22. // @icon https://greasyfork.org/system/screenshots/screenshots/000/000/615/original/icon.png
  23. // @screenshot https://greasyfork.org/system/screenshots/screenshots/000/000/614/original/large.png
  24. // @namespace http://twitter.com/xulapp
  25. // @author xulapp
  26. // @license MIT License
  27. // @version 0.6.0
  28. // ==/UserScript==
  29.  
  30.  
  31. (function feedlyNGFilter() {
  32. const CSS_STYLE_TEXT = $TEXT(() => {/*
  33. .unselectable {
  34. -moz-user-select: none;
  35. }
  36. .goog-inline-block {
  37. display: inline-block;
  38. position: relative;
  39. }
  40. .jfk-button {
  41. border-radius: 2px 2px 2px 2px;
  42. cursor: default;
  43. font-size: 11px;
  44. font-weight: bold;
  45. text-align: center;
  46. white-space: nowrap;
  47. margin-right: 16px;
  48. height: 27px;
  49. line-height: 27px;
  50. min-width: 54px;
  51. outline: 0px none;
  52. padding: 0px 8px;
  53. }
  54. .jfk-button-standard {
  55. background-color: rgb(245, 245, 245);
  56. background-image: -moz-linear-gradient(center top , rgb(245, 245, 245), rgb(241, 241, 241));
  57. color: rgb(68, 68, 68);
  58. border: 1px solid rgba(0, 0, 0, 0.1);
  59. }
  60. .jfk-button-standard:hover {
  61. background-color: #f8f8f8;
  62. background-image: -moz-linear-gradient(top, #f8f8f8, #f1f1f1);
  63. border: 1px solid #c6c6c6;
  64. color: #333;
  65. }
  66. .jfk-button-standard:focus {
  67. border: 1px solid #4d90fe;
  68. }
  69. .jfk-button-standard:active {
  70. box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
  71. }
  72. .jfk-button-standard.jfk-button-disabled {
  73. background: none;
  74. border: 1px solid rgba(0, 0, 0, 0.05);
  75. color: rgb(184, 184, 184);
  76. }
  77. .goog-flat-menu-button {
  78. border-radius: 2px 2px 2px 2px;
  79. background-color: rgb(245, 245, 245);
  80. background-image: -moz-linear-gradient(center top , rgb(245, 245, 245), rgb(241, 241, 241));
  81. border: 1px solid rgb(220, 220, 220);
  82. color: rgb(68, 68, 68);
  83. cursor: default;
  84. font-size: 11px;
  85. font-weight: bold;
  86. line-height: 27px;
  87. list-style: none outside none;
  88. margin: 0px 2px;
  89. min-width: 46px;
  90. outline: medium none;
  91. padding: 0px 18px 0px 6px;
  92. text-align: center;
  93. text-decoration: none;
  94. vertical-align: middle;
  95. }
  96. .goog-flat-menu-button-open,
  97. .goog-flat-menu-button:active {
  98. -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
  99. box-shadow:inset 0 1px 2px rgba(0, 0, 0, .1);
  100. background-color: #eee;
  101. background-image: -moz-linear-gradient(top, #eee, #e0e0e0);
  102. background-image: linear-gradient(top, #eee, #e0e0e0);
  103. border: 1px solid #ccc;
  104. color: #333;
  105. z-index: 2
  106. }
  107. .goog-flat-menu-button-collapse-left {
  108. margin-left: -1px;
  109. border-bottom-left-radius: 0px;
  110. border-top-left-radius: 0px;
  111. min-width: 0px;
  112. padding-left: 0px;
  113. vertical-align: top;
  114. }
  115. .jfk-button-collapse-left,
  116. .jfk-button-collapse-right {
  117. z-index: 1;
  118. }
  119. .jfk-button-collapse-right {
  120. margin-right: 0px;
  121. border-top-right-radius: 0px;
  122. border-bottom-right-radius: 0px;
  123. }
  124. .goog-flat-menu-button-caption {
  125. vertical-align: top;
  126. white-space: nowrap;
  127. }
  128. .goog-flat-menu-button-dropdown {
  129. border-color: rgb(119, 119, 119) transparent;
  130. border-style: solid;
  131. border-width: 4px 4px 0px;
  132. height: 0px;
  133. width: 0px;
  134. position: absolute;
  135. right: 5px;
  136. top: 12px;
  137. }
  138. .goog-menu {
  139. -moz-box-shadow:0 2px 4px rgba(0,0,0,0.2);
  140. box-shadow:0 2px 4px rgba(0,0,0,0.2);
  141. -moz-transition:opacity .218s;
  142. transition:opacity .218s;
  143. background:#fff;
  144. border:1px solid #ccc;
  145. border:1px solid rgba(0,0,0,.2);
  146. cursor:default;
  147. font-size:13px;
  148. margin:0;
  149. outline:none;
  150. padding:6px 0;
  151. position:absolute
  152. }
  153. .goog-menuitem {
  154. position: relative;
  155. color: #333;
  156. cursor: pointer;
  157. list-style: none;
  158. margin: 0;
  159. padding: 6px 7em 6px 30px;
  160. white-space: nowrap;
  161. }
  162. .goog-menuitem:hover {
  163. background-color: #eee;
  164. border-color: #eee;
  165. border-style: dotted;
  166. border-width: 1px 0;
  167. padding-top: 5px;
  168. padding-bottom: 5px
  169. color: #333;
  170. }
  171. .feedlyng-menu-button-container > .goog-menu-button {
  172. margin-left: -2px;
  173. }
  174. .feedlyng.goog-menu {
  175. position: absolute;
  176. z-index: 2147483646;
  177. }
  178. .feedlyng .goog-menuitem:hover {
  179. background-color: #eeeeee;
  180. }
  181. #feedlyng-open-panel {
  182. float: left;
  183. }
  184. .feedlyng-panel {
  185. position: fixed;
  186. background-color: #ffffff;
  187. color: #333333;
  188. box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5);
  189. z-index: 2147483646;
  190. }
  191. .feedlyng-panel :-moz-any(label, legend) {
  192. cursor: default;
  193. }
  194. .feedlyng-panel input[type="text"] {
  195. padding: 2px;
  196. border: 1px solid #b2b2b2;
  197. }
  198. .feedlyng-panel-body {
  199. margin: 8px;
  200. }
  201. .feedlyng-panel-body > fieldset {
  202. margin: 8px 0;
  203. }
  204. .feedlyng-panel.root > .feedlyng-panel-body > :-moz-any(.feedlyng-panel-name, fieldset) {
  205. display: none;
  206. }
  207. .feedlyng-panel-terms {
  208. border-spacing: 2px;
  209. }
  210. .feedlyng-panel-terms > tbody > tr > td {
  211. padding: 0;
  212. white-space: nowrap;
  213. }
  214. .feedlyng-panel-terms :-moz-any(input, label) {
  215. margin: 0;
  216. vertical-align: middle;
  217. }
  218. @-moz-keyframes error {
  219. 0% {
  220. background-color: #ffff00;
  221. border-color: #ff0000;
  222. }
  223. }
  224. .feedlyng-panel-terms-textbox.error {
  225. -moz-animation: error 1s;
  226. }
  227. .feedlyng-panel-terms-textbox-label {
  228. display: block;
  229. font-size: 90%;
  230. text-align: right;
  231. }
  232. .feedlyng-panel-terms-textbox-label:after {
  233. content: ":";
  234. }
  235. .feedlyng-panel-terms-checkbox-label {
  236. padding: 0 8px;
  237. }
  238. .feedlyng-panel-rules {
  239. display: table;
  240. }
  241. .feedlyng-panel-rule {
  242. display: table-row;
  243. }
  244. .feedlyng-panel-rule:hover {
  245. background-color: #eeeeee;
  246. }
  247. .feedlyng-panel-rule > div {
  248. display: table-cell;
  249. white-space: nowrap;
  250. }
  251. .feedlyng-panel-rule-name {
  252. width: 100%;
  253. padding-left: 16px;
  254. cursor: default;
  255. }
  256. .feedlyng-panel-rule-count {
  257. padding: 0 8px;
  258. font-weight: bold;
  259. cursor: default;
  260. }
  261. .feedlyng-panel-buttons {
  262. margin: 8px;
  263. text-align: right;
  264. white-space: nowrap;
  265. }
  266. .feedlyng-panel-addfilter {
  267. float: left;
  268. margin-right: 8px;
  269. }
  270. .feedlyng-panel-pastefilter {
  271. float: left;
  272. margin-right: 16px;
  273. }
  274. .feedlyng-panel-ok {
  275. margin-right: 8px;
  276. }
  277. .feedlyng-panel-cancel {
  278. margin-right: 0;
  279. }
  280. */});
  281.  
  282. var Locale = {
  283. data: Object.create(null, {
  284. _default: {
  285. get: function() this[navigator.language] || this['en-US'],
  286. },
  287. }),
  288. get: function(p, name) {
  289. return this.data[this.selectedLanguage][name];
  290. },
  291. get languages() Object.keys(this.data),
  292. select: function select(lang) {
  293. this.selectedLanguage = lang in this.data ? lang : '_default';
  294. },
  295. createReference: function createReference() {
  296. return Proxy.create(this);
  297. },
  298. };
  299. Locale.data['en-US'] = {
  300. __proto__: null,
  301. app_name: 'Feedly NG Filter',
  302. ok: 'OK',
  303. cancel: 'Cancel',
  304. add: 'Add',
  305. copy: 'Copy',
  306. paste: 'Paste',
  307. new_filter: 'New Filter',
  308. rules: ' Rules',
  309. title: 'Title',
  310. url: 'URL',
  311. source_title: 'Feed Title',
  312. source_url: 'Feed URL',
  313. author: 'Author',
  314. body: 'Contents',
  315. ignore_case: 'Ignore Case',
  316. edit: 'Edit',
  317. delete: 'Delete',
  318. hit_count: 'Hit Count',
  319. last_hit: 'Last Hit',
  320. ng_setting: 'NG Setting',
  321. setting: 'Setting',
  322. import_setting: 'Import Configuration',
  323. import_success: 'Preferences were successfully imported.',
  324. export_setting: 'Export Configuration',
  325. language: 'Language',
  326. ng_setting_modified: 'NG Settings were modified.\nNew filters take effect after next refresh.',
  327. };
  328. Locale.data['ja'] = {
  329. __proto__: Locale.data['en-US'],
  330. cancel: 'キャンセル',
  331. add: '追加',
  332. copy: 'コピー',
  333. paste: '貼り付け',
  334. new_filter: '新しいフィルタ',
  335. rules: 'のルール',
  336. title: 'タイトル',
  337. source_title: 'フィードのタイトル',
  338. source_url: 'フィードの URL',
  339. author: '著者',
  340. body: '本文',
  341. ignore_case: '大/小文字を区別しない',
  342. edit: '編集',
  343. delete: '削除',
  344. hit_count: 'ヒット数',
  345. last_hit: '最終ヒット',
  346. ng_setting: 'NG 設定',
  347. setting: '設定',
  348. import_setting: '設定をインポート',
  349. import_success: '設定をインポートしました',
  350. export_setting: '設定をエクスポート',
  351. language: '言語',
  352. ng_setting_modified: 'NG 設定を更新しました。\n新しいフィルタは次回読み込み時から有効になります。',
  353. };
  354. Locale.select();
  355. var $str = Locale.createReference();
  356.  
  357. function Class(sup, pro) {
  358. if (sup && typeof sup === 'object')
  359. pro = sup, sup = Object;
  360.  
  361. var con = Object.getOwnPropertyDescriptor(pro, 'constructor');
  362. if (!con)
  363. con = {value: Function(), writable: true, configurable: true};
  364.  
  365. if (con.configurable) {
  366. con.enumerable = false;
  367. Object.defineProperty(pro, 'constructor', con);
  368. }
  369.  
  370. con = pro.constructor;
  371. con.prototype = pro;
  372. con.superclass = sup;
  373. con.__proto__ = Class.prototype;
  374. pro.__proto__ = sup && sup.prototype;
  375.  
  376. return Proxy.createFunction(con, function() con.createInstance(arguments));
  377. }
  378. Class = Class(Function, {
  379. constructor: Class,
  380. $super: function $super() {
  381. var sup = this.superclass;
  382. var method = sup.prototype[$super.caller === this ? 'constructor' : $super.caller.name];
  383. return Function.prototype.call.apply(method, arguments);
  384. },
  385. isSubClass: function isSubClass(cls) {
  386. return this.prototype instanceof cls;
  387. },
  388. createInstance: function createInstance(args) {
  389. var instance = Object.create(this.prototype);
  390. var result = this.apply(instance, args || []);
  391. return result instanceof Object ? result : instance;
  392. },
  393. toString: function toString() {
  394. var arr = [];
  395. var cls = this;
  396. do {
  397. arr.push(cls.name);
  398. } while (cls = cls.superclass);
  399. return '[object Class [class ' + arr.join(', ') + ']]';
  400. },
  401.  
  402. getOwnPropertyDescriptor: function(name) Object.getOwnPropertyDescriptor(this, name),
  403. getPropertyDescriptor: function(name) Object.getPropertyDescriptor(this, name),
  404. getOwnPropertyNames: function(name) Object.getOwnPropertyNames(this, name),
  405. getPropertyNames: function(name) Object.getPropertyNames(this, name),
  406. defineProperty: function(name) Object.defineProperty(this, name),
  407. delete: function(name) delete this[name],
  408. fix: function() {
  409. if (!Object.isFrozen(this))
  410. return void 0;
  411.  
  412. var res = {};
  413. Object.getOwnPropertyNames(this).forEach((name) => res[name] = Object.getOwnPropertyDescriptor(this, name));
  414.  
  415. return res;
  416. },
  417. has: function(name) name in this,
  418. hasOwn: function(name) Object.prototype.hasOwnProperty.call(this, name),
  419. get: function(receiver, name) {
  420. if (name in this)
  421. return this[name];
  422.  
  423. var method = this.prototype[name];
  424. if (typeof method === 'function')
  425. return Function.prototype.call.bind(method);
  426.  
  427. return void 0;
  428. },
  429. set: function(receiver, name, val) this[name] = val,
  430. enumerate: function() [name for (name in this)],
  431. keys: function() Object.keys(this),
  432. });
  433.  
  434. var Subject = Class({
  435. constructor: function Subject() {
  436. this.listeners = {};
  437. },
  438. on: function on(type, listener) {
  439. type += '';
  440.  
  441. if (type.trim().indexOf(' ') !== -1) {
  442. type.match(/\S+/g).forEach(function(t) this.on(t, listener), this);
  443. return;
  444. }
  445.  
  446. if (!(type in this.listeners))
  447. this.listeners[type] = [];
  448.  
  449. var arr = this.listeners[type];
  450. var index = arr.indexOf(listener);
  451. if (index === -1)
  452. arr.push(listener);
  453. },
  454. once: function once(type, listener) {
  455. function onetimeListener() {
  456. this.removeListener(onetimeListener);
  457. return listener.apply(this, arguments);
  458. };
  459. this.on(type, onetimeListener);
  460. return onetimeListener;
  461. },
  462. removeListener: function removeListener(type, listener) {
  463. if (!(type in this.listeners))
  464. return;
  465.  
  466. var arr = this.listeners[type];
  467. var index = arr.indexOf(listener);
  468. if (index !== -1)
  469. arr.splice(index, 1);
  470. },
  471. removeAllListeners: function removeAllListeners(type) {
  472. delete this.listeners[type];
  473. },
  474. dispatchEvent: function dispatchEvent(event) {
  475. event.timeStamp = Date.now();
  476. if (event.type in this.listeners) {
  477. this.listeners[event.type].concat().forEach(function(listener) {
  478. try {
  479. if (typeof listener === 'function')
  480. listener.call(this, event);
  481.  
  482. else
  483. listener.handleEvent(event);
  484.  
  485. } catch (e) {
  486. setTimeout(function() { throw e; }, 0);
  487. }
  488. }, this);
  489. }
  490. return !event.canceled;
  491. },
  492. emit: function emit(type, data) {
  493. var event = this.createEvent(type);
  494. if (data instanceof Object)
  495. extend(event, data);
  496.  
  497. return this.dispatchEvent(event);
  498. },
  499. createEvent: function createEvent(type) {
  500. return new Event(type, this);
  501. },
  502. });
  503.  
  504. var Event = Class({
  505. constructor: function Event(type, target) {
  506. this.type = type;
  507. this.target = target;
  508. },
  509. canceled: false,
  510. timeStamp: null,
  511. preventDefault: function preventDefault() {
  512. this.canceled = true;
  513. },
  514. });
  515.  
  516. var DataTransfer = Class(Subject, {
  517. constructor: function DataTransfer() {
  518. DataTransfer.$super(this);
  519. },
  520. set: function set(type, data) {
  521. this.purge();
  522. this.type = type;
  523. this.data = data;
  524. this.emit(type, {data: data});
  525. },
  526. purge: function purge() {
  527. this.emit('purge', {data: this.data});
  528. delete this.data;
  529. },
  530. setForCut: function setForCut(data) {
  531. this.set('cut', data);
  532. },
  533. setForCopy: function setForCopy(data) {
  534. this.set('copy', data);
  535. },
  536. receive: function receive() {
  537. var data = this.data;
  538. if (this.type === 'cut')
  539. this.purge();
  540.  
  541. return data;
  542. },
  543. });
  544.  
  545. var MenuCommand = Class({
  546. constructor: function MenuCommand(label, oncommand, disabled) {
  547. this.label = label;
  548. this.oncommand = oncommand;
  549. this.disabled = !!disabled;
  550.  
  551. this.register();
  552. },
  553. register: function register() {
  554. this.uuid = GM_registerMenuCommand(this.label, this.oncommand);
  555.  
  556. if (MenuCommand.contextmenu) {
  557. this.menuitem = document.createElement('menuitem');
  558. this.menuitem.label = this.label;
  559. this.menuitem.addEventListener('click', this.oncommand, false);
  560. MenuCommand.contextmenu.appendChild(this.menuitem);
  561. }
  562.  
  563. if (this.disabled)
  564. this.disable();
  565. },
  566. unregister: function unregister() {
  567. if (typeof GM_unregisterMenuCommand === 'function')
  568. GM_unregisterMenuCommand(this.uuid);
  569.  
  570. document.adoptNode(this.menuitem);
  571. },
  572. disable: function disable() {
  573. if (typeof GM_disableMenuCommand === 'function')
  574. GM_disableMenuCommand(this.uuid);
  575.  
  576. this.menuitem.disabled = true;
  577. },
  578. enable: function enable() {
  579. if (typeof GM_enableMenuCommand === 'function')
  580. GM_enableMenuCommand(this.uuid);
  581.  
  582. this.menuitem.disabled = false;
  583. },
  584. });
  585. MenuCommand.contextmenu = null;
  586.  
  587. var Preference = Class(Subject, {
  588. constructor: let (instance) function Preference() {
  589. if (instance)
  590. return instance;
  591.  
  592. Preference.$super(this);
  593. instance = this;
  594.  
  595. this.dict = {};
  596. },
  597. has: function has(key) key in this.dict,
  598. get: function get(key, def) this.has(key) ? this.dict[key] : def,
  599. set: function set(key, value) {
  600. var prev = this.dict[key];
  601. if (value !== prev) {
  602. this.dict[key] = value;
  603. this.emit('change', {
  604. propertyName: key,
  605. prevValue: prev,
  606. newValue: value,
  607. });
  608. }
  609. return value;
  610. },
  611. del: function del(key) {
  612. if (!this.has(key))
  613. return;
  614.  
  615. var prev = this.dict[key];
  616. delete this.dict[key];
  617.  
  618. this.emit('delete', {
  619. propertyName: key,
  620. prevValue: prev,
  621. });
  622. },
  623. load: function load(str) {
  624. if (!str)
  625. str = GM_getValue(Preference.prefName, Preference.defaultPref || '({})');
  626.  
  627. var obj = eval('(' + str + ')');
  628. if (!obj || typeof obj !== 'object')
  629. return;
  630.  
  631. this.dict = {};
  632. for (let [key, value] in Iterator(obj))
  633. this.set(key, value);
  634.  
  635. this.emit('load');
  636. },
  637. write: function write() {
  638. GM_setValue(Preference.prefName, this.toSource());
  639. },
  640. autoSave: function autoSave() {
  641. if (autoSave.reserved)
  642. return;
  643.  
  644. window.addEventListener('unload', () => this.write(), false);
  645. autoSave.reserved = true;
  646. },
  647. exportToFile: function exportToFile() {
  648. var blob = new Blob([this.toSource()], {
  649. type: 'application/octet-stream',
  650. });
  651. var url = URL.createObjectURL(blob);
  652. location.href = url;
  653. URL.revokeObjectURL(url);
  654. },
  655. importFromString: function importFromString(str) {
  656. try {
  657. this.load(str);
  658. } catch (e if e instanceof SyntaxError) {
  659. showMessage(e, 'warning');
  660. return false;
  661. }
  662. showMessage($str.import_success);
  663. return true;
  664. },
  665. importFromFile: function importFromFile() {
  666. openFilePicker(files => {
  667. if (!files)
  668. return;
  669.  
  670. var r = FileReader();
  671. r.addEventListener('load', () => this.importFromString(r.result), false);
  672. r.readAsText(files[0]);
  673. });
  674. },
  675. toString: function toString() '[object Preference]',
  676. toSource: function toSource() this.dict.toSource(),
  677. });
  678. Preference.prefName = 'settings';
  679.  
  680. var draggable = Class({
  681. constructor: function draggable(element) {
  682. this.element = element;
  683. element.addEventListener('mousedown', this, false, false);
  684. },
  685. isDraggableTarget: function isDraggableTarget(target) {
  686. if (!target)
  687. return false;
  688.  
  689. if (target === this.element)
  690. return true;
  691.  
  692. return !target.mozMatchesSelector(':-moz-any(select, button, input, textarea, [tabindex]), :-moz-any(select, button, input, textarea, [tabindex]) *');
  693. },
  694. detatch: function detatch() {
  695. this.element.removeEventListener('mousedown', this, false);
  696. },
  697. handleEvent: function handleEvent(event) {
  698. var name = 'on' + event.type;
  699. if (name in this)
  700. this[name](event);
  701. },
  702. onmousedown: function onMouseDown(event) {
  703. if (event.button !== 0)
  704. return;
  705.  
  706. if (!this.isDraggableTarget(event.target))
  707. return;
  708.  
  709. event.preventDefault();
  710.  
  711. var focused = this.element.querySelector(':focus');
  712. if (focused)
  713. focused.blur();
  714.  
  715. this.offsetX = event.pageX - this.element.offsetLeft;
  716. this.offsetY = event.pageY - this.element.offsetTop;
  717. document.addEventListener('mousemove', this, true, false);
  718. document.addEventListener('mouseup', this, true, false);
  719. },
  720. onmousemove: function onMouseMove(event) {
  721. event.preventDefault();
  722.  
  723. this.element.style.left = event.pageX - this.offsetX + 'px';
  724. this.element.style.top = event.pageY - this.offsetY + 'px';
  725. },
  726. onmouseup: function onMouseUp(event) {
  727. if (event.button !== 0)
  728. return;
  729.  
  730. event.preventDefault();
  731.  
  732. document.removeEventListener('mousemove', this, true);
  733. document.removeEventListener('mouseup', this, true);
  734. },
  735. });
  736.  
  737. var Filter = Class({
  738. constructor: function Filter(filter) {
  739. if (!(this instanceof Filter))
  740. return Filter.createInstance(arguments);
  741.  
  742. if (!(filter instanceof Object))
  743. filter = {};
  744.  
  745. this.name = filter.name || '';
  746. this.regexp = extend({}, filter.regexp || {});
  747. this.children = filter.children ? filter.children.map(Filter) : [];
  748. this.hitcount = filter.hitcount || 0;
  749. this.lasthit = filter.lasthit || 0;
  750. },
  751. test: function test(entry) {
  752. for (var [name, reg] in Iterator(this.regexp))
  753. if (!reg.test(entry[name] || ''))
  754. return false;
  755.  
  756. var hit = this.children.length ? this.children.some(filter => filter.test(entry)) : !!reg;
  757. if (hit) {
  758. this.hitcount++;
  759. this.lasthit = Date.now();
  760. }
  761.  
  762. return hit;
  763. },
  764. appendChild: function appendChild(filter) {
  765. if (!(filter instanceof this.constructor))
  766. return null;
  767.  
  768. this.removeChild(filter);
  769. this.children.push(filter);
  770. this.sortChildren();
  771. return filter;
  772. },
  773. removeChild: function removeChild(filter) {
  774. if (!(filter instanceof this.constructor))
  775. return null;
  776.  
  777. var index = this.children.indexOf(filter);
  778. if (index !== -1)
  779. this.children.splice(index, 1);
  780.  
  781. return filter;
  782. },
  783. sortChildren: function sortChildren() {
  784. return this.children.sort((a, b) => b.name < a.name);
  785. },
  786. });
  787.  
  788. var Entry = Class(let (div = document.createElement('div')) ({
  789. constructor: function Entry(data) {
  790. this.data = data;
  791. },
  792. get title() {
  793. div.innerHTML = this.data.title || '';
  794. Object.defineProperty(this, 'title', {configurable: true, value: div.textContent});
  795. return this.title;
  796. },
  797. get id() this.data.id,
  798. get url() ((this.data.alternate || 0)[0] || 0).href,
  799. get sourceTitle() this.data.origin.title,
  800. get sourceURL() this.data.origin.streamId.replace(/^[^/]+\//, ''),
  801. get body() (this.data.content || this.data.summary || 0).content,
  802. get author() this.data.author,
  803. get recrawled() this.data.recrawled,
  804. get published() this.data.published,
  805. get updated() this.data.updated,
  806. get keywords() this.data.keywords,
  807. get unread() this.data.unread,
  808. get tags() this.data.tags.map(tag => tag.label),
  809. }));
  810.  
  811. var Panel = Class(Subject, {
  812. constructor: function Panel() {
  813. Panel.$super(this);
  814.  
  815. var panel = document.createElement('form');
  816. panel.classList.add('feedlyng-panel');
  817. draggable(panel);
  818. panel.addEventListener('submit', event => {
  819. event.preventDefault();
  820. event.stopPropagation();
  821. this.apply();
  822. }, false);
  823.  
  824. var submit = document.createElement('input');
  825. submit.type = 'submit';
  826. submit.style.display = 'none';
  827.  
  828. var body = document.createElement('div');
  829. body.classList.add('feedlyng-panel-body');
  830.  
  831. var buttons = document.createElement('div');
  832. buttons.classList.add('feedlyng-panel-buttons');
  833.  
  834. var ok = createGoogButton($str.ok, () => this.apply());
  835. ok.classList.add('feedlyng-panel-ok');
  836.  
  837. var cancel = createGoogButton($str.cancel, () => this.close());
  838. cancel.classList.add('feedlyng-panel-cancel');
  839.  
  840. panel.appendChild(submit);
  841. panel.appendChild(body);
  842. panel.appendChild(buttons);
  843. buttons.appendChild(ok);
  844. buttons.appendChild(cancel);
  845.  
  846. this.dom = {
  847. element: panel,
  848. body: body,
  849. buttons: buttons,
  850. };
  851. },
  852. get opened() !!this.dom.element.parentNode,
  853. open: function open(anchorElement) {
  854. if (this.opened)
  855. return;
  856.  
  857. if (!this.emit('showing'))
  858. return;
  859.  
  860. if (!anchorElement || anchorElement.nodeType !== 1)
  861. anchorElement = null;
  862.  
  863. document.body.appendChild(this.dom.element);
  864. this.snapTo(anchorElement);
  865.  
  866. if (anchorElement) {
  867. let onWindowResize = this.snapTo.bind(this, anchorElement);
  868. window.addEventListener('resize', onWindowResize, false);
  869. this.on('hidden', window.removeEventListener.bind(window, 'resize', onWindowResize, false));
  870. }
  871.  
  872. var focused = document.querySelector(':focus');
  873. if (focused)
  874. focused.blur();
  875.  
  876. var tab = Array.slice(this.dom.element.querySelectorAll(':not(.feedlyng-panel) > :-moz-any(button, input, select, textarea, [tabindex])'))
  877. .sort((a, b) => (b.tabIndex || 0) < (a.tabIndex || 0))[0];
  878.  
  879. if (tab) {
  880. tab.focus();
  881. if (tab.select)
  882. tab.select();
  883. }
  884.  
  885. this.emit('shown');
  886. },
  887. apply: function apply() {
  888. if (this.emit('apply'))
  889. this.close();
  890. },
  891. close: function close() {
  892. if (!this.opened)
  893. return;
  894.  
  895. if (!this.emit('hiding'))
  896. return;
  897.  
  898. document.adoptNode(this.dom.element);
  899.  
  900. this.emit('hidden');
  901. },
  902. toggle: function toggle(anchorElement) {
  903. if (this.opened)
  904. this.close();
  905.  
  906. else
  907. this.open(anchorElement);
  908. },
  909. moveTo: function moveTo(x, y) {
  910. this.dom.element.style.left = x + 'px';
  911. this.dom.element.style.top = y + 'px';
  912. },
  913. snapTo: function snapTo(anchorElement) {
  914. var pad = 5;
  915. var x = pad;
  916. var y = pad;
  917. if (anchorElement) {
  918. var {left, bottom: top} = anchorElement.getBoundingClientRect();
  919. left += pad;
  920. top += pad;
  921.  
  922. var {width, height} = this.dom.element.getBoundingClientRect();
  923. var right = left + width + pad;
  924. var bottom = top + height + pad;
  925.  
  926. var {innerWidth, innerHeight} = window;
  927. if (innerWidth < right)
  928. left -= right - innerWidth;
  929.  
  930. if (innerHeight < bottom)
  931. top -= bottom - innerHeight;
  932.  
  933. x = Math.max(x, left);
  934. y = Math.max(y, top);
  935. }
  936. this.moveTo(x, y);
  937. },
  938. getFormData: function getFormData(asElement) {
  939. var data = {};
  940. Array.slice(this.dom.body.querySelectorAll('[name]')).forEach((elem) => {
  941. var value;
  942. if (asElement) {
  943. value = elem;
  944.  
  945. } else {
  946. if (elem.localName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio'))
  947. value = elem.checked;
  948.  
  949. else
  950. value = 'value' in elem ? elem.value : elem.getAttribute('value');
  951. }
  952.  
  953. var path = elem.name.split('.');
  954. var leaf = path.pop();
  955. var cd = path.reduce((parent, dirName) => {
  956. if (!(dirName in parent))
  957. parent[dirName] = {};
  958.  
  959. return parent[dirName];
  960. }, data);
  961.  
  962. var reg = /\[\]$/;
  963. if (reg.test(leaf)) {
  964. leaf = leaf.replace(reg, '');
  965. if (!(leaf in cd))
  966. cd[leaf] = [];
  967.  
  968. cd[leaf].push(value);
  969.  
  970. } else {
  971. cd[leaf] = value;
  972. }
  973. });
  974. return data;
  975. },
  976. appendContent: function appendContent(element) {
  977. if (element instanceof Array)
  978. return element.map(appendContent, this);
  979.  
  980. return this.dom.body.appendChild(element);
  981. },
  982. removeContents: function removeContents() {
  983. var range = node.ownerDocument.createRange();
  984. range.selectNodeContents(this.dom.body);
  985. range.deleteContents();
  986. range.detach();
  987. },
  988. });
  989.  
  990. var FilterListPanel = Class(Panel, {
  991. constructor: function FilterListPanel(filter, isRoot) {
  992. FilterListPanel.$super(this);
  993. this.filter = filter;
  994.  
  995. var self = this;
  996.  
  997. if (isRoot)
  998. this.dom.element.classList.add('root');
  999.  
  1000. var add = createGoogButton($str.add, () => {
  1001. var f = new Filter();
  1002. f.name = $str.new_filter;
  1003. this.on('apply', () => this.filter.appendChild(f));
  1004. this.appendFilter(f);
  1005. });
  1006. add.classList.add('feedlyng-panel-addfilter');
  1007. this.dom.buttons.insertBefore(add, this.dom.buttons.firstChild);
  1008.  
  1009. var paste = createGoogButton($str.paste, () => {
  1010. if (!clipboard.data)
  1011. return;
  1012.  
  1013. var f = new Filter(clipboard.receive());
  1014. this.on('apply', () => this.filter.appendChild(f));
  1015. this.appendFilter(f);
  1016. });
  1017. paste.classList.add('feedlyng-panel-pastefilter');
  1018. if (!clipboard.data)
  1019. paste.classList.add('jfk-button-disabled');
  1020.  
  1021. clipboard.on('copy', onCopy);
  1022. clipboard.on('purge', onPurge);
  1023.  
  1024. function onCopy() {
  1025. paste.classList.remove('jfk-button-disabled');
  1026. }
  1027. function onPurge() {
  1028. paste.classList.add('jfk-button-disabled');
  1029. }
  1030.  
  1031. this.dom.buttons.insertBefore(paste, add.nextSibling);
  1032.  
  1033. this.on('showing', this.initContents);
  1034. this.on('apply', this);
  1035. this.on('hidden', () => {
  1036. clipboard.removeListener('copy', onCopy);
  1037. clipboard.removeListener('purge', onPurge);
  1038. });
  1039. },
  1040. initContents: function initContents() {
  1041. var filter = this.filter;
  1042.  
  1043. var nameTextbox = document.createElement('input');
  1044. nameTextbox.classList.add('feedlyng-panel-name');
  1045. nameTextbox.type = 'text';
  1046. nameTextbox.name = 'name';
  1047. nameTextbox.size = '32';
  1048. nameTextbox.autocomplete = 'off';
  1049. nameTextbox.value = filter.name;
  1050.  
  1051. var terms = document.createElement('fieldset');
  1052. var legend = document.createElement('legend');
  1053. legend.textContent = filter.name + $str.rules;
  1054.  
  1055. var table = document.createElement('table');
  1056. table.classList.add('feedlyng-panel-terms');
  1057.  
  1058. var tbody = document.createElement('tbody');
  1059. for (let [type, labelText] in Iterator({
  1060. title: $str.title,
  1061. url: $str.url,
  1062. sourceTitle: $str.source_title,
  1063. sourceURL: $str.source_url,
  1064. author: $str.author,
  1065. body: $str.body,
  1066. })) {
  1067. let row = document.createElement('tr');
  1068.  
  1069. let left = document.createElement('td');
  1070. let center = document.createElement('td');
  1071. let right = document.createElement('td');
  1072.  
  1073. let textbox = document.createElement('input');
  1074. textbox.classList.add('feedlyng-panel-terms-textbox');
  1075. textbox.type = 'text';
  1076. textbox.name = 'regexp.' + type + '.source';
  1077. textbox.size = '32';
  1078. textbox.autocomplete = 'off';
  1079. if (type in filter.regexp)
  1080. textbox.value = filter.regexp[type].source.replace(/((?:^|[^\\])(?:\\\\)*)\\(?=\/)/g, '$1');
  1081.  
  1082. let label = createLabel(textbox, labelText);
  1083. label.classList.add('feedlyng-panel-terms-textbox-label');
  1084.  
  1085. let ic = document.createElement('input');
  1086. ic.classList.add('feedlyng-panel-terms-checkbox');
  1087. ic.type = 'checkbox';
  1088. ic.name = 'regexp.' + type + '.ignoreCase';
  1089. if (type in filter.regexp)
  1090. ic.checked = filter.regexp[type].ignoreCase;
  1091.  
  1092. let icl = createLabel(ic, 'i');
  1093. icl.classList.add('feedlyng-panel-terms-checkbox-label');
  1094. icl.title = $str.ignore_case;
  1095.  
  1096. tbody.appendChild(row);
  1097. row.appendChild(left);
  1098. left.appendChild(label);
  1099. row.appendChild(center);
  1100. center.appendChild(textbox);
  1101. row.appendChild(right);
  1102. right.appendChild(ic);
  1103. right.appendChild(icl);
  1104. }
  1105.  
  1106. var rules = document.createElement('div');
  1107. rules.classList.add('feedlyng-panel-rules');
  1108.  
  1109. terms.appendChild(legend);
  1110. terms.appendChild(table);
  1111. table.appendChild(tbody);
  1112. this.appendContent([nameTextbox, terms, rules]);
  1113.  
  1114. this.dom.rules = rules;
  1115. filter.children.forEach(this.appendFilter, this);
  1116. },
  1117. appendFilter: function appendFilter(filter) {
  1118. var panel;
  1119.  
  1120. var updateRow = () => {
  1121. var title = $str.hit_count + ':\t' + filter.hitcount;
  1122. if (filter.lasthit)
  1123. title += '\n' + $str.last_hit + ':\t' + new Date(filter.lasthit).toLocaleString();
  1124.  
  1125. rule.title = title;
  1126. name.textContent = filter.name;
  1127. count.textContent = filter.children.length || '';
  1128. };
  1129. var onEdit = () => {
  1130. if (panel) {
  1131. panel.close();
  1132. return;
  1133. }
  1134. panel = new FilterListPanel(filter);
  1135. panel.on('shown', () => edit.querySelector('.jfk-button').classList.add('jfk-button-checked'));
  1136. panel.on('hidden', () => {
  1137. edit.querySelector('.jfk-button').classList.remove('jfk-button-checked');
  1138. panel = null;
  1139. });
  1140. panel.on('apply', setTimeout.bind(null, updateRow, 0));
  1141. panel.open(this);
  1142. };
  1143. var onCopy = () => clipboard.setForCopy(filter);
  1144. var onDelete = () => {
  1145. document.adoptNode(rule);
  1146. this.on('apply', () => this.filter.removeChild(filter));
  1147. }
  1148.  
  1149. var rule = document.createElement('div');
  1150. rule.classList.add('feedlyng-panel-rule');
  1151. if (filter.children.length)
  1152. rule.classList.add('parent');
  1153.  
  1154. var name = document.createElement('div');
  1155. name.classList.add('feedlyng-panel-rule-name');
  1156. name.addEventListener('dblclick', onEdit, true);
  1157.  
  1158. var count = document.createElement('div');
  1159. count.classList.add('feedlyng-panel-rule-count');
  1160.  
  1161. var buttons = document.createElement('div');
  1162. buttons.classList.add('feedlyng-panel-rule-buttons');
  1163.  
  1164. var edit = createGoogMenuButton($str.edit, onEdit, [[$str.copy, onCopy], [$str.delete, onDelete]]);
  1165. edit.classList.add('feedlyng-panel-rule-edit');
  1166.  
  1167. updateRow();
  1168.  
  1169. rule.appendChild(name);
  1170. rule.appendChild(count);
  1171. rule.appendChild(buttons);
  1172. buttons.appendChild(edit);
  1173. this.dom.rules.appendChild(rule);
  1174. },
  1175. handleEvent: function handleEvent(event) {
  1176. if (event.type !== 'apply')
  1177. return;
  1178.  
  1179. var data = this.getFormData(true);
  1180. var filter = this.filter;
  1181. filter.name = data.name.value;
  1182.  
  1183. var regexp = {};
  1184. var error = false;
  1185. for (let [type, {source, ignoreCase}] in Iterator(data.regexp)) {
  1186. if (!source.value)
  1187. continue;
  1188.  
  1189. try {
  1190. regexp[type] = RegExp(source.value, ignoreCase.checked ? 'i' : '');
  1191. } catch (e if e instanceof SyntaxError) {
  1192. error = true;
  1193. event.preventDefault();
  1194. source.classList.remove('error');
  1195. source.offsetWidth;
  1196. source.classList.add('error');
  1197. }
  1198. }
  1199. if (error)
  1200. return;
  1201.  
  1202. filter.regexp = regexp;
  1203. filter.sortChildren();
  1204. },
  1205. });
  1206.  
  1207. var GoogMenu = Class({
  1208. constructor: function GoogMenu(anchorElement, items) {
  1209. this.items = items;
  1210. this.anchorElement = anchorElement;
  1211. anchorElement.addEventListener('mousedown', this, false);
  1212. },
  1213. get opened() !!((this.dom || 0).element || 0).parentNode,
  1214. init: function init() {
  1215. var menu = document.createElement('div');
  1216. menu.className = 'feedlyng goog-menu goog-menu-vertical';
  1217. menu.addEventListener('click', this, false);
  1218. this.items.forEach((item) => {
  1219. var menuitem = document.createElement('div');
  1220. if (typeof item === 'string') {
  1221. if (/^-+$/.test(item))
  1222. menuitem.className = 'goog-menuseparator';
  1223.  
  1224. } else {
  1225. var [label, fn] = item;
  1226. menuitem.className = 'goog-menuitem';
  1227. var content = document.createElement('div');
  1228. content.className = 'goog-menuitem-content';
  1229. content.textContent = label;
  1230. menuitem.appendChild(content);
  1231. if (fn)
  1232. menuitem.addEventListener('click', fn, false);
  1233. }
  1234. menu.appendChild(menuitem);
  1235. });
  1236.  
  1237. this.dom = {
  1238. element: menu,
  1239. };
  1240. },
  1241. open: function open() {
  1242. if (this.opened)
  1243. return;
  1244.  
  1245. var {right, bottom} = this.anchorElement.getBoundingClientRect();
  1246. var menu = this.dom.element;
  1247. document.body.appendChild(menu);
  1248. menu.style.left = right - menu.offsetWidth + 'px';
  1249. menu.style.top = bottom + 'px';
  1250.  
  1251. this.anchorElement.classList.add('goog-flat-menu-button-open');
  1252. document.addEventListener('mousedown', this, true);
  1253. document.addEventListener('blur', this, true);
  1254. },
  1255. close: function close() {
  1256. document.removeEventListener('mousedown', this, true);
  1257. document.removeEventListener('blur', this, true);
  1258. document.adoptNode(this.dom.element);
  1259. this.anchorElement.classList.remove('goog-flat-menu-button-open');
  1260. },
  1261. handleEvent: function handleEvent({type, target, currentTarget}) {
  1262. switch (type) {
  1263. case 'blur':
  1264. if (target === document)
  1265. this.close();
  1266.  
  1267. return;
  1268. case 'click':
  1269. if (target.mozMatchesSelector('.goog-menuitem, .goog-menuitem *'))
  1270. this.close();
  1271.  
  1272. return;
  1273. case 'mousedown':
  1274. var pos = this.anchorElement.compareDocumentPosition(target);
  1275. if (currentTarget === document && (!pos || pos & target.DOCUMENT_POSITION_CONTAINED_BY))
  1276. return;
  1277.  
  1278. if (this.opened) {
  1279. if (!target.mozMatchesSelector('.goog-menu *'))
  1280. this.close();
  1281.  
  1282. } else {
  1283. if (!this.dom)
  1284. this.init();
  1285.  
  1286. this.open();
  1287. }
  1288. return;
  1289. }
  1290. },
  1291. });
  1292.  
  1293. Preference.defaultPref = {
  1294. filter: {
  1295. name: '',
  1296. regexp: {},
  1297. children: [
  1298. {
  1299. name: 'AD',
  1300. regexp: {
  1301. title: /^\W?(?:ADV?|PR)\b/,
  1302. },
  1303. children: [],
  1304. },
  1305. ],
  1306. },
  1307. }.toSource();
  1308.  
  1309. evalInContent($TEXT(() => {/*
  1310. (() => {
  1311. var XHR = XMLHttpRequest;
  1312. var uniqueId = 0;
  1313.  
  1314. XMLHttpRequest = function XMLHttpRequest() {
  1315. var req = new XHR();
  1316. req.open = open;
  1317. req.setRequestHeader = setRequestHeader;
  1318. req.addEventListener('readystatechange', onReadyStateChange, false);
  1319. return req;
  1320. };
  1321. function open(method, url, async) {
  1322. this.__url__ = url;
  1323. return XHR.prototype.open.apply(this, arguments);
  1324. }
  1325. function setRequestHeader(header, value) {
  1326. if (header === 'Authorization')
  1327. this.__auth__ = value;
  1328.  
  1329. return XHR.prototype.setRequestHeader.apply(this, arguments);
  1330. }
  1331. function onReadyStateChange() {
  1332. if (this.readyState < 4 || this.status !== 200)
  1333. return;
  1334.  
  1335. if (!/^\/\/(?:cloud\.)?feedly\.com\/v3\/streams\/contents\b/.test(this.__url__))
  1336. return;
  1337.  
  1338. var pongEventType = 'streamcontentloaded_callback' + uniqueId++;
  1339.  
  1340. var data = JSON.stringify({
  1341. type: pongEventType,
  1342. auth: this.__auth__,
  1343. text: this.responseText,
  1344. });
  1345.  
  1346. try {
  1347. var event = new MessageEvent('streamcontentloaded', {
  1348. bubbles: true,
  1349. cancelable: false,
  1350. data: data,
  1351. origin: location.href,
  1352. source: null,
  1353. });
  1354. } catch (e) {
  1355. var event = document.createEvent('MessageEvent');
  1356. event.initMessageEvent('streamcontentloaded', true, false, data, location.href, '', null);
  1357. }
  1358.  
  1359. var onPong = ({data}) => Object.defineProperty(this, 'responseText', {configurable: true, value: data});
  1360. document.addEventListener(pongEventType, onPong, false);
  1361. document.dispatchEvent(event);
  1362. document.removeEventListener(pongEventType, onPong, false);
  1363. }
  1364. })();
  1365. */}));
  1366.  
  1367. document.addEventListener('streamcontentloaded', function(event) {
  1368. var {type: pongEventType, auth, text} = JSON.parse(event.data);
  1369. var data = JSON.parse(text);
  1370.  
  1371. var logging = pref.get('logging', true);
  1372. var filter = pref.get('filter');
  1373. var filteredEntryIds = [];
  1374. var hasUnread = false;
  1375.  
  1376. data.items = data.items.filter((item) => {
  1377. var entry = new Entry(item);
  1378. if (!filter.test(entry))
  1379. return true;
  1380.  
  1381. if (logging)
  1382. GM_log('filtered: "' + (entry.title || '') + '" ' + entry.url);
  1383.  
  1384. filteredEntryIds.push(entry.id);
  1385. if (entry.unread)
  1386. hasUnread = true;
  1387.  
  1388. return false;
  1389. });
  1390.  
  1391. if (!filteredEntryIds.length)
  1392. return;
  1393.  
  1394. var data = JSON.stringify(data);
  1395. try {
  1396. var ev = new MessageEvent(pongEventType, {
  1397. bubbles: true,
  1398. cancelable: false,
  1399. data: data,
  1400. origin: location.href,
  1401. source: window,
  1402. });
  1403. } catch (e if e instanceof TypeError) {
  1404. var ev = document.createEvent('MessageEvent');
  1405. ev.initMessageEvent(pongEventType, true, false, data, location.href, '', null);
  1406. }
  1407. document.dispatchEvent(ev);
  1408.  
  1409. if (!hasUnread)
  1410. return;
  1411.  
  1412. sendJSON({
  1413. url: '/v3/markers',
  1414. headers: {
  1415. Authorization: auth,
  1416. },
  1417. data: {
  1418. action: 'markAsRead',
  1419. entryIds: filteredEntryIds,
  1420. type: 'entries',
  1421. },
  1422. });
  1423. }, false);
  1424.  
  1425. var contextmenu = document.createElement('menu');
  1426. contextmenu.type = 'context';
  1427. contextmenu.id = 'feedlyng-contextmenu';
  1428. MenuCommand.contextmenu = contextmenu;
  1429.  
  1430. var rootFilterPanel;
  1431. var settingsMenuItem;
  1432. var clipboard = new DataTransfer();
  1433. var pref = new Preference();
  1434. pref.on('change', function({propertyName, newValue}) {
  1435. switch (propertyName) {
  1436. case 'filter':
  1437. if (!Filter.prototype.isPrototypeOf(newValue))
  1438. this.set('filter', new Filter(newValue));
  1439.  
  1440. break;
  1441.  
  1442. case 'language':
  1443. Locale.select(newValue);
  1444. break;
  1445. }
  1446. });
  1447.  
  1448. document.addEventListener('DOMContentLoaded', () => {
  1449. GM_addStyle(CSS_STYLE_TEXT);
  1450.  
  1451. pref.load();
  1452. pref.autoSave();
  1453.  
  1454. registerMenuCommands();
  1455. addSettingsMenuItem();
  1456. }, false);
  1457.  
  1458. function registerMenuCommands() {
  1459. menuCommand($str.setting + '...', togglePrefPanel);
  1460. menuCommand($str.language + '...', function() {
  1461. var langField = document.createElement('fieldset');
  1462.  
  1463. var title = document.createElement('legend');
  1464. title.textContent = $str.language;
  1465.  
  1466. var select = document.createElement('select');
  1467. Locale.languages.forEach((lang) => {
  1468. var option = document.createElement('option');
  1469. option.value = lang;
  1470. option.textContent = lang;
  1471. if (lang === Locale.selectedLanguage)
  1472. option.selected = true;
  1473.  
  1474. select.appendChild(option);
  1475. });
  1476.  
  1477. langField.appendChild(title);
  1478. langField.appendChild(select);
  1479.  
  1480. var p = new Panel();
  1481. p.appendContent(langField);
  1482. p.on('apply', () => pref.set('language', select.value));
  1483. p.open();
  1484. });
  1485. menuCommand($str.import_setting + '...', () => pref.importFromFile());
  1486. menuCommand($str.export_setting, () => pref.exportToFile());
  1487. }
  1488. function togglePrefPanel(anchorElement) {
  1489. if (rootFilterPanel) {
  1490. rootFilterPanel.close();
  1491. return;
  1492. }
  1493. rootFilterPanel = new FilterListPanel(pref.get('filter'), true);
  1494. rootFilterPanel.on('apply', () => showMessage($str.ng_setting_modified));
  1495. rootFilterPanel.on('hidden', () => {
  1496. clipboard.purge();
  1497. rootFilterPanel = null;
  1498. });
  1499. rootFilterPanel.open(anchorElement);
  1500. }
  1501. function onNGSettingCommand({target}) {
  1502. togglePrefPanel(target);
  1503. }
  1504. function createGoogButton(text, fn) {
  1505. var button = document.createElement('div');
  1506. button.className = 'goog-inline-block jfk-button jfk-button-standard unselectable';
  1507. button.tabIndex = 0;
  1508. button.textContent = text;
  1509. if (fn) {
  1510. button.addEventListener('click', fn, false);
  1511. button.addEventListener('keydown', function({which}) {
  1512. if (which === 13)
  1513. fn.apply(this, arguments);
  1514. }, false);
  1515. }
  1516.  
  1517. return button;
  1518. }
  1519. function createGoogMenuButton(text, fn, arr) {
  1520. var container = document.createElement('div');
  1521. container.className = 'goog-inline-block';
  1522.  
  1523. var button = createGoogButton(text, fn);
  1524. button.classList.add('jfk-button-collapse-right');
  1525.  
  1526. var options = document.createElement('div');
  1527. options.className = 'goog-inline-block goog-flat-menu-button goog-flat-menu-button-collapse-left unselectable';
  1528. options.tabIndex = 0;
  1529.  
  1530. container.appendChild(button);
  1531. container.appendChild(options);
  1532. options.insertAdjacentHTML('beforeend', '<div class="goog-inline-block goog-flat-menu-button-caption">&nbsp;</div>');
  1533. options.insertAdjacentHTML('beforeend', '<div class="goog-inline-block goog-flat-menu-button-dropdown"></div>');
  1534.  
  1535. new GoogMenu(options, arr);
  1536.  
  1537. return container;
  1538. }
  1539. function showMessage(str, type) {
  1540. if (typeof GM_notification === 'function')
  1541. GM_notification(str);
  1542. }
  1543. function addSettingsMenuItem() {
  1544. var feedlyTabs = document.getElementById('feedlyTabs');
  1545. if (!feedlyTabs) {
  1546. setTimeout(addSettingsMenuItem, 100);
  1547. return;
  1548. }
  1549.  
  1550. var prefListener;
  1551. var observer = new MutationObserver(function mutationCallback() {
  1552. if (!document.getElementById('feedly-ng-filter-setting'))
  1553. pref.removeListener('change', prefListener);
  1554.  
  1555. var prefItem = document.querySelector('#feedlyTabs .tab > .label[data-uri="preferences"]');
  1556. if (!prefItem)
  1557. return;
  1558.  
  1559. var prefItemTab = prefItem.parentNode;
  1560.  
  1561. var tab = document.createElement('div');
  1562. tab.className = 'tab';
  1563. tab.setAttribute('contextmenu', MenuCommand.contextmenu.id);
  1564. tab.addEventListener('click', onNGSettingCommand, false);
  1565.  
  1566. var label = document.createElement('div');
  1567. label.id = 'feedly-ng-filter-setting';
  1568. label.className = 'label primary iconless';
  1569. label.textContent = $str.ng_setting;
  1570.  
  1571. tab.appendChild(label);
  1572. prefItemTab.parentNode.insertBefore(tab, prefItemTab.nextSibling);
  1573. document.body.appendChild(contextmenu);
  1574.  
  1575. prefListener = ({propertyName}) => {
  1576. if (propertyName === 'language')
  1577. label.textContent = $str.ng_setting;
  1578. };
  1579. pref.on('change', prefListener);
  1580. });
  1581. observer.observe(feedlyTabs, {
  1582. childList: true,
  1583. });
  1584. }
  1585. function menuCommand(label, fn) {
  1586. return new MenuCommand($str.app_name + ' - ' + label, fn);
  1587. }
  1588. function xhr(details) {
  1589. var opt = extend({}, details);
  1590. var {data} = opt;
  1591.  
  1592. if (!opt.method)
  1593. opt.method = data ? 'POST' : 'GET';
  1594.  
  1595. if (data instanceof Object) {
  1596. opt.data = [pair.map(encodeURIComponent).join('=') for (pair in Iterator(data))].join('&');
  1597. if (!opt.headers)
  1598. opt.headers = {};
  1599.  
  1600. opt.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
  1601. }
  1602.  
  1603. setTimeout(GM_xmlhttpRequest, 0, opt);
  1604. }
  1605. function sendJSON(details) {
  1606. var opt = extend({}, details);
  1607. var {data} = opt;
  1608. if (!opt.headers)
  1609. opt.headers = {};
  1610.  
  1611. opt.method = 'POST';
  1612. opt.headers['Content-Type'] = 'application/json; charset=utf-8';
  1613. opt.data = JSON.stringify(data);
  1614.  
  1615. return xhr(opt);
  1616. }
  1617. function evalInContent(code) {
  1618. var script = document.createElement('script');
  1619. script.type = 'text/javascript;version=1.8';
  1620. script.textContent = code;
  1621.  
  1622. try {
  1623. location.href = 'javascript:' + encodeURIComponent(code) + ';void+0';
  1624. // document.adoptNode(document.appendChild(script));
  1625. } catch (e) {
  1626. document.adoptNode(document.documentElement.appendChild(script));
  1627. }
  1628. }
  1629. function openFilePicker(callback, multiple) {
  1630. var canceled = true;
  1631. var input = document.createElement('input');
  1632. input.type = 'file';
  1633. input.multiple = multiple;
  1634. input.addEventListener('change', () => {
  1635. canceled = false;
  1636. callback(Array.slice(input.files));
  1637. }, false);
  1638. input.click();
  1639. if (canceled)
  1640. setTimeout(callback, 0, null);
  1641. }
  1642. function createLabel(element, text) {
  1643. var label = document.createElement('label');
  1644. if (1 < arguments.length)
  1645. label.textContent = text;
  1646.  
  1647. var id = element.id;
  1648. if (!id) {
  1649. if (!('id' in createLabel))
  1650. createLabel.id = 0;
  1651.  
  1652. id = 'id_for_label_' + createLabel.id++;
  1653. element.id = id;
  1654. }
  1655. label.htmlFor = id;
  1656. return label;
  1657. }
  1658. function extend(dst, src) {
  1659. for (let [key, value] in Iterator(src))
  1660. dst[key] = value;
  1661.  
  1662. return dst;
  1663. }
  1664. function $TEXT(fn) String.replace(fn, /^\(\) => \{\/\*|\*\/}$/g, '');
  1665. })();