Patchouli

An image searching/browsing tool on Pixiv

目前為 2017-04-18 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name		Patchouli
// @description	An image searching/browsing tool on Pixiv
// @namespace	https://github.com/FlandreDaisuki
// @author		FlandreDaisuki
// @include		*://www.pixiv.net/*
// @require		https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js
// @require		https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.0/axios.min.js
// @version		2017.04.19
// @icon		http://i.imgur.com/VwoYc5w.png
// @grant		none
// @noframes
// ==/UserScript==
const version = '2017.04.19';

class L10N {
	constructor() {
		this.lang = document.documentElement.lang;
		this.following = this._following();
		this.bookmark = this._bookmark();
		this.koakumaGo = this._koakumaGo();
		this.koakumaPause = this._koakumaPause();
		this.koakumaEnd = this._koakumaEnd();
		this.koakumaFullwidth = this._koakumaFullwidth();
		this.koakumaSort = this._koakumaSort();
	}

	_following() {
		switch(this.lang) {
			case 'ja': return 'フォロー中';
			case 'zh-tw': return '關注中';
			case 'zh': return '关注中';
			default: return 'following';
		}
	}

	_bookmark() {
		switch(this.lang) {
			case 'ja': return 'ブックマーク';
			case 'zh-tw':
			case 'zh': return '收藏';
			default: return 'Bookmark';
		}
	}

	_koakumaGo() {
		switch(this.lang) {
			case 'ja': return '捜す';
			case 'zh-tw':
			case 'zh': return '找';
			default: return 'Go';
		}
	}

	_koakumaPause() {
		switch(this.lang) {
			case 'ja': return '中断';
			case 'zh-tw':
			case 'zh': return '停';
			default: return 'Pause';
		}
	}

	_koakumaEnd() {
		switch(this.lang) {
			case 'ja': return '終了';
			case 'zh-tw':
			case 'zh': return '完';
			default: return 'End';
		}
	}

	koakumaProcessed(n) {
		switch(this.lang) {
			case 'ja': return `${n} 件が処理された`;
			case 'zh-tw': return `已處理 ${n} 張`
			case 'zh': return `已处理 ${n} 张`;
			default: return `${n} pics processed`;
		}
	}

	_koakumaFullwidth() {
		switch(this.lang) {
			case 'ja': return '全幅';
			case 'zh-tw': return '全寬';
			case 'zh': return '全宽';
			default: return 'fullwidth';
		}
	}

	_koakumaSort() {
		switch(this.lang) {
			case 'ja': return 'ソート';
			case 'zh-tw':
			case 'zh': return '排序';
			default: return 'sorted';
		}
	}

	bookmarkTooltip(n) {
		switch(this.lang) {
			case 'ja': return `${n}件のブックマーク`;
			case 'zh-tw': return `${n}個收藏`
			case 'zh': return `${n}个收藏`;
			default: return `${n} bookmarks`;
		}
	}
}

class Pixiv {
	constructor() {
		this.tt = document.querySelector('input[name="tt"]').value;
	}

	fetch(url) {
		return axios.get(url)
			.then(res => {
				return new Promise((resolve, reject) => {
					(res.statusText !== 'OK') ?
						reject(res) : resolve(res.data);
				});
			})
			.catch(console.error);
	}

	static toDOM(html) {
		return (new DOMParser()).parseFromString(html, 'text/html');
	}

	static rmAnnoyance(doc = document) {
		[
			'iframe',
			//Ad
			'.ad',
			'.ads_area',
			'.ad-footer',
			'.ads_anchor',
			'.ads-top-info',
			'.comic-hot-works',
			'.user-ad-container',
			'.ads_area_no_margin',
			//Premium
			'.hover-item',
			'.ad-printservice',
			'.bookmark-ranges',
			'.require-premium',
			'.showcase-reminder',
			'.sample-user-search',
			'.popular-introduction',
		].forEach(cl => {
			Array.from(doc.querySelectorAll(cl)).forEach(el => {
				el.remove();
			});
		});
	}

