您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ルールにマッチするアイテムを既読にして取り除きます。ルールは正規表現で記述でき、複数のルールをツリー状に組み合わせることができます。
当前为
- // ==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==
- ;(function() {
- // ASSET: index.js
- var $Focm$exports = function() {
- var exports = this
- var module = {
- exports: this,
- }
- const fs = {}
- 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\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" +
- "@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"
- function __(strings, ...values) {
- let key = values.map((v, i) => `${strings[i]}{${i}}`).join('') + strings[strings.length - 1]
- if (!(key in __.data)) {
- throw new Error(`localized string not found: ${key}`)
- }
- return __.data[key].replace(/\{(\d+)\}/g, (_, cap) => values[cap])
- }
- 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 add({ locale, data }) {
- if (locale in this.locales) {
- throw new Error(`failed to add existing locale: ${locale}`)
- }
- this.locales[locale] = data
- },
- },
- use: {
- configurable: true,
- writable: true,
- value: function use(locale) {
- if (locale in this.locales) {
- this.config.locale = locale
- } else if (this.config.defaultLocale) {
- this.config.locale = this.config.defaultLocale
- } else {
- throw new Error(`unknown locale: ${locale}`)
- }
- },
- },
- })
- __.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:\t{0}': 'Hit Count:\t{0}',
- 'Last Hit:\t{0}': 'Last Hit:\t{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:\t{0}': 'ヒット数:\t{0}',
- 'Last Hit:\t{0}': '最終ヒット:\t{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(value, space) {
- return JSON.stringify(
- value,
- (key, value) => {
- if (value instanceof RegExp) {
- return {
- __serialized__: true,
- class: 'RegExp',
- args: [value.source, value.flags],
- }
- }
- return value
- },
- space,
- )
- }
- static parse(text) {
- return JSON.parse(text, (key, value) => {
- if (value === null || value === void 0 ? void 0 : value.__serialized__) {
- switch (value.class) {
- case 'RegExp':
- return new RegExp(...value.args)
- }
- }
- return value
- })
- }
- }
- class EventEmitter {
- constructor() {
- this.listeners = {}
- }
- on(type, listener) {
- if (type.trim().includes(' ')) {
- type.match(/\S+/g).forEach(t => this.on(t, listener))
- return
- }
- if (!(type in this.listeners)) {
- this.listeners[type] = new Set()
- }
- const set = this.listeners[type]
- for (const fn of set.values()) {
- if (EventEmitter.compareListener(fn, listener)) {
- return
- }
- }
- set.add(listener)
- }
- async once(type, listener) {
- return new Promise((resolve, reject) => {
- function wrapper(event) {
- this.off(wrapper)
- try {
- EventEmitter.applyListener(this, listener, event)
- resolve(event)
- } catch (e) {
- reject(e)
- }
- }
- wrapper[EventEmitter.original] = listener
- this.on(type, wrapper)
- })
- }
- off(type, listener) {
- if (!listener || !(type in this.listeners)) {
- return
- }
- const set = this.listeners[type]
- for (const fn of set.values()) {
- if (EventEmitter.compareListener(fn, listener)) {
- set.delete(fn)
- }
- }
- }
- removeAllListeners(type) {
- delete this.listeners[type]
- }
- dispatchEvent(event) {
- event.timestamp = Date.now()
- if (event.type in this.listeners) {
- this.listeners[event.type].forEach(listener => {
- try {
- EventEmitter.applyListener(this, listener, event)
- } catch (e) {
- setTimeout(
- () =>
- (function(e) {
- throw e
- })(e),
- 0,
- )
- }
- })
- }
- return !event.canceled
- }
- emit(type, data) {
- const event = this.createEvent(type)
- Object.assign(event, data)
- return this.dispatchEvent(event)
- }
- createEvent(type) {
- return new Event(type, this)
- }
- static compareListener(a, b) {
- return a === b || a === b[EventEmitter.original] || a[EventEmitter.original] === b
- }
- static applyListener(target, listener, ...args) {
- if (typeof listener === 'function') {
- listener.apply(target, args)
- } else {
- listener.handleEvent(...args)
- }
- }
- }
- EventEmitter.original = Symbol('fngf.original')
- class Event {
- constructor(type, target) {
- this.type = type
- this.target = target
- this.canceled = false
- this.timestamp = 0
- }
- preventDefault() {
- this.canceled = true
- }
- }
- class DataTransfer extends EventEmitter {
- set(type, data) {
- this.purge()
- this.type = type
- this.data = data
- this.emit(type, {
- data,
- })
- }
- purge() {
- this.emit('purge', {
- data: this.data,
- })
- delete this.data
- }
- cut(data) {
- this.set('cut', data)
- }
- copy(data) {
- this.set('copy', data)
- }
- receive() {
- const data = this.data
- if (this.type === 'cut') {
- this.purge()
- }
- return data
- }
- }
- class MenuCommand {
- constructor(label, oncommand) {
- this.label = label
- this.oncommand = oncommand
- }
- 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(...args) {
- const c = new MenuCommand(...args)
- c.register()
- return c
- }
- }
- MenuCommand.contextmenu = null
- class Preference extends EventEmitter {
- constructor() {
- super()
- if (Preference._instance) {
- return Preference._instance
- }
- Preference._instance = this
- this.dict = {}
- }
- has(key) {
- return key in this.dict
- }
- get(key, def) {
- return this.has(key) ? this.dict[key] : def
- }
- set(key, newValue) {
- const prevValue = this.dict[key]
- if (newValue !== prevValue) {
- this.dict[key] = newValue
- this.emit('change', {
- key,
- prevValue,
- newValue,
- })
- }
- return newValue
- }
- del(key) {
- if (!this.has(key)) {
- return
- }
- const prevValue = this.dict[key]
- delete this.dict[key]
- this.emit('delete', {
- key,
- prevValue,
- })
- }
- load(str) {
- 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() {
- var _this$dict, _ref
- this.dict.__version__ = GM_info.script.version
- ;(_ref = ((_this$dict = this.dict), Serializer.stringify.bind(Serializer)(_this$dict))),
- GM_setValue(Preference.prefName, _ref)
- }
- autosave() {
- if (this.autosaveReserved) {
- return
- }
- window.addEventListener('unload', this.write.bind(this), false)
- this.autosaveReserved = true
- }
- exportToFile() {
- const blob = new Blob([this.serialize()], {
- type: 'application/octet-stream',
- })
- const url = URL.createObjectURL(blob)
- location.assign(url)
- URL.revokeObjectURL(url)
- }
- importFromString(str) {
- try {
- this.load(str)
- } catch (e) {
- if (!(e instanceof SyntaxError)) {
- throw e
- }
- notify(e)
- return false
- }
- notify(__`Preferences were successfully imported.`)
- return true
- }
- importFromFile() {
- openFilePicker().then(([file]) => {
- const reader = new FileReader()
- reader.addEventListener('load', () => this.importFromString(reader.result), false)
- reader.readAsText(file)
- })
- }
- toString() {
- return '[object Preference]'
- }
- serialize() {
- return Serializer.stringify(this.dict)
- }
- }
- Preference.prefName = 'settings'
- class Draggable {
- constructor(element, ignore = 'select, button, input, textarea, [tabindex]') {
- this.element = element
- this.ignore = ignore
- this.attach()
- }
- isDraggableTarget(target) {
- if (!target) {
- return false
- }
- if (target === this.element) {
- return true
- }
- return !target.matches(`${this.ignore}, :-webkit-any(${this.ignore}) *`)
- }
- attach() {
- this.element.addEventListener('mousedown', this, false, false)
- }
- detach() {
- this.element.removeEventListener('mousedown', this, false)
- }
- handleEvent(event) {
- const name = `on${event.type}`
- if (name in this) {
- this[name](event)
- }
- }
- onmousedown(event) {
- var _this$element$querySe
- if (event.button !== 0) {
- return
- }
- if (!this.isDraggableTarget(event.target)) {
- return
- }
- event.preventDefault()
- ;(_this$element$querySe = this.element.querySelector(':focus')) === null || _this$element$querySe === void 0
- ? void 0
- : _this$element$querySe.blur()
- this.offsetX = event.pageX - this.element.offsetLeft
- this.offsetY = event.pageY - this.element.offsetTop
- document.addEventListener('mousemove', this, true, false)
- document.addEventListener('mouseup', this, true, false)
- }
- onmousemove(event) {
- event.preventDefault()
- this.element.style.left = `${event.pageX - this.offsetX}px`
- this.element.style.top = `${event.pageY - this.offsetY}px`
- }
- onmouseup(event) {
- if (event.button === 0) {
- event.preventDefault()
- document.removeEventListener('mousemove', this, true)
- document.removeEventListener('mouseup', this, true)
- }
- }
- }
- class Filter {
- constructor(filter = {}) {
- var _filter$children
- this.name = filter.name || ''
- this.regexp = { ...filter.regexp }
- this.children =
- ((_filter$children = filter.children) === null || _filter$children === void 0
- ? void 0
- : _filter$children.map(f => new Filter(f))) || []
- this.hitcount = filter.hitcount || 0
- this.lasthit = filter.lasthit || 0
- }
- test(entry) {
- let name
- for (name in this.regexp) {
- if (!this.regexp[name].test(entry[name] || '')) {
- return false
- }
- }
- const hit = this.children.length ? this.children.some(filter => filter.test(entry)) : !!name
- if (hit && entry.unread) {
- this.hitcount++
- this.lasthit = Date.now()
- }
- return hit
- }
- appendChild(filter) {
- if (!(filter instanceof Filter)) {
- return null
- }
- this.removeChild(filter)
- this.children.push(filter)
- this.sortChildren()
- return filter
- }
- removeChild(filter) {
- if (!(filter instanceof Filter)) {
- return null
- }
- const index = this.children.indexOf(filter)
- if (index !== -1) {
- this.children.splice(index, 1)
- }
- return filter
- }
- sortChildren() {
- return this.children.sort((a, b) => b.name < a.name)
- }
- }
- class Entry {
- constructor(data) {
- this.data = data
- }
- get title() {
- const value = $el`<div>${this.data.title || ''}`.first.textContent
- Object.defineProperty(this, 'title', {
- configurable: true,
- value,
- })
- return value
- }
- get id() {
- return this.data.id
- }
- get url() {
- var _this$data$alternate, _this$data$alternate$
- return (_this$data$alternate = this.data.alternate) === null || _this$data$alternate === void 0
- ? void 0
- : (_this$data$alternate$ = _this$data$alternate[0]) === null || _this$data$alternate$ === void 0
- ? void 0
- : _this$data$alternate$.href
- }
- get sourceTitle() {
- return this.data.origin.title
- }
- get sourceURL() {
- return this.data.origin.streamId.replace(/^[^/]+\//, '')
- }
- get body() {
- var _ref2
- return (_ref2 = this.data.content || this.data.summary) === null || _ref2 === void 0 ? void 0 : _ref2.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() {
- var _this$data$keywords
- return (
- ((_this$data$keywords = this.data.keywords) === null || _this$data$keywords === void 0
- ? void 0
- : _this$data$keywords.join(',')) || ''
- )
- }
- get unread() {
- return this.data.unread
- }
- get tags() {
- return this.data.tags.map(tag => tag.label)
- }
- }
- class Panel extends EventEmitter {
- constructor() {
- super()
- this.opened = false
- const onSubmit = event => {
- event.preventDefault()
- event.stopPropagation()
- this.apply()
- }
- const onKeyPress = event => {
- if (event.keyCode === KeyboardEvent.DOM_VK_ESCAPE) {
- this.emit('escape')
- }
- }
- const { element, body, buttons } = $el`
- <form class="fngf-panel" @submit="${onSubmit}" @keydown="${onKeyPress}" 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.bind(this)}">${__`OK`}</button>
- <button type="button" class="fngf-btn" @click="${this.close.bind(this)}">${__`Cancel`}</button>
- </div>
- </div>
- </form>
- `
- new Draggable(element)
- this.dom = {
- element,
- body,
- buttons,
- }
- }
- open(anchorElement) {
- var _anchorElement, _document$querySelect
- if (this.opened) {
- return
- }
- if (!this.emit('showing')) {
- return
- }
- if (
- ((_anchorElement = anchorElement) === null || _anchorElement === void 0
- ? void 0
- : _anchorElement.nodeType) !== 1
- ) {
- anchorElement = null
- }
- document.body.appendChild(this.dom.element)
- this.opened = true
- this.snapTo(anchorElement)
- if (anchorElement) {
- const onWindowResize = () => this.snapTo(anchorElement)
- window.addEventListener('resize', onWindowResize, false)
- this.on('hidden', () => window.removeEventListener('resize', onWindowResize, false))
- }
- ;(_document$querySelect = document.querySelector(':focus')) === null || _document$querySelect === void 0
- ? void 0
- : _document$querySelect.blur()
- const selector = ':not(.feedlyng-panel) > :-webkit-any(button, input, select, textarea, [tabindex])'
- const ctrl = Array.from(this.dom.element.querySelectorAll(selector)).sort(
- (a, b) => (b.tabIndex || 0) < (a.tabIndex || 0),
- )[0]
- if (ctrl) {
- ctrl.focus()
- if (ctrl.select) {
- ctrl.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(anchorElement) {
- if (this.opened) {
- this.close()
- } else {
- this.open(anchorElement)
- }
- }
- moveTo(x, y) {
- this.dom.element.style.left = `${x}px`
- this.dom.element.style.top = `${y}px`
- }
- snapTo(anchorElement) {
- const pad = 5
- let x = pad
- let y = pad
- if (anchorElement) {
- let { left, bottom: top } = anchorElement.getBoundingClientRect()
- left += pad
- top += pad
- const { width, height } = this.dom.element.getBoundingClientRect()
- const right = left + width + pad
- const bottom = top + height + pad
- const { innerWidth, innerHeight } = window
- if (innerWidth < right) {
- left -= right - innerWidth
- }
- if (innerHeight < bottom) {
- top -= bottom - innerHeight
- }
- x = Math.max(x, left)
- y = Math.max(y, top)
- }
- this.moveTo(x, y)
- }
- getFormData(asElement) {
- const data = {}
- const elements = this.dom.body.querySelectorAll('[name]')
- function getValue(el) {
- if (el.localName === 'input' && (el.type === 'checkbox' || el.type === 'radio')) {
- return el.checked
- }
- return 'value' in el ? el.value : el.getAttribute('value')
- }
- for (const el of elements) {
- const value = asElement ? el : getValue(el)
- const path = el.name.split('.')
- let leaf = path.pop()
- const cd = path.reduce((parent, key) => {
- if (!(key in parent)) {
- parent[key] = {}
- }
- return parent[key]
- }, data)
- if (leaf.endsWith('[]')) {
- leaf = leaf.slice(0, -2)
- if (!(leaf in cd)) {
- cd[leaf] = []
- }
- cd[leaf].push(value)
- } else {
- cd[leaf] = value
- }
- }
- return data
- }
- appendContent(element) {
- if (element instanceof Array) {
- return element.map(el => this.appendContent(el))
- }
- return this.dom.body.appendChild(element)
- }
- removeContents() {
- this.dom.body.innerHTML = ''
- }
- }
- class FilterListPanel extends Panel {
- constructor(filter, isRoot) {
- super()
- this.filter = filter
- if (isRoot) {
- this.dom.element.classList.add('root')
- }
- const onAdd = () => {
- const filter = new Filter()
- filter.name = __`New Filter`
- this.on('apply', () => this.filter.appendChild(filter))
- this.appendFilter(filter)
- }
- const onPaste = () => {
- if (!clipboard.data) {
- return
- }
- const filter = new Filter(clipboard.receive())
- this.on('apply', () => this.filter.appendChild(filter))
- this.appendFilter(filter)
- }
- const { buttons, paste } = $el`
- <div class="fngf-btn-group fngf-row" ref="buttons">
- <button type="button" class="fngf-btn" @click="${onAdd}">${__`Add`}</button>
- <button type="button" class="fngf-btn" @click="${onPaste}" ref="paste" disabled>${__`Paste`}</button>
- </div>
- `
- function pasteState() {
- paste.disabled = !clipboard.data
- }
- clipboard.on('copy', pasteState)
- clipboard.on('purge', pasteState)
- pasteState()
- this.dom.buttons.insertBefore(buttons, this.dom.buttons.firstChild)
- this.on('escape', this.close.bind(this))
- this.on('showing', this.initContents)
- this.on('apply', this)
- this.on('hidden', () => {
- clipboard.off('copy', pasteState)
- clipboard.off('purge', pasteState)
- })
- }
- initContents() {
- const filter = this.filter
- const { name, terms, rules } = $el`
- <div class="fngf-panel-name fngf-row fngf-align-center" ref="name">
- ${__`Rule Name`}
- <input type="text" value="${filter.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 labels = [
- ['title', __`Title`],
- ['url', __`URL`],
- ['sourceTitle', __`Feed Title`],
- ['sourceURL', __`Feed URL`],
- ['author', __`Author`],
- ['keywords', __`Keywords`],
- ['body', __`Contents`],
- ]
- for (const [type, labelText] of labels) {
- const randomId = `id-${Math.random().toFixed(8)}`
- const reg = filter.regexp[type]
- const sourceValue = reg ? reg.source.replace(/((?:^|[^\\])(?:\\\\)*)\\(?=\/)/g, '$1') : ''
- terms.appendChild($el`
- <label for="${randomId}">${labelText}</label>
- <input type="text" class="fngf-panel-terms-textbox" id="${randomId}" autocomplete="off" name="regexp.${type}.source" value="${sourceValue}">
- <label class="fngf-checkbox fngf-row" title="${__`Ignore Case`}">
- <input type="checkbox" name="regexp.${type}.ignoreCase" bool:checked="${
- reg === null || reg === void 0 ? void 0 : reg.ignoreCase
- }">
- <span class="fngf-btn" tabindex="0">i</span>
- </label>
- `)
- }
- this.appendContent([name, terms, rules])
- this.dom.rules = rules
- filter.children.forEach(this.appendFilter, this)
- }
- appendFilter(filter) {
- let panel
- const updateRow = () => {
- let title = __`Hit Count:\t${filter.hitcount}`
- if (filter.lasthit) {
- title += '\n'
- title += __`Last Hit:\t${new Date(filter.lasthit).toLocaleString()}`
- }
- rule.title = title
- name.textContent = filter.name
- count.textContent = filter.children.length || ''
- }
- const onEdit = () => {
- if (panel) {
- panel.close()
- return
- }
- panel = new FilterListPanel(filter)
- panel.on('shown', () => btnEdit.classList.add('active'))
- panel.on('hidden', () => {
- btnEdit.classList.remove('active')
- panel = null
- })
- panel.on('apply', () => setTimeout(updateRow, 0))
- panel.open(btnEdit)
- }
- const onCopy = () => clipboard.copy(filter)
- const onDelete = () => {
- document.adoptNode(rule)
- this.on('apply', () => this.filter.removeChild(filter))
- }
- const { rule, name, count, btnEdit } = $el`
- <div class="fngf-panel-rule fngf-row fngf-align-center" ref="rule">
- <div class="fngf-panel-rule-name" @dblclick="${onEdit}" 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="${onEdit}" 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="${onCopy}">${__`Copy`}</div>
- <div class="fngf-dropdown-menu-item" @click="${onDelete}">${__`Delete`}</div>
- </div>
- </div>
- </div>
- </div>
- `
- updateRow()
- this.dom.rules.appendChild(rule)
- }
- handleEvent(event) {
- if (event.type !== 'apply') {
- return
- }
- const data = this.getFormData(true)
- const filter = this.filter
- const regexp = {}
- let hasError = false
- for (const type in data.regexp) {
- const { source, ignoreCase } = data.regexp[type]
- if (!source.value) {
- continue
- }
- try {
- regexp[type] = new RegExp(source.value, ignoreCase.checked ? 'i' : '')
- } catch (e) {
- if (!(e instanceof SyntaxError)) {
- throw e
- }
- hasError = true
- event.preventDefault()
- source.classList.remove('error')
- source.offsetWidth.valueOf()
- source.classList.add('error')
- }
- }
- if (hasError) {
- return
- }
- const prevSource = Serializer.stringify(filter)
- filter.name = data.name.value
- filter.regexp = regexp
- if (Serializer.stringify(filter) !== prevSource) {
- filter.hitcount = 0
- filter.lasthit = 0
- }
- filter.sortChildren()
- }
- }
- Preference.defaultPref = Serializer.stringify({
- filter: {
- name: '',
- regexp: {},
- children: [
- {
- name: 'AD',
- regexp: {
- title: /^\W?(?:ADV?|PR)\b/,
- },
- children: [],
- },
- ],
- },
- })
- evalInContent(() => {
- const XHR = XMLHttpRequest
- let uniqueId = 0
- window.XMLHttpRequest = function XMLHttpRequest() {
- const req = new XHR()
- req.open = open
- req.setRequestHeader = setRequestHeader
- req.addEventListener('readystatechange', onReadyStateChange, false)
- return req
- }
- function open(method, url, ...args) {
- this.__url__ = url
- return XHR.prototype.open.call(this, method, url, ...args)
- }
- function setRequestHeader(header, value) {
- if (header === 'Authorization') {
- this.__auth__ = value
- }
- return XHR.prototype.setRequestHeader.call(this, header, value)
- }
- function onReadyStateChange() {
- if (this.readyState < 4 || this.status !== 200) {
- return
- }
- if (!/^(?:https?:)?\/\/(?:cloud\.)?feedly\.com\/v3\/streams\/contents\b/.test(this.__url__)) {
- return
- }
- const pongEventType = 'streamcontentloaded_callback' + uniqueId++
- const data = JSON.stringify({
- type: pongEventType,
- auth: this.__auth__,
- text: this.responseText,
- })
- const event = new MessageEvent('streamcontentloaded', {
- bubbles: true,
- cancelable: false,
- data: data,
- origin: location.href,
- source: null,
- })
- const onPong = ({ data }) =>
- Object.defineProperty(this, 'responseText', {
- configurable: true,
- value: data,
- })
- document.addEventListener(pongEventType, onPong, false)
- document.dispatchEvent(event)
- document.removeEventListener(pongEventType, onPong, 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, newValue }) {
- switch (key) {
- case 'filter':
- if (!(newValue instanceof Filter)) {
- this.set('filter', new Filter(newValue))
- }
- break
- case 'language':
- __.use(newValue)
- break
- }
- })
- document.addEventListener(
- 'streamcontentloaded',
- event => {
- const logging = pref.get('logging', true)
- const filter = pref.get('filter')
- const filteredEntryIds = []
- const { type: pongEventType, auth, text } = JSON.parse(event.data)
- const data = JSON.parse(text)
- let hasUnread = false
- data.items = data.items.filter(item => {
- const entry = new Entry(item)
- if (!filter.test(entry)) {
- return true
- }
- if (logging) {
- GM_log(`filtered: "${entry.title || ''}" ${entry.url}`)
- }
- filteredEntryIds.push(entry.id)
- if (entry.unread) {
- hasUnread = true
- }
- return false
- })
- if (!filteredEntryIds.length) {
- return
- }
- let ev = new MessageEvent(pongEventType, {
- bubbles: true,
- cancelable: false,
- data: JSON.stringify(data),
- origin: location.href,
- source: unsafeWindow,
- })
- document.dispatchEvent(ev)
- if (!hasUnread) {
- return
- }
- sendJSON({
- url: '/v3/markers',
- headers: {
- Authorization: auth,
- },
- data: {
- action: 'markAsRead',
- entryIds: filteredEntryIds,
- type: 'entries',
- },
- })
- },
- false,
- )
- document.addEventListener(
- 'DOMContentLoaded',
- () => {
- GM_addStyle(CSS_STYLE_TEXT)
- pref.load()
- pref.autosave()
- registerMenuCommands()
- addSettingsMenuItem()
- },
- false,
- )
- document.addEventListener(
- 'mousedown',
- ({ target }) => {
- if (target.matches('.fngf-dropdown')) {
- target.classList.toggle('active')
- }
- if (!target.closest('.fngf-dropdown')) {
- var _document$querySelect2
- ;(_document$querySelect2 = document.querySelector('.fngf-dropdown.active')) === null ||
- _document$querySelect2 === void 0
- ? void 0
- : _document$querySelect2.classList.remove('active')
- }
- },
- true,
- )
- document.addEventListener(
- 'click',
- ({ target }) => {
- if (target.closest('.fngf-dropdown-menu-item')) {
- var _target$closest
- ;(_target$closest = target.closest('.fngf-dropdown')) === null || _target$closest === void 0
- ? void 0
- : _target$closest.classList.remove('active')
- }
- },
- true,
- )
- function $el(strings, ...values) {
- let html = ''
- if (typeof strings === 'string') {
- html = strings
- } else {
- values.forEach((v, i) => {
- html += strings[i]
- if (v === null || v === undefined) {
- return
- }
- if (v instanceof Node || v instanceof NodeList || v instanceof HTMLCollection || v instanceof Array) {
- html += `<!--${$el.dataPrefix}${i}-->`
- if (v instanceof Node) {
- return
- }
- values[i] = document.createDocumentFragment()
- for (const item of v) {
- values[i].appendChild(item)
- }
- return
- }
- html += v instanceof Object ? i : v
- })
- html += strings[strings.length - 1]
- }
- const renderer = document.createElement('template')
- const container = document.createElement('body')
- const refs = document.createDocumentFragment()
- renderer.innerHTML = html
- container.appendChild(renderer.content)
- refs.first = container.firstElementChild
- refs.last = container.lastElementChild
- const exp = `
- .//*[@ref or @*[starts-with(name(), "@") or contains(name(), ":")]] |
- .//comment()[starts-with(., "${$el.dataPrefix}")]
- `
- const xpath = document.evaluate(exp, container, null, 7, null)
- for (let i = 0; i < xpath.snapshotLength; i++) {
- const el = xpath.snapshotItem(i)
- if (el.nodeType === document.COMMENT_NODE) {
- const index = el.data.substring($el.dataPrefix.length)
- el.parentNode.replaceChild(values[index], el)
- continue
- }
- for (const { name, value } of Array.from(el.attributes)) {
- const data = values[value]
- if (name === 'ref') {
- refs[value] = el
- } else if (name.startsWith('@')) {
- $el.func(el, name.substring(1), data)
- } else if (name === ':class') {
- for (const k of Object.keys(data)) {
- el.classList.toggle(k, data[k])
- }
- } else if (name.startsWith('bool:')) {
- el[name.substring(5)] = data
- } else {
- continue
- }
- el.removeAttribute(name)
- }
- }
- Array.from(container.childNodes).forEach(node => refs.appendChild(node))
- return refs
- }
- $el.dataPrefix = '$el.data:'
- $el.func = (el, type, fn) => {
- if (type) {
- el.addEventListener(type, fn, false)
- } else {
- try {
- fn.call(el, el)
- } catch (e) {
- console.error(e)
- }
- }
- }
- function xhr(details) {
- const opt = { ...details }
- const { data } = opt
- opt.method || (opt.method = data ? 'POST' : 'GET')
- if (data instanceof Object) {
- opt.headers || (opt.headers = {})
- opt.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
- opt.data = Object.entries(data)
- .map(kv => kv.map(encodeURIComponent).join('='))
- .join('&')
- }
- setTimeout(() => GM_xmlhttpRequest(opt), 0)
- }
- function registerMenuCommands() {
- MenuCommand.register(`${__`Setting`}...`, togglePrefPanel)
- MenuCommand.register(`${__`Language`}...`, () => {
- const { langField, select } = $el(`
- <fieldset ref="langField">
- <legend>${__`Language`}</legend>
- <select ref="select"></select>
- </fieldset>
- `)
- __.languages.forEach(lang => {
- const option = $el(`<option value="${lang}">${lang}</option>`).first
- if (lang === __.config.locale) {
- option.selected = true
- }
- select.appendChild(option)
- })
- const panel = new Panel()
- panel.appendContent(langField)
- panel.on('apply', () => pref.set('language', select.value))
- panel.open()
- })
- MenuCommand.register(`${__`Import Configuration`}...`, pref.importFromFile.bind(pref))
- MenuCommand.register(__`Export Configuration`, pref.exportToFile.bind(pref))
- }
- function sendJSON(details) {
- const opt = { ...details }
- const { data } = opt
- opt.headers || (opt.headers = {})
- opt.method = 'POST'
- opt.headers['Content-Type'] = 'application/json; charset=utf-8'
- opt.data = JSON.stringify(data)
- return xhr(opt)
- }
- function evalInContent(code) {
- const script = document.createElement('script')
- script.textContent = typeof code === 'function' ? `(${code})()` : code
- document.documentElement.appendChild(script)
- document.adoptNode(script)
- }
- function togglePrefPanel(anchorElement) {
- 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(anchorElement)
- }
- function onNGSettingCommand({ target }) {
- togglePrefPanel(target)
- }
- function addSettingsMenuItem() {
- if (!document.getElementById('filtertab')) {
- setTimeout(addSettingsMenuItem, 100)
- return
- }
- let prefListener
- function onMutation() {
- if (document.getElementById('feedly-ng-filter-setting')) {
- return
- }
- const nativeFilterItem = document.getElementById('filtertab')
- if (!nativeFilterItem) {
- return
- }
- if (prefListener) {
- pref.off('change', prefListener)
- }
- const { tab, label } = $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>
- `
- label.textContent = __`NG Setting`
- nativeFilterItem.parentNode.insertBefore(tab, nativeFilterItem.nextSibling)
- document.body.appendChild(contextmenu.parentNode)
- prefListener = ({ key }) => {
- if (key === 'language') {
- label.textContent = __`NG Setting`
- }
- }
- pref.on('change', prefListener)
- }
- new MutationObserver(onMutation).observe(document.getElementById('feedlyTabs'), {
- childList: true,
- subtree: true,
- })
- onMutation()
- }
- async function openFilePicker(multiple) {
- return new Promise(resolve => {
- const input = $el`<input type="file" @change="${() => {
- var _input$files, _ref3
- return (_ref3 = ((_input$files = input.files), Array.from(_input$files))), resolve(_ref3)
- }}">`.first
- input.multiple = multiple
- input.click()
- })
- }
- async function notify(body, options) {
- options = {
- body,
- ...notificationDefaults,
- ...options,
- }
- return new Promise((resolve, reject) => {
- Notification.requestPermission(status => {
- if (status !== 'granted') {
- reject(status)
- return
- }
- const n = new Notification(options.title, options)
- if (options.autoClose) {
- setTimeout(n.close.bind(n), options.autoClose)
- }
- resolve(n)
- })
- })
- }
- return module.exports
- }.call({})
- if (typeof exports === 'object' && typeof module !== 'undefined') {
- // CommonJS
- module.exports = $Focm$exports
- } else if (typeof define === 'function' && define.amd) {
- // RequireJS
- define(function() {
- return $Focm$exports
- })
- }
- })()