Greasy Fork is available in English.

Patchouli

An image searching/browsing tool on Pixiv

2017/03/01時点のページです。最新版はこちら。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name		Patchouli
// @description	An image searching/browsing tool on Pixiv
// @namespace	https://github.com/FlandreDaisuki
// @author		FlandreDaisuki
// @include		http://www.pixiv.net/*
// @require		https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.1/vue.min.js
// @require		https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js
// @version		2017.03.02
// @icon		http://i.imgur.com/VwoYc5w.png
// @grant		none
// @noframes
// ==/UserScript==
const version = '2017.03.02';

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;
	}`);
}