Feedly NG Filter

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

当前为 2019-02-13 提交的版本,查看 最新版本

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