Feedly NG Filter

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

当前为 2015-11-07 提交的版本,查看 最新版本

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