	static storageGet() {
		if(!localStorage.getItem('むきゅー')) {
			Pixiv.storageSet({});
		}
		return JSON.parse(localStorage.getItem('むきゅー'));
	}

	static storageSet(obj) {
		localStorage.setItem('むきゅー', JSON.stringify(obj));
	}

	static hrefAttr(elem) {
		const a = elem;
		if (!a) {
			return '';
		} else if(a.href) {
			// Firefox
			return a.href;
		} else {
			// Chrome
			const m = a.outerHTML.match(/href="([^"]+)"/);
			if (!m) {
				return '';
			}
			const query = m[1].replace(/&/g, '&');

			return `${location.pathname}${query}`;
		}
	}

	getBookmarkCount(illust_id) {
		const url = `/bookmark_detail.php?illust_id=${illust_id}`;

		return this.fetch(url)
			.then(Pixiv.toDOM)
			.then(doc => {
				const _a = doc.querySelector('a.bookmark-count');
				const bookmark_count = _a ? parseInt(_a.innerText) : 0;

				return {
					bookmark_count,
					illust_id,
				};
			})
			.catch(console.error);
	}

	getBookmarksDetail(illust_ids) {
		const _f = this.getBookmarkCount.bind(this);
		const bookmarks = illust_ids.map(_f);

		return Promise.all(bookmarks)
			.then(arr => arr.reduce((a, b) => {
				a[b.illust_id] = b;
				return a;
			}, {}))
			.catch(console.error);
	}

	getIllustPageDetail(illust_id) {
		const url = `/member_illust.php?mode=medium&illust_id=${illust_id}`;

		return this.fetch(url)
			.then(Pixiv.toDOM)
			.then(doc => {
				const _a = doc.querySelector('.score-count');
				const rating_score = _a ? parseInt(_a.innerText) : 0;
				return {
					illust_id,
					rating_score,
				};
			})
			.catch(console.error);
	}

	getIllustPagesDetail(illust_ids) {
		const _f = this.getIllustPageDetail.bind(this);
		const pages = illust_ids.map(_f);

		return Promise.all(pages)
			.then(arr => arr.reduce((a, b) => {
				a[b.illust_id] = b;
				return a;
			}, {}))
			.catch(console.error);
	}

	getIllustsDetail(illust_ids) {
		const _a = illust_ids.join(',');
		const url = `/rpc/index.php?mode=get_illust_detail_by_ids&illust_ids=${_a}&tt=${this.tt}`;

		return this.fetch(url)
			.then(json => json.body)
			.catch(console.error);
	}

	getUsersDetail(user_ids) {
		const _a = user_ids.join(',');
		const url = `/rpc/get_profile.php?user_ids=${_a}&tt=${this.tt}`;

		return this.fetch(url)
			.then(json => json.body)
			.then(arr => arr.reduce((a, b) => {
				// make the same output of getIllustsDetail
				a[b.user_id] = b;
				return a;
			}, {}))
			.catch(console.error);
	}

	getPageInformationAndNext(url) {
		return this.fetch(url)
		.then(Pixiv.toDOM)
		.then(doc => {
			Pixiv.rmAnnoyance(doc);
			const _a = doc.querySelector('.next a');
			const next_url = Pixiv.hrefAttr(_a); //FF & Chrome issue
			const _b = doc.querySelectorAll('.image-item img');
			const illusts = Array.from(_b).map(x => ({
								illust_id: x.dataset.id,
								thumb_src: x.dataset.src,
								user_id: x.dataset.userId,
								tags: x.dataset.tags.split(' '),
							})).filter(x => x.illust_id !== '0');
			return {
				next_url,
				illusts,
			};
		})
		.catch(console.error);
	}

	postBookmarkadd(illust_id) {
		const data = [
			'mode=save_illust_bookmark',
			`illust_id=${illust_id}`,
			'restrict=0',
			'comment=',
			'tags=',
			`tt=${this.tt}`,
		].join('&');
		const config = {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'};

		return axios.post('/rpc/index.php', data, config)
			.then(res => {
				return new Promise((resolve, reject) => {
					(res.statusText === 'OK' && !res.data.error) ? resolve(true) : reject(res);
				});
			})
			.catch(console.error);
	}
}

