From ca604692628dcba95681964e8deec5ca75049c4e Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 15 Feb 2018 12:36:42 +0900
Subject: [PATCH] wip

---
 src/web/app/common/define-widget.ts           |   4 +-
 .../views/components/widgets/calendar.vue     | 192 ++++++++++++++++++
 .../views/components/widgets/donation.vue     |  45 ++++
 .../views/components/widgets/messaging.vue    |  59 ++++++
 .../common/views/components/widgets/nav.vue   |  29 +++
 .../views/components/widgets/photo-stream.vue | 122 +++++++++++
 .../views/components/widgets/profile.vue      |   4 +-
 .../views/components/widgets/slideshow.vue    | 154 ++++++++++++++
 .../common/views/components/widgets/tips.vue  | 109 ++++++++++
 .../desktop/-tags/home-widgets/calendar.tag   | 167 ---------------
 .../desktop/-tags/home-widgets/donation.tag   |  36 ----
 .../desktop/-tags/home-widgets/messaging.tag  |  52 -----
 .../app/desktop/-tags/home-widgets/nav.tag    |  23 ---
 .../-tags/home-widgets/photo-stream.tag       | 118 -----------
 .../desktop/-tags/home-widgets/slideshow.tag  | 151 --------------
 .../app/desktop/-tags/home-widgets/tips.tag   |  94 ---------
 webpack/plugins/index.ts                      |   4 +-
 17 files changed, 716 insertions(+), 647 deletions(-)
 create mode 100644 src/web/app/common/views/components/widgets/calendar.vue
 create mode 100644 src/web/app/common/views/components/widgets/donation.vue
 create mode 100644 src/web/app/common/views/components/widgets/messaging.vue
 create mode 100644 src/web/app/common/views/components/widgets/nav.vue
 create mode 100644 src/web/app/common/views/components/widgets/photo-stream.vue
 create mode 100644 src/web/app/common/views/components/widgets/slideshow.vue
 create mode 100644 src/web/app/common/views/components/widgets/tips.vue
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/calendar.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/donation.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/messaging.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/nav.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/photo-stream.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/slideshow.tag
 delete mode 100644 src/web/app/desktop/-tags/home-widgets/tips.tag

diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 5102ee1abf..782a69a624 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -2,7 +2,7 @@ import Vue from 'vue';
 
 export default function<T extends object>(data: {
 	name: string;
-	props: T;
+	props?: T;
 }) {
 	return Vue.extend({
 		props: {
@@ -26,7 +26,7 @@ export default function<T extends object>(data: {
 		},
 		data() {
 			return {
-				props: data.props
+				props: data.props || {}
 			};
 		},
 		watch: {
diff --git a/src/web/app/common/views/components/widgets/calendar.vue b/src/web/app/common/views/components/widgets/calendar.vue
new file mode 100644
index 0000000000..308f43cd99
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/calendar.vue
@@ -0,0 +1,192 @@
+<template>
+<div class="mkw-calendar"
+	:data-melt="props.design == 1"
+	:data-special="special"
+>
+	<div class="calendar" :data-is-holiday="isHoliday">
+		<p class="month-and-year">
+			<span class="year">{{ year }}年</span>
+			<span class="month">{{ month }}月</span>
+		</p>
+		<p class="day">{{ day }}日</p>
+		<p class="week-day">{{ weekDay }}曜日</p>
+	</div>
+	<div class="info">
+		<div>
+			<p>今日:<b>{{ dayP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${dayP}%` }"></div>
+			</div>
+		</div>
+		<div>
+			<p>今月:<b>{{ monthP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${monthP}%` }"></div>
+			</div>
+		</div>
+		<div>
+			<p>今年:<b>{{ yearP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${yearP}%` }"></div>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../define-widget';
+export default define({
+	name: 'calendar',
+	props: {
+		design: 0
+	}
+}).extend({
+	data() {
+		return {
+			now: new Date(),
+			year: null,
+			month: null,
+			day: null,
+			weekDay: null,
+			yearP: null,
+			dayP: null,
+			monthP: null,
+			isHoliday: null,
+			special: null,
+			clock: null
+		};
+	},
+	created() {
+		this.tick();
+		this.clock = setInterval(this.tick, 1000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		func() {
+			if (this.props.design == 2) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		},
+		tick() {
+			const now = new Date();
+			const nd = now.getDate();
+			const nm = now.getMonth();
+			const ny = now.getFullYear();
+
+			this.year = ny;
+			this.month = nm + 1;
+			this.day = nd;
+			this.weekDay = ['日', '月', '火', '水', '木', '金', '土'][now.getDay()];
+
+			const dayNumer   = now.getTime() - new Date(ny, nm, nd).getTime();
+			const dayDenom   = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/;
+			const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime();
+			const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime();
+			const yearNumer  = now.getTime() - new Date(ny, 0, 1).getTime();
+			const yearDenom  = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime();
+
+			this.dayP   = dayNumer   / dayDenom   * 100;
+			this.monthP = monthNumer / monthDenom * 100;
+			this.yearP  = yearNumer  / yearDenom  * 100;
+
+			this.isHoliday = now.getDay() == 0 || now.getDay() == 6;
+
+			this.special =
+				nm == 0 && nd == 1 ? 'on-new-years-day' :
+				false;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-calendar
+	padding 16px 0
+	color #777
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	&[data-special='on-new-years-day']
+		border-color #ef95a0
+
+	&[data-melt]
+		background transparent
+		border none
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	> .calendar
+		float left
+		width 60%
+		text-align center
+
+		&[data-is-holiday]
+			> .day
+				color #ef95a0
+
+		> p
+			margin 0
+			line-height 18px
+			font-size 14px
+
+			> span
+				margin 0 4px
+
+		> .day
+			margin 10px 0
+			line-height 32px
+			font-size 28px
+
+	> .info
+		display block
+		float left
+		width 40%
+		padding 0 16px 0 0
+
+		> div
+			margin-bottom 8px
+
+			&:last-child
+				margin-bottom 4px
+
+			> p
+				margin 0 0 2px 0
+				font-size 12px
+				line-height 18px
+				color #888
+
+				> b
+					margin-left 2px
+
+			> .meter
+				width 100%
+				overflow hidden
+				background #eee
+				border-radius 8px
+
+				> .val
+					height 4px
+					background $theme-color
+
+			&:nth-child(1)
+				> .meter > .val
+					background #f7796c
+
+			&:nth-child(2)
+				> .meter > .val
+					background #a1de41
+
+			&:nth-child(3)
+				> .meter > .val
+					background #41ddde
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/donation.vue b/src/web/app/common/views/components/widgets/donation.vue
new file mode 100644
index 0000000000..50adc531bf
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/donation.vue
@@ -0,0 +1,45 @@
+<template>
+<div class="mkw-donation">
+	<article>
+		<h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1>
+		<p>
+			{{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr(0, '%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('{')) }}
+			<a href="/syuilo" data-user-preview="@syuilo">@syuilo</a>
+			{{ '%i18n:desktop.tags.mk-donation-home-widget.text%'.substr('%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('}') + 1) }}
+		</p>
+	</article>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../define-widget';
+export default define({
+	name: 'donation'
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-donation
+	background #fff
+	border solid 1px #ead8bb
+	border-radius 6px
+
+	> article
+		padding 20px
+
+		> h1
+			margin 0 0 5px 0
+			font-size 1em
+			color #888
+
+			> [data-fa]
+				margin-right 0.25em
+
+		> p
+			display block
+			z-index 1
+			margin 0
+			font-size 0.8em
+			color #999
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/messaging.vue b/src/web/app/common/views/components/widgets/messaging.vue
new file mode 100644
index 0000000000..19ef704310
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/messaging.vue
@@ -0,0 +1,59 @@
+<template>
+<div class="mkw-messaging">
+	<p class="title" v-if="props.design == 0">%fa:comments%%i18n:desktop.tags.mk-messaging-home-widget.title%</p>
+	<mk-messaging ref="index" compact @navigate="navigate"/>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../define-widget';
+export default define({
+	name: 'messaging',
+	props: {
+		design: 0
+	}
+}).extend({
+	methods: {
+		navigate(user) {
+			if (this.platform == 'desktop') {
+				this.wapi_openMessagingRoomWindow(user);
+			} else {
+				// TODO: open room page in new tab
+			}
+		},
+		func() {
+			if (this.props.design == 1) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-messaging
+	overflow hidden
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	> .title
+		z-index 2
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> mk-messaging
+		max-height 250px
+		overflow auto
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/nav.vue b/src/web/app/common/views/components/widgets/nav.vue
new file mode 100644
index 0000000000..77e1eea492
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/nav.vue
@@ -0,0 +1,29 @@
+<template>
+<div class="mkw-nav">
+	<mk-nav-links/>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../define-widget';
+export default define({
+	name: 'nav'
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-nav
+	padding 16px
+	font-size 12px
+	color #aaa
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	a
+		color #999
+
+	i
+		color #ccc
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/photo-stream.vue b/src/web/app/common/views/components/widgets/photo-stream.vue
new file mode 100644
index 0000000000..12e568ca00
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/photo-stream.vue
@@ -0,0 +1,122 @@
+<template>
+<div class="mkw-photo-stream" :data-melt="props.design == 2">
+	<p class="title" v-if="props.design == 0">%fa:camera%%i18n:desktop.tags.mk-photo-stream-home-widget.title%</p>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<div class="stream" v-if="!fetching && images.length > 0">
+		<div v-for="image in images" :key="image.id" class="img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div>
+	</div>
+	<p class="empty" v-if="!fetching && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../define-widget';
+export default define({
+	name: 'photo-stream',
+	props: {
+		design: 0
+	}
+}).extend({
+	data() {
+		return {
+			images: [],
+			fetching: true,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+
+		this.connection.on('drive_file_created', this.onDriveFileCreated);
+
+		this.$root.$data.os.api('drive/stream', {
+			type: 'image/*',
+			limit: 9
+		}).then(images => {
+			this.fetching = false;
+			this.images = images;
+		});
+	},
+	beforeDestroy() {
+		this.connection.off('drive_file_created', this.onDriveFileCreated);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+	},
+	methods: {
+		onStreamDriveFileCreated(file) {
+			if (/^image\/.+$/.test(file.type)) {
+				this.images.unshift(file);
+				if (this.images.length > 9) this.images.pop();
+			}
+		},
+		func() {
+			if (this.props.design == 2) {
+				this.props.design = 0;
+			} else {
+				this.props.design++;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-photo-stream
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	&[data-melt]
+		background transparent !important
+		border none !important
+
+		> .stream
+			padding 0
+
+			> .img
+				border solid 4px transparent
+				border-radius 8px
+
+	> .title
+		z-index 1
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> .stream
+		display -webkit-flex
+		display -moz-flex
+		display -ms-flex
+		display flex
+		justify-content center
+		flex-wrap wrap
+		padding 8px
+
+		> .img
+			flex 1 1 33%
+			width 33%
+			height 80px
+			background-position center center
+			background-size cover
+			border solid 2px transparent
+			border-radius 4px
+
+	> .fetching
+	> .empty
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/profile.vue b/src/web/app/common/views/components/widgets/profile.vue
index e589eb20b9..70902c7cf5 100644
--- a/src/web/app/common/views/components/widgets/profile.vue
+++ b/src/web/app/common/views/components/widgets/profile.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mkw-profile"
-	data-compact={ data.design == 1 || data.design == 2 }
-	data-melt={ data.design == 2 }
+	:data-compact="props.design == 1 || props.design == 2"
+	:data-melt="props.design == 2"
 >
 	<div class="banner"
 		style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' }
diff --git a/src/web/app/common/views/components/widgets/slideshow.vue b/src/web/app/common/views/components/widgets/slideshow.vue
new file mode 100644
index 0000000000..6dcd453e25
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/slideshow.vue
@@ -0,0 +1,154 @@
+<template>
+<div class="mkw-slideshow">
+	<div @click="choose">
+		<p v-if="data.folder === undefined">クリックしてフォルダを指定してください</p>
+		<p v-if="data.folder !== undefined && images.length == 0 && !fetching">このフォルダには画像がありません</p>
+		<div ref="slideA" class="slide a"></div>
+		<div ref="slideB" class="slide b"></div>
+	</div>
+	<button @click="resize">%fa:expand%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+import define from '../../../define-widget';
+export default define({
+	name: 'slideshow',
+	props: {
+		folder: undefined,
+		size: 0
+	}
+}).extend({
+	data() {
+		return {
+			images: [],
+			fetching: true,
+			clock: null
+		};
+	},
+	mounted() {
+		Vue.nextTick(() => {
+			this.applySize();
+		});
+
+		if (this.props.folder !== undefined) {
+			this.fetch();
+		}
+
+		this.clock = setInterval(this.change, 10000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		applySize() {
+			let h;
+
+			if (this.props.size == 1) {
+				h = 250;
+			} else {
+				h = 170;
+			}
+
+			this.$el.style.height = `${h}px`;
+		},
+		resize() {
+			if (this.props.size == 1) {
+				this.props.size = 0;
+			} else {
+				this.props.size++;
+			}
+
+			this.applySize();
+		},
+		change() {
+			if (this.images.length == 0) return;
+
+			const index = Math.floor(Math.random() * this.images.length);
+			const img = `url(${ this.images[index].url }?thumbnail&size=1024)`;
+
+			(this.$refs.slideB as any).style.backgroundImage = img;
+
+			anime({
+				targets: this.$refs.slideB,
+				opacity: 1,
+				duration: 1000,
+				easing: 'linear',
+				complete: () => {
+					(this.$refs.slideA as any).style.backgroundImage = img;
+					anime({
+						targets: this.$refs.slideB,
+						opacity: 0,
+						duration: 0
+					});
+				}
+			});
+		},
+		fetch() {
+			this.fetching = true;
+
+			this.$root.$data.os.api('drive/files', {
+				folder_id: this.props.folder,
+				type: 'image/*',
+				limit: 100
+			}).then(images => {
+				this.fetching = false;
+				this.images = images;
+				(this.$refs.slideA as any).style.backgroundImage = '';
+				(this.$refs.slideB as any).style.backgroundImage = '';
+				this.change();
+			});
+		},
+		choose() {
+			this.wapi_selectDriveFolder().then(folder => {
+				this.props.folder = folder ? folder.id : null;
+				this.fetch();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-slideshow
+	overflow hidden
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	&:hover > button
+		display block
+
+	> button
+		position absolute
+		left 0
+		bottom 0
+		display none
+		padding 4px
+		font-size 24px
+		color #fff
+		text-shadow 0 0 8px #000
+
+	> div
+		width 100%
+		height 100%
+		cursor pointer
+
+		> *
+			pointer-events none
+
+		> .slide
+			position absolute
+			top 0
+			left 0
+			width 100%
+			height 100%
+			background-size cover
+			background-position center
+
+			&.b
+				opacity 0
+
+</style>
diff --git a/src/web/app/common/views/components/widgets/tips.vue b/src/web/app/common/views/components/widgets/tips.vue
new file mode 100644
index 0000000000..f38ecfe441
--- /dev/null
+++ b/src/web/app/common/views/components/widgets/tips.vue
@@ -0,0 +1,109 @@
+<template>
+<div class="mkw-tips">
+	<p ref="tip">%fa:R lightbulb%<span v-html="tip"></span></p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as anime from 'animejs';
+import define from '../../../define-widget';
+
+const tips = [
+	'<kbd>t</kbd>でタイムラインにフォーカスできます',
+	'<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開きます',
+	'投稿フォームにはファイルをドラッグ&ドロップできます',
+	'投稿フォームにクリップボードにある画像データをペーストできます',
+	'ドライブにファイルをドラッグ&ドロップしてアップロードできます',
+	'ドライブでファイルをドラッグしてフォルダ移動できます',
+	'ドライブでフォルダをドラッグしてフォルダ移動できます',
+	'ホームは設定からカスタマイズできます',
+	'MisskeyはMIT Licenseです',
+	'タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れます',
+	'投稿の ... をクリックして、投稿をユーザーページにピン留めできます',
+	'ドライブの容量は(デフォルトで)1GBです',
+	'投稿に添付したファイルは全てドライブに保存されます',
+	'ホームのカスタマイズ中、ウィジェットを右クリックしてデザインを変更できます',
+	'タイムライン上部にもウィジェットを設置できます',
+	'投稿をダブルクリックすると詳細が見れます',
+	'「**」でテキストを囲むと**強調表示**されます',
+	'チャンネルウィジェットを利用すると、よく利用するチャンネルを素早く確認できます',
+	'いくつかのウィンドウはブラウザの外に切り離すことができます',
+	'カレンダーウィジェットのパーセンテージは、経過の割合を示しています',
+	'APIを利用してbotの開発なども行えます',
+	'MisskeyはLINEを通じてでも利用できます',
+	'まゆかわいいよまゆ',
+	'Misskeyは2014年にサービスを開始しました',
+	'対応ブラウザではMisskeyを開いていなくても通知を受け取れます'
+]
+
+export default define({
+	name: 'tips'
+}).extend({
+	data() {
+		return {
+			tip: null,
+			clock: null
+		};
+	},
+	mounted() {
+		Vue.nextTick(() => {
+			this.set();
+		});
+
+		this.clock = setInterval(this.change, 20000);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		set() {
+			this.tip = tips[Math.floor(Math.random() * tips.length)];
+		},
+		change() {
+			anime({
+				targets: this.$refs.tip,
+				opacity: 0,
+				duration: 500,
+				easing: 'linear',
+				complete: this.set
+			});
+
+			setTimeout(() => {
+				anime({
+					targets: this.$refs.tip,
+					opacity: 1,
+					duration: 500,
+					easing: 'linear'
+				});
+			}, 500);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-tips
+	overflow visible !important
+
+	> p
+		display block
+		margin 0
+		padding 0 12px
+		text-align center
+		font-size 0.7em
+		color #999
+
+		> [data-fa]
+			margin-right 4px
+
+		kbd
+			display inline
+			padding 0 6px
+			margin 0 2px
+			font-size 1em
+			font-family inherit
+			border solid 1px #999
+			border-radius 2px
+
+</style>
diff --git a/src/web/app/desktop/-tags/home-widgets/calendar.tag b/src/web/app/desktop/-tags/home-widgets/calendar.tag
deleted file mode 100644
index 46d47662b9..0000000000
--- a/src/web/app/desktop/-tags/home-widgets/calendar.tag
+++ /dev/null
@@ -1,167 +0,0 @@
-<mk-calendar-home-widget data-melt={ data.design == 1 } data-special={ special }>
-	<div class="calendar" data-is-holiday={ isHoliday }>
-		<p class="month-and-year"><span class="year">{ year }年</span><span class="month">{ month }月</span></p>
-		<p class="day">{ day }日</p>
-		<p class="week-day">{ weekDay }曜日</p>
-	</div>
-	<div class="info">
-		<div>
-			<p>今日:<b>{ dayP.toFixed(1) }%</b></p>
-			<div class="meter">
-				<div class="val" style={ 'width:' + dayP + '%' }></div>
-			</div>
-		</div>
-		<div>
-			<p>今月:<b>{ monthP.toFixed(1) }%</b></p>
-			<div class="meter">
-				<div class="val" style={ 'width:' + monthP + '%' }></div>
-			</div>
-		</div>
-		<div>
-			<p>今年:<b>{ yearP.toFixed(1) }%</b></p>
-			<div class="meter">
-				<div class="val" style={ 'width:' + yearP + '%' }></div>
-			</div>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 16px 0
-			color #777
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			&[data-special='on-new-years-day']
-				border-color #ef95a0
-
-			&[data-melt]
-				background transparent
-				border none
-
-			&:after
-				content ""
-				display block
-				clear both
-
-			> .calendar
-				float left
-				width 60%
-				text-align center
-
-				&[data-is-holiday]
-					> .day
-						color #ef95a0
-
-				> p
-					margin 0
-					line-height 18px
-					font-size 14px
-
-					> span
-						margin 0 4px
-
-				> .day
-					margin 10px 0
-					line-height 32px
-					font-size 28px
-
-			> .info
-				display block
-				float left
-				width 40%
-				padding 0 16px 0 0
-
-				> div
-					margin-bottom 8px
-
-					&:last-child
-						margin-bottom 4px
-
-					> p
-						margin 0 0 2px 0
-						font-size 12px
-						line-height 18px
-						color #888
-
-						> b
-							margin-left 2px
-
-					> .meter
-						width 100%
-						overflow hidden
-						background #eee
-						border-radius 8px
-
-						> .val
-							height 4px
-							background $theme-color
-
-					&:nth-child(1)
-						> .meter > .val
-							background #f7796c
-
-					&:nth-child(2)
-						> .meter > .val
-							background #a1de41
-
-					&:nth-child(3)
-						> .meter > .val
-							background #41ddde
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.draw = () => {
-			const now = new Date();
-			const nd = now.getDate();
-			const nm = now.getMonth();
-			const ny = now.getFullYear();
-
-			this.year = ny;
-			this.month = nm + 1;
-			this.day = nd;
-			this.weekDay = ['日', '月', '火', '水', '木', '金', '土'][now.getDay()];
-
-			this.dayNumer   = now - new Date(ny, nm, nd);
-			this.dayDenom   = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/;
-			this.monthNumer = now - new Date(ny, nm, 1);
-			this.monthDenom = new Date(ny, nm + 1, 1) - new Date(ny, nm, 1);
-			this.yearNumer  = now - new Date(ny, 0, 1);
-			this.yearDenom  = new Date(ny + 1, 0, 1) - new Date(ny, 0, 1);
-
-			this.dayP   = this.dayNumer   / this.dayDenom   * 100;
-			this.monthP = this.monthNumer / this.monthDenom * 100;
-			this.yearP  = this.yearNumer  / this.yearDenom  * 100;
-
-			this.isHoliday = now.getDay() == 0 || now.getDay() == 6;
-
-			this.special =
-				nm == 0 && nd == 1 ? 'on-new-years-day' :
-				false;
-
-			this.update();
-		};
-
-		this.draw();
-
-		this.on('mount', () => {
-			this.clock = setInterval(this.draw, 1000);
-		});
-
-		this.on('unmount', () => {
-			clearInterval(this.clock);
-		});
-
-		this.func = () => {
-			if (++this.data.design == 2) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-calendar-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/donation.tag b/src/web/app/desktop/-tags/home-widgets/donation.tag
deleted file mode 100644
index 5ed5c137b5..0000000000
--- a/src/web/app/desktop/-tags/home-widgets/donation.tag
+++ /dev/null
@@ -1,36 +0,0 @@
-<mk-donation-home-widget>
-	<article>
-		<h1>%fa:heart%%i18n:desktop.tags.mk-donation-home-widget.title%</h1>
-		<p>{'%i18n:desktop.tags.mk-donation-home-widget.text%'.substr(0, '%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('{'))}<a href="/syuilo" data-user-preview="@syuilo">@syuilo</a>{'%i18n:desktop.tags.mk-donation-home-widget.text%'.substr('%i18n:desktop.tags.mk-donation-home-widget.text%'.indexOf('}') + 1)}</p>
-	</article>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px #ead8bb
-			border-radius 6px
-
-			> article
-				padding 20px
-
-				> h1
-					margin 0 0 5px 0
-					font-size 1em
-					color #888
-
-					> [data-fa]
-						margin-right 0.25em
-
-				> p
-					display block
-					z-index 1
-					margin 0
-					font-size 0.8em
-					color #999
-
-	</style>
-	<script lang="typescript">
-		this.mixin('widget');
-		this.mixin('user-preview');
-	</script>
-</mk-donation-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/messaging.tag b/src/web/app/desktop/-tags/home-widgets/messaging.tag
deleted file mode 100644
index d3b77b58cc..0000000000
--- a/src/web/app/desktop/-tags/home-widgets/messaging.tag
+++ /dev/null
@@ -1,52 +0,0 @@
-<mk-messaging-home-widget>
-	<template v-if="data.design == 0">
-		<p class="title">%fa:comments%%i18n:desktop.tags.mk-messaging-home-widget.title%</p>
-	</template>
-	<mk-messaging ref="index" compact={ true }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow hidden
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			> .title
-				z-index 2
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> mk-messaging
-				max-height 250px
-				overflow auto
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.on('mount', () => {
-			this.$refs.index.on('navigate-user', user => {
-				riot.mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
-					user: user
-				});
-			});
-		});
-
-		this.func = () => {
-			if (++this.data.design == 2) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-messaging-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/nav.tag b/src/web/app/desktop/-tags/home-widgets/nav.tag
deleted file mode 100644
index 890fb4d8f7..0000000000
--- a/src/web/app/desktop/-tags/home-widgets/nav.tag
+++ /dev/null
@@ -1,23 +0,0 @@
-<mk-nav-home-widget>
-	<mk-nav-links/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			padding 16px
-			font-size 12px
-			color #aaa
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			a
-				color #999
-
-			i
-				color #ccc
-
-	</style>
-	<script lang="typescript">
-		this.mixin('widget');
-	</script>
-</mk-nav-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/photo-stream.tag b/src/web/app/desktop/-tags/home-widgets/photo-stream.tag
deleted file mode 100644
index a2d95dede3..0000000000
--- a/src/web/app/desktop/-tags/home-widgets/photo-stream.tag
+++ /dev/null
@@ -1,118 +0,0 @@
-<mk-photo-stream-home-widget data-melt={ data.design == 2 }>
-	<template v-if="data.design == 0">
-		<p class="title">%fa:camera%%i18n:desktop.tags.mk-photo-stream-home-widget.title%</p>
-	</template>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<div class="stream" v-if="!initializing && images.length > 0">
-		<template each={ image in images }>
-			<div class="img" style={ 'background-image: url(' + image.url + '?thumbnail&size=256)' }></div>
-		</template>
-	</div>
-	<p class="empty" v-if="!initializing && images.length == 0">%i18n:desktop.tags.mk-photo-stream-home-widget.no-photos%</p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			&[data-melt]
-				background transparent !important
-				border none !important
-
-				> .stream
-					padding 0
-
-					> .img
-						border solid 4px transparent
-						border-radius 8px
-
-			> .title
-				z-index 1
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> .stream
-				display -webkit-flex
-				display -moz-flex
-				display -ms-flex
-				display flex
-				justify-content center
-				flex-wrap wrap
-				padding 8px
-
-				> .img
-					flex 1 1 33%
-					width 33%
-					height 80px
-					background-position center center
-					background-size cover
-					border solid 2px transparent
-					border-radius 4px
-
-			> .initializing
-			> .empty
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			design: 0
-		};
-
-		this.mixin('widget');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.images = [];
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.connection.on('drive_file_created', this.onStreamDriveFileCreated);
-
-			this.api('drive/stream', {
-				type: 'image/*',
-				limit: 9
-			}).then(images => {
-				this.update({
-					initializing: false,
-					images: images
-				});
-			});
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('drive_file_created', this.onStreamDriveFileCreated);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.onStreamDriveFileCreated = file => {
-			if (/^image\/.+$/.test(file.type)) {
-				this.images.unshift(file);
-				if (this.images.length > 9) this.images.pop();
-				this.update();
-			}
-		};
-
-		this.func = () => {
-			if (++this.data.design == 3) this.data.design = 0;
-			this.save();
-		};
-	</script>
-</mk-photo-stream-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/slideshow.tag b/src/web/app/desktop/-tags/home-widgets/slideshow.tag
deleted file mode 100644
index a69ab74b70..0000000000
--- a/src/web/app/desktop/-tags/home-widgets/slideshow.tag
+++ /dev/null
@@ -1,151 +0,0 @@
-<mk-slideshow-home-widget>
-	<div @click="choose">
-		<p v-if="data.folder === undefined">クリックしてフォルダを指定してください</p>
-		<p v-if="data.folder !== undefined && images.length == 0 && !fetching">このフォルダには画像がありません</p>
-		<div ref="slideA" class="slide a"></div>
-		<div ref="slideB" class="slide b"></div>
-	</div>
-	<button @click="resize">%fa:expand%</button>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow hidden
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			&:hover > button
-				display block
-
-			> button
-				position absolute
-				left 0
-				bottom 0
-				display none
-				padding 4px
-				font-size 24px
-				color #fff
-				text-shadow 0 0 8px #000
-
-			> div
-				width 100%
-				height 100%
-				cursor pointer
-
-				> *
-					pointer-events none
-
-				> .slide
-					position absolute
-					top 0
-					left 0
-					width 100%
-					height 100%
-					background-size cover
-					background-position center
-
-					&.b
-						opacity 0
-
-	</style>
-	<script lang="typescript">
-		import * as anime from 'animejs';
-
-		this.data = {
-			folder: undefined,
-			size: 0
-		};
-
-		this.mixin('widget');
-
-		this.images = [];
-		this.fetching = true;
-
-		this.on('mount', () => {
-			this.applySize();
-
-			if (this.data.folder !== undefined) {
-				this.fetch();
-			}
-
-			this.clock = setInterval(this.change, 10000);
-		});
-
-		this.on('unmount', () => {
-			clearInterval(this.clock);
-		});
-
-		this.applySize = () => {
-			let h;
-
-			if (this.data.size == 1) {
-				h = 250;
-			} else {
-				h = 170;
-			}
-
-			this.root.style.height = `${h}px`;
-		};
-
-		this.resize = () => {
-			this.data.size++;
-			if (this.data.size == 2) this.data.size = 0;
-
-			this.applySize();
-			this.save();
-		};
-
-		this.change = () => {
-			if (this.images.length == 0) return;
-
-			const index = Math.floor(Math.random() * this.images.length);
-			const img = `url(${ this.images[index].url }?thumbnail&size=1024)`;
-
-			this.$refs.slideB.style.backgroundImage = img;
-
-			anime({
-				targets: this.$refs.slideB,
-				opacity: 1,
-				duration: 1000,
-				easing: 'linear',
-				complete: () => {
-					this.$refs.slideA.style.backgroundImage = img;
-					anime({
-						targets: this.$refs.slideB,
-						opacity: 0,
-						duration: 0
-					});
-				}
-			});
-		};
-
-		this.fetch = () => {
-			this.update({
-				fetching: true
-			});
-
-			this.api('drive/files', {
-				folder_id: this.data.folder,
-				type: 'image/*',
-				limit: 100
-			}).then(images => {
-				this.update({
-					fetching: false,
-					images: images
-				});
-				this.$refs.slideA.style.backgroundImage = '';
-				this.$refs.slideB.style.backgroundImage = '';
-				this.change();
-			});
-		};
-
-		this.choose = () => {
-			const i = riot.mount(document.body.appendChild(document.createElement('mk-select-folder-from-drive-window')))[0];
-			i.one('selected', folder => {
-				this.data.folder = folder ? folder.id : null;
-				this.fetch();
-				this.save();
-			});
-		};
-	</script>
-</mk-slideshow-home-widget>
diff --git a/src/web/app/desktop/-tags/home-widgets/tips.tag b/src/web/app/desktop/-tags/home-widgets/tips.tag
deleted file mode 100644
index efe9c90fc2..0000000000
--- a/src/web/app/desktop/-tags/home-widgets/tips.tag
+++ /dev/null
@@ -1,94 +0,0 @@
-<mk-tips-home-widget>
-	<p ref="tip">%fa:R lightbulb%<span ref="text"></span></p>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			overflow visible !important
-
-			> p
-				display block
-				margin 0
-				padding 0 12px
-				text-align center
-				font-size 0.7em
-				color #999
-
-				> [data-fa]
-					margin-right 4px
-
-				kbd
-					display inline
-					padding 0 6px
-					margin 0 2px
-					font-size 1em
-					font-family inherit
-					border solid 1px #999
-					border-radius 2px
-
-	</style>
-	<script lang="typescript">
-		import * as anime from 'animejs';
-
-		this.mixin('widget');
-
-		this.tips = [
-			'<kbd>t</kbd>でタイムラインにフォーカスできます',
-			'<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開きます',
-			'投稿フォームにはファイルをドラッグ&ドロップできます',
-			'投稿フォームにクリップボードにある画像データをペーストできます',
-			'ドライブにファイルをドラッグ&ドロップしてアップロードできます',
-			'ドライブでファイルをドラッグしてフォルダ移動できます',
-			'ドライブでフォルダをドラッグしてフォルダ移動できます',
-			'ホームは設定からカスタマイズできます',
-			'MisskeyはMIT Licenseです',
-			'タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れます',
-			'投稿の ... をクリックして、投稿をユーザーページにピン留めできます',
-			'ドライブの容量は(デフォルトで)1GBです',
-			'投稿に添付したファイルは全てドライブに保存されます',
-			'ホームのカスタマイズ中、ウィジェットを右クリックしてデザインを変更できます',
-			'タイムライン上部にもウィジェットを設置できます',
-			'投稿をダブルクリックすると詳細が見れます',
-			'「**」でテキストを囲むと**強調表示**されます',
-			'チャンネルウィジェットを利用すると、よく利用するチャンネルを素早く確認できます',
-			'いくつかのウィンドウはブラウザの外に切り離すことができます',
-			'カレンダーウィジェットのパーセンテージは、経過の割合を示しています',
-			'APIを利用してbotの開発なども行えます',
-			'MisskeyはLINEを通じてでも利用できます',
-			'まゆかわいいよまゆ',
-			'Misskeyは2014年にサービスを開始しました',
-			'対応ブラウザではMisskeyを開いていなくても通知を受け取れます'
-		]
-
-		this.on('mount', () => {
-			this.set();
-			this.clock = setInterval(this.change, 20000);
-		});
-
-		this.on('unmount', () => {
-			clearInterval(this.clock);
-		});
-
-		this.set = () => {
-			this.$refs.text.innerHTML = this.tips[Math.floor(Math.random() * this.tips.length)];
-		};
-
-		this.change = () => {
-			anime({
-				targets: this.$refs.tip,
-				opacity: 0,
-				duration: 500,
-				easing: 'linear',
-				complete: this.set
-			});
-
-			setTimeout(() => {
-				anime({
-					targets: this.$refs.tip,
-					opacity: 1,
-					duration: 500,
-					easing: 'linear'
-				});
-			}, 500);
-		};
-	</script>
-</mk-tips-home-widget>
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index 9850db485c..d97f781558 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -11,11 +11,11 @@ const isProduction = env === 'production';
 export default (version, lang) => {
 	const plugins = [
 		consts(lang),
-		new StringReplacePlugin(),
-		hoist()
+		new StringReplacePlugin()
 	];
 
 	if (isProduction) {
+		plugins.push(hoist());
 		plugins.push(minify());
 	}