Feedly NG Filter

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

// ==UserScript==
// @name           Feedly NG Filter
// @namespace      https://github.com/matzkoh
// @version        1.0.0
// @description    ルールにマッチするアイテムを既読にして取り除きます。ルールは正規表現で記述でき、複数のルールをツリー状に組み合わせることができます。
// @author         matzkoh
// @include        https://feedly.com/*
// @icon           https://raw.githubusercontent.com/matzkoh/userscripts/master/packages/feedly-ng-filter/images/icon.png
// @screenshot     https://raw.githubusercontent.com/matzkoh/userscripts/master/packages/feedly-ng-filter/images/screenshot.png
// @run-at         document-start
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_addStyle
// @grant          GM_xmlhttpRequest
// @grant          GM_registerMenuCommand
// @grant          GM_unregisterMenuCommand
// @grant          GM_log
// ==/UserScript==

var $parcel$global =
  typeof globalThis !== 'undefined'
    ? globalThis
    : typeof self !== 'undefined'
    ? self
    : typeof window !== 'undefined'
    ? window
    : typeof global !== 'undefined'
    ? global
    : {}
var $parcel$modules = {}
var $parcel$inits = {}

var parcelRequire = $parcel$global['parcelRequire7c40']
if (parcelRequire == null) {
  parcelRequire = function (id) {
    if (id in $parcel$modules) {
      return $parcel$modules[id].exports
    }
    if (id in $parcel$inits) {
      var init = $parcel$inits[id]
      delete $parcel$inits[id]
      var module = { id: id, exports: {} }
      $parcel$modules[id] = module
      init.call(module.exports, module, module.exports)
      return module.exports
    }
    var err = new Error("Cannot find module '" + id + "'")
    err.code = 'MODULE_NOT_FOUND'
    throw err
  }

  parcelRequire.register = function register(id, init) {
    $parcel$inits[id] = init
  }

  $parcel$global['parcelRequire7c40'] = parcelRequire
}
parcelRequire.register('cG8Vr', function (module, exports) {
  const notificationDefaults = {
    title: 'Feedly NG Filter',
    icon: GM_info.script.icon,
    tag: 'feedly-ng-filter',
    autoClose: 5000,
  }
  const CSS_STYLE_TEXT =
    ".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  &::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\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) {\n  > .fngf-dropdown-menu {\n    display: none;\n  }\n}\n\n.fngf-dropdown-menu-item {\n  padding: 10px;\n\n  &:hover {\n    background-color: #eee;\n  }\n}\n\n.fngf-checkbox > input[type='checkbox'] {\n  display: none;\n}\n\n.fngf-only:not(:only-child) {\n  display: none;\n}\n" +
    "@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  &:focus {\n    box-shadow: 0 0 0 1px #999 inset;\n  }\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  > .fngf-btn-group:not(:first-child) {\n    margin-left: 10px;\n  }\n}\n"
  function __(strings1, ...values1) {
    let key1 = values1.map((v1, i1) => `${strings1[i1]}{${i1}}`).join('') + strings1[strings1.length - 1]
    if (!(key1 in __.data)) throw new Error(`localized string not found: ${key1}`)
    return __.data[key1].replace(/\{(\d+)\}/g, (_1, cap1) => values1[cap1])
  }
  Object.defineProperties(__, {
    config: {
      configurable: true,
      writable: true,
      value: {
        defaultLocale: 'en-US',
      },
    },
    locales: {
      configurable: true,
      writable: true,
      value: {},
    },
    data: {
      configurable: true,
      get() {
        return this.locales[this.config.locale]
      },
    },
    languages: {
      configurable: true,
      get() {
        return Object.keys(this.locales)
      },
    },
    add: {
      configurable: true,
      writable: true,
      value: function add1({ locale: locale1, data: data1 }) {
        if (locale1 in this.locales) throw new Error(`failed to add existing locale: ${locale1}`)
        this.locales[locale1] = data1
      },
    },
    use: {
      configurable: true,
      writable: true,
      value: function use1(locale1) {
        if (locale1 in this.locales) this.config.locale = locale1
        else if (this.config.defaultLocale) this.config.locale = this.config.defaultLocale
        else throw new Error(`unknown locale: ${locale1}`)
      },
    },
  })
  __.add({
    locale: 'en-US',
    data: {
      'Feedly NG Filter': 'Feedly NG Filter',
      OK: 'OK',
      Cancel: 'Cancel',
      Add: 'Add',
      Copy: 'Copy',
      Paste: 'Paste',
      'New Filter': 'New Filter',
      'Rule Name': 'Rule Name',
      'No Rules': 'No Rules',
      Title: 'Title',
      URL: 'URL',
      'Feed Title': 'Feed Title',
      'Feed URL': 'Feed URL',
      Author: 'Author',
      Keywords: 'Keywords',
      Contents: 'Contents',
      'Ignore Case': 'Ignore Case',
      Edit: 'Edit',
      Delete: 'Delete',
      'Hit Count:	{0}': 'Hit Count:	{0}',
      'Last Hit:	{0}': 'Last Hit:	{0}',
      'NG Setting': 'NG Setting',
      Setting: 'Setting',
      'Import Configuration': 'Import Configuration',
      'Preferences were successfully imported.': 'Preferences were successfully imported.',
      'Export Configuration': 'Export Configuration',
      Language: 'Language',
      'NG Settings were modified.\nNew filters take effect after next refresh.':
        'NG Settings were modified.\nNew filters take effect after next refresh.',
    },
  })
  __.add({
    locale: 'ja',
    data: {
      'Feedly NG Filter': 'Feedly NG Filter',
      OK: 'OK',
      Cancel: 'キャンセル',
      Add: '追加',
      Copy: 'コピー',
      Paste: '貼り付け',
      'New Filter': '新しいフィルタ',
      'Rule Name': 'ルール名',
      'No Rules': 'ルールはありません',
      Title: 'タイトル',
      URL: 'URL',
      'Feed Title': 'フィードのタイトル',
      'Feed URL': 'フィードの URL',
      Author: '著者',
      Keywords: 'キーワード',
      Contents: '本文',
      'Ignore Case': '大/小文字を区別しない',
      Edit: '編集',
      Delete: '削除',
      'Hit Count:	{0}': 'ヒット数:	{0}',
      'Last Hit:	{0}': '最終ヒット:	{0}',
      'NG Setting': 'NG 設定',
      Setting: '設定',
      'Import Configuration': '設定をインポート',
      'Preferences were successfully imported.': '設定をインポートしました',
      'Export Configuration': '設定をエクスポート',
      Language: '言語',
      'NG Settings were modified.\nNew filters take effect after next refresh.':
        'NG 設定を更新しました。\n新しいフィルタは次回読み込み時から有効になります。',
    },
  })
  __.use(navigator.language)
  class Serializer {
    static stringify(value1, space1) {
      return JSON.stringify(
        value1,
        (key1, value1) => {
          if (value1 instanceof RegExp)
            return {
              __serialized__: true,
              class: 'RegExp',
              args: [value1.source, value1.flags],
            }
          return value1
        },
        space1,
      )
    }
    static parse(text1) {
      return JSON.parse(text1, (key1, value1) => {
        if (value1?.__serialized__)
          switch (value1.class) {
            case 'RegExp':
              return new RegExp(...value1.args)
          }
        return value1
      })
    }
  }
  class EventEmitter {
    constructor() {
      this.listeners = {}
    }
    on(type1, listener1) {
      if (type1.trim().includes(' ')) {
        type1.match(/\S+/g).forEach(t1 => this.on(t1, listener1))
        return
      }
      if (!(type1 in this.listeners)) this.listeners[type1] = new Set()
      const set1 = this.listeners[type1]
      for (const fn1 of set1.values()) {
        if (EventEmitter.compareListener(fn1, listener1)) return
      }
      set1.add(listener1)
    }
    async once(type1, listener1) {
      return new Promise((resolve1, reject1) => {
        function wrapper1(event1) {
          this.off(wrapper1)
          try {
            EventEmitter.applyListener(this, listener1, event1)
            resolve1(event1)
          } catch (e1) {
            reject1(e1)
          }
        }
        wrapper1[EventEmitter.original] = listener1
        this.on(type1, wrapper1)
      })
    }
    off(type1, listener1) {
      if (!listener1 || !(type1 in this.listeners)) return
      const set1 = this.listeners[type1]
      for (const fn1 of set1.values()) if (EventEmitter.compareListener(fn1, listener1)) set1.delete(fn1)
    }
    removeAllListeners(type1) {
      delete this.listeners[type1]
    }
    dispatchEvent(event1) {
      event1.timestamp = Date.now()
      if (event1.type in this.listeners)
        this.listeners[event1.type].forEach(listener1 => {
          try {
            EventEmitter.applyListener(this, listener1, event1)
          } catch (e1) {
            setTimeout(() => {
              throw e1
            }, 0)
          }
        })
      return !event1.canceled
    }
    emit(type1, data1) {
      const event1 = this.createEvent(type1)
      Object.assign(event1, data1)
      return this.dispatchEvent(event1)
    }
    createEvent(type1) {
      return new Event(type1, this)
    }
    static compareListener(a1, b1) {
      return a1 === b1 || a1 === b1[EventEmitter.original] || a1[EventEmitter.original] === b1
    }
    static applyListener(target1, listener1, ...args1) {
      if (typeof listener1 === 'function') listener1.apply(target1, args1)
      else listener1.handleEvent(...args1)
    }
  }
  EventEmitter.original = Symbol('fngf.original')
  class Event {
    constructor(type1, target1) {
      this.type = type1
      this.target = target1
      this.canceled = false
      this.timestamp = 0
    }
    preventDefault() {
      this.canceled = true
    }
  }
  class DataTransfer extends EventEmitter {
    set(type1, data1) {
      this.purge()
      this.type = type1
      this.data = data1
      this.emit(type1, {
        data: data1,
      })
    }
    purge() {
      this.emit('purge', {
        data: this.data,
      })
      delete this.data
    }
    cut(data1) {
      this.set('cut', data1)
    }
    copy(data1) {
      this.set('copy', data1)
    }
    receive() {
      const data1 = this.data
      if (this.type === 'cut') this.purge()
      return data1
    }
  }
  class MenuCommand {
    constructor(label1, oncommand1) {
      this.label = label1
      this.oncommand = oncommand1
    }
    register() {
      if (typeof GM_registerMenuCommand === 'function')
        this.uuid = GM_registerMenuCommand(`${__`Feedly NG Filter`} - ${this.label}`, this.oncommand)
      if (MenuCommand.contextmenu) {
        this.menuitem = $el`<menuitem label="${this.label}" @click="${this.oncommand}">`.first
        MenuCommand.contextmenu.appendChild(this.menuitem)
      }
    }
    unregister() {
      if (typeof GM_unregisterMenuCommand === 'function') GM_unregisterMenuCommand(this.uuid)
      delete this.uuid
      document.adoptNode(this.menuitem)
    }
    static register(...args1) {
      const c1 = new MenuCommand(...args1)
      c1.register()
      return c1
    }
  }
  MenuCommand.contextmenu = null
  class Preference extends EventEmitter {
    constructor() {
      super()
      if (Preference._instance) return Preference._instance
      Preference._instance = this
      this.dict = {}
    }
    has(key1) {
      return key1 in this.dict
    }
    get(key1, def1) {
      return this.has(key1) ? this.dict[key1] : def1
    }
    set(key1, newValue1) {
      const prevValue1 = this.dict[key1]
      if (newValue1 !== prevValue1) {
        this.dict[key1] = newValue1
        this.emit('change', {
          key: key1,
          prevValue: prevValue1,
          newValue: newValue1,
        })
      }
      return newValue1
    }
    del(key1) {
      if (!this.has(key1)) return
      const prevValue1 = this.dict[key1]
      delete this.dict[key1]
      this.emit('delete', {
        key: key1,
        prevValue: prevValue1,
      })
    }
    load(str) {
      str ||= GM_getValue(Preference.prefName, Preference.defaultPref || '({})')
      let obj
      try {
        obj = Serializer.parse(str)
      } catch (e) {
        if (e instanceof SyntaxError) obj = eval(`(${str})`)
      }
      if (!obj || typeof obj !== 'object') return
      this.dict = {}
      for (const key in obj) this.set(key, obj[key])
      this.emit('load')
    }
    write() {
      this.dict.__version__ = GM_info.script.version
      GM_setValue(Preference.prefName, Serializer.stringify(this.dict))
    }
    autosave() {
      if (this.autosaveReserved) return
      window.addEventListener('unload', () => this.write(), false)
      this.autosaveReserved = true
    }
    exportToFile() {
      const blob1 = new Blob([this.serialize()], {
        type: 'application/octet-stream',
      })
      const url1 = URL.createObjectURL(blob1)
      location.assign(url1)
      URL.revokeObjectURL(url1)
    }
    importFromString(str1) {
      try {
        this.load(str1)
      } catch (e1) {
        if (!(e1 instanceof SyntaxError)) throw e1
        notify(e1)
        return false
      }
      notify(__`Preferences were successfully imported.`)
      return true
    }
    importFromFile() {
      openFilePicker().then(([file1]) => {
        const reader1 = new FileReader()
        reader1.addEventListener('load', () => this.importFromString(reader1.result), false)
        reader1.readAsText(file1)
      })
    }
    toString() {
      return '[object Preference]'
    }
    serialize() {
      return Serializer.stringify(this.dict)
    }
  }
  Preference.prefName = 'settings'
  class Draggable {
    constructor(element1, ignore1 = 'select, button, input, textarea, [tabindex]') {
      this.element = element1
      this.ignore = ignore1
      this.attach()
    }
    isDraggableTarget(target1) {
      if (!target1) return false
      if (target1 === this.element) return true
      return !target1.matches(`${this.ignore}, :-webkit-any(${this.ignore}) *`)
    }
    attach() {
      this.element.addEventListener('mousedown', this, false, false)
    }
    detach() {
      this.element.removeEventListener('mousedown', this, false)
    }
    handleEvent(event1) {
      const name1 = `on${event1.type}`
      if (name1 in this) this[name1](event1)
    }
    onmousedown(event1) {
      if (event1.button !== 0) return
      if (!this.isDraggableTarget(event1.target)) return
      event1.preventDefault()
      this.element.querySelector(':focus')?.blur()
      this.offsetX = event1.pageX - this.element.offsetLeft
      this.offsetY = event1.pageY - this.element.offsetTop
      document.addEventListener('mousemove', this, true, false)
      document.addEventListener('mouseup', this, true, false)
    }
    onmousemove(event1) {
      event1.preventDefault()
      this.element.style.left = `${event1.pageX - this.offsetX}px`
      this.element.style.top = `${event1.pageY - this.offsetY}px`
    }
    onmouseup(event1) {
      if (event1.button === 0) {
        event1.preventDefault()
        document.removeEventListener('mousemove', this, true)
        document.removeEventListener('mouseup', this, true)
      }
    }
  }
  class Filter {
    constructor(filter1 = {}) {
      this.name = filter1.name || ''
      this.regexp = {
        ...filter1.regexp,
      }
      this.children = filter1.children?.map(f1 => new Filter(f1)) || []
      this.hitcount = filter1.hitcount || 0
      this.lasthit = filter1.lasthit || 0
    }
    test(entry1) {
      let name1
      for (name1 in this.regexp) {
        if (!this.regexp[name1].test(entry1[name1] || '')) return false
      }
      const hit1 = this.children.length ? this.children.some(filter1 => filter1.test(entry1)) : !!name1
      if (hit1 && entry1.unread) {
        this.hitcount++
        this.lasthit = Date.now()
      }
      return hit1
    }
    appendChild(filter1) {
      if (!(filter1 instanceof Filter)) return null
      this.removeChild(filter1)
      this.children.push(filter1)
      this.sortChildren()
      return filter1
    }
    removeChild(filter1) {
      if (!(filter1 instanceof Filter)) return null
      const index1 = this.children.indexOf(filter1)
      if (index1 !== -1) this.children.splice(index1, 1)
      return filter1
    }
    sortChildren() {
      return this.children.sort((a1, b1) => b1.name < a1.name)
    }
  }
  class Entry {
    constructor(data1) {
      this.data = data1
    }
    get title() {
      const value1 = $el`<div>${this.data.title || ''}`.first.textContent
      Object.defineProperty(this, 'title', {
        configurable: true,
        value: value1,
      })
      return value1
    }
    get id() {
      return this.data.id
    }
    get url() {
      return this.data.alternate?.[0]?.href
    }
    get sourceTitle() {
      return this.data.origin.title
    }
    get sourceURL() {
      return this.data.origin.streamId.replace(/^[^/]+\//, '')
    }
    get body() {
      return (this.data.content || this.data.summary)?.content
    }
    get author() {
      return this.data.author
    }
    get recrawled() {
      return this.data.recrawled
    }
    get published() {
      return this.data.published
    }
    get updated() {
      return this.data.updated
    }
    get keywords() {
      return this.data.keywords?.join(',') || ''
    }
    get unread() {
      return this.data.unread
    }
    get tags() {
      return this.data.tags.map(tag1 => tag1.label)
    }
  }
  class Panel extends EventEmitter {
    constructor() {
      super()
      this.opened = false
      const onSubmit1 = event1 => {
        event1.preventDefault()
        event1.stopPropagation()
        this.apply()
      }
      const onKeyPress1 = event1 => {
        if (event1.keyCode === KeyboardEvent.DOM_VK_ESCAPE) this.emit('escape')
      }
      const {
        element: element1,
        body: body1,
        buttons: buttons1,
      } = $el`
      <form class="fngf-panel" @submit="${onSubmit1}" @keydown="${onKeyPress1}" ref="element">
        <input type="submit" style="display: none;">
        <div class="fngf-panel-body fngf-column" ref="body"></div>
        <div class="fngf-panel-buttons fngf-row" ref="buttons">
          <div class="fngf-btn-group fngf-row">
            <button type="button" class="fngf-btn" @click="${() => this.apply()}">${__`OK`}</button>
            <button type="button" class="fngf-btn" @click="${() => this.close()}">${__`Cancel`}</button>
          </div>
        </div>
      </form>
    `
      new Draggable(element1)
      this.dom = {
        element: element1,
        body: body1,
        buttons: buttons1,
      }
    }
    open(anchorElement1) {
      if (this.opened) return
      if (!this.emit('showing')) return
      if (anchorElement1?.nodeType !== 1) anchorElement1 = null
      document.body.appendChild(this.dom.element)
      this.opened = true
      this.snapTo(anchorElement1)
      if (anchorElement1) {
        const onWindowResize1 = () => this.snapTo(anchorElement1)
        window.addEventListener('resize', onWindowResize1, false)
        this.on('hidden', () => window.removeEventListener('resize', onWindowResize1, false))
      }
      document.querySelector(':focus')?.blur()
      const selector1 = ':not(.feedlyng-panel) > :-webkit-any(button, input, select, textarea, [tabindex])'
      const ctrl1 = Array.from(this.dom.element.querySelectorAll(selector1)).sort(
        (a1, b1) => (b1.tabIndex || 0) < (a1.tabIndex || 0),
      )[0]
      if (ctrl1) {
        ctrl1.focus()
        if (ctrl1.select) ctrl1.select()
      }
      this.emit('shown')
    }
    apply() {
      if (this.emit('apply')) this.close()
    }
    close() {
      if (!this.opened) return
      if (!this.emit('hiding')) return
      document.adoptNode(this.dom.element)
      this.opened = false
      this.emit('hidden')
    }
    toggle(anchorElement1) {
      if (this.opened) this.close()
      else this.open(anchorElement1)
    }
    moveTo(x1, y1) {
      this.dom.element.style.left = `${x1}px`
      this.dom.element.style.top = `${y1}px`
    }
    snapTo(anchorElement1) {
      const pad1 = 5
      let x1 = pad1
      let y1 = pad1
      if (anchorElement1) {
        let { left: left1, bottom: top1 } = anchorElement1.getBoundingClientRect()
        left1 += pad1
        top1 += pad1
        const { width: width1, height: height1 } = this.dom.element.getBoundingClientRect()
        const right1 = left1 + width1 + pad1
        const bottom1 = top1 + height1 + pad1
        const { innerWidth: innerWidth1, innerHeight: innerHeight1 } = window
        if (innerWidth1 < right1) left1 -= right1 - innerWidth1
        if (innerHeight1 < bottom1) top1 -= bottom1 - innerHeight1
        x1 = Math.max(x1, left1)
        y1 = Math.max(y1, top1)
      }
      this.moveTo(x1, y1)
    }
    getFormData(asElement1) {
      const data1 = {}
      const elements1 = this.dom.body.querySelectorAll('[name]')
      function getValue1(el1) {
        if (el1.localName === 'input' && (el1.type === 'checkbox' || el1.type === 'radio')) return el1.checked
        return 'value' in el1 ? el1.value : el1.getAttribute('value')
      }
      for (const el1 of elements1) {
        const value1 = asElement1 ? el1 : getValue1(el1)
        const path1 = el1.name.split('.')
        let leaf1 = path1.pop()
        const cd1 = path1.reduce((parent1, key1) => {
          if (!(key1 in parent1)) parent1[key1] = {}
          return parent1[key1]
        }, data1)
        if (leaf1.endsWith('[]')) {
          leaf1 = leaf1.slice(0, -2)
          if (!(leaf1 in cd1)) cd1[leaf1] = []
          cd1[leaf1].push(value1)
        } else cd1[leaf1] = value1
      }
      return data1
    }
    appendContent(element1) {
      if (element1 instanceof Array) return element1.map(el1 => this.appendContent(el1))
      return this.dom.body.appendChild(element1)
    }
    removeContents() {
      this.dom.body.innerHTML = ''
    }
  }
  class FilterListPanel extends Panel {
    constructor(filter1, isRoot1) {
      super()
      this.filter = filter1
      if (isRoot1) this.dom.element.classList.add('root')
      const onAdd1 = () => {
        const filter1 = new Filter()
        filter1.name = __`New Filter`
        this.on('apply', () => this.filter.appendChild(filter1))
        this.appendFilter(filter1)
      }
      const onPaste1 = () => {
        if (!clipboard.data) return
        const filter1 = new Filter(clipboard.receive())
        this.on('apply', () => this.filter.appendChild(filter1))
        this.appendFilter(filter1)
      }
      const { buttons: buttons1, paste: paste1 } = $el`
      <div class="fngf-btn-group fngf-row" ref="buttons">
        <button type="button" class="fngf-btn" @click="${onAdd1}">${__`Add`}</button>
        <button type="button" class="fngf-btn" @click="${onPaste1}" ref="paste" disabled>${__`Paste`}</button>
      </div>
    `
      function pasteState1() {
        paste1.disabled = !clipboard.data
      }
      clipboard.on('copy', pasteState1)
      clipboard.on('purge', pasteState1)
      pasteState1()
      this.dom.buttons.insertBefore(buttons1, this.dom.buttons.firstChild)
      this.on('escape', () => this.close())
      this.on('showing', this.initContents)
      this.on('apply', this)
      this.on('hidden', () => {
        clipboard.off('copy', pasteState1)
        clipboard.off('purge', pasteState1)
      })
    }
    initContents() {
      const filter1 = this.filter
      const {
        name: name1,
        terms: terms1,
        rules: rules1,
      } = $el`
      <div class="fngf-panel-name fngf-row fngf-align-center" ref="name">
        ${__`Rule Name`}&nbsp;
        <input type="text" value="${filter1.name}" autocomplete="off" name="name" class="fngf-grow">
      </div>
      <div class="fngf-panel-terms" ref="terms"></div>
      <div class="fngf-panel-rules fngf-column" ref="rules">
        <div class="fngf-panel-rule fngf-row fngf-align-center fngf-only">${__`No Rules`}</div>
      </div>
    `
      const labels1 = [
        ['title', __`Title`],
        ['url', __`URL`],
        ['sourceTitle', __`Feed Title`],
        ['sourceURL', __`Feed URL`],
        ['author', __`Author`],
        ['keywords', __`Keywords`],
        ['body', __`Contents`],
      ]
      for (const [type1, labelText1] of labels1) {
        const randomId1 = `id-${Math.random().toFixed(8)}`
        const reg1 = filter1.regexp[type1]
        const sourceValue1 = reg1 ? reg1.source.replace(/((?:^|[^\\])(?:\\\\)*)\\(?=\/)/g, '$1') : ''
        terms1.appendChild($el`
        <label for="${randomId1}">${labelText1}</label>
        <input type="text" class="fngf-panel-terms-textbox" id="${randomId1}" autocomplete="off" name="regexp.${type1}.source" value="${sourceValue1}">
        <label class="fngf-checkbox fngf-row" title="${__`Ignore Case`}">
          <input type="checkbox" name="regexp.${type1}.ignoreCase" bool:checked="${reg1?.ignoreCase}">
          <span class="fngf-btn" tabindex="0">i</span>
        </label>
      `)
      }
      this.appendContent([name1, terms1, rules1])
      this.dom.rules = rules1
      filter1.children.forEach(this.appendFilter, this)
    }
    appendFilter(filter1) {
      let panel1
      const updateRow1 = () => {
        let title1 = __`Hit Count:\t${filter1.hitcount}`
        if (filter1.lasthit) {
          title1 += '\n'
          title1 += __`Last Hit:\t${new Date(filter1.lasthit).toLocaleString()}`
        }
        rule1.title = title1
        name1.textContent = filter1.name
        count1.textContent = filter1.children.length || ''
      }
      const onEdit1 = () => {
        if (panel1) {
          panel1.close()
          return
        }
        panel1 = new FilterListPanel(filter1)
        panel1.on('shown', () => btnEdit1.classList.add('active'))
        panel1.on('hidden', () => {
          btnEdit1.classList.remove('active')
          panel1 = null
        })
        panel1.on('apply', () => setTimeout(updateRow1, 0))
        panel1.open(btnEdit1)
      }
      const onCopy1 = () => clipboard.copy(filter1)
      const onDelete1 = () => {
        document.adoptNode(rule1)
        this.on('apply', () => this.filter.removeChild(filter1))
      }
      const {
        rule: rule1,
        name: name1,
        count: count1,
        btnEdit: btnEdit1,
      } = $el`
      <div class="fngf-panel-rule fngf-row fngf-align-center" ref="rule">
        <div class="fngf-panel-rule-name" @dblclick="${onEdit1}" ref="name"></div>
        <div class="fngf-panel-rule-count fngf-badge" ref="count"></div>
        <div class="fngf-panel-rule-actions fngf-btn-group fngf-menu-btn fngf-row" ref="buttons">
          <button type="button" class="fngf-btn" @click="${onEdit1}" ref="btnEdit">${__`Edit`}</button>
          <div class="fngf-dropdown fngf-btn" tabindex="0">
            <div class="fngf-dropdown-menu fngf-column">
              <div class="fngf-dropdown-menu-item" @click="${onCopy1}">${__`Copy`}</div>
              <div class="fngf-dropdown-menu-item" @click="${onDelete1}">${__`Delete`}</div>
            </div>
          </div>
        </div>
      </div>
    `
      updateRow1()
      this.dom.rules.appendChild(rule1)
    }
    handleEvent(event1) {
      if (event1.type !== 'apply') return
      const data1 = this.getFormData(true)
      const filter1 = this.filter
      const regexp1 = {}
      let hasError1 = false
      for (const type1 in data1.regexp) {
        const { source: source1, ignoreCase: ignoreCase1 } = data1.regexp[type1]
        if (!source1.value) continue
        try {
          regexp1[type1] = new RegExp(source1.value, ignoreCase1.checked ? 'i' : '')
        } catch (e1) {
          if (!(e1 instanceof SyntaxError)) throw e1
          hasError1 = true
          event1.preventDefault()
          source1.classList.remove('error')
          source1.offsetWidth.valueOf()
          source1.classList.add('error')
        }
      }
      if (hasError1) return
      const prevSource1 = Serializer.stringify(filter1)
      filter1.name = data1.name.value
      filter1.regexp = regexp1
      if (Serializer.stringify(filter1) !== prevSource1) {
        filter1.hitcount = 0
        filter1.lasthit = 0
      }
      filter1.sortChildren()
    }
  }
  Preference.defaultPref = Serializer.stringify({
    filter: {
      name: '',
      regexp: {},
      children: [
        {
          name: 'AD',
          regexp: {
            title: /^\W?(?:ADV?|PR)\b/,
          },
          children: [],
        },
      ],
    },
  })
  evalInContent(() => {
    const XHR1 = XMLHttpRequest
    let uniqueId1 = 0
    window.XMLHttpRequest = function XMLHttpRequest1() {
      const req1 = new XHR1()
      req1.open = open1
      req1.setRequestHeader = setRequestHeader1
      req1.addEventListener('readystatechange', onReadyStateChange1, false)
      return req1
    }
    function open1(method1, url1, ...args1) {
      this.__url__ = url1
      return XHR1.prototype.open.call(this, method1, url1, ...args1)
    }
    function setRequestHeader1(header1, value1) {
      if (header1 === 'Authorization') this.__auth__ = value1
      return XHR1.prototype.setRequestHeader.call(this, header1, value1)
    }
    function onReadyStateChange1() {
      if (this.readyState < 4 || this.status !== 200) return
      if (!/^(?:https?:)?\/\/(?:(?:api|cloud)\.)?feedly\.com\/v3\/streams\/contents\b/.test(this.__url__)) return
      const pongEventType1 = 'streamcontentloaded_callback' + uniqueId1++
      const data1 = JSON.stringify({
        type: pongEventType1,
        auth: this.__auth__,
        text: this.responseText,
      })
      const event1 = new MessageEvent('streamcontentloaded', {
        bubbles: true,
        cancelable: false,
        data: data1,
        origin: location.href,
        source: null,
      })
      const onPong1 = ({ data: data1 }) =>
        Object.defineProperty(this, 'responseText', {
          configurable: true,
          value: data1,
        })
      document.addEventListener(pongEventType1, onPong1, false)
      document.dispatchEvent(event1)
      document.removeEventListener(pongEventType1, onPong1, false)
    }
  })
  const clipboard = new DataTransfer()
  const pref = new Preference()
  let rootFilterPanel
  let { contextmenu } = $el`
  <menu type="context" id="feedlyng-contextmenu">
    <menu type="context" label="${__`Feedly NG Filter`}" ref="contextmenu"></menu>
  </menu>
`
  MenuCommand.contextmenu = contextmenu
  pref.on('change', function ({ key: key1, newValue: newValue1 }) {
    switch (key1) {
      case 'filter':
        if (!(newValue1 instanceof Filter)) this.set('filter', new Filter(newValue1))
        break
      case 'language':
        __.use(newValue1)
        break
    }
  })
  document.addEventListener(
    'streamcontentloaded',
    event1 => {
      const logging1 = pref.get('logging', true)
      const filter1 = pref.get('filter')
      const filteredEntryIds1 = []
      const { type: pongEventType1, auth: auth1, text: text1 } = JSON.parse(event1.data)
      const data1 = JSON.parse(text1)
      let hasUnread1 = false
      data1.items = data1.items.filter(item1 => {
        const entry1 = new Entry(item1)
        if (!filter1.test(entry1)) return true
        if (logging1) GM_log(`filtered: "${entry1.title || ''}" ${entry1.url}`)
        filteredEntryIds1.push(entry1.id)
        if (entry1.unread) hasUnread1 = true
        return false
      })
      if (!filteredEntryIds1.length) return
      let ev1 = new MessageEvent(pongEventType1, {
        bubbles: true,
        cancelable: false,
        data: JSON.stringify(data1),
        origin: location.href,
        source: unsafeWindow,
      })
      document.dispatchEvent(ev1)
      if (!hasUnread1) return
      sendJSON({
        url: '/v3/markers',
        headers: {
          Authorization: auth1,
        },
        data: {
          action: 'markAsRead',
          entryIds: filteredEntryIds1,
          type: 'entries',
        },
      })
    },
    false,
  )
  document.addEventListener(
    'DOMContentLoaded',
    () => {
      GM_addStyle(CSS_STYLE_TEXT)
      pref.load()
      pref.autosave()
      registerMenuCommands()
      addSettingsMenuItem()
    },
    false,
  )
  document.addEventListener(
    'mousedown',
    ({ target: target1 }) => {
      if (target1.matches('.fngf-dropdown')) target1.classList.toggle('active')
      if (!target1.closest('.fngf-dropdown'))
        document.querySelector('.fngf-dropdown.active')?.classList.remove('active')
    },
    true,
  )
  document.addEventListener(
    'click',
    ({ target: target1 }) => {
      if (target1.closest('.fngf-dropdown-menu-item')) target1.closest('.fngf-dropdown')?.classList.remove('active')
    },
    true,
  )
  function $el(strings1, ...values1) {
    let html1 = ''
    if (typeof strings1 === 'string') html1 = strings1
    else {
      values1.forEach((v1, i1) => {
        html1 += strings1[i1]
        if (v1 === null || v1 === undefined) return
        if (v1 instanceof Node || v1 instanceof NodeList || v1 instanceof HTMLCollection || v1 instanceof Array) {
          html1 += `<!--${$el.dataPrefix}${i1}-->`
          if (v1 instanceof Node) return
          values1[i1] = document.createDocumentFragment()
          for (const item1 of v1) values1[i1].appendChild(item1)
          return
        }
        html1 += v1 instanceof Object ? i1 : v1
      })
      html1 += strings1[strings1.length - 1]
    }
    const renderer1 = document.createElement('template')
    const container1 = document.createElement('body')
    const refs1 = document.createDocumentFragment()
    renderer1.innerHTML = html1
    container1.appendChild(renderer1.content)
    refs1.first = container1.firstElementChild
    refs1.last = container1.lastElementChild
    const exp1 = `
    .//*[@ref or @*[starts-with(name(), "@") or contains(name(), ":")]] |
    .//comment()[starts-with(., "${$el.dataPrefix}")]
  `
    const xpath1 = document.evaluate(exp1, container1, null, 7, null)
    for (let i1 = 0; i1 < xpath1.snapshotLength; i1++) {
      const el1 = xpath1.snapshotItem(i1)
      if (el1.nodeType === document.COMMENT_NODE) {
        const index1 = el1.data.substring($el.dataPrefix.length)
        el1.parentNode.replaceChild(values1[index1], el1)
        continue
      }
      for (const { name: name1, value: value1 } of Array.from(el1.attributes)) {
        const data1 = values1[value1]
        if (name1 === 'ref') refs1[value1] = el1
        else if (name1.startsWith('@')) $el.func(el1, name1.substring(1), data1)
        else if (name1 === ':class') for (const k1 of Object.keys(data1)) el1.classList.toggle(k1, data1[k1])
        else if (name1.startsWith('bool:')) el1[name1.substring(5)] = data1
        else continue
        el1.removeAttribute(name1)
      }
    }
    Array.from(container1.childNodes).forEach(node1 => refs1.appendChild(node1))
    return refs1
  }
  $el.dataPrefix = '$el.data:'
  $el.func = (el1, type1, fn1) => {
    if (type1) el1.addEventListener(type1, fn1, false)
    else
      try {
        fn1.call(el1, el1)
      } catch (e1) {
        console.error(e1)
      }
  }
  function xhr(details1) {
    const opt1 = {
      ...details1,
    }
    const { data: data1 } = opt1
    opt1.method ||= data1 ? 'POST' : 'GET'
    if (data1 instanceof Object) {
      opt1.headers ||= {}
      opt1.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
      opt1.data = Object.entries(data1)
        .map(kv1 => kv1.map(encodeURIComponent).join('='))
        .join('&')
    }
    setTimeout(() => GM_xmlhttpRequest(opt1), 0)
  }
  function registerMenuCommands() {
    MenuCommand.register(`${__`Setting`}...`, togglePrefPanel)
    MenuCommand.register(`${__`Language`}...`, () => {
      const { langField: langField1, select: select1 } = $el(`
      <fieldset ref="langField">
        <legend>${__`Language`}</legend>
        <select ref="select"></select>
      </fieldset>
    `)
      __.languages.forEach(lang1 => {
        const option1 = $el(`<option value="${lang1}">${lang1}</option>`).first
        if (lang1 === __.config.locale) option1.selected = true
        select1.appendChild(option1)
      })
      const panel1 = new Panel()
      panel1.appendContent(langField1)
      panel1.on('apply', () => pref.set('language', select1.value))
      panel1.open()
    })
    MenuCommand.register(`${__`Import Configuration`}...`, () => pref.importFromFile())
    MenuCommand.register(__`Export Configuration`, () => pref.exportToFile())
  }
  function sendJSON(details1) {
    const opt1 = {
      ...details1,
    }
    const { data: data1 } = opt1
    opt1.headers ||= {}
    opt1.method = 'POST'
    opt1.headers['Content-Type'] = 'application/json; charset=utf-8'
    opt1.data = JSON.stringify(data1)
    return xhr(opt1)
  }
  function evalInContent(code1) {
    const script1 = document.createElement('script')
    script1.textContent = typeof code1 === 'function' ? `(${code1})()` : code1
    document.documentElement.appendChild(script1)
    document.adoptNode(script1)
  }
  function togglePrefPanel(anchorElement1) {
    if (rootFilterPanel) {
      rootFilterPanel.close()
      return
    }
    rootFilterPanel = new FilterListPanel(pref.get('filter'), true)
    rootFilterPanel.on('apply', () =>
      notify(__`NG Settings were modified.\nNew filters take effect after next refresh.`),
    )
    rootFilterPanel.on('hidden', () => {
      clipboard.purge()
      rootFilterPanel = null
    })
    rootFilterPanel.open(anchorElement1)
  }
  function onNGSettingCommand({ target: target1 }) {
    togglePrefPanel(target1)
  }
  function addSettingsMenuItem() {
    if (!document.getElementById('filtertab')) {
      setTimeout(addSettingsMenuItem, 100)
      return
    }
    let prefListener1
    function onMutation1() {
      if (document.getElementById('feedly-ng-filter-setting')) return
      const nativeFilterItem1 = document.getElementById('filtertab')
      if (!nativeFilterItem1) return
      if (prefListener1) pref.off('change', prefListener1)
      const { tab: tab1, label: label1 } = $el`
      <div class="tab" contextmenu="${MenuCommand.contextmenu.parentNode.id}" @click="${onNGSettingCommand}" ref="tab">
        <div class="header target">
          <img class="icon" src="${GM_info.script.icon}" style="cursor: pointer;">
          <div class="label nonEmpty" id="feedly-ng-filter-setting" ref="label"></div>
        </div>
      </div>
    `
      label1.textContent = __`NG Setting`
      nativeFilterItem1.parentNode.insertBefore(tab1, nativeFilterItem1.nextSibling)
      document.body.appendChild(contextmenu.parentNode)
      prefListener1 = ({ key: key1 }) => {
        if (key1 === 'language') label1.textContent = __`NG Setting`
      }
      pref.on('change', prefListener1)
    }
    new MutationObserver(onMutation1).observe(document.getElementById('feedlyTabs'), {
      childList: true,
      subtree: true,
    })
    onMutation1()
  }
  async function openFilePicker(multiple1) {
    return new Promise(resolve1 => {
      const input1 = $el`<input type="file" @change="${() => resolve1(Array.from(input1.files))}">`.first
      input1.multiple = multiple1
      input1.click()
    })
  }
  async function notify(body1, options1) {
    options1 = {
      body: body1,
      ...notificationDefaults,
      ...options1,
    }
    return new Promise((resolve1, reject1) => {
      Notification.requestPermission(status1 => {
        if (status1 !== 'granted') {
          reject1(status1)
          return
        }
        const n1 = new Notification(options1.title, options1)
        if (options1.autoClose) setTimeout(() => n1.close(), options1.autoClose)
        resolve1(n1)
      })
    })
  }
})

parcelRequire('cG8Vr')