(() => {
	// version check
	const s = Pixiv.storageGet();
	if (!s.version || s.version < version) {
		Pixiv.storageSet({
			version,
		});
	}
})();

const utils = {
	linkStyle: function(url) {
		const link = document.createElement('link');
		link.rel = 'stylesheet';
		link.type = 'text/css';
		link.href = url;
		document.head.appendChild(link);
	},
	linkScript: function(url) {
		const script = document.createElement('script');
		script.src = url;
		document.head.appendChild(script);
	},
	createIcon: function(name, options = {}) {
		const el = document.createElement('i');
		el.classList.add('fa');
		el.classList.add(`fa-${name}`);
		el.setAttribute('aria-hidden', 'true');
		return el;
	},
	addStyle: function(text) {
		const style = document.createElement('style');
		style.innerHTML = text;
		document.head.appendChild(style);
	},
	asyncWhile: function(condition, action, options = {}) {
		options = Object.assign({
			first: undefined,
			ctx: this,
		}, options);
		const ctx = options.ctx;
		const first = options.first;
		const whilst = function(data) {
			return condition.call(ctx, data) ?
				Promise.resolve(action.call(ctx, data)).then(whilst) :
				data;
		};

		return whilst(first);
	}
};

const globalStore = {
	api: new Pixiv(),
	l10n: new L10N(),
	books: [],
	filters: {
		limit: 0,
		orderBy: 'illust_id',
	},
	patchouliToMount: (() => {
		const _a = document.querySelector('li.image-item');
		return _a ? _a.parentElement : null;
	})(),
	koakumaToMount: (() => {
		return document.querySelector('#toolbar-items');
	})(),
	page: (() => {
		let type = 'default';
		let supported = true;
		let path = location.pathname;
		let search = new URLSearchParams(location.search);

		/** type - for patchouli <image-item>
		 *
		 *	default: thumb + title + user + count-list
		 *	member-illust: default w/o user
		 *	mybookmark:  default with bookmark-edit
		 */

		switch(path) {
			case '/search.php':
			case '/bookmark_new_illust.php':
			case '/new_illust.php':
			case '/mypixiv_new_illust.php':
			case '/new_illust_r18.php':
			case '/bookmark_new_illust_r18.php':
				break;
			case '/member_illust.php':
				if (search.has('id')) {
					type = 'member-illust';
				} else {
					supported = false;
				}
				break;
			case '/bookmark.php':
				const t = search.get('type')
				if(search.has('id')) {
					type = 'default';
				} else if(!t || t === 'illust_all') {
					type = 'mybookmark';
				} else {
					// e.g. http://www.pixiv.net/bookmark.php?type=reg_user
					supported = false;
				}
				break;
			default:
				supported = false;
		}

		return {
			supported,
			type,
		};
	})(),
};

globalStore.favorite = (()=>{
	const _f = Object.assign({
		fullwidth: 1,
		sort: 0,
	}, Pixiv.storageGet());

	if (_f.fullwidth) {
		document.querySelector('#wrapper').classList.add('fullwidth');
	}

	if (_f.sort) {
		globalStore.filters.orderBy = 'bookmark_count';
	}

	Pixiv.storageSet(_f);
	return _f;
})();

