Feedly NG Filter

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

Version vom 06.04.2015. Aktuellste Version

// ==UserScript==
// @name           Feedly NG Filter
// @id             feedlyngfilter
// @description    ルールにマッチするアイテムを既読にして取り除きます。ルールは正規表現で記述でき、複数のルールをツリー状に組み合わせることができます。
// @include        http://feedly.com/*
// @include        https://feedly.com/*
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_addStyle
// @grant          GM_xmlhttpRequest
// @grant          GM_registerMenuCommand
// @grant          GM_unregisterMenuCommand
// @grant          GM_notification
// @grant          GM_log
// @charset        utf-8
// @compatibility  Firefox
// @run-at         document-start
// @jsversion      1.8
// @priority       1
// @homepage       https://greasyfork.org/ja/scripts/9030-feedly-ng-filter
// @supportURL     http://twitter.com/?status=%40xulapp+
// @icon           https://greasyfork.org/system/screenshots/screenshots/000/000/615/original/icon.png
// @screenshot     https://greasyfork.org/system/screenshots/screenshots/000/000/614/original/large.png
// @namespace      http://twitter.com/xulapp
// @author         xulapp
// @license        MIT License
// @version        0.6.0
// ==/UserScript==


(function feedlyNGFilter() {
	const CSS_STYLE_TEXT = $TEXT(() => {/*
		.unselectable {
			-moz-user-select: none;
		}
		.goog-inline-block {
			display: inline-block;
			position: relative;
		}
		.jfk-button {
			border-radius: 2px 2px 2px 2px;
			cursor: default;
			font-size: 11px;
			font-weight: bold;
			text-align: center;
			white-space: nowrap;
			margin-right: 16px;
			height: 27px;
			line-height: 27px;
			min-width: 54px;
			outline: 0px none;
			padding: 0px 8px;
		}
		.jfk-button-standard {
			background-color: rgb(245, 245, 245);
			background-image: -moz-linear-gradient(center top , rgb(245, 245, 245), rgb(241, 241, 241));
			color: rgb(68, 68, 68);
			border: 1px solid rgba(0, 0, 0, 0.1);
		}
		.jfk-button-standard:hover {
			background-color: #f8f8f8;
			background-image: -moz-linear-gradient(top, #f8f8f8, #f1f1f1);
			border: 1px solid #c6c6c6;
			color: #333;
		}
		.jfk-button-standard:focus {
			border: 1px solid #4d90fe;
		}
		.jfk-button-standard:active {
			box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
		}
		.jfk-button-standard.jfk-button-disabled {
			background: none;
			border: 1px solid rgba(0, 0, 0, 0.05);
			color: rgb(184, 184, 184);
		}
		.goog-flat-menu-button {
			border-radius: 2px 2px 2px 2px;
			background-color: rgb(245, 245, 245);
			background-image: -moz-linear-gradient(center top , rgb(245, 245, 245), rgb(241, 241, 241));
			border: 1px solid rgb(220, 220, 220);
			color: rgb(68, 68, 68);
			cursor: default;
			font-size: 11px;
			font-weight: bold;
			line-height: 27px;
			list-style: none outside none;
			margin: 0px 2px;
			min-width: 46px;
			outline: medium none;
			padding: 0px 18px 0px 6px;
			text-align: center;
			text-decoration: none;
			vertical-align: middle;
		}
		.goog-flat-menu-button-open,
		.goog-flat-menu-button:active {
			-moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
			box-shadow:inset 0 1px 2px rgba(0, 0, 0, .1);
			background-color: #eee;
			background-image: -moz-linear-gradient(top, #eee, #e0e0e0);
			background-image: linear-gradient(top, #eee, #e0e0e0);
			border: 1px solid #ccc;
			color: #333;
			z-index: 2
		}
		.goog-flat-menu-button-collapse-left {
			margin-left: -1px;
			border-bottom-left-radius: 0px;
			border-top-left-radius: 0px;
			min-width: 0px;
			padding-left: 0px;
			vertical-align: top;
		}
		.jfk-button-collapse-left,
		.jfk-button-collapse-right {
			z-index: 1;
		}
		.jfk-button-collapse-right {
			margin-right: 0px;
			border-top-right-radius: 0px;
			border-bottom-right-radius: 0px;
		}
		.goog-flat-menu-button-caption {
			vertical-align: top;
			white-space: nowrap;
		}
		.goog-flat-menu-button-dropdown {
			border-color: rgb(119, 119, 119) transparent;
			border-style: solid;
			border-width: 4px 4px 0px;
			height: 0px;
			width: 0px;
			position: absolute;
			right: 5px;
			top: 12px;
		}
		.goog-menu {
			-moz-box-shadow:0 2px 4px rgba(0,0,0,0.2);
			box-shadow:0 2px 4px rgba(0,0,0,0.2);
			-moz-transition:opacity .218s;
			transition:opacity .218s;
			background:#fff;
			border:1px solid #ccc;
			border:1px solid rgba(0,0,0,.2);
			cursor:default;
			font-size:13px;
			margin:0;
			outline:none;
			padding:6px 0;
			position:absolute
		}
		.goog-menuitem {
			position: relative;
			color: #333;
			cursor: pointer;
			list-style: none;
			margin: 0;
			padding: 6px 7em 6px 30px;
			white-space: nowrap;
		}
		.goog-menuitem:hover {
			background-color: #eee;
			border-color: #eee;
			border-style: dotted;
			border-width: 1px 0;
			padding-top: 5px;
			padding-bottom: 5px
			color: #333;
		}
		.feedlyng-menu-button-container > .goog-menu-button {
			margin-left: -2px;
		}
		.feedlyng.goog-menu {
			position: absolute;
			z-index: 2147483646;
		}
		.feedlyng .goog-menuitem:hover {
			background-color: #eeeeee;
		}
		#feedlyng-open-panel {
			float: left;
		}
		.feedlyng-panel {
			position: fixed;
			background-color: #ffffff;
			color: #333333;
			box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.5);
			z-index: 2147483646;
		}
		.feedlyng-panel :-moz-any(label, legend) {
			cursor: default;
		}
		.feedlyng-panel input[type="text"] {
			padding: 2px;
			border: 1px solid #b2b2b2;
		}
		.feedlyng-panel-body {
			margin: 8px;
		}
		.feedlyng-panel-body > fieldset {
			margin: 8px 0;
		}
		.feedlyng-panel.root > .feedlyng-panel-body > :-moz-any(.feedlyng-panel-name, fieldset) {
			display: none;
		}
		.feedlyng-panel-terms {
			border-spacing: 2px;
		}
		.feedlyng-panel-terms > tbody > tr > td {
			padding: 0;
			white-space: nowrap;
		}
		.feedlyng-panel-terms :-moz-any(input, label) {
			margin: 0;
			vertical-align: middle;
		}
		@-moz-keyframes error {
			0% {
				background-color: #ffff00;
				border-color: #ff0000;
			}
		}
		.feedlyng-panel-terms-textbox.error {
			-moz-animation: error 1s;
		}
		.feedlyng-panel-terms-textbox-label {
			display: block;
			font-size: 90%;
			text-align: right;
		}
		.feedlyng-panel-terms-textbox-label:after {
			content: ":";
		}
		.feedlyng-panel-terms-checkbox-label {
			padding: 0 8px;
		}
		.feedlyng-panel-rules {
			display: table;
		}
		.feedlyng-panel-rule {
			display: table-row;
		}
		.feedlyng-panel-rule:hover {
			background-color: #eeeeee;
		}
		.feedlyng-panel-rule > div {
			display: table-cell;
			white-space: nowrap;
		}
		.feedlyng-panel-rule-name {
			width: 100%;
			padding-left: 16px;
			cursor: default;
		}
		.feedlyng-panel-rule-count {
			padding: 0 8px;
			font-weight: bold;
			cursor: default;
		}
		.feedlyng-panel-buttons {
			margin: 8px;
			text-align: right;
			white-space: nowrap;
		}
		.feedlyng-panel-addfilter {
			float: left;
			margin-right: 8px;
		}
		.feedlyng-panel-pastefilter {
			float: left;
			margin-right: 16px;
		}
		.feedlyng-panel-ok {
			margin-right: 8px;
		}
		.feedlyng-panel-cancel {
			margin-right: 0;
		}
	*/});

	var Locale = {
		data: Object.create(null, {
			_default: {
				get: function() this[navigator.language] || this['en-US'],
			},
		}),
		get: function(p, name) {
			return this.data[this.selectedLanguage][name];
		},
		get languages() Object.keys(this.data),
		select: function select(lang) {
			this.selectedLanguage = lang in this.data ? lang : '_default';
		},
		createReference: function createReference() {
			return Proxy.create(this);
		},
	};
	Locale.data['en-US'] = {
		__proto__: null,
		app_name: 'Feedly NG Filter',
		ok: 'OK',
		cancel: 'Cancel',
		add: 'Add',
		copy: 'Copy',
		paste: 'Paste',
		new_filter: 'New Filter',
		rules: ' Rules',
		title: 'Title',
		url: 'URL',
		source_title: 'Feed Title',
		source_url: 'Feed URL',
		author: 'Author',
		body: 'Contents',
		ignore_case: 'Ignore Case',
		edit: 'Edit',
		delete: 'Delete',
		hit_count: 'Hit Count',
		last_hit: 'Last Hit',
		ng_setting: 'NG Setting',
		setting: 'Setting',
		import_setting: 'Import Configuration',
		import_success: 'Preferences were successfully imported.',
		export_setting: 'Export Configuration',
		language: 'Language',
		ng_setting_modified: 'NG Settings were modified.\nNew filters take effect after next refresh.',
	};
	Locale.data['ja'] = {
		__proto__: Locale.data['en-US'],
		cancel: 'キャンセル',
		add: '追加',
		copy: 'コピー',
		paste: '貼り付け',
		new_filter: '新しいフィルタ',
		rules: 'のルール',
		title: 'タイトル',
		source_title: 'フィードのタイトル',
		source_url: 'フィードの URL',
		author: '著者',
		body: '本文',
		ignore_case: '大/小文字を区別しない',
		edit: '編集',
		delete: '削除',
		hit_count: 'ヒット数',
		last_hit: '最終ヒット',
		ng_setting: 'NG 設定',
		setting: '設定',
		import_setting: '設定をインポート',
		import_success: '設定をインポートしました',
		export_setting: '設定をエクスポート',
		language: '言語',
		ng_setting_modified: 'NG 設定を更新しました。\n新しいフィルタは次回読み込み時から有効になります。',
	};
	Locale.select();
	var $str = Locale.createReference();

	function Class(sup, pro) {
		if (sup && typeof sup === 'object')
			pro = sup, sup = Object;

		var con = Object.getOwnPropertyDescriptor(pro, 'constructor');
		if (!con)
			con = {value: Function(), writable: true, configurable: true};

		if (con.configurable) {
			con.enumerable = false;
			Object.defineProperty(pro, 'constructor', con);
		}

		con = pro.constructor;
		con.prototype = pro;
		con.superclass = sup;
		con.__proto__ = Class.prototype;
		pro.__proto__ = sup && sup.prototype;

		return Proxy.createFunction(con, function() con.createInstance(arguments));
	}
	Class = Class(Function, {
		constructor: Class,
		$super: function $super() {
			var sup = this.superclass;
			var method = sup.prototype[$super.caller === this ? 'constructor' : $super.caller.name];
			return Function.prototype.call.apply(method, arguments);
		},
		isSubClass: function isSubClass(cls) {
			return this.prototype instanceof cls;
		},
		createInstance: function createInstance(args) {
			var instance = Object.create(this.prototype);
			var result = this.apply(instance, args || []);
			return result instanceof Object ? result : instance;
		},
		toString: function toString() {
			var arr = [];
			var cls = this;
			do {
				arr.push(cls.name);
			} while (cls = cls.superclass);
			return '[object Class [class ' + arr.join(', ') + ']]';
		},

		getOwnPropertyDescriptor: function(name) Object.getOwnPropertyDescriptor(this, name),
		getPropertyDescriptor: function(name) Object.getPropertyDescriptor(this, name),
		getOwnPropertyNames: function(name) Object.getOwnPropertyNames(this, name),
		getPropertyNames: function(name) Object.getPropertyNames(this, name),
		defineProperty: function(name) Object.defineProperty(this, name),
		delete: function(name) delete this[name],
		fix: function() {
			if (!Object.isFrozen(this))
				return void 0;

			var res = {};
			Object.getOwnPropertyNames(this).forEach((name) => res[name] = Object.getOwnPropertyDescriptor(this, name));

			return res;
		},
		has: function(name) name in this,
		hasOwn: function(name) Object.prototype.hasOwnProperty.call(this, name),
		get: function(receiver, name) {
			if (name in this)
				return this[name];

			var method = this.prototype[name];
			if (typeof method === 'function')
				return Function.prototype.call.bind(method);

			return void 0;
		},
		set: function(receiver, name, val) this[name] = val,
		enumerate: function() [name for (name in this)],
		keys: function() Object.keys(this),
	});

	var Subject = Class({
		constructor: function Subject() {
			this.listeners = {};
		},
		on: function on(type, listener) {
			type += '';

			if (type.trim().indexOf(' ') !== -1) {
				type.match(/\S+/g).forEach(function(t) this.on(t, listener), this);
				return;
			}

			if (!(type in this.listeners))
				this.listeners[type] = [];

			var arr = this.listeners[type];
			var index = arr.indexOf(listener);
			if (index === -1)
				arr.push(listener);
		},
		once: function once(type, listener) {
			function onetimeListener() {
				this.removeListener(onetimeListener);
				return listener.apply(this, arguments);
			};
			this.on(type, onetimeListener);
			return onetimeListener;
		},
		removeListener: function removeListener(type, listener) {
			if (!(type in this.listeners))
				return;

			var arr = this.listeners[type];
			var index = arr.indexOf(listener);
			if (index !== -1)
				arr.splice(index, 1);
		},
		removeAllListeners: function removeAllListeners(type) {
			delete this.listeners[type];
		},
		dispatchEvent: function dispatchEvent(event) {
			event.timeStamp = Date.now();
			if (event.type in this.listeners) {
				this.listeners[event.type].concat().forEach(function(listener) {
					try {
						if (typeof listener === 'function')
							listener.call(this, event);

						else
							listener.handleEvent(event);

					} catch (e) {
						setTimeout(function() { throw e; }, 0);
					}
				}, this);
			}
			return !event.canceled;
		},
		emit: function emit(type, data) {
			var event = this.createEvent(type);
			if (data instanceof Object)
				extend(event, data);

			return this.dispatchEvent(event);
		},
		createEvent: function createEvent(type) {
			return new Event(type, this);
		},
	});

	var Event = Class({
		constructor: function Event(type, target) {
			this.type = type;
			this.target = target;
		},
		canceled: false,
		timeStamp: null,
		preventDefault: function preventDefault() {
			this.canceled = true;
		},
	});

	var DataTransfer = Class(Subject, {
		constructor: function DataTransfer() {
			DataTransfer.$super(this);
		},
		set: function set(type, data) {
			this.purge();
			this.type = type;
			this.data = data;
			this.emit(type, {data: data});
		},
		purge: function purge() {
			this.emit('purge', {data: this.data});
			delete this.data;
		},
		setForCut: function setForCut(data) {
			this.set('cut', data);
		},
		setForCopy: function setForCopy(data) {
			this.set('copy', data);
		},
		receive: function receive() {
			var data = this.data;
			if (this.type === 'cut')
				this.purge();

			return data;
		},
	});

	var MenuCommand = Class({
		constructor: function MenuCommand(label, oncommand, disabled) {
			this.label = label;
			this.oncommand = oncommand;
			this.disabled = !!disabled;

			this.register();
		},
		register: function register() {
			this.uuid = GM_registerMenuCommand(this.label, this.oncommand);

			if (MenuCommand.contextmenu) {
				this.menuitem = document.createElement('menuitem');
				this.menuitem.label = this.label;
				this.menuitem.addEventListener('click', this.oncommand, false);
				MenuCommand.contextmenu.appendChild(this.menuitem);
			}

			if (this.disabled)
				this.disable();
		},
		unregister: function unregister() {
			if (typeof GM_unregisterMenuCommand === 'function')
				GM_unregisterMenuCommand(this.uuid);

			document.adoptNode(this.menuitem);
		},
		disable: function disable() {
			if (typeof GM_disableMenuCommand === 'function')
				GM_disableMenuCommand(this.uuid);

			this.menuitem.disabled = true;
		},
		enable: function enable() {
			if (typeof GM_enableMenuCommand === 'function')
				GM_enableMenuCommand(this.uuid);

			this.menuitem.disabled = false;
		},
	});
	MenuCommand.contextmenu = null;

	var Preference = Class(Subject, {
		constructor: let (instance) function Preference() {
			if (instance)
				return instance;

			Preference.$super(this);
			instance = this;

			this.dict = {};
		},
		has: function has(key) key in this.dict,
		get: function get(key, def) this.has(key) ? this.dict[key] : def,
		set: function set(key, value) {
			var prev = this.dict[key];
			if (value !== prev) {
				this.dict[key] = value;
				this.emit('change', {
					propertyName: key,
					prevValue: prev,
					newValue: value,
				});
			}
			return value;
		},
		del: function del(key) {
			if (!this.has(key))
				return;

			var prev = this.dict[key];
			delete this.dict[key];

			this.emit('delete', {
				propertyName: key,
				prevValue: prev,
			});
		},
		load: function load(str) {
			if (!str)
				str = GM_getValue(Preference.prefName, Preference.defaultPref || '({})');

			var obj = eval('(' + str + ')');
			if (!obj || typeof obj !== 'object')
				return;

			this.dict = {};
			for (let [key, value] in Iterator(obj))
				this.set(key, value);

			this.emit('load');
		},
		write: function write() {
			GM_setValue(Preference.prefName, this.toSource());
		},
		autoSave: function autoSave() {
			if (autoSave.reserved)
				return;

			window.addEventListener('unload', () => this.write(), false);
			autoSave.reserved = true;
		},
		exportToFile: function exportToFile() {
			var blob = new Blob([this.toSource()], {
				type: 'application/octet-stream',
			});
			var url = URL.createObjectURL(blob);
			location.href = url;
			URL.revokeObjectURL(url);
		},
		importFromString: function importFromString(str) {
			try {
				this.load(str);
			} catch (e if e instanceof SyntaxError) {
				showMessage(e, 'warning');
				return false;
			}
			showMessage($str.import_success);
			return true;
		},
		importFromFile: function importFromFile() {
			openFilePicker(files => {
				if (!files)
					return;

				var r = FileReader();
				r.addEventListener('load', () => this.importFromString(r.result), false);
				r.readAsText(files[0]);
			});
		},
		toString: function toString() '[object Preference]',
		toSource: function toSource() this.dict.toSource(),
	});
	Preference.prefName = 'settings';

	var draggable = Class({
		constructor: function draggable(element) {
			this.element = element;
			element.addEventListener('mousedown', this, false, false);
		},
		isDraggableTarget: function isDraggableTarget(target) {
			if (!target)
				return false;

			if (target === this.element)
				return true;

			return !target.mozMatchesSelector(':-moz-any(select, button, input, textarea, [tabindex]), :-moz-any(select, button, input, textarea, [tabindex]) *');
		},
		detatch: function detatch() {
			this.element.removeEventListener('mousedown', this, false);
		},
		handleEvent: function handleEvent(event) {
			var name = 'on' + event.type;
			if (name in this)
				this[name](event);
		},
		onmousedown: function onMouseDown(event) {
			if (event.button !== 0)
				return;

			if (!this.isDraggableTarget(event.target))
				return;

			event.preventDefault();

			var focused = this.element.querySelector(':focus');
			if (focused)
				focused.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: function onMouseMove(event) {
			event.preventDefault();

			this.element.style.left = event.pageX - this.offsetX + 'px';
			this.element.style.top = event.pageY - this.offsetY + 'px';
		},
		onmouseup: function onMouseUp(event) {
			if (event.button !== 0)
				return;

			event.preventDefault();

			document.removeEventListener('mousemove', this, true);
			document.removeEventListener('mouseup', this, true);
		},
	});

	var Filter = Class({
		constructor: function Filter(filter) {
			if (!(this instanceof Filter))
				return Filter.createInstance(arguments);

			if (!(filter instanceof Object))
				filter = {};

			this.name = filter.name || '';
			this.regexp = extend({}, filter.regexp || {});
			this.children = filter.children ? filter.children.map(Filter) : [];
			this.hitcount = filter.hitcount || 0;
			this.lasthit = filter.lasthit || 0;
		},
		test: function test(entry) {
			for (var [name, reg] in Iterator(this.regexp))
				if (!reg.test(entry[name] || ''))
					return false;

			var hit = this.children.length ? this.children.some(filter => filter.test(entry)) : !!reg;
			if (hit) {
				this.hitcount++;
				this.lasthit = Date.now();
			}

			return hit;
		},
		appendChild: function appendChild(filter) {
			if (!(filter instanceof this.constructor))
				return null;

			this.removeChild(filter);
			this.children.push(filter);
			this.sortChildren();
			return filter;
		},
		removeChild: function removeChild(filter) {
			if (!(filter instanceof this.constructor))
				return null;

			var index = this.children.indexOf(filter);
			if (index !== -1)
				this.children.splice(index, 1);

			return filter;
		},
		sortChildren: function sortChildren() {
			return this.children.sort((a, b) => b.name < a.name);
		},
	});

	var Entry = Class(let (div = document.createElement('div')) ({
		constructor: function Entry(data) {
			this.data = data;
		},
		get title() {
			div.innerHTML = this.data.title || '';
			Object.defineProperty(this, 'title', {configurable: true, value: div.textContent});
			return this.title;
		},
		get id()          this.data.id,
		get url()         ((this.data.alternate || 0)[0] || 0).href,
		get sourceTitle() this.data.origin.title,
		get sourceURL()   this.data.origin.streamId.replace(/^[^/]+\//, ''),
		get body()        (this.data.content || this.data.summary || 0).content,
		get author()      this.data.author,
		get recrawled()   this.data.recrawled,
		get published()   this.data.published,
		get updated()     this.data.updated,
		get keywords()    this.data.keywords,
		get unread()      this.data.unread,
		get tags()        this.data.tags.map(tag => tag.label),
	}));

	var Panel = Class(Subject, {
		constructor: function Panel() {
			Panel.$super(this);

			var panel = document.createElement('form');
			panel.classList.add('feedlyng-panel');
			draggable(panel);
			panel.addEventListener('submit', event => {
				event.preventDefault();
				event.stopPropagation();
				this.apply();
			}, false);

			var submit = document.createElement('input');
			submit.type = 'submit';
			submit.style.display = 'none';

			var body = document.createElement('div');
			body.classList.add('feedlyng-panel-body');

			var buttons = document.createElement('div');
			buttons.classList.add('feedlyng-panel-buttons');

			var ok = createGoogButton($str.ok, () => this.apply());
			ok.classList.add('feedlyng-panel-ok');

			var cancel = createGoogButton($str.cancel, () => this.close());
			cancel.classList.add('feedlyng-panel-cancel');

			panel.appendChild(submit);
			panel.appendChild(body);
			panel.appendChild(buttons);
			buttons.appendChild(ok);
			buttons.appendChild(cancel);

			this.dom = {
				element: panel,
				body: body,
				buttons: buttons,
			};
		},
		get opened() !!this.dom.element.parentNode,
		open: function open(anchorElement) {
			if (this.opened)
				return;

			if (!this.emit('showing'))
				return;

			if (!anchorElement || anchorElement.nodeType !== 1)
				anchorElement = null;

			document.body.appendChild(this.dom.element);
			this.snapTo(anchorElement);

			if (anchorElement) {
				let onWindowResize = this.snapTo.bind(this, anchorElement);
				window.addEventListener('resize', onWindowResize, false);
				this.on('hidden', window.removeEventListener.bind(window, 'resize', onWindowResize, false));
			}

			var focused = document.querySelector(':focus');
			if (focused)
				focused.blur();

			var tab = Array.slice(this.dom.element.querySelectorAll(':not(.feedlyng-panel) > :-moz-any(button, input, select, textarea, [tabindex])'))
				.sort((a, b) => (b.tabIndex || 0) < (a.tabIndex || 0))[0];

			if (tab) {
				tab.focus();
				if (tab.select)
					tab.select();
			}

			this.emit('shown');
		},
		apply: function apply() {
			if (this.emit('apply'))
				this.close();
		},
		close: function close() {
			if (!this.opened)
				return;

			if (!this.emit('hiding'))
				return;

			document.adoptNode(this.dom.element);

			this.emit('hidden');
		},
		toggle: function toggle(anchorElement) {
			if (this.opened)
				this.close();

			else
				this.open(anchorElement);
		},
		moveTo: function moveTo(x, y) {
			this.dom.element.style.left = x + 'px';
			this.dom.element.style.top = y + 'px';
		},
		snapTo: function snapTo(anchorElement) {
			var pad = 5;
			var x = pad;
			var y = pad;
			if (anchorElement) {
				var {left, bottom: top} = anchorElement.getBoundingClientRect();
				left += pad;
				top += pad;

				var {width, height} = this.dom.element.getBoundingClientRect();
				var right = left + width + pad;
				var bottom = top + height + pad;

				var {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: function getFormData(asElement) {
			var data = {};
			Array.slice(this.dom.body.querySelectorAll('[name]')).forEach((elem) => {
				var value;
				if (asElement) {
					value = elem;

				} else {
					if (elem.localName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio'))
						value = elem.checked;

					else
						value = 'value' in elem ? elem.value : elem.getAttribute('value');
				}

				var path = elem.name.split('.');
				var leaf = path.pop();
				var cd = path.reduce((parent, dirName) => {
					if (!(dirName in parent))
						parent[dirName] = {};

					return parent[dirName];
				}, data);

				var reg = /\[\]$/;
				if (reg.test(leaf)) {
					leaf = leaf.replace(reg, '');
					if (!(leaf in cd))
						cd[leaf] = [];

					cd[leaf].push(value);

				} else {
					cd[leaf] = value;
				}
			});
			return data;
		},
		appendContent: function appendContent(element) {
			if (element instanceof Array)
				return element.map(appendContent, this);

			return this.dom.body.appendChild(element);
		},
		removeContents: function removeContents() {
			var range = node.ownerDocument.createRange();
			range.selectNodeContents(this.dom.body);
			range.deleteContents();
			range.detach();
		},
	});

	var FilterListPanel = Class(Panel, {
		constructor: function FilterListPanel(filter, isRoot) {
			FilterListPanel.$super(this);
			this.filter = filter;

			var self = this;

			if (isRoot)
				this.dom.element.classList.add('root');

			var add = createGoogButton($str.add, () => {
				var f = new Filter();
				f.name = $str.new_filter;
				this.on('apply', () => this.filter.appendChild(f));
				this.appendFilter(f);
			});
			add.classList.add('feedlyng-panel-addfilter');
			this.dom.buttons.insertBefore(add, this.dom.buttons.firstChild);

			var paste = createGoogButton($str.paste, () => {
				if (!clipboard.data)
					return;

				var f = new Filter(clipboard.receive());
				this.on('apply', () => this.filter.appendChild(f));
				this.appendFilter(f);
			});
			paste.classList.add('feedlyng-panel-pastefilter');
			if (!clipboard.data)
				paste.classList.add('jfk-button-disabled');

			clipboard.on('copy', onCopy);
			clipboard.on('purge', onPurge);

			function onCopy() {
				paste.classList.remove('jfk-button-disabled');
			}
			function onPurge() {
				paste.classList.add('jfk-button-disabled');
			}

			this.dom.buttons.insertBefore(paste, add.nextSibling);

			this.on('showing', this.initContents);
			this.on('apply', this);
			this.on('hidden', () => {
				clipboard.removeListener('copy', onCopy);
				clipboard.removeListener('purge', onPurge);
			});
		},
		initContents: function initContents() {
			var filter = this.filter;

			var nameTextbox = document.createElement('input');
			nameTextbox.classList.add('feedlyng-panel-name');
			nameTextbox.type = 'text';
			nameTextbox.name = 'name';
			nameTextbox.size = '32';
			nameTextbox.autocomplete = 'off';
			nameTextbox.value = filter.name;

			var terms = document.createElement('fieldset');
			var legend = document.createElement('legend');
			legend.textContent = filter.name + $str.rules;

			var table = document.createElement('table');
			table.classList.add('feedlyng-panel-terms');

			var tbody = document.createElement('tbody');
			for (let [type, labelText] in Iterator({
				title:       $str.title,
				url:         $str.url,
				sourceTitle: $str.source_title,
				sourceURL:   $str.source_url,
				author:      $str.author,
				body:        $str.body,
			})) {
				let row = document.createElement('tr');

				let left = document.createElement('td');
				let center = document.createElement('td');
				let right = document.createElement('td');

				let textbox = document.createElement('input');
				textbox.classList.add('feedlyng-panel-terms-textbox');
				textbox.type = 'text';
				textbox.name = 'regexp.' + type + '.source';
				textbox.size = '32';
				textbox.autocomplete = 'off';
				if (type in filter.regexp)
					textbox.value = filter.regexp[type].source.replace(/((?:^|[^\\])(?:\\\\)*)\\(?=\/)/g, '$1');

				let label = createLabel(textbox, labelText);
				label.classList.add('feedlyng-panel-terms-textbox-label');

				let ic = document.createElement('input');
				ic.classList.add('feedlyng-panel-terms-checkbox');
				ic.type = 'checkbox';
				ic.name = 'regexp.' + type + '.ignoreCase';
				if (type in filter.regexp)
					ic.checked = filter.regexp[type].ignoreCase;

				let icl = createLabel(ic, 'i');
				icl.classList.add('feedlyng-panel-terms-checkbox-label');
				icl.title = $str.ignore_case;

				tbody.appendChild(row);
					row.appendChild(left);
						left.appendChild(label);
					row.appendChild(center);
						center.appendChild(textbox);
					row.appendChild(right);
						right.appendChild(ic);
						right.appendChild(icl);
			}

			var rules = document.createElement('div');
			rules.classList.add('feedlyng-panel-rules');

			terms.appendChild(legend);
			terms.appendChild(table);
			table.appendChild(tbody);
			this.appendContent([nameTextbox, terms, rules]);

			this.dom.rules = rules;
			filter.children.forEach(this.appendFilter, this);
		},
		appendFilter: function appendFilter(filter) {
			var panel;

			var updateRow = () => {
				var title = $str.hit_count + ':\t' + filter.hitcount;
				if (filter.lasthit)
					title += '\n' + $str.last_hit + ':\t' + new Date(filter.lasthit).toLocaleString();

				rule.title = title;
				name.textContent = filter.name;
				count.textContent = filter.children.length || '';
			};
			var onEdit = () => {
				if (panel) {
					panel.close();
					return;
				}
				panel = new FilterListPanel(filter);
				panel.on('shown', () => edit.querySelector('.jfk-button').classList.add('jfk-button-checked'));
				panel.on('hidden', () => {
					edit.querySelector('.jfk-button').classList.remove('jfk-button-checked');
					panel = null;
				});
				panel.on('apply', setTimeout.bind(null, updateRow, 0));
				panel.open(this);
			};
			var onCopy = () => clipboard.setForCopy(filter);
			var onDelete = () => {
				document.adoptNode(rule);
				this.on('apply', () => this.filter.removeChild(filter));
			}

			var rule = document.createElement('div');
			rule.classList.add('feedlyng-panel-rule');
			if (filter.children.length)
				rule.classList.add('parent');

			var name = document.createElement('div');
			name.classList.add('feedlyng-panel-rule-name');
			name.addEventListener('dblclick', onEdit, true);

			var count = document.createElement('div');
			count.classList.add('feedlyng-panel-rule-count');

			var buttons = document.createElement('div');
			buttons.classList.add('feedlyng-panel-rule-buttons');

			var edit = createGoogMenuButton($str.edit, onEdit, [[$str.copy, onCopy], [$str.delete, onDelete]]);
			edit.classList.add('feedlyng-panel-rule-edit');

			updateRow();

			rule.appendChild(name);
			rule.appendChild(count);
			rule.appendChild(buttons);
			buttons.appendChild(edit);
			this.dom.rules.appendChild(rule);
		},
		handleEvent: function handleEvent(event) {
			if (event.type !== 'apply')
				return;

			var data = this.getFormData(true);
			var filter = this.filter;
			filter.name = data.name.value;

			var regexp = {};
			var error = false;
			for (let [type, {source, ignoreCase}] in Iterator(data.regexp)) {
				if (!source.value)
					continue;

				try {
					regexp[type] = RegExp(source.value, ignoreCase.checked ? 'i' : '');
				} catch (e if e instanceof SyntaxError) {
					error = true;
					event.preventDefault();
					source.classList.remove('error');
					source.offsetWidth;
					source.classList.add('error');
				}
			}
			if (error)
				return;

			filter.regexp = regexp;
			filter.sortChildren();
		},
	});

	var GoogMenu = Class({
		constructor: function GoogMenu(anchorElement, items) {
			this.items = items;
			this.anchorElement = anchorElement;
			anchorElement.addEventListener('mousedown', this, false);
		},
		get opened() !!((this.dom || 0).element || 0).parentNode,
		init: function init() {
			var menu = document.createElement('div');
			menu.className = 'feedlyng goog-menu goog-menu-vertical';
			menu.addEventListener('click', this, false);
			this.items.forEach((item) => {
				var menuitem = document.createElement('div');
				if (typeof item === 'string') {
					if (/^-+$/.test(item))
						menuitem.className = 'goog-menuseparator';

				} else {
					var [label, fn] = item;
					menuitem.className = 'goog-menuitem';
					var content = document.createElement('div');
					content.className = 'goog-menuitem-content';
					content.textContent = label;
					menuitem.appendChild(content);
					if (fn)
						menuitem.addEventListener('click', fn, false);
				}
				menu.appendChild(menuitem);
			});

			this.dom = {
				element: menu,
			};
		},
		open: function open() {
			if (this.opened)
				return;

			var {right, bottom} = this.anchorElement.getBoundingClientRect();
			var menu = this.dom.element;
			document.body.appendChild(menu);
			menu.style.left = right - menu.offsetWidth + 'px';
			menu.style.top = bottom + 'px';

			this.anchorElement.classList.add('goog-flat-menu-button-open');
			document.addEventListener('mousedown', this, true);
			document.addEventListener('blur', this, true);
		},
		close: function close() {
			document.removeEventListener('mousedown', this, true);
			document.removeEventListener('blur', this, true);
			document.adoptNode(this.dom.element);
			this.anchorElement.classList.remove('goog-flat-menu-button-open');
		},
		handleEvent: function handleEvent({type, target, currentTarget}) {
			switch (type) {
			case 'blur':
				if (target === document)
					this.close();

				return;
			case 'click':
				if (target.mozMatchesSelector('.goog-menuitem, .goog-menuitem *'))
					this.close();

				return;
			case 'mousedown':
				var pos = this.anchorElement.compareDocumentPosition(target);
				if (currentTarget === document && (!pos || pos & target.DOCUMENT_POSITION_CONTAINED_BY))
					return;

				if (this.opened) {
					if (!target.mozMatchesSelector('.goog-menu *'))
						this.close();

				} else {
					if (!this.dom)
						this.init();

					this.open();
				}
				return;
			}
		},
	});

	Preference.defaultPref = {
		filter: {
			name: '',
			regexp: {},
			children: [
				{
					name: 'AD',
					regexp: {
						title: /^\W?(?:ADV?|PR)\b/,
					},
					children: [],
				},
			],
		},
	}.toSource();

	evalInContent($TEXT(() => {/*
		(() => {
			var XHR = XMLHttpRequest;
			var uniqueId = 0;

			XMLHttpRequest = function XMLHttpRequest() {
				var req = new XHR();
				req.open = open;
				req.setRequestHeader = setRequestHeader;
				req.addEventListener('readystatechange', onReadyStateChange, false);
				return req;
			};
			function open(method, url, async) {
				this.__url__ = url;
				return XHR.prototype.open.apply(this, arguments);
			}
			function setRequestHeader(header, value) {
				if (header === 'Authorization')
					this.__auth__ = value;

				return XHR.prototype.setRequestHeader.apply(this, arguments);
			}
			function onReadyStateChange() {
				if (this.readyState < 4 || this.status !== 200)
					return;

				if (!/^\/\/(?:cloud\.)?feedly\.com\/v3\/streams\/contents\b/.test(this.__url__))
					return;

				var pongEventType = 'streamcontentloaded_callback' + uniqueId++;

				var data = JSON.stringify({
					type: pongEventType,
					auth: this.__auth__,
					text: this.responseText,
				});

				try {
					var event = new MessageEvent('streamcontentloaded', {
						bubbles: true,
						cancelable: false,
						data: data,
						origin: location.href,
						source: null,
					});
				} catch (e) {
					var event = document.createEvent('MessageEvent');
					event.initMessageEvent('streamcontentloaded', true, false, data, location.href, '', null);
				}

				var onPong = ({data}) => Object.defineProperty(this, 'responseText', {configurable: true, value: data});
				document.addEventListener(pongEventType, onPong, false);
				document.dispatchEvent(event);
				document.removeEventListener(pongEventType, onPong, false);
			}
		})();
	*/}));

	document.addEventListener('streamcontentloaded', function(event) {
		var {type: pongEventType, auth, text} = JSON.parse(event.data);
		var data = JSON.parse(text);

		var logging = pref.get('logging', true);
		var filter = pref.get('filter');
		var filteredEntryIds = [];
		var hasUnread = false;

		data.items = data.items.filter((item) => {
			var 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;

		var data = JSON.stringify(data);
		try {
			var ev = new MessageEvent(pongEventType, {
				bubbles: true,
				cancelable: false,
				data: data,
				origin: location.href,
				source: window,
			});
		} catch (e if e instanceof TypeError) {
			var ev = document.createEvent('MessageEvent');
			ev.initMessageEvent(pongEventType, true, false, data, location.href, '', null);
		}
		document.dispatchEvent(ev);

		if (!hasUnread)
			return;

		sendJSON({
			url: '/v3/markers',
			headers: {
				Authorization: auth,
			},
			data: {
				action: 'markAsRead',
				entryIds: filteredEntryIds,
				type: 'entries',
			},
		});
	}, false);

	var contextmenu = document.createElement('menu');
	contextmenu.type = 'context';
	contextmenu.id = 'feedlyng-contextmenu';
	MenuCommand.contextmenu = contextmenu;

	var rootFilterPanel;
	var settingsMenuItem;
	var clipboard = new DataTransfer();
	var pref = new Preference();
	pref.on('change', function({propertyName, newValue}) {
		switch (propertyName) {
		case 'filter':
			if (!Filter.prototype.isPrototypeOf(newValue))
				this.set('filter', new Filter(newValue));

			break;

		case 'language':
			Locale.select(newValue);
			break;
		}
	});

	document.addEventListener('DOMContentLoaded', () => {
		GM_addStyle(CSS_STYLE_TEXT);

		pref.load();
		pref.autoSave();

		registerMenuCommands();
		addSettingsMenuItem();
	}, false);

	function registerMenuCommands() {
		menuCommand($str.setting + '...', togglePrefPanel);
		menuCommand($str.language + '...', function() {
			var langField = document.createElement('fieldset');

			var title = document.createElement('legend');
			title.textContent = $str.language;

			var select = document.createElement('select');
			Locale.languages.forEach((lang) => {
				var option = document.createElement('option');
				option.value = lang;
				option.textContent = lang;
				if (lang === Locale.selectedLanguage)
					option.selected = true;

				select.appendChild(option);
			});

			langField.appendChild(title);
			langField.appendChild(select);

			var p = new Panel();
			p.appendContent(langField);
			p.on('apply', () => pref.set('language', select.value));
			p.open();
		});
		menuCommand($str.import_setting + '...', () => pref.importFromFile());
		menuCommand($str.export_setting, () => pref.exportToFile());
	}
	function togglePrefPanel(anchorElement) {
		if (rootFilterPanel) {
			rootFilterPanel.close();
			return;
		}
		rootFilterPanel = new FilterListPanel(pref.get('filter'), true);
		rootFilterPanel.on('apply', () => showMessage($str.ng_setting_modified));
		rootFilterPanel.on('hidden', () => {
			clipboard.purge();
			rootFilterPanel = null;
		});
		rootFilterPanel.open(anchorElement);
	}
	function onNGSettingCommand({target}) {
		togglePrefPanel(target);
	}
	function createGoogButton(text, fn) {
		var button = document.createElement('div');
		button.className = 'goog-inline-block jfk-button jfk-button-standard unselectable';
		button.tabIndex = 0;
		button.textContent = text;
		if (fn) {
			button.addEventListener('click', fn, false);
			button.addEventListener('keydown', function({which}) {
				if (which === 13)
					fn.apply(this, arguments);
			}, false);
		}

		return button;
	}
	function createGoogMenuButton(text, fn, arr) {
		var container = document.createElement('div');
		container.className = 'goog-inline-block';

		var button = createGoogButton(text, fn);
		button.classList.add('jfk-button-collapse-right');

		var options = document.createElement('div');
		options.className = 'goog-inline-block goog-flat-menu-button goog-flat-menu-button-collapse-left unselectable';
		options.tabIndex = 0;

		container.appendChild(button);
		container.appendChild(options);
		options.insertAdjacentHTML('beforeend', '<div class="goog-inline-block goog-flat-menu-button-caption">&nbsp;</div>');
		options.insertAdjacentHTML('beforeend', '<div class="goog-inline-block goog-flat-menu-button-dropdown"></div>');

		new GoogMenu(options, arr);

		return container;
	}
	function showMessage(str, type) {
		if (typeof GM_notification === 'function')
			GM_notification(str);
	}
	function addSettingsMenuItem() {
		var feedlyTabs = document.getElementById('feedlyTabs');
		if (!feedlyTabs) {
			setTimeout(addSettingsMenuItem, 100);
			return;
		}

		var prefListener;
		var observer = new MutationObserver(function mutationCallback() {
			if (!document.getElementById('feedly-ng-filter-setting'))
				pref.removeListener('change', prefListener);

			var prefItem = document.querySelector('#feedlyTabs .tab > .label[data-uri="preferences"]');
			if (!prefItem)
				return;

			var prefItemTab = prefItem.parentNode;

			var tab = document.createElement('div');
			tab.className = 'tab';
			tab.setAttribute('contextmenu', MenuCommand.contextmenu.id);
			tab.addEventListener('click', onNGSettingCommand, false);

			var label = document.createElement('div');
			label.id = 'feedly-ng-filter-setting';
			label.className = 'label primary iconless';
			label.textContent = $str.ng_setting;

			tab.appendChild(label);
			prefItemTab.parentNode.insertBefore(tab, prefItemTab.nextSibling);
			document.body.appendChild(contextmenu);

			prefListener = ({propertyName}) => {
				if (propertyName === 'language')
					label.textContent = $str.ng_setting;
			};
			pref.on('change', prefListener);
		});
		observer.observe(feedlyTabs, {
			childList: true,
		});
	}
	function menuCommand(label, fn) {
		return new MenuCommand($str.app_name + ' - ' + label, fn);
	}
	function xhr(details) {
		var opt = extend({}, details);
		var {data} = opt;

		if (!opt.method)
			opt.method = data ? 'POST' : 'GET';

		if (data instanceof Object) {
			opt.data = [pair.map(encodeURIComponent).join('=') for (pair in Iterator(data))].join('&');
			if (!opt.headers)
				opt.headers = {};

			opt.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
		}

		setTimeout(GM_xmlhttpRequest, 0, opt);
	}
	function sendJSON(details) {
		var opt = extend({}, details);
		var {data} = opt;
		if (!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) {
		var script = document.createElement('script');
		script.type = 'text/javascript;version=1.8';
		script.textContent = code;

		try {
			location.href = 'javascript:' + encodeURIComponent(code) + ';void+0';
//			document.adoptNode(document.appendChild(script));
		} catch (e) {
			document.adoptNode(document.documentElement.appendChild(script));
		}
	}
	function openFilePicker(callback, multiple) {
		var canceled = true;
		var input = document.createElement('input');
		input.type = 'file';
		input.multiple = multiple;
		input.addEventListener('change', () => {
			canceled = false;
			callback(Array.slice(input.files));
		}, false);
		input.click();
		if (canceled)
			setTimeout(callback, 0, null);
	}
	function createLabel(element, text) {
		var label = document.createElement('label');
		if (1 < arguments.length)
			label.textContent = text;

		var id = element.id;
		if (!id) {
			if (!('id' in createLabel))
				createLabel.id = 0;

			id = 'id_for_label_' + createLabel.id++;
			element.id = id;
		}
		label.htmlFor = id;
		return label;
	}
	function extend(dst, src) {
		for (let [key, value] in Iterator(src))
			dst[key] = value;

		return dst;
	}
	function $TEXT(fn) String.replace(fn, /^\(\) => \{\/\*|\*\/}$/g, '');
})();