From b0f989dbacbdbdd091b0d220496d22f47c795576 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 6 Jun 2018 19:22:45 +0900
Subject: [PATCH] =?UTF-8?q?Deck=E3=81=AB=E3=82=A6=E3=82=A3=E3=82=B8?=
 =?UTF-8?q?=E3=82=A7=E3=83=83=E3=83=88=E3=82=92=E7=BD=AE=E3=81=91=E3=82=8B?=
 =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/app/common/define-widget.ts        |  21 +--
 .../app/common/scripts/streaming/home.ts      |  25 ++-
 .../app/common/views/components/menu.vue      |  10 +-
 .../app/common/views/widgets/broadcast.vue    |   2 +-
 .../app/common/views/widgets/calendar.vue     |   4 +-
 .../app/common/views/widgets/donation.vue     |   2 +-
 src/client/app/common/views/widgets/rss.vue   |   2 +-
 .../app/common/views/widgets/slideshow.vue    |   2 +-
 .../app/desktop/views/components/home.vue     |   4 +-
 .../views/components/widget-container.vue     |   2 +-
 .../desktop/views/pages/deck/deck.column.vue  |  74 ++++++---
 .../pages/deck/deck.notifications-column.vue  |   2 +-
 .../app/desktop/views/pages/deck/deck.vue     |  14 +-
 .../views/pages/deck/deck.widgets-column.vue  | 152 ++++++++++++++++++
 src/client/app/mobile/views/pages/widgets.vue |   4 +-
 src/client/app/store.ts                       |  89 ++++++----
 src/server/api/endpoints.ts                   |   5 +
 src/server/api/endpoints/i/update_home.ts     |  51 +-----
 .../api/endpoints/i/update_mobile_home.ts     |  52 +-----
 src/server/api/endpoints/i/update_widget.ts   |  79 +++++++++
 20 files changed, 417 insertions(+), 179 deletions(-)
 create mode 100644 src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
 create mode 100644 src/server/api/endpoints/i/update_widget.ts

diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts
index 0b2bc36566..2fae28be72 100644
--- a/src/client/app/common/define-widget.ts
+++ b/src/client/app/common/define-widget.ts
@@ -9,9 +9,9 @@ export default function<T extends object>(data: {
 			widget: {
 				type: Object
 			},
-			isMobile: {
-				type: Boolean,
-				default: false
+			platform: {
+				type: String,
+				required: true
 			},
 			isCustomizeMode: {
 				type: Boolean,
@@ -66,17 +66,10 @@ export default function<T extends object>(data: {
 
 				this.bakeProps();
 
-				if (this.isMobile) {
-					(this as any).api('i/update_mobile_home', {
-						id: this.id,
-						data: this.props
-					});
-				} else {
-					(this as any).api('i/update_home', {
-						id: this.id,
-						data: this.props
-					});
-				}
+				(this as any).api('i/update_widget', {
+					id: this.id,
+					data: this.props
+				});
 			}
 		}
 	});
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
index a27c55a60d..dd18c70d70 100644
--- a/src/client/app/common/scripts/streaming/home.ts
+++ b/src/client/app/common/scripts/streaming/home.ts
@@ -58,25 +58,18 @@ export class HomeStream extends Stream {
 		});
 
 		this.on('home_updated', x => {
-			if (x.home) {
-				os.store.commit('settings/setHome', x.home);
-			} else {
-				os.store.commit('settings/setHomeWidget', {
-					id: x.id,
-					data: x.data
-				});
-			}
+			os.store.commit('settings/setHome', x);
 		});
 
 		this.on('mobile_home_updated', x => {
-			if (x.home) {
-				os.store.commit('settings/setMobileHome', x.home);
-			} else {
-				os.store.commit('settings/setMobileHomeWidget', {
-					id: x.id,
-					data: x.data
-				});
-			}
+			os.store.commit('settings/setMobileHome', x);
+		});
+
+		this.on('widgetUpdated', x => {
+			os.store.commit('settings/setWidget', {
+				id: x.id,
+				data: x.data
+			});
 		});
 
 		// トークンが再生成されたとき
diff --git a/src/client/app/common/views/components/menu.vue b/src/client/app/common/views/components/menu.vue
index e5df8345b9..73c8403ad3 100644
--- a/src/client/app/common/views/components/menu.vue
+++ b/src/client/app/common/views/components/menu.vue
@@ -2,7 +2,10 @@
 <div class="mk-menu">
 	<div class="backdrop" ref="backdrop" @click="close"></div>
 	<div class="popover" :class="{ compact }" ref="popover">
-		<button v-for="item in items" @click="clicked(item.onClick)" v-html="item.content"></button>
+		<template v-for="item in items">
+			<div v-if="item == null"></div>
+			<button v-else @click="clicked(item.onClick)" v-html="item.content"></button>
+		</template>
 	</div>
 </div>
 </template>
@@ -150,4 +153,9 @@ $border-color = rgba(27, 31, 35, 0.15)
 				color $theme-color-foreground
 				background darken($theme-color, 10%)
 
+		> div
+			margin 8px 0
+			height 1px
+			background #eee
+
 </style>
diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
index f337cec853..69b2a54fe9 100644
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ b/src/client/app/common/views/widgets/broadcast.vue
@@ -2,7 +2,7 @@
 <div class="mkw-broadcast"
 	:data-found="broadcasts.length != 0"
 	:data-melt="props.design == 1"
-	:data-mobile="isMobile"
+	:data-mobile="platform == 'mobile'"
 >
 	<div class="icon">
 		<svg height="32" version="1.1" viewBox="0 0 32 32" width="32">
diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
index 0e9714960a..333b56f629 100644
--- a/src/client/app/common/views/widgets/calendar.vue
+++ b/src/client/app/common/views/widgets/calendar.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mkw-calendar" :data-special="special" :data-mobile="isMobile">
+<div class="mkw-calendar" :data-special="special" :data-mobile="platform == 'mobile'">
 	<mk-widget-container :naked="props.design == 1" :show-header="false">
 		<div class="mkw-calendar--body">
 			<div class="calendar" :data-is-holiday="isHoliday">
@@ -67,7 +67,7 @@ export default define({
 	},
 	methods: {
 		func() {
-			if (this.isMobile) return;
+			if (this.platform == 'mobile') return;
 			if (this.props.design == 2) {
 				this.props.design = 0;
 			} else {
diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue
index 75f5db808a..470576d5e6 100644
--- a/src/client/app/common/views/widgets/donation.vue
+++ b/src/client/app/common/views/widgets/donation.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mkw-donation" :data-mobile="isMobile">
+<div class="mkw-donation" :data-mobile="platform == 'mobile'">
 	<article>
 		<h1>%fa:heart%%i18n:@title%</h1>
 		<p>
diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue
index 7ac453e450..a777388cdb 100644
--- a/src/client/app/common/views/widgets/rss.vue
+++ b/src/client/app/common/views/widgets/rss.vue
@@ -4,7 +4,7 @@
 		<template slot="header">%fa:rss-square%RSS</template>
 		<button slot="func" title="設定" @click="setting">%fa:cog%</button>
 
-		<div class="mkw-rss--body" :data-mobile="isMobile">
+		<div class="mkw-rss--body" :data-mobile="platform == 'mobile'">
 			<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 			<div class="feed" v-else>
 				<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue
index 459b24a32f..e1c28f5115 100644
--- a/src/client/app/common/views/widgets/slideshow.vue
+++ b/src/client/app/common/views/widgets/slideshow.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mkw-slideshow" :data-mobile="isMobile">
+<div class="mkw-slideshow" :data-mobile="platform == 'mobile'">
 	<div @click="choose">
 		<p v-if="props.folder === undefined">
 			<template v-if="isCustomizeMode">フォルダを指定するには、カスタマイズモードを終了してください</template>
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index c30ca68210..826753c169 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -47,7 +47,7 @@
 				:key="place"
 			>
 				<div v-for="widget in widgets[place]" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
-					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/>
+					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="desktop"/>
 				</div>
 			</x-draggable>
 			<div class="main">
@@ -60,7 +60,7 @@
 		</template>
 		<template v-else>
 			<div v-for="place in ['left', 'right']" :class="place">
-				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp"/>
+				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp" platform="desktop"/>
 			</div>
 			<div class="main">
 				<mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/>
diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue
index 488e9cb249..7cfcd68eba 100644
--- a/src/client/app/desktop/views/components/widget-container.vue
+++ b/src/client/app/desktop/views/components/widget-container.vue
@@ -36,7 +36,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 root(isDark)
 	background isDark ? #282C37 : #fff
-	border solid 1px rgba(#000, 0.075)
+	border solid 1px rgba(#000, isDark ? 0.2 : 0.075)
 	border-radius 6px
 	overflow hidden
 
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 3dc2da1c77..e9f013734a 100644
--- a/src/client/app/desktop/views/pages/deck/deck.column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.column.vue
@@ -1,8 +1,8 @@
 <template>
-<div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs">
+<div class="dnpfarvgbnfmyzbdquhhzyxcmstpdqzs" :class="{ naked, narrow }">
 	<header :class="{ indicate }">
 		<slot name="header"></slot>
-		<button ref="menu" @click="menu">%fa:caret-down%</button>
+		<button ref="menu" @click="showMenu">%fa:caret-down%</button>
 	</header>
 	<div ref="body">
 		<slot></slot>
@@ -19,6 +19,20 @@ export default Vue.extend({
 		id: {
 			type: String,
 			required: false
+		},
+		menu: {
+			type: Array,
+			required: false
+		},
+		naked: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		narrow: {
+			type: Boolean,
+			required: false,
+			default: false
 		}
 	},
 
@@ -59,26 +73,33 @@ export default Vue.extend({
 			}
 		},
 
-		menu() {
+		showMenu() {
+			const items = [{
+				content: '%fa:arrow-left% %i18n:@swap-left%',
+				onClick: () => {
+					this.$store.dispatch('settings/swapLeftDeckColumn', this.id);
+				}
+			}, {
+				content: '%fa:arrow-right% %i18n:@swap-right%',
+				onClick: () => {
+					this.$store.dispatch('settings/swapRightDeckColumn', this.id);
+				}
+			}, {
+				content: '%fa:trash-alt R% %i18n:@remove%',
+				onClick: () => {
+					this.$store.dispatch('settings/removeDeckColumn', this.id);
+				}
+			}];
+
+			if (this.menu) {
+				items.unshift(null);
+				this.menu.reverse().forEach(i => items.unshift(i));
+			}
+
 			this.os.new(Menu, {
 				source: this.$refs.menu,
 				compact: false,
-				items: [{
-					content: '%fa:arrow-left% %i18n:@swap-left%',
-					onClick: () => {
-						this.$store.dispatch('settings/swapLeftDeckColumn', this.id);
-					}
-				}, {
-					content: '%fa:arrow-right% %i18n:@swap-right%',
-					onClick: () => {
-						this.$store.dispatch('settings/swapRightDeckColumn', this.id);
-					}
-				}, {
-					content: '%fa:trash-alt R% %i18n:@remove%',
-					onClick: () => {
-						this.$store.dispatch('settings/removeDeckColumn', this.id);
-					}
-				}]
+				items
 			});
 		}
 	}
@@ -100,6 +121,21 @@ root(isDark)
 	box-shadow 0 2px 16px rgba(#000, 0.1)
 	overflow hidden
 
+	&.narrow
+		min-width 285px
+		max-width 285px
+
+	&.naked
+		background rgba(#000, isDark ? 0.25 : 0.1)
+
+		> header
+			background transparent
+			box-shadow none
+
+			if !isDark
+				> button
+					color #bbb
+
 	> header
 		z-index 1
 		line-height $header-height
diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue b/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue
index bfc2af1935..b92614314c 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue
@@ -1,7 +1,7 @@
 <template>
 <div>
 	<x-column :id="id">
-		<span slot="header">%fa:bell R% %i18n:@notifications%</span>
+		<span slot="header">%fa:bell R%%i18n:@notifications%</span>
 
 		<x-notifications/>
 	</x-column>
diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue
index ebec4f096c..4935d9a5bd 100644
--- a/src/client/app/desktop/views/pages/deck/deck.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.vue
@@ -2,6 +2,7 @@
 <mk-ui :class="$style.root">
 	<div class="qlvquzbjribqcaozciifydkngcwtyzje" :data-darkmode="$store.state.device.darkmode">
 		<template v-for="column in columns">
+			<x-widgets-column v-if="column.type == 'widgets'" :key="column.id" :column="column"/>
 			<x-notifications-column v-if="column.type == 'notifications'" :key="column.id" :id="column.id"/>
 			<x-tl-column v-if="column.type == 'home'" :key="column.id" :column="column"/>
 			<x-tl-column v-if="column.type == 'local'" :key="column.id" :column="column"/>
@@ -17,6 +18,7 @@
 import Vue from 'vue';
 import XTlColumn from './deck.tl-column.vue';
 import XNotificationsColumn from './deck.notifications-column.vue';
+import XWidgetsColumn from './deck.widgets-column.vue';
 import Menu from '../../../../common/views/components/menu.vue';
 import MkUserListsWindow from '../../components/user-lists-window.vue';
 import * as uuid from 'uuid';
@@ -24,7 +26,8 @@ import * as uuid from 'uuid';
 export default Vue.extend({
 	components: {
 		XTlColumn,
-		XNotificationsColumn
+		XNotificationsColumn,
+		XWidgetsColumn
 	},
 	computed: {
 		columns() {
@@ -110,6 +113,15 @@ export default Vue.extend({
 							type: 'notifications'
 						});
 					}
+				}, {
+					content: '%i18n:@widgets%',
+					onClick: () => {
+						this.$store.dispatch('settings/addDeckColumn', {
+							id: uuid(),
+							type: 'widgets',
+							widgets: []
+						});
+					}
 				}]
 			});
 		}
diff --git a/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue b/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
new file mode 100644
index 0000000000..0b2cc305f4
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
@@ -0,0 +1,152 @@
+<template>
+<div class="wtdtxvecapixsepjtcupubtsmometobz">
+	<x-column :id="column.id" :menu="menu" :naked="true" :narrow="true">
+		<span slot="header">%fa:calculator%%i18n:@widgets%</span>
+
+		<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
+			<template v-if="edit">
+				<header>
+					<select v-model="widgetAdderSelected">
+						<option value="profile">%i18n:common.widgets.profile%</option>
+						<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
+						<option value="calendar">%i18n:common.widgets.calendar%</option>
+						<option value="timemachine">%i18n:common.widgets.timemachine%</option>
+						<option value="activity">%i18n:common.widgets.activity%</option>
+						<option value="rss">%i18n:common.widgets.rss%</option>
+						<option value="trends">%i18n:common.widgets.trends%</option>
+						<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
+						<option value="slideshow">%i18n:common.widgets.slideshow%</option>
+						<option value="version">%i18n:common.widgets.version%</option>
+						<option value="broadcast">%i18n:common.widgets.broadcast%</option>
+						<option value="notifications">%i18n:common.widgets.notifications%</option>
+						<option value="users">%i18n:common.widgets.users%</option>
+						<option value="polls">%i18n:common.widgets.polls%</option>
+						<option value="post-form">%i18n:common.widgets.post-form%</option>
+						<option value="messaging">%i18n:common.widgets.messaging%</option>
+						<option value="memo">%i18n:common.widgets.memo%</option>
+						<option value="server">%i18n:common.widgets.server%</option>
+						<option value="donation">%i18n:common.widgets.donation%</option>
+						<option value="nav">%i18n:common.widgets.nav%</option>
+						<option value="tips">%i18n:common.widgets.tips%</option>
+					</select>
+					<button @click="addWidget">追加</button>
+				</header>
+				<x-draggable
+					:list="column.widgets"
+					:options="{ handle: '.handle', animation: 150 }"
+					@sort="onWidgetSort"
+				>
+					<div v-for="widget in column.widgets" class="customize-container" :key="widget.id">
+						<header>
+							<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
+						</header>
+						<div @click="widgetFunc(widget.id)">
+							<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
+						</div>
+					</div>
+				</x-draggable>
+			</template>
+			<template v-else>
+				<component class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="deck"/>
+			</template>
+		</div>
+	</x-column>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XColumn from './deck.column.vue';
+import * as XDraggable from 'vuedraggable';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+	components: {
+		XColumn,
+		XDraggable
+	},
+
+	props: {
+		column: {
+			type: Object,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			edit: false,
+			menu: null,
+			widgetAdderSelected: null
+		}
+	},
+
+	created() {
+		this.menu = [{
+			content: '%fa:cog% %i18n:@edit%',
+			onClick: () => {
+				this.edit = !this.edit;
+			}
+		}];
+	},
+
+	methods: {
+		widgetFunc(id) {
+			const w = this.$refs[id][0];
+			if (w.func) w.func();
+		},
+
+		onWidgetSort() {
+			this.saveWidgets();
+		},
+
+		addWidget() {
+			this.$store.dispatch('settings/addDeckWidget', {
+				id: this.column.id,
+				widget: {
+					name: this.widgetAdderSelected,
+					id: uuid(),
+					data: {}
+				}
+			});
+		},
+
+		removeWidget(widget) {
+			this.$store.dispatch('settings/removeDeckWidget', {
+				id: this.column.id,
+				widget
+			});
+		},
+
+		saveWidgets() {
+			this.$store.dispatch('settings/saveDeck');
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+root(isDark)
+	.gqpwvtwtprsbmnssnbicggtwqhmylhnq
+		.widget, .customize-container
+			margin 8px
+
+			&:first-of-type
+				margin-top 0
+
+		.customize-container
+			background #fff
+
+		> header
+			color isDark ? #fff : #000
+
+.wtdtxvecapixsepjtcupubtsmometobz[data-darkmode]
+	root(true)
+
+.wtdtxvecapixsepjtcupubtsmometobz:not([data-darkmode])
+	root(false)
+
+</style>
+
diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue
index a0893770e8..eab0ca6a38 100644
--- a/src/client/app/mobile/views/pages/widgets.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -35,13 +35,13 @@
 						<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button>
 					</header>
 					<div @click="widgetFunc(widget.id)">
-						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/>
+						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="mobile"/>
 					</div>
 				</div>
 			</x-draggable>
 		</template>
 		<template v-else>
-			<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true"/>
+			<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="mobile"/>
 		</template>
 	</main>
 </mk-ui>
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 17faeb97e5..5582ff6c57 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -124,13 +124,6 @@ export default (os: MiOS) => new Vuex.Store({
 					state.home = data;
 				},
 
-				setHomeWidget(state, x) {
-					const w = state.home.find(w => w.id == x.id);
-					if (w) {
-						w.data = x.data;
-					}
-				},
-
 				addHomeWidget(state, widget) {
 					state.home.unshift(widget);
 				},
@@ -139,11 +132,36 @@ export default (os: MiOS) => new Vuex.Store({
 					state.mobileHome = data;
 				},
 
-				setMobileHomeWidget(state, x) {
-					const w = state.mobileHome.find(w => w.id == x.id);
-					if (w) {
-						w.data = x.data;
+				setWidget(state, x) {
+					let w;
+
+					//#region Decktop home
+					if (state.home) {
+						w = state.home.find(w => w.id == x.id);
+						if (w) {
+							w.data = x.data;
+						}
 					}
+					//#endregion
+
+					//#region Mobile home
+					if (state.mobileHome) {
+						w = state.mobileHome.find(w => w.id == x.id);
+						if (w) {
+							w.data = x.data;
+						}
+					}
+					//#endregion
+
+					//#region Deck
+					if (state.deck && state.deck.columns) {
+						state.deck.columns.filter(c => c.type == 'widgets').forEach(c => {
+							c.widgets.forEach(w => {
+								if (w.id == x.id) w.data = x.data;
+							});
+						});
+					}
+					//#endregion
 				},
 
 				addMobileHomeWidget(state, widget) {
@@ -190,6 +208,20 @@ export default (os: MiOS) => new Vuex.Store({
 							return true;
 						}
 					});
+				},
+
+				addDeckWidget(state, x) {
+					if (state.deck.columns == null) return;
+					const column = state.deck.columns.find(c => c.id == x.id);
+					if (column == null) return;
+					column.widgets.unshift(x.widget);
+				},
+
+				removeDeckWidget(state, x) {
+					if (state.deck.columns == null) return;
+					const column = state.deck.columns.find(c => c.id == x.id);
+					if (column == null) return;
+					column.widgets = column.widgets.filter(w => w.id != x.widget.id);
 				}
 			},
 
@@ -212,40 +244,41 @@ export default (os: MiOS) => new Vuex.Store({
 					}
 				},
 
-				addDeckColumn(ctx, column) {
-					ctx.commit('addDeckColumn', column);
-
+				saveDeck(ctx) {
 					os.api('i/update_client_setting', {
 						name: 'deck',
 						value: ctx.state.deck
 					});
 				},
 
+				addDeckColumn(ctx, column) {
+					ctx.commit('addDeckColumn', column);
+					ctx.dispatch('saveDeck');
+				},
+
 				removeDeckColumn(ctx, id) {
 					ctx.commit('removeDeckColumn', id);
-
-					os.api('i/update_client_setting', {
-						name: 'deck',
-						value: ctx.state.deck
-					});
+					ctx.dispatch('saveDeck');
 				},
 
 				swapLeftDeckColumn(ctx, id) {
 					ctx.commit('swapLeftDeckColumn', id);
-
-					os.api('i/update_client_setting', {
-						name: 'deck',
-						value: ctx.state.deck
-					});
+					ctx.dispatch('saveDeck');
 				},
 
 				swapRightDeckColumn(ctx, id) {
 					ctx.commit('swapRightDeckColumn', id);
+					ctx.dispatch('saveDeck');
+				},
 
-					os.api('i/update_client_setting', {
-						name: 'deck',
-						value: ctx.state.deck
-					});
+				addDeckWidget(ctx, x) {
+					ctx.commit('addDeckWidget', x);
+					ctx.dispatch('saveDeck');
+				},
+
+				removeDeckWidget(ctx, x) {
+					ctx.commit('removeDeckWidget', x);
+					ctx.dispatch('saveDeck');
 				},
 
 				addHomeWidget(ctx, widget) {
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index e9392d236b..94e649d29b 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -189,6 +189,11 @@ const endpoints: Endpoint[] = [
 		withCredential: true,
 		secure: true
 	},
+	{
+		name: 'i/update_widget',
+		withCredential: true,
+		secure: true
+	},
 	{
 		name: 'i/change_password',
 		withCredential: true,
diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts
index 8ce551957e..48f6dbbb7a 100644
--- a/src/server/api/endpoints/i/update_home.ts
+++ b/src/server/api/endpoints/i/update_home.ts
@@ -1,6 +1,3 @@
-/**
- * Module dependencies
- */
 import $ from 'cafy';
 import User from '../../../../models/user';
 import event from '../../../../publishers/stream';
@@ -13,50 +10,16 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 			.have('id', $.str)
 			.have('place', $.str)
 			.have('data', $.obj))
-		.optional()
 		.get(params.home);
 	if (homeErr) return rej('invalid home param');
 
-	// Get 'id' parameter
-	const [id, idErr] = $.str.optional().get(params.id);
-	if (idErr) return rej('invalid id param');
+	await User.update(user._id, {
+		$set: {
+			'clientSettings.home': home
+		}
+	});
 
-	// Get 'data' parameter
-	const [data, dataErr] = $.obj.optional().get(params.data);
-	if (dataErr) return rej('invalid data param');
+	res();
 
-	if (home) {
-		await User.update(user._id, {
-			$set: {
-				'clientSettings.home': home
-			}
-		});
-
-		res();
-
-		event(user._id, 'home_updated', {
-			home
-		});
-	} else {
-		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
-
-		const _home = user.clientSettings.home;
-		const widget = _home.find(w => w.id == id);
-
-		if (widget == null) return rej('widget not found');
-
-		widget.data = data;
-
-		await User.update(user._id, {
-			$set: {
-				'clientSettings.home': _home
-			}
-		});
-
-		res();
-
-		event(user._id, 'home_updated', {
-			id, data
-		});
-	}
+	event(user._id, 'home_updated', home);
 });
diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts
index d79a77072b..d285a0a72d 100644
--- a/src/server/api/endpoints/i/update_mobile_home.ts
+++ b/src/server/api/endpoints/i/update_mobile_home.ts
@@ -1,6 +1,3 @@
-/**
- * Module dependencies
- */
 import $ from 'cafy';
 import User from '../../../../models/user';
 import event from '../../../../publishers/stream';
@@ -12,49 +9,16 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
 			.have('name', $.str)
 			.have('id', $.str)
 			.have('data', $.obj))
-		.optional().get(params.home);
+		.get(params.home);
 	if (homeErr) return rej('invalid home param');
 
-	// Get 'id' parameter
-	const [id, idErr] = $.str.optional().get(params.id);
-	if (idErr) return rej('invalid id param');
+	await User.update(user._id, {
+		$set: {
+			'clientSettings.mobileHome': home
+		}
+	});
 
-	// Get 'data' parameter
-	const [data, dataErr] = $.obj.optional().get(params.data);
-	if (dataErr) return rej('invalid data param');
+	res();
 
-	if (home) {
-		await User.update(user._id, {
-			$set: {
-				'clientSettings.mobileHome': home
-			}
-		});
-
-		res();
-
-		event(user._id, 'mobile_home_updated', {
-			home
-		});
-	} else {
-		if (id == null && data == null) return rej('you need to set id and data params if home param unset');
-
-		const _home = user.clientSettings.mobileHome || [];
-		const widget = _home.find(w => w.id == id);
-
-		if (widget == null) return rej('widget not found');
-
-		widget.data = data;
-
-		await User.update(user._id, {
-			$set: {
-				'clientSettings.mobileHome': _home
-			}
-		});
-
-		res();
-
-		event(user._id, 'mobile_home_updated', {
-			id, data
-		});
-	}
+	event(user._id, 'mobile_home_updated', home);
 });
diff --git a/src/server/api/endpoints/i/update_widget.ts b/src/server/api/endpoints/i/update_widget.ts
new file mode 100644
index 0000000000..b37761bde1
--- /dev/null
+++ b/src/server/api/endpoints/i/update_widget.ts
@@ -0,0 +1,79 @@
+import $ from 'cafy';
+import User from '../../../../models/user';
+import event from '../../../../publishers/stream';
+
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+	// Get 'id' parameter
+	const [id, idErr] = $.str.get(params.id);
+	if (idErr) return rej('invalid id param');
+
+	// Get 'data' parameter
+	const [data, dataErr] = $.obj.get(params.data);
+	if (dataErr) return rej('invalid data param');
+
+	if (id == null && data == null) return rej('you need to set id and data params if home param unset');
+
+	let widget;
+
+	//#region Desktop home
+	if (widget == null && user.clientSettings.home) {
+		const desktopHome = user.clientSettings.home;
+		widget = desktopHome.find(w => w.id == id);
+		if (widget) {
+				widget.data = data;
+
+			await User.update(user._id, {
+				$set: {
+					'clientSettings.home': desktopHome
+				}
+			});
+		}
+	}
+	//#endregion
+
+	//#region Mobile home
+	if (widget == null && user.clientSettings.mobileHome) {
+		const mobileHome = user.clientSettings.mobileHome;
+		widget = mobileHome.find(w => w.id == id);
+		if (widget) {
+				widget.data = data;
+
+			await User.update(user._id, {
+				$set: {
+					'clientSettings.mobileHome': mobileHome
+				}
+			});
+		}
+	}
+	//#endregion
+
+	//#region Deck
+	if (widget == null && user.clientSettings.deck && user.clientSettings.deck.columns) {
+		const deck = user.clientSettings.deck;
+		deck.columns.filter(c => c.type == 'widgets').forEach(c => {
+			c.widgets.forEach(w => {
+				if (w.id == id) widget = w;
+			});
+		});
+		if (widget) {
+				widget.data = data;
+
+			await User.update(user._id, {
+				$set: {
+					'clientSettings.deck': deck
+				}
+			});
+		}
+	}
+	//#endregion
+
+	if (widget) {
+		event(user._id, 'widgetUpdated', {
+			id, data
+		});
+
+		res();
+	} else {
+		rej('widget not found');
+	}
+});