Vue.component('koakuma-settings', {
	props: ['favorite', 'l10n'],
	methods: {
		fullwidthClick(event) {
			this.$emit('fullwidthUpdate', event.target.checked);
		},
		sortClick(event) {
			this.$emit('sortUpdate', event.target.checked);
		},
	},
	template: `
	<div>
		<input id="koakuma-settings-fullwidth" type="checkbox"
			:checked="favorite.fullwidth"
			@click="fullwidthClick"> {{l10n.koakumaFullwidth}}
		<input id="koakuma-settings-sort" type="checkbox"
			:checked="favorite.sort"
			@click="sortClick"> {{l10n.koakumaSort}}
	</div>`,
});

Vue.component('koakuma-bookmark', {
	props: ['limit', 'l10n'],
	methods: {
		blur(event) {
			const self = event.target;
			if(!self.validity.valid) {
				console.error('koakuma-bookmark', self.validationMessage);
			}
		},
		input(event) {
			let val = parseInt(event.target.value);
			val = Math.max(0, val);
			this.$emit('limitUpdate', val);
		},
		wheel(event) {
			let val;
			if (event.deltaY < 0) {
				val = this.limit + 20;
			} else {
				val = Math.max(0, this.limit - 20);
			}
			this.$emit('limitUpdate', val);
		},
	},
	template:`
	<div id="koakuma-bookmark">
		<label for="koakuma-bookmark-input">★{{l10n.bookmark}}</label>
		<input id="koakuma-bookmark-input"
			type="number" min="0" step="1"
			:value="limit"
			@wheel.stop.prevent="wheel"
			@blur="blur"
			@input="input"/>
	</div>`,
});

// Vue.component('koakuma-tag-filter', {
// //todo
// });

