From 3f79c9ae4927d4186c708e130ecbb1ea4f72d9fa Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 9 Nov 2018 16:00:29 +0900
Subject: [PATCH] Refactor client (#3178)

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
---
 .../app/common/scripts/check-for-update.ts    |   7 +-
 .../app/common/scripts/fuck-ad-block.ts       |   4 +-
 src/client/app/common/scripts/note-mixin.ts   |   4 +-
 .../common/views/components/api-settings.vue  |   2 +-
 .../games/reversi/reversi.index.vue           |   2 +-
 .../views/components/messaging-room.form.vue  |   2 +-
 .../views/components/password-settings.vue    |  10 +-
 .../app/common/views/widgets/slideshow.vue    |   2 +-
 .../app/desktop/api/choose-drive-file.ts      |  29 ----
 .../app/desktop/api/choose-drive-folder.ts    |  16 --
 src/client/app/desktop/api/contextmenu.ts     |  15 --
 src/client/app/desktop/api/dialog.ts          |  18 ---
 src/client/app/desktop/api/input.ts           |  19 ---
 src/client/app/desktop/api/notify.ts          |   9 --
 src/client/app/desktop/api/post.ts            |  22 ---
 src/client/app/desktop/api/update-avatar.ts   |  25 ++-
 src/client/app/desktop/api/update-banner.ts   |  25 ++-
 src/client/app/desktop/script.ts              | 142 +++++++++++++++---
 .../desktop/views/components/drive.file.vue   |  19 ++-
 .../desktop/views/components/drive.folder.vue |  15 +-
 .../app/desktop/views/components/drive.vue    |  11 +-
 .../app/desktop/views/components/home.vue     |   2 +-
 .../desktop/views/components/post-form.vue    |   8 +-
 .../desktop/views/components/renote-form.vue  |   4 +-
 .../desktop/views/components/settings.2fa.vue |  10 +-
 .../app/desktop/views/components/settings.vue |  10 +-
 .../app/desktop/views/components/timeline.vue |   2 +-
 .../views/components/ui.header.post.vue       |   2 +-
 .../desktop/views/components/ui.sidebar.vue   |   2 +-
 .../app/desktop/views/components/ui.vue       |   2 +-
 .../views/components/user-lists-window.vue    |   2 +-
 .../desktop/views/pages/deck/deck.column.vue  |   5 +-
 .../app/desktop/views/pages/deck/deck.vue     |   2 +-
 .../desktop/views/pages/user/user.header.vue  |   2 +-
 .../desktop/views/pages/user/user.profile.vue |   2 +-
 src/client/app/init.ts                        |  36 +++--
 src/client/app/mios.ts                        |  47 ------
 .../app/mobile/api/choose-drive-file.ts       |  18 ---
 .../app/mobile/api/choose-drive-folder.ts     |  17 ---
 src/client/app/mobile/api/dialog.ts           |  18 ---
 src/client/app/mobile/api/input.ts            |   8 -
 src/client/app/mobile/api/notify.ts           |   3 -
 src/client/app/mobile/api/post.ts             |  24 ---
 src/client/app/mobile/script.ts               | 101 +++++++++++--
 .../views/components/drive.file-detail.vue    |   2 +-
 .../app/mobile/views/components/drive.vue     |   2 +-
 .../mobile/views/components/note-detail.vue   |   4 +-
 .../app/mobile/views/components/post-form.vue |   4 +-
 .../app/mobile/views/components/ui.header.vue |   2 +-
 src/client/app/mobile/views/pages/home.vue    |   2 +-
 .../app/mobile/views/pages/settings.vue       |   6 +-
 .../app/mobile/views/pages/user-lists.vue     |   2 +-
 52 files changed, 328 insertions(+), 421 deletions(-)
 delete mode 100644 src/client/app/desktop/api/choose-drive-file.ts
 delete mode 100644 src/client/app/desktop/api/choose-drive-folder.ts
 delete mode 100644 src/client/app/desktop/api/contextmenu.ts
 delete mode 100644 src/client/app/desktop/api/dialog.ts
 delete mode 100644 src/client/app/desktop/api/input.ts
 delete mode 100644 src/client/app/desktop/api/notify.ts
 delete mode 100644 src/client/app/desktop/api/post.ts
 delete mode 100644 src/client/app/mobile/api/choose-drive-file.ts
 delete mode 100644 src/client/app/mobile/api/choose-drive-folder.ts
 delete mode 100644 src/client/app/mobile/api/dialog.ts
 delete mode 100644 src/client/app/mobile/api/input.ts
 delete mode 100644 src/client/app/mobile/api/notify.ts
 delete mode 100644 src/client/app/mobile/api/post.ts

diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
index a4c59fd350..ac0b5d09c8 100644
--- a/src/client/app/common/scripts/check-for-update.ts
+++ b/src/client/app/common/scripts/check-for-update.ts
@@ -1,8 +1,7 @@
-import MiOS from '../../mios';
 import { clientVersion as current } from '../../config';
 
-export default async function(mios: MiOS, force = false, silent = false) {
-	const meta = await mios.getMeta(force);
+export default async function($root: any, force = false, silent = false) {
+	const meta = await $root.getMeta(force);
 	const newer = meta.clientVersion;
 
 	if (newer != current) {
@@ -23,7 +22,7 @@ export default async function(mios: MiOS, force = false, silent = false) {
 		}
 
 		if (!silent) {
-			mios.apis.dialog({
+			$root.$dialog({
 				title: '%i18n:common.update-available-title%',
 				text: '%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current)
 			});
diff --git a/src/client/app/common/scripts/fuck-ad-block.ts b/src/client/app/common/scripts/fuck-ad-block.ts
index 0c802f1648..b749d363f2 100644
--- a/src/client/app/common/scripts/fuck-ad-block.ts
+++ b/src/client/app/common/scripts/fuck-ad-block.ts
@@ -1,10 +1,10 @@
 declare const fuckAdBlock: any;
 
-export default (os) => {
+export default ($root: any) => {
 	require('fuckadblock');
 
 	function adBlockDetected() {
-		os.apis.dialog({
+		$root.$dialog({
 			title: '%fa:exclamation-triangle%%i18n:common.adblock.detected%',
 			text: '%i18n:common.adblock.warning%',
 			actins: [{
diff --git a/src/client/app/common/scripts/note-mixin.ts b/src/client/app/common/scripts/note-mixin.ts
index 6d86c70820..dd5098f4cd 100644
--- a/src/client/app/common/scripts/note-mixin.ts
+++ b/src/client/app/common/scripts/note-mixin.ts
@@ -88,7 +88,7 @@ export default (opts: Opts = {}) => ({
 
 	methods: {
 		reply(viaKeyboard = false) {
-			this.$root.apis.post({
+			this.$root.$post({
 				reply: this.appearNote,
 				animation: !viaKeyboard,
 				cb: () => {
@@ -98,7 +98,7 @@ export default (opts: Opts = {}) => ({
 		},
 
 		renote(viaKeyboard = false) {
-			this.$root.apis.post({
+			this.$root.$post({
 				renote: this.appearNote,
 				animation: !viaKeyboard,
 				cb: () => {
diff --git a/src/client/app/common/views/components/api-settings.vue b/src/client/app/common/views/components/api-settings.vue
index a6b74d7bb4..062218b3f4 100644
--- a/src/client/app/common/views/components/api-settings.vue
+++ b/src/client/app/common/views/components/api-settings.vue
@@ -50,7 +50,7 @@ export default Vue.extend({
 
 	methods: {
 		regenerateToken() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('enter-password'),
 				type: 'password'
 			}).then(password => {
diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue
index e1372e5246..b82a60a360 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.index.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue
@@ -100,7 +100,7 @@ export default Vue.extend({
 		},
 
 		match() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('enter-username')
 			}).then(username => {
 				this.$root.api('users/show', {
diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue
index d25c0c86db..97e2e16e4b 100644
--- a/src/client/app/common/views/components/messaging-room.form.vue
+++ b/src/client/app/common/views/components/messaging-room.form.vue
@@ -131,7 +131,7 @@ export default Vue.extend({
 		},
 
 		chooseFileFromDrive() {
-			this.$root.apis.chooseDriveFile({
+			this.$chooseDriveFile({
 				multiple: false
 			}).then(file => {
 				this.file = file;
diff --git a/src/client/app/common/views/components/password-settings.vue b/src/client/app/common/views/components/password-settings.vue
index 048770401f..8661b51538 100644
--- a/src/client/app/common/views/components/password-settings.vue
+++ b/src/client/app/common/views/components/password-settings.vue
@@ -12,20 +12,20 @@ export default Vue.extend({
 	i18n: i18n('common/views/components/password-settings.vue'),
 	methods: {
 		reset() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('enter-current-password'),
 				type: 'password'
 			}).then(currentPassword => {
-				this.$root.apis.input({
+				this.$input({
 					title: this.$t('enter-new-password'),
 					type: 'password'
 				}).then(newPassword => {
-					this.$root.apis.input({
+					this.$input({
 						title: this.$t('enter-new-password-again'),
 						type: 'password'
 					}).then(newPassword2 => {
 						if (newPassword !== newPassword2) {
-							this.$root.apis.dialog({
+							this.$dialog({
 								title: null,
 								text: this.$t('not-match'),
 								actions: [{
@@ -38,7 +38,7 @@ export default Vue.extend({
 							currentPasword: currentPassword,
 							newPassword: newPassword
 						}).then(() => {
-							this.$root.apis.notify(this.$t('changed'));
+							this.$notify(this.$t('changed'));
 						});
 					});
 				});
diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue
index 58b19d0ab3..a454a41cde 100644
--- a/src/client/app/common/views/widgets/slideshow.vue
+++ b/src/client/app/common/views/widgets/slideshow.vue
@@ -114,7 +114,7 @@ export default define({
 			});
 		},
 		choose() {
-			this.$root.apis.chooseDriveFolder().then(folder => {
+			this.$chooseDriveFolder().then(folder => {
 				this.props.folder = folder ? folder.id : null;
 				this.save();
 				this.fetch();
diff --git a/src/client/app/desktop/api/choose-drive-file.ts b/src/client/app/desktop/api/choose-drive-file.ts
deleted file mode 100644
index a362a1289b..0000000000
--- a/src/client/app/desktop/api/choose-drive-file.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import OS from '../../mios';
-import { url } from '../../config';
-import MkChooseFileFromDriveWindow from '../views/components/choose-file-from-drive-window.vue';
-
-export default (os: OS) => opts => {
-	return new Promise((res, rej) => {
-		const o = opts || {};
-
-		if (document.body.clientWidth > 800) {
-			const w = os.new(MkChooseFileFromDriveWindow, {
-				title: o.title,
-				multiple: o.multiple,
-				initFolder: o.currentFolder
-			});
-			w.$once('selected', file => {
-				res(file);
-			});
-			document.body.appendChild(w.$el);
-		} else {
-			window['cb'] = file => {
-				res(file);
-			};
-
-			window.open(url + `/selectdrive?multiple=${o.multiple}`,
-				'choose_drive_window',
-				'height=500, width=800');
-		}
-	});
-};
diff --git a/src/client/app/desktop/api/choose-drive-folder.ts b/src/client/app/desktop/api/choose-drive-folder.ts
deleted file mode 100644
index 68dc7988b5..0000000000
--- a/src/client/app/desktop/api/choose-drive-folder.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import OS from '../../mios';
-import MkChooseFolderFromDriveWindow from '../views/components/choose-folder-from-drive-window.vue';
-
-export default (os: OS) => opts => {
-	return new Promise((res, rej) => {
-		const o = opts || {};
-		const w = os.new(MkChooseFolderFromDriveWindow, {
-			title: o.title,
-			initFolder: o.currentFolder
-		});
-		w.$once('selected', folder => {
-			res(folder);
-		});
-		document.body.appendChild(w.$el);
-	});
-};
diff --git a/src/client/app/desktop/api/contextmenu.ts b/src/client/app/desktop/api/contextmenu.ts
deleted file mode 100644
index c92f087551..0000000000
--- a/src/client/app/desktop/api/contextmenu.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import OS from '../../mios';
-import Ctx from '../views/components/context-menu.vue';
-
-export default (os: OS) => (e, menu, opts?) => {
-	const o = opts || {};
-	const vm = os.new(Ctx, {
-		menu,
-		x: e.pageX - window.pageXOffset,
-		y: e.pageY - window.pageYOffset,
-	});
-	vm.$once('closed', () => {
-		if (o.closed) o.closed();
-	});
-	document.body.appendChild(vm.$el);
-};
diff --git a/src/client/app/desktop/api/dialog.ts b/src/client/app/desktop/api/dialog.ts
deleted file mode 100644
index 23f35b7aa9..0000000000
--- a/src/client/app/desktop/api/dialog.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import OS from '../../mios';
-import Dialog from '../views/components/dialog.vue';
-
-export default (os: OS) => opts => {
-	return new Promise<string>((res, rej) => {
-		const o = opts || {};
-		const d = os.new(Dialog, {
-			title: o.title,
-			text: o.text,
-			modal: o.modal,
-			buttons: o.actions
-		});
-		d.$once('clicked', id => {
-			res(id);
-		});
-		document.body.appendChild(d.$el);
-	});
-};
diff --git a/src/client/app/desktop/api/input.ts b/src/client/app/desktop/api/input.ts
deleted file mode 100644
index bd7bfa0129..0000000000
--- a/src/client/app/desktop/api/input.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import OS from '../../mios';
-import InputDialog from '../views/components/input-dialog.vue';
-
-export default (os: OS) => opts => {
-	return new Promise<string>((res, rej) => {
-		const o = opts || {};
-		const d = os.new(InputDialog, {
-			title: o.title,
-			placeholder: o.placeholder,
-			default: o.default,
-			type: o.type || 'text',
-			allowEmpty: o.allowEmpty
-		});
-		d.$once('done', text => {
-			res(text);
-		});
-		document.body.appendChild(d.$el);
-	});
-};
diff --git a/src/client/app/desktop/api/notify.ts b/src/client/app/desktop/api/notify.ts
deleted file mode 100644
index 72e5827607..0000000000
--- a/src/client/app/desktop/api/notify.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import OS from '../../mios';
-import Notification from '../views/components/ui-notification.vue';
-
-export default (os: OS) => message => {
-	const vm = os.new(Notification, {
-		message
-	});
-	document.body.appendChild(vm.$el);
-};
diff --git a/src/client/app/desktop/api/post.ts b/src/client/app/desktop/api/post.ts
deleted file mode 100644
index 3ff9c5bb8c..0000000000
--- a/src/client/app/desktop/api/post.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import OS from '../../mios';
-import PostFormWindow from '../views/components/post-form-window.vue';
-import RenoteFormWindow from '../views/components/renote-form-window.vue';
-
-export default (os: OS) => opts => {
-	const o = opts || {};
-	if (o.renote) {
-		const vm = os.new(RenoteFormWindow, {
-			note: o.renote,
-			animation: o.animation == null ? true : o.animation
-		});
-		if (o.cb) vm.$once('closed', o.cb);
-		document.body.appendChild(vm.$el);
-	} else {
-		const vm = os.new(PostFormWindow, {
-			reply: o.reply,
-			animation: o.animation == null ? true : o.animation
-		});
-		if (o.cb) vm.$once('closed', o.cb);
-		document.body.appendChild(vm.$el);
-	}
-};
diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts
index f08e8a2b4e..ae8b723ea6 100644
--- a/src/client/app/desktop/api/update-avatar.ts
+++ b/src/client/app/desktop/api/update-avatar.ts
@@ -1,15 +1,14 @@
-import OS from '../../mios';
 import { apiUrl } from '../../config';
 import CropWindow from '../views/components/crop-window.vue';
 import ProgressDialog from '../views/components/progress-dialog.vue';
 
-export default (os: OS) => {
+export default ($root: any) => {
 
 	const cropImage = file => new Promise((resolve, reject) => {
 
 		const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$');
 		if (!regex.test(file.name) ) {
-			os.apis.dialog({
+			$root.$dialog({
 				title: '%fa:info-circle% %i18n:desktop.invalid-filetype%',
 				text: null,
 				actions: [{
@@ -19,7 +18,7 @@ export default (os: OS) => {
 			return reject('invalid-filetype');
 		}
 
-		const w = os.new(CropWindow, {
+		const w = $root.new(CropWindow, {
 			image: file,
 			title: '%i18n:desktop.avatar-crop-title%',
 			aspectRatio: 1 / 1
@@ -27,14 +26,14 @@ export default (os: OS) => {
 
 		w.$once('cropped', blob => {
 			const data = new FormData();
-			data.append('i', os.store.state.i.token);
+			data.append('i', $root.$store.state.i.token);
 			data.append('file', blob, file.name + '.cropped.png');
 
-			os.api('drive/folders/find', {
+			$root.api('drive/folders/find', {
 				name: '%i18n:desktop.avatar%'
 			}).then(avatarFolder => {
 				if (avatarFolder.length === 0) {
-					os.api('drive/folders/create', {
+					$root.api('drive/folders/create', {
 						name: '%i18n:desktop.avatar%'
 					}).then(iconFolder => {
 						resolve(upload(data, iconFolder));
@@ -55,7 +54,7 @@ export default (os: OS) => {
 	});
 
 	const upload = (data, folder) => new Promise((resolve, reject) => {
-		const dialog = os.new(ProgressDialog, {
+		const dialog = $root.new(ProgressDialog, {
 			title: '%i18n:desktop.uploading-avatar%'
 		});
 		document.body.appendChild(dialog.$el);
@@ -79,19 +78,19 @@ export default (os: OS) => {
 	});
 
 	const setAvatar = file => {
-		return os.api('i/update', {
+		return $root.api('i/update', {
 			avatarId: file.id
 		}).then(i => {
-			os.store.commit('updateIKeyValue', {
+			$root.$store.commit('updateIKeyValue', {
 				key: 'avatarId',
 				value: i.avatarId
 			});
-			os.store.commit('updateIKeyValue', {
+			$root.$store.commit('updateIKeyValue', {
 				key: 'avatarUrl',
 				value: i.avatarUrl
 			});
 
-			os.apis.dialog({
+			$root.$dialog({
 				title: '%fa:info-circle% %i18n:desktop.avatar-updated%',
 				text: null,
 				actions: [{
@@ -106,7 +105,7 @@ export default (os: OS) => {
 	return (file = null) => {
 		const selectedFile = file
 			? Promise.resolve(file)
-			: os.apis.chooseDriveFile({
+			: $root.$chooseDriveFile({
 				multiple: false,
 				title: '%fa:image% %i18n:desktop.choose-avatar%'
 			});
diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts
index 42c9d69349..c338d4e95c 100644
--- a/src/client/app/desktop/api/update-banner.ts
+++ b/src/client/app/desktop/api/update-banner.ts
@@ -1,15 +1,14 @@
-import OS from '../../mios';
 import { apiUrl } from '../../config';
 import CropWindow from '../views/components/crop-window.vue';
 import ProgressDialog from '../views/components/progress-dialog.vue';
 
-export default (os: OS) => {
+export default ($root: any) => {
 
 	const cropImage = file => new Promise((resolve, reject) => {
 
 		const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$');
 		if (!regex.test(file.name) ) {
-			os.apis.dialog({
+			$root.dialog({
 				title: '%fa:info-circle% %i18n:desktop.invalid-filetype%',
 				text: null,
 				actions: [{
@@ -19,7 +18,7 @@ export default (os: OS) => {
 			return reject('invalid-filetype');
 		}
 
-		const w = os.new(CropWindow, {
+		const w = $root.new(CropWindow, {
 			image: file,
 			title: '%i18n:desktop.banner-crop-title%',
 			aspectRatio: 16 / 9
@@ -27,14 +26,14 @@ export default (os: OS) => {
 
 		w.$once('cropped', blob => {
 			const data = new FormData();
-			data.append('i', os.store.state.i.token);
+			data.append('i', $root.$store.state.i.token);
 			data.append('file', blob, file.name + '.cropped.png');
 
-			os.api('drive/folders/find', {
+			$root.api('drive/folders/find', {
 				name: '%i18n:desktop.banner%'
 			}).then(bannerFolder => {
 				if (bannerFolder.length === 0) {
-					os.api('drive/folders/create', {
+					$root.api('drive/folders/create', {
 						name: '%i18n:desktop.banner%'
 					}).then(iconFolder => {
 						resolve(upload(data, iconFolder));
@@ -55,7 +54,7 @@ export default (os: OS) => {
 	});
 
 	const upload = (data, folder) => new Promise((resolve, reject) => {
-		const dialog = os.new(ProgressDialog, {
+		const dialog = $root.new(ProgressDialog, {
 			title: '%i18n:desktop.uploading-banner%'
 		});
 		document.body.appendChild(dialog.$el);
@@ -79,19 +78,19 @@ export default (os: OS) => {
 	});
 
 	const setBanner = file => {
-		return os.api('i/update', {
+		return $root.api('i/update', {
 			bannerId: file.id
 		}).then(i => {
-			os.store.commit('updateIKeyValue', {
+			$root.$store.commit('updateIKeyValue', {
 				key: 'bannerId',
 				value: i.bannerId
 			});
-			os.store.commit('updateIKeyValue', {
+			$root.$store.commit('updateIKeyValue', {
 				key: 'bannerUrl',
 				value: i.bannerUrl
 			});
 
-			os.apis.dialog({
+			$root.$dialog({
 				title: '%fa:info-circle% %i18n:desktop.banner-updated%',
 				text: null,
 				actions: [{
@@ -106,7 +105,7 @@ export default (os: OS) => {
 	return (file = null) => {
 		const selectedFile = file
 			? Promise.resolve(file)
-			: os.apis.chooseDriveFile({
+			: $root.$chooseDriveFile({
 				multiple: false,
 				title: '%fa:image% %i18n:desktop.choose-banner%'
 			});
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index ca9771e2fe..f2d27fa16e 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -2,6 +2,7 @@
  * Desktop Client
  */
 
+import Vue from 'vue';
 import VueRouter from 'vue-router';
 
 // Style
@@ -11,15 +12,6 @@ import init from '../init';
 import fuckAdBlock from '../common/scripts/fuck-ad-block';
 import composeNotification from '../common/scripts/compose-notification';
 
-import chooseDriveFolder from './api/choose-drive-folder';
-import chooseDriveFile from './api/choose-drive-file';
-import dialog from './api/dialog';
-import input from './api/input';
-import post from './api/post';
-import notify from './api/notify';
-import updateAvatar from './api/update-avatar';
-import updateBanner from './api/update-banner';
-
 import MkIndex from './views/pages/index.vue';
 import MkHome from './views/pages/home.vue';
 import MkDeck from './views/pages/deck/deck.vue';
@@ -36,12 +28,131 @@ import MkTag from './views/pages/tag.vue';
 import MkReversi from './views/pages/games/reversi.vue';
 import MkShare from './views/pages/share.vue';
 import MkFollow from '../common/views/pages/follow.vue';
+
+import Ctx from './views/components/context-menu.vue';
+import PostFormWindow from './views/components/post-form-window.vue';
+import RenoteFormWindow from './views/components/renote-form-window.vue';
+import MkChooseFileFromDriveWindow from './views/components/choose-file-from-drive-window.vue';
+import MkChooseFolderFromDriveWindow from './views/components/choose-folder-from-drive-window.vue';
+import Dialog from './views/components/dialog.vue';
+import InputDialog from './views/components/input-dialog.vue';
+import Notification from './views/components/ui-notification.vue';
+
+import { url } from '../config';
 import MiOS from '../mios';
 
 /**
  * init
  */
 init(async (launch) => {
+	Vue.mixin({
+		methods: {
+			$contextmenu(e, menu, opts?) {
+				const o = opts || {};
+				const vm = this.$root.new(Ctx, {
+					menu,
+					x: e.pageX - window.pageXOffset,
+					y: e.pageY - window.pageYOffset,
+				});
+				vm.$once('closed', () => {
+					if (o.closed) o.closed();
+				});
+			},
+
+			$post(opts) {
+				const o = opts || {};
+				if (o.renote) {
+					const vm = this.$root.new(RenoteFormWindow, {
+						note: o.renote,
+						animation: o.animation == null ? true : o.animation
+					});
+					if (o.cb) vm.$once('closed', o.cb);
+				} else {
+					const vm = this.$root.new(PostFormWindow, {
+						reply: o.reply,
+						animation: o.animation == null ? true : o.animation
+					});
+					if (o.cb) vm.$once('closed', o.cb);
+				}
+			},
+
+			$chooseDriveFile(opts) {
+				return new Promise((res, rej) => {
+					const o = opts || {};
+
+					if (document.body.clientWidth > 800) {
+						const w = this.$root.new(MkChooseFileFromDriveWindow, {
+							title: o.title,
+							multiple: o.multiple,
+							initFolder: o.currentFolder
+						});
+						w.$once('selected', file => {
+							res(file);
+						});
+					} else {
+						window['cb'] = file => {
+							res(file);
+						};
+
+						window.open(url + `/selectdrive?multiple=${o.multiple}`,
+							'choose_drive_window',
+							'height=500, width=800');
+					}
+				});
+			},
+
+			$chooseDriveFolder(opts) {
+				return new Promise((res, rej) => {
+					const o = opts || {};
+					const w = this.$root.new(MkChooseFolderFromDriveWindow, {
+						title: o.title,
+						initFolder: o.currentFolder
+					});
+					w.$once('selected', folder => {
+						res(folder);
+					});
+				});
+			},
+
+			$dialog(opts) {
+				return new Promise<string>((res, rej) => {
+					const o = opts || {};
+					const d = this.$root.new(Dialog, {
+						title: o.title,
+						text: o.text,
+						modal: o.modal,
+						buttons: o.actions
+					});
+					d.$once('clicked', id => {
+						res(id);
+					});
+				});
+			},
+
+			$input(opts) {
+				return new Promise<string>((res, rej) => {
+					const o = opts || {};
+					const d = this.$root.new(InputDialog, {
+						title: o.title,
+						placeholder: o.placeholder,
+						default: o.default,
+						type: o.type || 'text',
+						allowEmpty: o.allowEmpty
+					});
+					d.$once('done', text => {
+						res(text);
+					});
+				});
+			},
+
+			$notify(message) {
+				this.$root.new(Notification, {
+					message
+				});
+			}
+		}
+	});
+
 	// Register directives
 	require('./views/directives');
 
@@ -75,22 +186,13 @@ init(async (launch) => {
 	});
 
 	// Launch the app
-	const [, os] = launch(router, os => ({
-		chooseDriveFolder: chooseDriveFolder(os),
-		chooseDriveFile: chooseDriveFile(os),
-		dialog: dialog(os),
-		input: input(os),
-		post: post(os),
-		notify: notify(os),
-		updateAvatar: updateAvatar(os),
-		updateBanner: updateBanner(os)
-	}));
+	const [app, os] = launch(router);
 
 	if (os.store.getters.isSignedIn) {
 		/**
 		 * Fuck AD Block
 		 */
-		fuckAdBlock(os);
+		fuckAdBlock(app);
 	}
 
 	/**
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index c403418b21..7840d7e748 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -35,7 +35,6 @@
 import Vue from 'vue';
 import i18n from '../../../i18n';
 import * as anime from 'animejs';
-import contextmenu from '../../api/contextmenu';
 import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
 
 export default Vue.extend({
@@ -70,7 +69,7 @@ export default Vue.extend({
 
 		onContextmenu(e) {
 			this.isContextmenuShowing = true;
-			contextmenu((this as any).os)(e, [{
+			this.$contextmenu(e, [{
 				type: 'item',
 				text: this.$t('contextmenu.rename'),
 				icon: 'i-cursor',
@@ -116,10 +115,10 @@ export default Vue.extend({
 					action: this.addApp
 				}]
 			}*/], {
-					closed: () => {
-						this.isContextmenuShowing = false;
-					}
-				});
+				closed: () => {
+					this.isContextmenuShowing = false;
+				}
+			});
 		},
 
 		onDragstart(e) {
@@ -149,7 +148,7 @@ export default Vue.extend({
 		},
 
 		rename() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('contextmenu.rename-file'),
 				placeholder: this.$t('contextmenu.input-new-file-name'),
 				default: this.file.name,
@@ -171,7 +170,7 @@ export default Vue.extend({
 
 		copyUrl() {
 			copyToClipboard(this.file.url);
-			this.$root.apis.dialog({
+			this.$dialog({
 				title: this.$t('contextmenu.copied'),
 				text: this.$t('contextmenu.copied-url-to-clipboard'),
 				actions: [{
@@ -181,11 +180,11 @@ export default Vue.extend({
 		},
 
 		setAsAvatar() {
-			this.$root.apis.updateAvatar(this.file);
+			this.$updateAvatar(this.file);
 		},
 
 		setAsBanner() {
-			this.$root.apis.updateBanner(this.file);
+			this.$updateBanner(this.file);
 		},
 
 		addApp() {
diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue
index 2f595ddbe3..fe9bad2c28 100644
--- a/src/client/app/desktop/views/components/drive.folder.vue
+++ b/src/client/app/desktop/views/components/drive.folder.vue
@@ -26,7 +26,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import i18n from '../../../i18n';
-import contextmenu from '../../api/contextmenu';
 
 export default Vue.extend({
 	i18n: i18n('desktop/views/components/drive.folder.vue'),
@@ -54,7 +53,7 @@ export default Vue.extend({
 
 		onContextmenu(e) {
 			this.isContextmenuShowing = true;
-			contextmenu((this as any).os)(e, [{
+			this.$contextmenu(e, [{
 				type: 'item',
 				text: this.$t('contextmenu.move-to-this-folder'),
 				icon: 'arrow-right',
@@ -75,10 +74,10 @@ export default Vue.extend({
 				icon: ['far', 'trash-alt'],
 				action: this.deleteFolder
 			}], {
-					closed: () => {
-						this.isContextmenuShowing = false;
-					}
-				});
+				closed: () => {
+					this.isContextmenuShowing = false;
+				}
+			});
 		},
 
 		onMouseover() {
@@ -156,7 +155,7 @@ export default Vue.extend({
 				}).catch(err => {
 					switch (err) {
 						case 'detected-circular-definition':
-							this.$root.apis.dialog({
+							this.$dialog({
 								title: this.$t('unable-to-process'),
 								text: this.$t('circular-reference-detected'),
 								actions: [{
@@ -196,7 +195,7 @@ export default Vue.extend({
 		},
 
 		rename() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('contextmenu.rename-folder'),
 				placeholder: this.$t('contextmenu.input-new-folder-name'),
 				default: this.folder.name
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
index cf9b8e7152..1e89aa9adf 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/app/desktop/views/components/drive.vue
@@ -65,7 +65,6 @@ import XNavFolder from './drive.nav-folder.vue';
 import XFolder from './drive.folder.vue';
 import XFile from './drive.file.vue';
 import contains from '../../../common/scripts/contains';
-import contextmenu from '../../api/contextmenu';
 import { url } from '../../../config';
 
 export default Vue.extend({
@@ -137,7 +136,7 @@ export default Vue.extend({
 	},
 	methods: {
 		onContextmenu(e) {
-			contextmenu((this as any).os)(e, [{
+			this.$contextmenu(e, [{
 				type: 'item',
 				text: this.$t('contextmenu.create-folder'),
 				icon: ['far', 'folder'],
@@ -314,7 +313,7 @@ export default Vue.extend({
 				}).catch(err => {
 					switch (err) {
 						case 'detected-circular-definition':
-							this.$root.apis.dialog({
+							this.$dialog({
 								title: this.$t('unable-to-process'),
 								text: this.$t('circular-reference-detected'),
 								actions: [{
@@ -335,7 +334,7 @@ export default Vue.extend({
 		},
 
 		urlUpload() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('url-upload'),
 				placeholder: this.$t('url-of-file')
 			}).then(url => {
@@ -344,7 +343,7 @@ export default Vue.extend({
 					folderId: this.folder ? this.folder.id : undefined
 				});
 
-				this.$root.apis.dialog({
+				this.$dialog({
 					title: this.$t('url-upload-requested'),
 					text: this.$t('may-take-time'),
 					actions: [{
@@ -355,7 +354,7 @@ export default Vue.extend({
 		},
 
 		createFolder() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('create-folder'),
 				placeholder: this.$t('folder-name')
 			}).then(name => {
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index aa25b5b952..500773ee9f 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -186,7 +186,7 @@ export default Vue.extend({
 
 	methods: {
 		hint() {
-			this.$root.apis.dialog({
+			this.$dialog({
 				title: this.$t('@.customization-tips.title'),
 				text: this.$t('@.customization-tips.paragraph'),
 				actions: [{
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index 87fc3388d2..44178d9414 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -245,7 +245,7 @@ export default Vue.extend({
 		},
 
 		chooseFileFromDrive() {
-			this.$root.apis.chooseDriveFile({
+			this.$chooseDriveFile({
 				multiple: true
 			}).then(files => {
 				files.forEach(this.attachMedia);
@@ -363,7 +363,7 @@ export default Vue.extend({
 		},
 
 		addVisibleUser() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('enter-username')
 			}).then(acct => {
 				if (acct.startsWith('@')) acct = acct.substr(1);
@@ -401,13 +401,13 @@ export default Vue.extend({
 				this.clear();
 				this.deleteDraft();
 				this.$emit('posted');
-				this.$root.apis.notify(this.renote
+				this.$notify(this.renote
 					? this.$t('reposted')
 					: this.reply
 						? this.$t('replied')
 						: this.$t('posted'));
 			}).catch(err => {
-				this.$root.apis.notify(this.renote
+				this.$notify(this.renote
 					? this.$t('renote-failed')
 					: this.reply
 						? this.$t('reply-failed')
diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue
index d2b3c25c0c..c538b90dac 100644
--- a/src/client/app/desktop/views/components/renote-form.vue
+++ b/src/client/app/desktop/views/components/renote-form.vue
@@ -34,9 +34,9 @@ export default Vue.extend({
 				renoteId: this.note.id
 			}).then(data => {
 				this.$emit('posted');
-				this.$root.apis.notify(this.$t('success'));
+				this.$notify(this.$t('success'));
 			}).catch(err => {
-				this.$root.apis.notify(this.$t('failure'));
+				this.$notify(this.$t('failure'));
 			}).then(() => {
 				this.wait = false;
 			});
diff --git a/src/client/app/desktop/views/components/settings.2fa.vue b/src/client/app/desktop/views/components/settings.2fa.vue
index 885b19d424..e106038f03 100644
--- a/src/client/app/desktop/views/components/settings.2fa.vue
+++ b/src/client/app/desktop/views/components/settings.2fa.vue
@@ -35,7 +35,7 @@ export default Vue.extend({
 	},
 	methods: {
 		register() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('enter-password'),
 				type: 'password'
 			}).then(password => {
@@ -48,14 +48,14 @@ export default Vue.extend({
 		},
 
 		unregister() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('enter-password'),
 				type: 'password'
 			}).then(password => {
 				this.$root.api('i/2fa/unregister', {
 					password: password
 				}).then(() => {
-					this.$root.apis.notify(this.$t('unregistered'));
+					this.$notify(this.$t('unregistered'));
 					this.$store.state.i.twoFactorEnabled = false;
 				});
 			});
@@ -65,10 +65,10 @@ export default Vue.extend({
 			this.$root.api('i/2fa/done', {
 				token: this.token
 			}).then(() => {
-				this.$root.apis.notify(this.$t('success'));
+				this.$notify(this.$t('success'));
 				this.$store.state.i.twoFactorEnabled = true;
 			}).catch(() => {
-				this.$root.apis.notify(this.$t('failed'));
+				this.$notify(this.$t('failed'));
 			});
 		}
 	}
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index f834e84309..7080943da8 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -513,7 +513,7 @@ export default Vue.extend({
 			this.$emit('done');
 		},
 		updateWallpaper() {
-			this.$root.apis.chooseDriveFile({
+			this.$chooseDriveFile({
 				multiple: false
 			}).then(file => {
 				this.$root.api('i/update', {
@@ -533,16 +533,16 @@ export default Vue.extend({
 		},
 		checkForUpdate() {
 			this.checkingForUpdate = true;
-			checkForUpdate((this as any).os, true, true).then(newer => {
+			checkForUpdate(this.$root, true, true).then(newer => {
 				this.checkingForUpdate = false;
 				this.latestVersion = newer;
 				if (newer == null) {
-					this.$root.apis.dialog({
+					this.$dialog({
 						title: this.$t('no-updates'),
 						text: this.$t('no-updates-desc')
 					});
 				} else {
-					this.$root.apis.dialog({
+					this.$dialog({
 						title: this.$t('update-available'),
 						text: this.$t('update-available-desc')
 					});
@@ -551,7 +551,7 @@ export default Vue.extend({
 		},
 		clean() {
 			localStorage.clear();
-			this.$root.apis.dialog({
+			this.$dialog({
 				title: this.$t('cache-cleared'),
 				text: this.$t('cache-cleared-desc')
 			});
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index d0929c4dc4..410b4e25f7 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -109,7 +109,7 @@ export default Vue.extend({
 				icon: 'plus',
 				text: this.$t('add-list'),
 				action: () => {
-					this.$root.apis.input({
+					this.$input({
 						title: this.$t('list-name'),
 					}).then(async title => {
 						const list = await this.$root.api('users/lists/create', {
diff --git a/src/client/app/desktop/views/components/ui.header.post.vue b/src/client/app/desktop/views/components/ui.header.post.vue
index 79c1abd872..b273ad8d4d 100644
--- a/src/client/app/desktop/views/components/ui.header.post.vue
+++ b/src/client/app/desktop/views/components/ui.header.post.vue
@@ -12,7 +12,7 @@ export default Vue.extend({
 	i18n: i18n('desktop/views/components/ui.header.post.vue'),
 	methods: {
 		post() {
-			this.$root.apis.post();
+			this.$post();
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/components/ui.sidebar.vue b/src/client/app/desktop/views/components/ui.sidebar.vue
index ac6740b93c..76990fa5a4 100644
--- a/src/client/app/desktop/views/components/ui.sidebar.vue
+++ b/src/client/app/desktop/views/components/ui.sidebar.vue
@@ -139,7 +139,7 @@ export default Vue.extend({
 		},
 
 		post() {
-			this.$root.apis.post();
+			this.$post();
 		},
 
 		drive() {
diff --git a/src/client/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue
index 09a3aa7900..fa5efbc93e 100644
--- a/src/client/app/desktop/views/components/ui.vue
+++ b/src/client/app/desktop/views/components/ui.vue
@@ -67,7 +67,7 @@ export default Vue.extend({
 
 	methods: {
 		post() {
-			this.$root.apis.post();
+			this.$post();
 		},
 
 		toggleZenMode() {
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
index 2f4ed16d83..8c52ea4af2 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
 	},
 	methods: {
 		add() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('list-name'),
 			}).then(async title => {
 				const list = await this.$root.api('users/lists/create', {
diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue
index 1fad1ddd81..ead5ee2bdb 100644
--- a/src/client/app/desktop/views/pages/deck/deck.column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.column.vue
@@ -29,7 +29,6 @@
 import Vue from 'vue';
 import i18n from '../../../../i18n';
 import Menu from '../../../../common/views/components/menu.vue';
-import contextmenu from '../../../api/contextmenu';
 import { countIf } from '../../../../../../prelude/array';
 
 export default Vue.extend({
@@ -168,7 +167,7 @@ export default Vue.extend({
 				icon: 'pencil-alt',
 				text: this.$t('rename'),
 				action: () => {
-					this.$root.apis.input({
+					this.$input({
 						title: this.$t('rename'),
 						default: this.name,
 						allowEmpty: false
@@ -230,7 +229,7 @@ export default Vue.extend({
 
 		onContextmenu(e) {
 			if (this.isTemporaryColumn) return;
-			contextmenu((this as any).os)(e, this.getMenu());
+			this.$contextmenu(e, this.getMenu());
 		},
 
 		showMenu() {
diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue
index a702053c18..ea204d0fc0 100644
--- a/src/client/app/desktop/views/pages/deck/deck.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.vue
@@ -255,7 +255,7 @@ export default Vue.extend({
 					icon: 'hashtag',
 					text: this.$t('@deck.hashtag'),
 					action: () => {
-						this.$root.apis.input({
+						this.$input({
 							title: this.$t('enter-hashtag-tl-title')
 						}).then(title => {
 							this.$store.dispatch('settings/addDeckColumn', {
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 903a465c9c..a2f3f183b8 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -81,7 +81,7 @@ export default Vue.extend({
 		onBannerClick() {
 			if (!this.$store.getters.isSignedIn || this.$store.state.i.id != this.user.id) return;
 
-			this.$root.apis.updateBanner().then(i => {
+			this.$updateBanner().then(i => {
 				this.user.bannerUrl = i.bannerUrl;
 			});
 		}
diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue
index d6bcd72f5a..41e2fa6e5e 100644
--- a/src/client/app/desktop/views/pages/user/user.profile.vue
+++ b/src/client/app/desktop/views/pages/user/user.profile.vue
@@ -101,7 +101,7 @@ export default Vue.extend({
 					listId: list.id,
 					userId: this.user.id
 				});
-				this.$root.apis.dialog({
+				this.$dialog({
 					title: 'Done!',
 					text: this.$t('list-pushed').replace('{user}', this.user.name).replace('{list}', list.title)
 				});
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index 10745aadea..d5833019db 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -13,7 +13,7 @@ import VueI18n from 'vue-i18n';
 import VueHotkey from './common/hotkey';
 import App from './app.vue';
 import checkForUpdate from './common/scripts/check-for-update';
-import MiOS, { API } from './mios';
+import MiOS from './mios';
 import { clientVersion as version, codename, lang } from './config';
 import { builtinThemes, lightTheme, applyTheme } from './theme';
 
@@ -180,16 +180,14 @@ if (localStorage.getItem('should-refresh') == 'true') {
 }
 
 // MiOSを初期化してコールバックする
-export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API) => [Vue, MiOS]) => void, sw = false) => {
+export default (callback: (launch: (router: VueRouter) => [Vue, MiOS]) => void, sw = false) => {
 	const os = new MiOS(sw);
 
 	os.init(() => {
 		// アプリ基底要素マウント
 		document.body.innerHTML = '<div id="app"></div>';
 
-		const launch = (router: VueRouter, api?: (os: MiOS) => API) => {
-			os.apis = api ? api(os) : null;
-
+		const launch = (router: VueRouter) => {
 			//#region theme
 			os.store.watch(s => {
 				return s.device.darkmode;
@@ -285,7 +283,6 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
 							windows: os.windows
 						},
 						stream: os.stream,
-						apis: os.apis,
 						instanceName: os.instanceName
 					};
 				},
@@ -293,7 +290,14 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
 					api: os.api,
 					getMeta: os.getMeta,
 					getMetaSync: os.getMetaSync,
-					new: os.new,
+					new(vm, props) {
+						const x = new vm({
+							parent: this,
+							propsData: props
+						}).$mount();
+						document.body.appendChild(x.$el);
+						return x;
+					},
 				},
 				router,
 				render: createEl => createEl(App)
@@ -304,18 +308,18 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
 			// マウント
 			app.$mount('#app');
 
+			//#region 更新チェック
+			const preventUpdate = os.store.state.device.preventUpdate;
+			if (!preventUpdate) {
+				setTimeout(() => {
+					checkForUpdate(app);
+				}, 3000);
+			}
+			//#endregion
+
 			return [app, os] as [Vue, MiOS];
 		};
 
 		callback(launch);
-
-		//#region 更新チェック
-		const preventUpdate = os.store.state.device.preventUpdate;
-		if (!preventUpdate) {
-			setTimeout(() => {
-				checkForUpdate(os);
-			}, 3000);
-		}
-		//#endregion
 	});
 };
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index 88dae04622..07021c9b60 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -15,41 +15,6 @@ let spinner = null;
 let pending = 0;
 //#endregion
 
-export type API = {
-	chooseDriveFile: (opts: {
-		title?: string;
-		currentFolder?: any;
-		multiple?: boolean;
-	}) => Promise<any>;
-
-	chooseDriveFolder: (opts: {
-		title?: string;
-		currentFolder?: any;
-	}) => Promise<any>;
-
-	dialog: (opts: {
-		title: string;
-		text: string;
-		actions?: Array<{
-			text: string;
-			id?: string;
-		}>;
-	}) => Promise<string>;
-
-	input: (opts: {
-		title: string;
-		placeholder?: string;
-		default?: string;
-	}) => Promise<string>;
-
-	post: (opts?: {
-		reply?: any;
-		renote?: any;
-	}) => void;
-
-	notify: (message: string) => void;
-};
-
 /**
  * Misskey Operating System
  */
@@ -70,16 +35,6 @@ export default class MiOS extends EventEmitter {
 
 	public app: Vue;
 
-	@autobind
-	public new(vm, props) {
-		const x = new vm({
-			parent: this.app,
-			propsData: props
-		}).$mount();
-		document.body.appendChild(x.$el);
-		return x;
-	}
-
 	/**
 	 * Whether is debug mode
 	 */
@@ -89,8 +44,6 @@ export default class MiOS extends EventEmitter {
 
 	public store: ReturnType<typeof initStore>;
 
-	public apis: API;
-
 	/**
 	 * A connection manager of home stream
 	 */
diff --git a/src/client/app/mobile/api/choose-drive-file.ts b/src/client/app/mobile/api/choose-drive-file.ts
deleted file mode 100644
index b1a78f2364..0000000000
--- a/src/client/app/mobile/api/choose-drive-file.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import Chooser from '../views/components/drive-file-chooser.vue';
-
-export default function(opts) {
-	return new Promise((res, rej) => {
-		const o = opts || {};
-		const w = new Chooser({
-			propsData: {
-				title: o.title,
-				multiple: o.multiple,
-				initFolder: o.currentFolder
-			}
-		}).$mount();
-		w.$once('selected', file => {
-			res(file);
-		});
-		document.body.appendChild(w.$el);
-	});
-}
diff --git a/src/client/app/mobile/api/choose-drive-folder.ts b/src/client/app/mobile/api/choose-drive-folder.ts
deleted file mode 100644
index d1f97d1487..0000000000
--- a/src/client/app/mobile/api/choose-drive-folder.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import Chooser from '../views/components/drive-folder-chooser.vue';
-
-export default function(opts) {
-	return new Promise((res, rej) => {
-		const o = opts || {};
-		const w = new Chooser({
-			propsData: {
-				title: o.title,
-				initFolder: o.currentFolder
-			}
-		}).$mount();
-		w.$once('selected', folder => {
-			res(folder);
-		});
-		document.body.appendChild(w.$el);
-	});
-}
diff --git a/src/client/app/mobile/api/dialog.ts b/src/client/app/mobile/api/dialog.ts
deleted file mode 100644
index 23f35b7aa9..0000000000
--- a/src/client/app/mobile/api/dialog.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import OS from '../../mios';
-import Dialog from '../views/components/dialog.vue';
-
-export default (os: OS) => opts => {
-	return new Promise<string>((res, rej) => {
-		const o = opts || {};
-		const d = os.new(Dialog, {
-			title: o.title,
-			text: o.text,
-			modal: o.modal,
-			buttons: o.actions
-		});
-		d.$once('clicked', id => {
-			res(id);
-		});
-		document.body.appendChild(d.$el);
-	});
-};
diff --git a/src/client/app/mobile/api/input.ts b/src/client/app/mobile/api/input.ts
deleted file mode 100644
index 38d0fb61eb..0000000000
--- a/src/client/app/mobile/api/input.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export default function(opts) {
-	return new Promise<string>((res, rej) => {
-		const x = window.prompt(opts.title);
-		if (x) {
-			res(x);
-		}
-	});
-}
diff --git a/src/client/app/mobile/api/notify.ts b/src/client/app/mobile/api/notify.ts
deleted file mode 100644
index 82780d196f..0000000000
--- a/src/client/app/mobile/api/notify.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function(message) {
-	alert(message);
-}
diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts
deleted file mode 100644
index a64ed1c43e..0000000000
--- a/src/client/app/mobile/api/post.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import PostForm from '../views/components/post-form-dialog.vue';
-
-export default (os) => (opts) => {
-	const o = opts || {};
-
-	document.documentElement.style.overflow = 'hidden';
-
-	function recover() {
-		document.documentElement.style.overflow = 'auto';
-	}
-
-	const vm = new PostForm({
-		parent: os.app,
-		propsData: {
-			reply: o.reply,
-			renote: o.renote
-		}
-	}).$mount();
-	vm.$once('cancel', recover);
-	vm.$once('posted', recover);
-	if (o.cb) vm.$once('closed', o.cb);
-	document.body.appendChild(vm.$el);
-	(vm as any).focus();
-};
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 9412c85980..707aae087b 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -2,6 +2,7 @@
  * Mobile Client
  */
 
+import Vue from 'vue';
 import VueRouter from 'vue-router';
 
 // Style
@@ -9,13 +10,6 @@ import './style.styl';
 
 import init from '../init';
 
-import chooseDriveFolder from './api/choose-drive-folder';
-import chooseDriveFile from './api/choose-drive-file';
-import dialog from './api/dialog';
-import input from './api/input';
-import post from './api/post';
-import notify from './api/notify';
-
 import MkIndex from './views/pages/index.vue';
 import MkSignup from './views/pages/signup.vue';
 import MkUser from './views/pages/user.vue';
@@ -39,10 +33,94 @@ import MkTag from './views/pages/tag.vue';
 import MkShare from './views/pages/share.vue';
 import MkFollow from '../common/views/pages/follow.vue';
 
+import PostForm from './views/components/post-form-dialog.vue';
+import FileChooser from './views/components/drive-file-chooser.vue';
+import FolderChooser from './views/components/drive-folder-chooser.vue';
+import Dialog from './views/components/dialog.vue';
+
 /**
  * init
  */
 init((launch) => {
+	Vue.mixin({
+		methods: {
+			$post(opts) {
+				const o = opts || {};
+
+				document.documentElement.style.overflow = 'hidden';
+
+				function recover() {
+					document.documentElement.style.overflow = 'auto';
+				}
+
+				const vm = this.$root.new(PostForm, {
+					reply: o.reply,
+					renote: o.renote
+				});
+
+				vm.$once('cancel', recover);
+				vm.$once('posted', recover);
+				if (o.cb) vm.$once('closed', o.cb);
+				(vm as any).focus();
+			},
+
+			$chooseDriveFile(opts) {
+				return new Promise((res, rej) => {
+					const o = opts || {};
+					const vm = this.$root.new(FileChooser, {
+						title: o.title,
+						multiple: o.multiple,
+						initFolder: o.currentFolder
+					});
+					vm.$once('selected', file => {
+						res(file);
+					});
+				});
+			},
+
+			$chooseDriveFolder(opts) {
+				return new Promise((res, rej) => {
+					const o = opts || {};
+					const vm = this.$root.new(FolderChooser, {
+						title: o.title,
+						initFolder: o.currentFolder
+					});
+					vm.$once('selected', folder => {
+						res(folder);
+					});
+				});
+			},
+
+			$input(opts) {
+				return new Promise<string>((res, rej) => {
+					const x = window.prompt(opts.title);
+					if (x) {
+						res(x);
+					}
+				});
+			},
+
+			$dialog(opts) {
+				return new Promise<string>((res, rej) => {
+					const o = opts || {};
+					const d = this.$root.new(Dialog, {
+						title: o.title,
+						text: o.text,
+						modal: o.modal,
+						buttons: o.actions
+					});
+					d.$once('clicked', id => {
+						res(id);
+					});
+				});
+			},
+
+			$notify(message) {
+				alert(message);
+			}
+		}
+	});
+
 	// Register directives
 	require('./views/directives');
 
@@ -85,12 +163,5 @@ init((launch) => {
 	});
 
 	// Launch the app
-	launch(router, os => ({
-		chooseDriveFolder,
-		chooseDriveFile,
-		dialog: dialog(os),
-		input,
-		post: post(os),
-		notify
-	}));
+	launch(router);
 }, true);
diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
index 68a16a8350..3aa04d045c 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -101,7 +101,7 @@ export default Vue.extend({
 		},
 
 		move() {
-			this.$root.apis.chooseDriveFolder().then(folder => {
+			this.$chooseDriveFolder().then(folder => {
 				this.$root.api('drive/files/update', {
 					fileId: this.file.id,
 					folderId: folder == null ? null : folder.id
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
index 90e2a325ca..e7a5d99832 100644
--- a/src/client/app/mobile/views/components/drive.vue
+++ b/src/client/app/mobile/views/components/drive.vue
@@ -439,7 +439,7 @@ export default Vue.extend({
 				alert(this.$t('root-move-alert'));
 				return;
 			}
-			this.$root.apis.chooseDriveFolder().then(folder => {
+			this.$chooseDriveFolder().then(folder => {
 				this.$root.api('drive/folders/update', {
 					parentId: folder ? folder.id : null,
 					folderId: this.folder.id
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index bd9bc0a4d0..4f5d160542 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -196,13 +196,13 @@ export default Vue.extend({
 		},
 
 		reply() {
-			this.$root.apis.post({
+			this.$post({
 				reply: this.p
 			});
 		},
 
 		renote() {
-			this.$root.apis.post({
+			this.$post({
 				renote: this.p
 			});
 		},
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index b1914d4afb..ca6c0b41fa 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -220,7 +220,7 @@ export default Vue.extend({
 		},
 
 		chooseFileFromDrive() {
-			this.$root.apis.chooseDriveFile({
+			this.$chooseDriveFile({
 				multiple: true
 			}).then(files => {
 				files.forEach(this.attachMedia);
@@ -279,7 +279,7 @@ export default Vue.extend({
 		},
 
 		addVisibleUser() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('username-prompt')
 			}).then(acct => {
 				if (acct.startsWith('@')) acct = acct.substr(1);
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index f5b9161286..2bd472f38e 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -7,7 +7,7 @@
 			<button class="nav" @click="$parent.isDrawerOpening = true"><fa icon="bars"/></button>
 			<i v-if="hasUnreadNotification || hasUnreadMessagingMessage || hasGameInvitation" class="circle"><fa icon="circle"/></i>
 			<h1>
-				<slot>{{ os.instanceName }}</slot>
+				<slot>{{ $root.instanceName }}</slot>
 			</h1>
 			<slot name="func"></slot>
 		</div>
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index dd4785e0ad..ed057c2207 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -139,7 +139,7 @@ export default Vue.extend({
 
 	methods: {
 		fn() {
-			this.$root.apis.post();
+			this.$post();
 		},
 
 		saveSrc() {
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index 1158e45094..445471200a 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -339,16 +339,16 @@ export default Vue.extend({
 
 		checkForUpdate() {
 			this.checkingForUpdate = true;
-			checkForUpdate((this as any).os, true, true).then(newer => {
+			checkForUpdate(this.$root, true, true).then(newer => {
 				this.checkingForUpdate = false;
 				this.latestVersion = newer;
 				if (newer == null) {
-					this.$root.apis.dialog({
+					this.$dialog({
 						title: this.$t('no-updates'),
 						text: this.$t('no-updates-desc')
 					});
 				} else {
-					this.$root.apis.dialog({
+					this.$dialog({
 						title: this.$t('update-available'),
 						text: this.$t('update-available-desc')
 					});
diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue
index 2f0c25b4a8..2222a22487 100644
--- a/src/client/app/mobile/views/pages/user-lists.vue
+++ b/src/client/app/mobile/views/pages/user-lists.vue
@@ -38,7 +38,7 @@ export default Vue.extend({
 	},
 	methods: {
 		fn() {
-			this.$root.apis.input({
+			this.$input({
 				title: this.$t('enter-list-name'),
 			}).then(async title => {
 				const list = await this.$root.api('users/lists/create', {