Feedly NG Filter

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

当前为 2015-09-26 提交的版本,查看 最新版本

  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.1
  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: function Preference() {
  589. if (Preference._instance)
  590. return Preference._instance;
  591.  
  592. Preference.$super(this);
  593. Preference._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({
  789. constructor: function Entry(data) {
  790. this.data = data;
  791. },
  792. get title() {
  793. var div = document.createElement('div');
  794. div.innerHTML = this.data.title || '';
  795. Object.defineProperty(this, 'title', {configurable: true, value: div.textContent});
  796. return this.title;
  797. },
  798. get id() this.data.id,
  799. get url() ((this.data.alternate || 0)[0] || 0).href,
  800. get sourceTitle() this.data.origin.title,
  801. get sourceURL() this.data.origin.streamId.replace(/^[^/]+\//, ''),
  802. get body() (this.data.content || this.data.summary || 0).content,
  803. get author() this.data.author,
  804. get recrawled() this.data.recrawled,
  805. get published() this.data.published,
  806. get updated() this.data.updated,
  807. get keywords() this.data.keywords,
  808. get unread() this.data.unread,
  809. get tags() this.data.tags.map(tag => tag.label),
  810. });
  811.  
  812. var Panel = Class(Subject, {
  813. constructor: function Panel() {
  814. Panel.$super(this);
  815.  
  816. var panel = document.createElement('form');
  817. panel.classList.add('feedlyng-panel');
  818. draggable(panel);
  819. panel.addEventListener('submit', event => {
  820. event.preventDefault();
  821. event.stopPropagation();
  822. this.apply();
  823. }, false);
  824.  
  825. var submit = document.createElement('input');
  826. submit.type = 'submit';
  827. submit.style.display = 'none';
  828.  
  829. var body = document.createElement('div');
  830. body.classList.add('feedlyng-panel-body');
  831.  
  832. var buttons = document.createElement('div');
  833. buttons.classList.add('feedlyng-panel-buttons');
  834.  
  835. var ok = createGoogButton($str.ok, () => this.apply());
  836. ok.classList.add('feedlyng-panel-ok');
  837.  
  838. var cancel = createGoogButton($str.cancel, () => this.close());
  839. cancel.classList.add('feedlyng-panel-cancel');
  840.  
  841. panel.appendChild(submit);
  842. panel.appendChild(body);
  843. panel.appendChild(buttons);
  844. buttons.appendChild(ok);
  845. buttons.appendChild(cancel);
  846.  
  847. this.dom = {
  848. element: panel,
  849. body: body,
  850. buttons: buttons,
  851. };
  852. },
  853. get opened() !!this.dom.element.parentNode,
  854. open: function open(anchorElement) {
  855. if (this.opened)
  856. return;
  857.  
  858. if (!this.emit('showing'))
  859. return;
  860.  
  861. if (!anchorElement || anchorElement.nodeType !== 1)
  862. anchorElement = null;
  863.  
  864. document.body.appendChild(this.dom.element);
  865. this.snapTo(anchorElement);
  866.  
  867. if (anchorElement) {
  868. let onWindowResize = this.snapTo.bind(this, anchorElement);
  869. window.addEventListener('resize', onWindowResize, false);
  870. this.on('hidden', window.removeEventListener.bind(window, 'resize', onWindowResize, false));
  871. }
  872.  
  873. var focused = document.querySelector(':focus');
  874. if (focused)
  875. focused.blur();
  876.  
  877. var tab = Array.slice(this.dom.element.querySelectorAll(':not(.feedlyng-panel) > :-moz-any(button, input, select, textarea, [tabindex])'))
  878. .sort((a, b) => (b.tabIndex || 0) < (a.tabIndex || 0))[0];
  879.  
  880. if (tab) {
  881. tab.focus();
  882. if (tab.select)
  883. tab.select();
  884. }
  885.  
  886. this.emit('shown');
  887. },
  888. apply: function apply() {
  889. if (this.emit('apply'))
  890. this.close();
  891. },
  892. close: function close() {
  893. if (!this.opened)
  894. return;
  895.  
  896. if (!this.emit('hiding'))
  897. return;
  898.  
  899. document.adoptNode(this.dom.element);
  900.  
  901. this.emit('hidden');
  902. },
  903. toggle: function toggle(anchorElement) {
  904. if (this.opened)
  905. this.close();
  906.  
  907. else
  908. this.open(anchorElement);
  909. },
  910. moveTo: function moveTo(x, y) {
  911. this.dom.element.style.left = x + 'px';
  912. this.dom.element.style.top = y + 'px';
  913. },
  914. snapTo: function snapTo(anchorElement) {
  915. var pad = 5;
  916. var x = pad;
  917. var y = pad;
  918. if (anchorElement) {
  919. var {left, bottom: top} = anchorElement.getBoundingClientRect();
  920. left += pad;
  921. top += pad;
  922.  
  923. var {width, height} = this.dom.element.getBoundingClientRect();
  924. var right = left + width + pad;
  925. var bottom = top + height + pad;
  926.  
  927. var {innerWidth, innerHeight} = window;
  928. if (innerWidth < right)
  929. left -= right - innerWidth;
  930.  
  931. if (innerHeight < bottom)
  932. top -= bottom - innerHeight;
  933.  
  934. x = Math.max(x, left);
  935. y = Math.max(y, top);
  936. }
  937. this.moveTo(x, y);
  938. },
  939. getFormData: function getFormData(asElement) {
  940. var data = {};
  941. Array.slice(this.dom.body.querySelectorAll('[name]')).forEach((elem) => {
  942. var value;
  943. if (asElement) {
  944. value = elem;
  945.  
  946. } else {
  947. if (elem.localName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio'))
  948. value = elem.checked;
  949.  
  950. else
  951. value = 'value' in elem ? elem.value : elem.getAttribute('value');
  952. }
  953.  
  954. var path = elem.name.split('.');
  955. var leaf = path.pop();
  956. var cd = path.reduce((parent, dirName) => {
  957. if (!(dirName in parent))
  958. parent[dirName] = {};
  959.  
  960. return parent[dirName];
  961. }, data);
  962.  
  963. var reg = /\[\]$/;
  964. if (reg.test(leaf)) {
  965. leaf = leaf.replace(reg, '');
  966. if (!(leaf in cd))
  967. cd[leaf] = [];
  968.  
  969. cd[leaf].push(value);
  970.  
  971. } else {
  972. cd[leaf] = value;
  973. }
  974. });
  975. return data;
  976. },
  977. appendContent: function appendContent(element) {
  978. if (element instanceof Array)
  979. return element.map(appendContent, this);
  980.  
  981. return this.dom.body.appendChild(element);
  982. },
  983. removeContents: function removeContents() {
  984. var range = node.ownerDocument.createRange();
  985. range.selectNodeContents(this.dom.body);
  986. range.deleteContents();
  987. range.detach();
  988. },
  989. });
  990.  
  991. var FilterListPanel = Class(Panel, {
  992. constructor: function FilterListPanel(filter, isRoot) {
  993. FilterListPanel.$super(this);
  994. this.filter = filter;
  995.  
  996. var self = this;
  997.  
  998. if (isRoot)
  999. this.dom.element.classList.add('root');
  1000.  
  1001. var add = createGoogButton($str.add, () => {
  1002. var f = new Filter();
  1003. f.name = $str.new_filter;
  1004. this.on('apply', () => this.filter.appendChild(f));
  1005. this.appendFilter(f);
  1006. });
  1007. add.classList.add('feedlyng-panel-addfilter');
  1008. this.dom.buttons.insertBefore(add, this.dom.buttons.firstChild);
  1009.  
  1010. var paste = createGoogButton($str.paste, () => {
  1011. if (!clipboard.data)
  1012. return;
  1013.  
  1014. var f = new Filter(clipboard.receive());
  1015. this.on('apply', () => this.filter.appendChild(f));
  1016. this.appendFilter(f);
  1017. });
  1018. paste.classList.add('feedlyng-panel-pastefilter');
  1019. if (!clipboard.data)
  1020. paste.classList.add('jfk-button-disabled');
  1021.  
  1022. clipboard.on('copy', onCopy);
  1023. clipboard.on('purge', onPurge);
  1024.  
  1025. function onCopy() {
  1026. paste.classList.remove('jfk-button-disabled');
  1027. }
  1028. function onPurge() {
  1029. paste.classList.add('jfk-button-disabled');
  1030. }
  1031.  
  1032. this.dom.buttons.insertBefore(paste, add.nextSibling);
  1033.  
  1034. this.on('showing', this.initContents);
  1035. this.on('apply', this);
  1036. this.on('hidden', () => {
  1037. clipboard.removeListener('copy', onCopy);
  1038. clipboard.removeListener('purge', onPurge);
  1039. });
  1040. },
  1041. initContents: function initContents() {
  1042. var filter = this.filter;
  1043.  
  1044. var nameTextbox = document.createElement('input');
  1045. nameTextbox.classList.add('feedlyng-panel-name');
  1046. nameTextbox.type = 'text';
  1047. nameTextbox.name = 'name';
  1048. nameTextbox.size = '32';
  1049. nameTextbox.autocomplete = 'off';
  1050. nameTextbox.value = filter.name;
  1051.  
  1052. var terms = document.createElement('fieldset');
  1053. var legend = document.createElement('legend');
  1054. legend.textContent = filter.name + $str.rules;
  1055.  
  1056. var table = document.createElement('table');
  1057. table.classList.add('feedlyng-panel-terms');
  1058.  
  1059. var tbody = document.createElement('tbody');
  1060. for (let [type, labelText] in Iterator({
  1061. title: $str.title,
  1062. url: $str.url,
  1063. sourceTitle: $str.source_title,
  1064. sourceURL: $str.source_url,
  1065. author: $str.author,
  1066. body: $str.body,
  1067. })) {
  1068. let row = document.createElement('tr');
  1069.  
  1070. let left = document.createElement('td');
  1071. let center = document.createElement('td');
  1072. let right = document.createElement('td');
  1073.  
  1074. let textbox = document.createElement('input');
  1075. textbox.classList.add('feedlyng-panel-terms-textbox');
  1076. textbox.type = 'text';
  1077. textbox.name = 'regexp.' + type + '.source';
  1078. textbox.size = '32';
  1079. textbox.autocomplete = 'off';
  1080. if (type in filter.regexp)
  1081. textbox.value = filter.regexp[type].source.replace(/((?:^|[^\\])(?:\\\\)*)\\(?=\/)/g, '$1');
  1082.  
  1083. let label = createLabel(textbox, labelText);
  1084. label.classList.add('feedlyng-panel-terms-textbox-label');
  1085.  
  1086. let ic = document.createElement('input');
  1087. ic.classList.add('feedlyng-panel-terms-checkbox');
  1088. ic.type = 'checkbox';
  1089. ic.name = 'regexp.' + type + '.ignoreCase';
  1090. if (type in filter.regexp)
  1091. ic.checked = filter.regexp[type].ignoreCase;
  1092.  
  1093. let icl = createLabel(ic, 'i');
  1094. icl.classList.add('feedlyng-panel-terms-checkbox-label');
  1095. icl.title = $str.ignore_case;
  1096.  
  1097. tbody.appendChild(row);
  1098. row.appendChild(left);
  1099. left.appendChild(label);
  1100. row.appendChild(center);
  1101. center.appendChild(textbox);
  1102. row.appendChild(right);
  1103. right.appendChild(ic);
  1104. right.appendChild(icl);
  1105. }
  1106.  
  1107. var rules = document.createElement('div');
  1108. rules.classList.add('feedlyng-panel-rules');
  1109.  
  1110. terms.appendChild(legend);
  1111. terms.appendChild(table);
  1112. table.appendChild(tbody);
  1113. this.appendContent([nameTextbox, terms, rules]);
  1114.  
  1115. this.dom.rules = rules;
  1116. filter.children.forEach(this.appendFilter, this);
  1117. },
  1118. appendFilter: function appendFilter(filter) {
  1119. var panel;
  1120.  
  1121. var updateRow = () => {
  1122. var title = $str.hit_count + ':\t' + filter.hitcount;
  1123. if (filter.lasthit)
  1124. title += '\n' + $str.last_hit + ':\t' + new Date(filter.lasthit).toLocaleString();
  1125.  
  1126. rule.title = title;
  1127. name.textContent = filter.name;
  1128. count.textContent = filter.children.length || '';
  1129. };
  1130. var onEdit = () => {
  1131. if (panel) {
  1132. panel.close();
  1133. return;
  1134. }
  1135. panel = new FilterListPanel(filter);
  1136. panel.on('shown', () => edit.querySelector('.jfk-button').classList.add('jfk-button-checked'));
  1137. panel.on('hidden', () => {
  1138. edit.querySelector('.jfk-button').classList.remove('jfk-button-checked');
  1139. panel = null;
  1140. });
  1141. panel.on('apply', setTimeout.bind(null, updateRow, 0));
  1142. panel.open(this);
  1143. };
  1144. var onCopy = () => clipboard.setForCopy(filter);
  1145. var onDelete = () => {
  1146. document.adoptNode(rule);
  1147. this.on('apply', () => this.filter.removeChild(filter));
  1148. }
  1149.  
  1150. var rule = document.createElement('div');
  1151. rule.classList.add('feedlyng-panel-rule');
  1152. if (filter.children.length)
  1153. rule.classList.add('parent');
  1154.  
  1155. var name = document.createElement('div');
  1156. name.classList.add('feedlyng-panel-rule-name');
  1157. name.addEventListener('dblclick', onEdit, true);
  1158.  
  1159. var count = document.createElement('div');
  1160. count.classList.add('feedlyng-panel-rule-count');
  1161.  
  1162. var buttons = document.createElement('div');
  1163. buttons.classList.add('feedlyng-panel-rule-buttons');
  1164.  
  1165. var edit = createGoogMenuButton($str.edit, onEdit, [[$str.copy, onCopy], [$str.delete, onDelete]]);
  1166. edit.classList.add('feedlyng-panel-rule-edit');
  1167.  
  1168. updateRow();
  1169.  
  1170. rule.appendChild(name);
  1171. rule.appendChild(count);
  1172. rule.appendChild(buttons);
  1173. buttons.appendChild(edit);
  1174. this.dom.rules.appendChild(rule);
  1175. },
  1176. handleEvent: function handleEvent(event) {
  1177. if (event.type !== 'apply')
  1178. return;
  1179.  
  1180. var data = this.getFormData(true);
  1181. var filter = this.filter;
  1182. filter.name = data.name.value;
  1183.  
  1184. var regexp = {};
  1185. var error = false;
  1186. for (let [type, {source, ignoreCase}] in Iterator(data.regexp)) {
  1187. if (!source.value)
  1188. continue;
  1189.  
  1190. try {
  1191. regexp[type] = RegExp(source.value, ignoreCase.checked ? 'i' : '');
  1192. } catch (e if e instanceof SyntaxError) {
  1193. error = true;
  1194. event.preventDefault();
  1195. source.classList.remove('error');
  1196. source.offsetWidth;
  1197. source.classList.add('error');
  1198. }
  1199. }
  1200. if (error)
  1201. return;
  1202.  
  1203. filter.regexp = regexp;
  1204. filter.sortChildren();
  1205. },
  1206. });
  1207.  
  1208. var GoogMenu = Class({
  1209. constructor: function GoogMenu(anchorElement, items) {
  1210. this.items = items;
  1211. this.anchorElement = anchorElement;
  1212. anchorElement.addEventListener('mousedown', this, false);
  1213. },
  1214. get opened() !!((this.dom || 0).element || 0).parentNode,
  1215. init: function init() {
  1216. var menu = document.createElement('div');
  1217. menu.className = 'feedlyng goog-menu goog-menu-vertical';
  1218. menu.addEventListener('click', this, false);
  1219. this.items.forEach((item) => {
  1220. var menuitem = document.createElement('div');
  1221. if (typeof item === 'string') {
  1222. if (/^-+$/.test(item))
  1223. menuitem.className = 'goog-menuseparator';
  1224.  
  1225. } else {
  1226. var [label, fn] = item;
  1227. menuitem.className = 'goog-menuitem';
  1228. var content = document.createElement('div');
  1229. content.className = 'goog-menuitem-content';
  1230. content.textContent = label;
  1231. menuitem.appendChild(content);
  1232. if (fn)
  1233. menuitem.addEventListener('click', fn, false);
  1234. }
  1235. menu.appendChild(menuitem);
  1236. });
  1237.  
  1238. this.dom = {
  1239. element: menu,
  1240. };
  1241. },
  1242. open: function open() {
  1243. if (this.opened)
  1244. return;
  1245.  
  1246. var {right, bottom} = this.anchorElement.getBoundingClientRect();
  1247. var menu = this.dom.element;
  1248. document.body.appendChild(menu);
  1249. menu.style.left = right - menu.offsetWidth + 'px';
  1250. menu.style.top = bottom + 'px';
  1251.  
  1252. this.anchorElement.classList.add('goog-flat-menu-button-open');
  1253. document.addEventListener('mousedown', this, true);
  1254. document.addEventListener('blur', this, true);
  1255. },
  1256. close: function close() {
  1257. document.removeEventListener('mousedown', this, true);
  1258. document.removeEventListener('blur', this, true);
  1259. document.adoptNode(this.dom.element);
  1260. this.anchorElement.classList.remove('goog-flat-menu-button-open');
  1261. },
  1262. handleEvent: function handleEvent({type, target, currentTarget}) {
  1263. switch (type) {
  1264. case 'blur':
  1265. if (target === document)
  1266. this.close();
  1267.  
  1268. return;
  1269. case 'click':
  1270. if (target.mozMatchesSelector('.goog-menuitem, .goog-menuitem *'))
  1271. this.close();
  1272.  
  1273. return;
  1274. case 'mousedown':
  1275. var pos = this.anchorElement.compareDocumentPosition(target);
  1276. if (currentTarget === document && (!pos || pos & target.DOCUMENT_POSITION_CONTAINED_BY))
  1277. return;
  1278.  
  1279. if (this.opened) {
  1280. if (!target.mozMatchesSelector('.goog-menu *'))
  1281. this.close();
  1282.  
  1283. } else {
  1284. if (!this.dom)
  1285. this.init();
  1286.  
  1287. this.open();
  1288. }
  1289. return;
  1290. }
  1291. },
  1292. });
  1293.  
  1294. Preference.defaultPref = {
  1295. filter: {
  1296. name: '',
  1297. regexp: {},
  1298. children: [
  1299. {
  1300. name: 'AD',
  1301. regexp: {
  1302. title: /^\W?(?:ADV?|PR)\b/,
  1303. },
  1304. children: [],
  1305. },
  1306. ],
  1307. },
  1308. }.toSource();
  1309.  
  1310. evalInContent($TEXT(() => {/*
  1311. (() => {
  1312. var XHR = XMLHttpRequest;
  1313. var uniqueId = 0;
  1314.  
  1315. XMLHttpRequest = function XMLHttpRequest() {
  1316. var req = new XHR();
  1317. req.open = open;
  1318. req.setRequestHeader = setRequestHeader;
  1319. req.addEventListener('readystatechange', onReadyStateChange, false);
  1320. return req;
  1321. };
  1322. function open(method, url, async) {
  1323. this.__url__ = url;
  1324. return XHR.prototype.open.apply(this, arguments);
  1325. }
  1326. function setRequestHeader(header, value) {
  1327. if (header === 'Authorization')
  1328. this.__auth__ = value;
  1329.  
  1330. return XHR.prototype.setRequestHeader.apply(this, arguments);
  1331. }
  1332. function onReadyStateChange() {
  1333. if (this.readyState < 4 || this.status !== 200)
  1334. return;
  1335.  
  1336. if (!/^\/\/(?:cloud\.)?feedly\.com\/v3\/streams\/contents\b/.test(this.__url__))
  1337. return;
  1338.  
  1339. var pongEventType = 'streamcontentloaded_callback' + uniqueId++;
  1340.  
  1341. var data = JSON.stringify({
  1342. type: pongEventType,
  1343. auth: this.__auth__,
  1344. text: this.responseText,
  1345. });
  1346.  
  1347. try {
  1348. var event = new MessageEvent('streamcontentloaded', {
  1349. bubbles: true,
  1350. cancelable: false,
  1351. data: data,
  1352. origin: location.href,
  1353. source: null,
  1354. });
  1355. } catch (e) {
  1356. var event = document.createEvent('MessageEvent');
  1357. event.initMessageEvent('streamcontentloaded', true, false, data, location.href, '', null);
  1358. }
  1359.  
  1360. var onPong = ({data}) => Object.defineProperty(this, 'responseText', {configurable: true, value: data});
  1361. document.addEventListener(pongEventType, onPong, false);
  1362. document.dispatchEvent(event);
  1363. document.removeEventListener(pongEventType, onPong, false);
  1364. }
  1365. })();
  1366. */}));
  1367.  
  1368. document.addEventListener('streamcontentloaded', function(event) {
  1369. var {type: pongEventType, auth, text} = JSON.parse(event.data);
  1370. var data = JSON.parse(text);
  1371.  
  1372. var logging = pref.get('logging', true);
  1373. var filter = pref.get('filter');
  1374. var filteredEntryIds = [];
  1375. var hasUnread = false;
  1376.  
  1377. data.items = data.items.filter((item) => {
  1378. var entry = new Entry(item);
  1379. if (!filter.test(entry))
  1380. return true;
  1381.  
  1382. if (logging)
  1383. GM_log('filtered: "' + (entry.title || '') + '" ' + entry.url);
  1384.  
  1385. filteredEntryIds.push(entry.id);
  1386. if (entry.unread)
  1387. hasUnread = true;
  1388.  
  1389. return false;
  1390. });
  1391.  
  1392. if (!filteredEntryIds.length)
  1393. return;
  1394.  
  1395. var data = JSON.stringify(data);
  1396. try {
  1397. var ev = new MessageEvent(pongEventType, {
  1398. bubbles: true,
  1399. cancelable: false,
  1400. data: data,
  1401. origin: location.href,
  1402. source: window,
  1403. });
  1404. } catch (e if e instanceof TypeError) {
  1405. var ev = document.createEvent('MessageEvent');
  1406. ev.initMessageEvent(pongEventType, true, false, data, location.href, '', null);
  1407. }
  1408. document.dispatchEvent(ev);
  1409.  
  1410. if (!hasUnread)
  1411. return;
  1412.  
  1413. sendJSON({
  1414. url: '/v3/markers',
  1415. headers: {
  1416. Authorization: auth,
  1417. },
  1418. data: {
  1419. action: 'markAsRead',
  1420. entryIds: filteredEntryIds,
  1421. type: 'entries',
  1422. },
  1423. });
  1424. }, false);
  1425.  
  1426. var contextmenu = document.createElement('menu');
  1427. contextmenu.type = 'context';
  1428. contextmenu.id = 'feedlyng-contextmenu';
  1429. MenuCommand.contextmenu = contextmenu;
  1430.  
  1431. var rootFilterPanel;
  1432. var settingsMenuItem;
  1433. var clipboard = new DataTransfer();
  1434. var pref = new Preference();
  1435. pref.on('change', function({propertyName, newValue}) {
  1436. switch (propertyName) {
  1437. case 'filter':
  1438. if (!Filter.prototype.isPrototypeOf(newValue))
  1439. this.set('filter', new Filter(newValue));
  1440.  
  1441. break;
  1442.  
  1443. case 'language':
  1444. Locale.select(newValue);
  1445. break;
  1446. }
  1447. });
  1448.  
  1449. document.addEventListener('DOMContentLoaded', () => {
  1450. GM_addStyle(CSS_STYLE_TEXT);
  1451.  
  1452. pref.load();
  1453. pref.autoSave();
  1454.  
  1455. registerMenuCommands();
  1456. addSettingsMenuItem();
  1457. }, false);
  1458.  
  1459. function registerMenuCommands() {
  1460. menuCommand($str.setting + '...', togglePrefPanel);
  1461. menuCommand($str.language + '...', function() {
  1462. var langField = document.createElement('fieldset');
  1463.  
  1464. var title = document.createElement('legend');
  1465. title.textContent = $str.language;
  1466.  
  1467. var select = document.createElement('select');
  1468. Locale.languages.forEach((lang) => {
  1469. var option = document.createElement('option');
  1470. option.value = lang;
  1471. option.textContent = lang;
  1472. if (lang === Locale.selectedLanguage)
  1473. option.selected = true;
  1474.  
  1475. select.appendChild(option);
  1476. });
  1477.  
  1478. langField.appendChild(title);
  1479. langField.appendChild(select);
  1480.  
  1481. var p = new Panel();
  1482. p.appendContent(langField);
  1483. p.on('apply', () => pref.set('language', select.value));
  1484. p.open();
  1485. });
  1486. menuCommand($str.import_setting + '...', () => pref.importFromFile());
  1487. menuCommand($str.export_setting, () => pref.exportToFile());
  1488. }
  1489. function togglePrefPanel(anchorElement) {
  1490. if (rootFilterPanel) {
  1491. rootFilterPanel.close();
  1492. return;
  1493. }
  1494. rootFilterPanel = new FilterListPanel(pref.get('filter'), true);
  1495. rootFilterPanel.on('apply', () => showMessage($str.ng_setting_modified));
  1496. rootFilterPanel.on('hidden', () => {
  1497. clipboard.purge();
  1498. rootFilterPanel = null;
  1499. });
  1500. rootFilterPanel.open(anchorElement);
  1501. }
  1502. function onNGSettingCommand({target}) {
  1503. togglePrefPanel(target);
  1504. }
  1505. function createGoogButton(text, fn) {
  1506. var button = document.createElement('div');
  1507. button.className = 'goog-inline-block jfk-button jfk-button-standard unselectable';
  1508. button.tabIndex = 0;
  1509. button.textContent = text;
  1510. if (fn) {
  1511. button.addEventListener('click', fn, false);
  1512. button.addEventListener('keydown', function({which}) {
  1513. if (which === 13)
  1514. fn.apply(this, arguments);
  1515. }, false);
  1516. }
  1517.  
  1518. return button;
  1519. }
  1520. function createGoogMenuButton(text, fn, arr) {
  1521. var container = document.createElement('div');
  1522. container.className = 'goog-inline-block';
  1523.  
  1524. var button = createGoogButton(text, fn);
  1525. button.classList.add('jfk-button-collapse-right');
  1526.  
  1527. var options = document.createElement('div');
  1528. options.className = 'goog-inline-block goog-flat-menu-button goog-flat-menu-button-collapse-left unselectable';
  1529. options.tabIndex = 0;
  1530.  
  1531. container.appendChild(button);
  1532. container.appendChild(options);
  1533. options.insertAdjacentHTML('beforeend', '<div class="goog-inline-block goog-flat-menu-button-caption">&nbsp;</div>');
  1534. options.insertAdjacentHTML('beforeend', '<div class="goog-inline-block goog-flat-menu-button-dropdown"></div>');
  1535.  
  1536. new GoogMenu(options, arr);
  1537.  
  1538. return container;
  1539. }
  1540. function showMessage(str, type) {
  1541. if (typeof GM_notification === 'function')
  1542. GM_notification(str);
  1543. }
  1544. function addSettingsMenuItem() {
  1545. var feedlyTabs = document.getElementById('feedlyTabs');
  1546. if (!feedlyTabs) {
  1547. setTimeout(addSettingsMenuItem, 100);
  1548. return;
  1549. }
  1550.  
  1551. var prefListener;
  1552. var observer = new MutationObserver(function mutationCallback() {
  1553. if (!document.getElementById('feedly-ng-filter-setting'))
  1554. pref.removeListener('change', prefListener);
  1555.  
  1556. var prefItem = document.querySelector('#feedlyTabs .tab > .label[data-uri="preferences"]');
  1557. if (!prefItem)
  1558. return;
  1559.  
  1560. var prefItemTab = prefItem.parentNode;
  1561.  
  1562. var tab = document.createElement('div');
  1563. tab.className = 'tab';
  1564. tab.setAttribute('contextmenu', MenuCommand.contextmenu.id);
  1565. tab.addEventListener('click', onNGSettingCommand, false);
  1566.  
  1567. var label = document.createElement('div');
  1568. label.id = 'feedly-ng-filter-setting';
  1569. label.className = 'label primary iconless';
  1570. label.textContent = $str.ng_setting;
  1571.  
  1572. tab.appendChild(label);
  1573. prefItemTab.parentNode.insertBefore(tab, prefItemTab.nextSibling);
  1574. document.body.appendChild(contextmenu);
  1575.  
  1576. prefListener = ({propertyName}) => {
  1577. if (propertyName === 'language')
  1578. label.textContent = $str.ng_setting;
  1579. };
  1580. pref.on('change', prefListener);
  1581. });
  1582. observer.observe(feedlyTabs, {
  1583. childList: true,
  1584. });
  1585. }
  1586. function menuCommand(label, fn) {
  1587. return new MenuCommand($str.app_name + ' - ' + label, fn);
  1588. }
  1589. function xhr(details) {
  1590. var opt = extend({}, details);
  1591. var {data} = opt;
  1592.  
  1593. if (!opt.method)
  1594. opt.method = data ? 'POST' : 'GET';
  1595.  
  1596. if (data instanceof Object) {
  1597. opt.data = [pair.map(encodeURIComponent).join('=') for (pair in Iterator(data))].join('&');
  1598. if (!opt.headers)
  1599. opt.headers = {};
  1600.  
  1601. opt.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
  1602. }
  1603.  
  1604. setTimeout(GM_xmlhttpRequest, 0, opt);
  1605. }
  1606. function sendJSON(details) {
  1607. var opt = extend({}, details);
  1608. var {data} = opt;
  1609. if (!opt.headers)
  1610. opt.headers = {};
  1611.  
  1612. opt.method = 'POST';
  1613. opt.headers['Content-Type'] = 'application/json; charset=utf-8';
  1614. opt.data = JSON.stringify(data);
  1615.  
  1616. return xhr(opt);
  1617. }
  1618. function evalInContent(code) {
  1619. var script = document.createElement('script');
  1620. script.type = 'text/javascript;version=1.8';
  1621. script.textContent = code;
  1622.  
  1623. try {
  1624. location.href = 'javascript:' + encodeURIComponent(code) + ';void+0';
  1625. // document.adoptNode(document.appendChild(script));
  1626. } catch (e) {
  1627. document.adoptNode(document.documentElement.appendChild(script));
  1628. }
  1629. }
  1630. function openFilePicker(callback, multiple) {
  1631. var canceled = true;
  1632. var input = document.createElement('input');
  1633. input.type = 'file';
  1634. input.multiple = multiple;
  1635. input.addEventListener('change', () => {
  1636. canceled = false;
  1637. callback(Array.slice(input.files));
  1638. }, false);
  1639. input.click();
  1640. if (canceled)
  1641. setTimeout(callback, 0, null);
  1642. }
  1643. function createLabel(element, text) {
  1644. var label = document.createElement('label');
  1645. if (1 < arguments.length)
  1646. label.textContent = text;
  1647.  
  1648. var id = element.id;
  1649. if (!id) {
  1650. if (!('id' in createLabel))
  1651. createLabel.id = 0;
  1652.  
  1653. id = 'id_for_label_' + createLabel.id++;
  1654. element.id = id;
  1655. }
  1656. label.htmlFor = id;
  1657. return label;
  1658. }
  1659. function extend(dst, src) {
  1660. for (let [key, value] in Iterator(src))
  1661. dst[key] = value;
  1662.  
  1663. return dst;
  1664. }
  1665. function $TEXT(fn) String.replace(fn, /^\(\) => \{\/\*|\*\/}$/g, '');
  1666. })();