From dde809825d52a91694a9f11713e694dde19b0d34 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 14 Oct 2018 19:44:30 +0900
Subject: [PATCH] Resolve #2900

---
 locales/ja-JP.yml                             |   5 +
 .../app/desktop/views/components/settings.vue |  12 +
 .../desktop/views/components/ui.sidebar.vue   | 237 ++++++++++++++++++
 .../app/desktop/views/components/ui.vue       |  25 +-
 src/client/app/store.ts                       |   1 +
 5 files changed, 277 insertions(+), 3 deletions(-)
 create mode 100644 src/client/app/desktop/views/components/ui.sidebar.vue

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 560dca665..68503425a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -883,6 +883,11 @@ desktop/views/components/settings.vue:
   task-manager: "タスクマネージャ"
   third-parties: "サードパーティ"
 
+  navbar-position: "ナビゲーションバーの位置"
+  navbar-position-top: "上"
+  navbar-position-left: "左"
+  navbar-position-right: "右"
+
 desktop/views/components/settings.2fa.vue:
   intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。"
   detail: "詳細..."
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 1cb8d4d4c..778302a7a 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -88,6 +88,13 @@
 			<ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
 			<ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
 			<ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
+
+			<section>
+				<header>%i18n:@navbar-position%</header>
+				<ui-radio v-model="navbar" value="top">%i18n:@navbar-position-top%</ui-radio>
+				<ui-radio v-model="navbar" value="left">%i18n:@navbar-position-left%</ui-radio>
+				<ui-radio v-model="navbar" value="right">%i18n:@navbar-position-right%</ui-radio>
+			</section>
 		</section>
 
 		<section class="web" v-show="page == 'web'">
