您需要先安装一个扩展,例如 篡改猴、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 _ref, _this$dict this.dict.__version__ = GM_info.script.version ;(_ref = ((_this$dict = this.dict), Serializer.stringify.call(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 _ref3, _input$files 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 }) } })()
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址