const koakuma = new Vue({
	// make koakuma to left side
	data: {
		l10n: globalStore.l10n,
		books: globalStore.books,
		filters: globalStore.filters,
		api: globalStore.api,
		favorite: globalStore.favorite,
		next_url: location.href,
		isStoped: true,
		isEnded: false,
	},
	methods: {
		start(times = Infinity) {
			this.isStoped = false;

			return utils.asyncWhile(next_url => {
				if (!next_url) {
					this.isStoped = this.isEnded = true;
					return false;
				}
				if (times > 0) {
					times--;
				} else {
					this.isStoped = true;
				}
				return !this.isStoped;
			}, url => {
				return this.api.getPageInformationAndNext(url)
					.then(inf => {
						return new Promise((resolve, reject) => {
							if (inf.next_url === this.next_url) {
								reject(`Duplicated url: ${url}`);
							} else {
								resolve(inf);
							}
						});
					})
					.then(inf => {
						this.next_url = inf.next_url;

						const user_ids = inf.illusts.map(x => x.user_id);
						const illust_ids = inf.illusts.map(x => x.illust_id);
						const usersDetail = this.api.getUsersDetail(user_ids);
						const illustsDetail = this.api.getIllustsDetail(illust_ids);
						const illustPagesDetail = this.api.getIllustPagesDetail(illust_ids);
						const bookmarkDetail = this.api.getBookmarksDetail(illust_ids);

						return new Promise((resolve, reject) => {
							Promise.all([usersDetail, illustsDetail, illustPagesDetail, bookmarkDetail])
								.then(resolve)
								.catch(reject);
						})
						.then(details => {
							return {
								next_url: inf.next_url,
								illusts: inf.illusts,
								usersDetail: details[0],
								illustsDetail: details[1],
								illustPagesDetail: details[2],
								bookmarkDetail: details[3],
							};
						})
						.catch(console.error);
					})
					.then(inf => {
						const books = inf.illusts.map(illust => {
							const ud = inf.usersDetail;
							const ild = inf.illustsDetail;
							const ipd = inf.illustPagesDetail;
							const bd = inf.bookmarkDetail;

							return {
								illust_id: illust.illust_id,
								thumb_src: illust.thumb_src,
								user_id: illust.user_id,
								// tags: illust.tags,
								user_name: ud[illust.user_id].user_name,
								is_follow: ud[illust.user_id].is_follow,
								illust_title: ild[illust.illust_id].illust_title,
								is_multiple: ild[illust.illust_id].is_multiple,
								is_bookmarked: ild[illust.illust_id].is_bookmarked,
								is_manga: ild[illust.illust_id].illust_type === '1',
								is_ugoira: !!ild[illust.illust_id].ugoira_meta,
								bookmark_count: bd[illust.illust_id].bookmark_count,
								rating_score: ipd[illust.illust_id].rating_score,
							};
						});

						this.books.push(...books);
						return inf;
					})
					.then(inf => {
						return inf.next_url;
					})
					.catch(console.error);
			}, {
				first: this.next_url,
			});
		},
		stop() {
			this.isStoped = true;
		},
		switchSearching() {
			if (this.isStoped) {
				this.start();
			} else {
				this.stop();
			}
		},
		limitUpdate(value) {
			globalStore.filters.limit = isNaN(value) ? 0 : value;
		},
		fullwidthUpdate(todo) {
			if(todo) {
				document.querySelector('#wrapper').classList.add('fullwidth');
				globalStore.favorite.fullwidth = 1;
			} else {
				document.querySelector('#wrapper').classList.remove('fullwidth');
				globalStore.favorite.fullwidth = 0;
			}
			Pixiv.storageSet(globalStore.favorite);
		},
		sortUpdate(todo) {
			if(todo) {
				globalStore.filters.orderBy = 'bookmark_count';
				globalStore.favorite.sort = 1;
			} else {
				globalStore.filters.orderBy = 'illust_id';
				globalStore.favorite.sort = 0;
			}
			Pixiv.storageSet(globalStore.favorite);
		},
	},
	computed: {
		switchText() {
			return this.isEnded ? this.l10n.koakumaEnd :
				(this.isStoped ? this.l10n.koakumaGo : this.l10n.koakumaPause);
		},
		switchStyle() {
			return {
				ended: this.isEnded,
				toSearch: !this.isEnded && this.isStoped,
				toStop: !this.isEnded && !this.isStoped,
			};
		},
	},
	template: `
		<div id="こあくま">
			<div>{{l10n.koakumaProcessed(books.length)}}</div>
			<koakuma-bookmark :l10n="l10n"
				:limit="filters.limit"
				@limitUpdate="limitUpdate"></koakuma-bookmark>
			<button id="koakuma-switch"
				@click="switchSearching"
				:disabled="isEnded"
				:class="switchStyle">{{ switchText }}</button>
			<koakuma-settings :l10n="l10n"
				:favorite="favorite"
				@fullwidthUpdate="fullwidthUpdate"
				@sortUpdate="sortUpdate"></koakuma-settings>
		</div>`,
});

Vue.component('image-item-thumb', {
	props:['detail'],
	computed: {
		thumbStyle() {
			return {
				multiple: this.detail.multiple,
				manga: this.detail.manga,
				'ugoku-illust': this.detail.ugoira,
			};
		},
	},
	template:`
		<a class="work _work" :href="detail.href" :class="thumbStyle">
			<div><img :src="detail.src"></div>
		</a>`,
});

Vue.component('image-item-title', {
	props:['detail'],
	template:`
		<a :href="detail.href">
			<h1 class="title" :title="detail.title">{{ detail.title }}</h1>
		</a>`,
});

Vue.component('image-item-user', {
	props:['user'],
	computed: {
		href() {
			return `/member_illust.php?id=${this.user.id}`;
		},
		userStyle() {
			return {
				following: this.user.is_follow,
			};
		},
	},
	template:`
		<span>
			<a class="user ui-profile-popup"
				:href="href"
				:title="user.name"
				:data-user_id="user.id">{{ user.name }}</a>
			<i class="fa fa-feed" aria-hidden="true" v-show="user.is_follow"></i>
		</span>`,
});