@@ -293,6 +300,11 @@ export default Vue.extend({
 			set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
 		},
 
+		navbar: {
+			get() { return this.$store.state.device.navbar; },
+			set(value) { this.$store.commit('device/set', { key: 'navbar', value }); }
+		},
+
 		enableSounds: {
 			get() { return this.$store.state.device.enableSounds; },
 			set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
diff --git a/src/client/app/desktop/views/components/ui.sidebar.vue b/src/client/app/desktop/views/components/ui.sidebar.vue
new file mode 100644
index 000000000..1455421a9
--- /dev/null
+++ b/src/client/app/desktop/views/components/ui.sidebar.vue
@@ -0,0 +1,237 @@
+<template>
+<div class="header" :class="$store.state.device.navbar">
+	<div class="post">
+		<button @click="post" title="%i18n:@post%">%fa:pencil-alt%</button>
+	</div>
+
+	<div class="nav" v-if="$store.getters.isSignedIn">
+		<div class="home" :class="{ active: $route.name == 'index' }" @click="goToTop">
+			<router-link to="/">%fa:home%</router-link>
+		</div>
+		<div class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
+			<router-link to="/deck">%fa:columns%</router-link>
+		</div>
+		<div class="messaging">
+			<a @click="messaging">%fa:comments%<template v-if="hasUnreadMessagingMessage">%fa:circle%</template></a>
+		</div>
+		<div class="game">
+			<a @click="game">%fa:gamepad%<template v-if="hasGameInvitations">%fa:circle%</template></a>
+		</div>
+	</div>
+
+	<div class="nav bottom" v-if="$store.getters.isSignedIn">
+		<div>
+			<a @click="drive">%fa:cloud%</a>
+		</div>
+		<div>
+			<router-link to="/i/favorites">%fa:star%</router-link>
+		</div>
+		<div v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
+			<a @click="followRequests">%fa:envelope R%<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></a>
+		</div>
+		<div>
+			<a @click="settings">%fa:cog%</a>
+		</div>
+		<div>
+			<a @click="dark"><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></a>
+		</div>
+		<div class="signout">
+			<a @click="signout">%fa:power-off%</a>
+		</div>
+	</div>
+
+	<div class="account">
+		<router-link :to="`/@${ $store.state.i.username }`">
+			<mk-avatar class="avatar" :user="$store.state.i"/>
+		</router-link>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import MkUserListsWindow from './user-lists-window.vue';
+import MkFollowRequestsWindow from './received-follow-requests-window.vue';
+import MkSettingsWindow from './settings-window.vue';
+import MkDriveWindow from './drive-window.vue';
+import MkMessagingWindow from './messaging-window.vue';
+import MkGameWindow from './game-window.vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			hasGameInvitations: false,
+			connection: null
+		};
+	},
+
+	computed: {
+		hasUnreadMessagingMessage(): boolean {
+			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
+		}
+	},
+
+	mounted() {
+		if (this.$store.getters.isSignedIn) {
+			this.connection = (this as any).os.stream.useSharedConnection('main');
+
+			this.connection.on('reversiInvited', this.onReversiInvited);
+			this.connection.on('reversi_no_invites', this.onReversiNoInvites);
+		}
+	},
+
+	beforeDestroy() {
+		if (this.$store.getters.isSignedIn) {
+			this.connection.dispose();
+		}
+	},
+
+	methods: {
+		onReversiInvited() {
+			this.hasGameInvitations = true;
+		},
+
+		onReversiNoInvites() {
+			this.hasGameInvitations = false;
+		},
+
+		messaging() {
+			(this as any).os.new(MkMessagingWindow);
+		},
+
+		game() {
+			(this as any).os.new(MkGameWindow);
+		},
+
+		post() {
+			(this as any).apis.post();
+		},
+
+		drive() {
+			(this as any).os.new(MkDriveWindow);
+		},
+
+		list() {
+			const w = (this as any).os.new(MkUserListsWindow);
+			w.$once('choosen', list => {
+				this.$router.push(`i/lists/${ list.id }`);
+			});
+		},
+
+		followRequests() {
+			(this as any).os.new(MkFollowRequestsWindow);
+		},
+
+		settings() {
+			(this as any).os.new(MkSettingsWindow);
+		},
+
+		signout() {
+			(this as any).os.signout();
+		},
+
+		dark() {
+			this.$store.commit('device/set', {
+				key: 'darkmode',
+				value: !this.$store.state.device.darkmode
+			});
+		},
+
+		goToTop() {
+			window.scrollTo({
+				top: 0,
+				behavior: 'smooth'
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.header
+	$width = 68px
+
+	position fixed
+	top 0
+	z-index 1000
+	width $width
+	height 100%
+	background var(--desktopHeaderBg)
+	box-shadow var(--shadow)
+
+	&.left
+		left 0
+
+	&.right
+		right 0
+
+	> .nav
+		> *
+			> *
+				display block
+				width $width
+				line-height 56px
+				text-align center
+				font-size 18px
+				color var(--desktopHeaderFg)
+
+				&:hover
+					color var(--desktopHeaderHoverFg)
+					text-decoration none
+
+	> .nav.bottom
+		position absolute
+		bottom 64px
+		left 0
+
+	> .post
+		width $width
+		height $width
+		padding 10px
+
+		> button
+			display inline-block
+			margin 0
+			padding 0 10px
+			height 100%
+			width 100%
+			font-size 1.2em
+			font-weight normal
+			text-decoration none
+			color var(--primaryForeground)
+			background var(--primary) !important
+			outline none
+			border none
+			border-radius 4px
+			transition background 0.1s ease
+			cursor pointer
+
+			*
+				pointer-events none
+
+			&:hover
+				background var(--primaryLighten10) !important
+
+			&:active
+				background var(--primaryDarken10) !important
+				transition background 0s ease
+
+	> .account
+		position absolute
+		bottom 0
+		left 0
+		width $width
+		height $width
+		padding 12px
+
+		> *
+			display block
+			width 100%
+			height 100%
+
+			> .avatar
+				pointer-events none
+				width 100%
+				height 100%
+
+</style>
diff --git a/src/client/app/desktop/views/components/ui.vue b/src/client/app/desktop/views/components/ui.vue
index 2d1e98447..2aa259ab1 100644
--- a/src/client/app/desktop/views/components/ui.vue
+++ b/src/client/app/desktop/views/components/ui.vue
@@ -1,8 +1,9 @@
 <template>
 <div class="mk-ui" v-hotkey.global="keymap">
 	<div class="bg" v-if="$store.getters.isSignedIn && $store.state.i.wallpaperUrl" :style="style"></div>
-	<x-header class="header" v-show="!zenMode" ref="header"/>
-	<div class="content">
+	<x-header class="header" v-if="navbar == 'top'" v-show="!zenMode" ref="header"/>
+	<x-sidebar class="sidebar" v-if="navbar != 'top'" ref="sidebar"/>
+	<div class="content" :class="[{ sidebar: navbar != 'top' }, navbar]">
 		<slot></slot>
 	</div>
 	<mk-stream-indicator v-if="$store.getters.isSignedIn"/>
@@ -12,10 +13,12 @@
 <script lang="ts">
 import Vue from 'vue';
 import XHeader from './ui.header.vue';
+import XSidebar from './ui.sidebar.vue';
 
 export default Vue.extend({
 	components: {
-		XHeader
+		XHeader,
+		XSidebar
 	},
 
 	data() {
@@ -25,6 +28,10 @@ export default Vue.extend({
 	},
 
 	computed: {
+		navbar(): string {
+			return this.$store.state.device.navbar;
+		},
+
 		style(): any {
 			if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
 			return {
@@ -45,6 +52,12 @@ export default Vue.extend({
 	watch: {
 		'$store.state.uiHeaderHeight'() {
 			this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
+		},
+
+		navbar() {
+			if (this.navbar != 'top') {
+				this.$store.commit('setUiHeaderHeight', 0);
+			}
 		}
 	},
 
@@ -87,4 +100,10 @@ export default Vue.extend({
 		@media (max-width 1000px)
 			display none
 
+	> .content.sidebar.left
+		padding-left 64px
+
+	> .content.sidebar.right
+		padding-right 64px
+
 </style>
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 545261225..63365f7fb 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -56,6 +56,7 @@ const defaultDeviceSettings = {
 	loadRawImages: false,
 	alwaysShowNsfw: false,
 	postStyle: 'standard',
+	navbar: 'top',
 	mobileNotificationPosition: 'bottom'
 };