Vue.component('image-item-count-list', {
	props:['api', 'detail', 'l10n'],
	data() {
		return {
			bookmarked: this.detail.bookmarked,
		};
	},
	computed: {
		tooltip() {
			return this.l10n.bookmarkTooltip(this.detail.bmkcount);
		},
		shortRating() {
			return (this.detail.rating > 10000) ? `${(this.detail.rating / 1e3).toFixed(1)}K` : this.detail.rating;
		},
		bookmarkStyle() {
			return this.bookmarked ? 'fa-bookmark' : 'fa-bookmark-o';
		},
	},
	methods: {
		click(event) {
			if(!this.bookmarked) {
				this.api.postBookmarkadd(this.detail.illust_id);
				this.$emit('bookmarkUpdate', this.detail.illust_id);
				this.bookmarked = true;
			}
		},
	},
	template:`
		<ul class="count-list">
			<li v-if="detail.bmkcount > 0">
				<a class="bookmark-count _ui-tooltip"
					:href="detail.bmkhref"
					:data-tooltip="tooltip">
					<i class="_icon sprites-bookmark-badge"></i>{{ detail.bmkcount }}</a>
			</li>
			<li v-if="detail.rating > 0">
				<span class="rating-score">
					<i class="fa fa-star" aria-hidden="true"></i>{{ shortRating }}
				</span>
			</li>
			<li>
				<a class="is-bookmarked" @click.prevent="click">
					<i class="fa" :class="bookmarkStyle" aria-hidden="true"></i>
				</a>
			</li>
		</ul>`,
});

Vue.component('image-item', {
	props:['api', 'l10n', 'detail', 'pagetype'],
	computed: {
		illust_page_href() {
			return `/member_illust.php?mode=medium&illust_id=${this.detail.illust_id}`;
		},
		bookmark_detail_href() {
			return `/bookmark_detail.php?illust_id=${this.detail.illust_id}`;
		},
		thumb_detail() {
			return {
				href: this.illust_page_href,
				src: this.detail.thumb_src,
				multiple: this.detail.is_multiple,
				manga: this.detail.is_manga,
				ugoira: this.detail.is_ugoira,
			};
		},
		user_detail() {
			return {
				id: this.detail.user_id,
				name: this.detail.user_name,
				is_follow: this.detail.is_follow,
			};
		},
		title_detail() {
			return {
				href: this.illust_page_href,
				title: this.detail.illust_title,
			};
		},
		count_detail() {
			return {
				bmkhref: this.bookmark_detail_href,
				bmkcount: this.detail.bookmark_count,
				rating: this.detail.rating_score,
				bookmarked: this.detail.is_bookmarked,
				illust_id: this.detail.illust_id,
			};
		},
	},
	template: `
		<li class="image-item">
			<image-item-thumb :detail="thumb_detail"></image-item-thumb>
			<image-item-title :detail="title_detail"></image-item-title>
			<image-item-user :user="user_detail" v-if="pagetype !== 'member-illust'"></image-item-user>
			<image-item-count-list :detail="count_detail" :api="api" :l10n="l10n"></image-item-count-list>
		</li>`,
});

const patchouli = new Vue({
	data: {
		api: globalStore.api,
		l10n: globalStore.l10n,
		books: globalStore.books,
		filters: globalStore.filters,
		pagetype: globalStore.page.type,
	},
	computed: {
		sortedBooks() {
			const _limit = this.filters.limit;
			const _order = this.filters.orderBy;
			const _books = this.books.filter(b => b.bookmark_count >= _limit);
			return _books.sort((a, b) => b[_order] - a[_order]);
		},
	},
	methods: {
		bookmarkUpdate(illust_id) {
			const _a = this.books.filter(b => b.illust_id === illust_id);
			if (_a.length) {
				_a[0].is_bookmarked = true;
			}
		},
	},
	template:`
	<ul id="パチュリー">
		<image-item v-for="book in sortedBooks"
			:key="book.illust_id"
			:api="api"
			:l10n="l10n"
			:detail="book"
			:pagetype="pagetype"></image-item>
	</ul>`,
});

console.log('Vue.version:', Vue.version);
console.log('Patchouli version:', version);

if (globalStore.page.supported) {
	koakuma.$mount(globalStore.koakumaToMount);
	koakuma.start(1).then(() => {
		patchouli.$mount(globalStore.patchouliToMount);
	});
}
Pixiv.rmAnnoyance();
utils.linkStyle('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css');

if (globalStore.page.supported) {
	utils.addStyle(`
	#wrapper.fullwidth,
	#wrapper.fullwidth .layout-a,
	#wrapper.fullwidth .layout-body {
		width: initial;
	}
	#wrapper.fullwidth .layout-a {
		display: flex;
		flex-direction: row-reverse;
	}
	#wrapper.fullwidth .layout-column-2{
		flex: 1;
		margin-left: 20px;
	}
	#wrapper.fullwidth .layout-body,
	#wrapper.fullwidth .layout-a {
		margin: 10px 20px;
	}


	.fa-feed {
		color: dodgerblue;
		cursor: default;
	}
	.fa-feed:hover::after {
		content:'${globalStore.l10n.following}';
		position: absolute;
		color: white;
		white-space: nowrap;
		background-color: dodgerblue;
		padding: 2px;
		border-radius: 3px;
		margin-left: 8px;
		font-family: "Helvetica Neue","arial","Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
	}
	.rating-score {
		background-color: #FFEE88;
		color: #FF7700;
		border-radius: 3px;
		display: inline-block !important;
		margin: 0 1px;
		padding: 0 6px !important;
		font: bold 10px/18px "lucida grande", sans-serif !important;
		text-decoration: none;
		cursor: default;
	}
	.is-bookmarked {
		cursor: pointer;
	}
	#パチュリー {
		display: flex;
		flex-wrap: wrap;
		justify-content: space-around;
	}


	#koakuma-bookmark {
		display: flex;
	}
	#koakuma-bookmark label{
		white-space: nowrap;
		color: #0069b1 !important;
		background-color: #cceeff;
		border-radius: 3px;
		padding: 0 6px;
	}
	#koakuma-bookmark-input::-webkit-inner-spin-button,
	#koakuma-bookmark-input::-webkit-outer-spin-button {
		-webkit-appearance: none;
		margin: 0;
	}
	#koakuma-bookmark-input {
		-moz-appearance: textfield;
		border: none;
		background-color: transparent;
		padding: 0px;
		color: blue;
		font-size: 16px;
		display: inline-block;
		cursor: ns-resize;
		text-align: center;
		min-width: 0;
	}
	#koakuma-bookmark-input:focus {
		cursor: initial;
	}
	#koakuma-switch {
		border: 0;
		padding: 3px 20px;
		border-radius: 3px;
		font-size: 16px;
	}
	#koakuma-switch:hover {
		box-shadow: 1px 1px gray;
	}
	#koakuma-switch:active {
		box-shadow: 1px 1px gray inset;
	}
	#koakuma-switch:focus {
		outline: 0;
	}
	#koakuma-switch.toSearch {
		background-color: lightgreen;
	}
	#koakuma-switch.toStop {
		background-color: lightpink;
	}
	#koakuma-switch.ended {
		background-color: lightgrey;
	}
	#koakuma-switch.ended:hover,
	#koakuma-switch.ended:hover {
		box-shadow: unset;
	}
	#こあくま {
		position: fixed;
		left: 10px;
		bottom: 10px;
		z-index: 1;
		background-color: aliceblue;
		border-radius: 10px;
		padding: 5px;
		font-size: 16px;
		text-align: center;
		width: 160px;
	}
	#こあくま > * {
		margin: 2px 0;
	}`);
}