From 21b10603fea5a709535ec1450a05f68314701c45 Mon Sep 17 00:00:00 2001
From: Ekke <ekke@ekke.jp>
Date: Mon, 20 Mar 2023 20:21:54 +0900
Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E3=83=8A=E3=83=93=E3=82=B2?=
 =?UTF-8?q?=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=90=E3=83=BC=E3=81=AE?=
 =?UTF-8?q?=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=9E=E3=82=A4=E3=82=BA=E3=82=92?=
 =?UTF-8?q?=E3=83=89=E3=83=A9=E3=83=83=E3=82=B0&=E3=83=89=E3=83=AD?=
 =?UTF-8?q?=E3=83=83=E3=83=97=E3=81=A7=E8=A1=8C=E3=81=88=E3=82=8B=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B=20(#10356)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat(frontend): ナビゲーションバーのカスタマイズをドラッグ&ドロップで行えるようにする

* eslintのエラーを修正

* ハンドルをつかんでドラッグするように変更

* eslintのエラーを修正

* デザインの軽微な変更

* 修正

* Update CHANGELOG.md

* Update CHANGELOG.md

* ドラッグハンドルを3本線から2本線に

---------

Co-authored-by: root <root@Adam-Windows>
---
 CHANGELOG.md                                  |   3 +
 .../frontend/src/pages/settings/navbar.vue    | 131 +++++++++++++++---
 2 files changed, 117 insertions(+), 17 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e2ea1102..fc016c464 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,9 @@
 - APオブジェクトを入力してフェッチする機能とユーザーやノートの検索機能を分離
 - ナビゲーションバーの項目に「プロフィール」を追加できるように
 - AiScriptを0.13.1に更新
+- ナビゲーションバーのカスタマイズをドラッグ&ドロップで行えるように
+
+### Bugfixes
 - oEmbedをサポートしているウェブサイトのプレビューができるように
 	- YouTubeをoEmbedでロードし、プレビューで共有ボタンを押すとOSの共有画面がでるように
 	- ([FirefoxでSpotifyのプレビューを開けるとフルサイズじゃなくプレビューサイズだけ再生できる問題](https://bugzilla.mozilla.org/show_bug.cgi?id=1792395)があります)
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index ead551e7c..dc1347726 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -1,9 +1,34 @@
 <template>
 <div class="_gaps_m">
-	<MkTextarea v-model="items" tall manual-save>
+	<FormSlot>
 		<template #label>{{ i18n.ts.navbar }}</template>
-		<template #caption><button class="_textButton" @click="addItem">{{ i18n.ts.addItem }}</button></template>
-	</MkTextarea>
+		<MkContainer :show-header="false">
+			<Sortable 
+				v-model="items"
+				:animation="150"
+				class="navbar_items"
+				handle=".item_handle"
+				@start="e=>e.item.classList.add('active')"
+				@end="e=>e.item.classList.remove('active')"
+			>
+				<template #item="{element,index}">
+					<div
+						v-if="element === '-' || navbarItemDef[element]"
+						class="item"
+					>
+						<button class="item_handle _button" ><i class="ti ti-menu"></i></button>
+						<i class="icon ti-fw" :class="navbarItemDef[element]?.icon"></i><span class="text">{{ navbarItemDef[element]?.title ?? i18n.ts.divider }}</span>
+						<button class="navbar_item_remove _button" @click="removeItem(index)"><i class="ti ti-trash"></i></button>
+					</div>
+				</template>
+			</Sortable>
+		</MkContainer>
+	</FormSlot>
+	<div class="_buttons">
+		<MkButton @click="addItem">{{ i18n.ts.addItem }}</MkButton>
+		<MkButton danger @click="reset"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
+		<MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+	</div>
 
 	<MkRadios v-model="menuDisplay">
 		<template #label>{{ i18n.ts.display }}</template>
@@ -12,26 +37,27 @@
 		<option value="top">{{ i18n.ts._menuDisplay.top }}</option>
 		<!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ i18n.ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
 	</MkRadios>
-
-	<MkButton danger @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { computed, ref, watch } from 'vue';
-import MkTextarea from '@/components/MkTextarea.vue';
+import { computed, defineAsyncComponent, ref, watch } from 'vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkButton from '@/components/MkButton.vue';
+import FormSlot from '@/components/form/slot.vue';
+import MkContainer from '@/components/MkContainer.vue';
 import * as os from '@/os';
 import { navbarItemDef } from '@/navbar';
 import { defaultStore } from '@/store';
 import { unisonReload } from '@/scripts/unison-reload';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { deepClone } from '@/scripts/clone';
 
-const items = ref(defaultStore.state.menu.join('\n'));
+const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
+
+const items = ref(deepClone(defaultStore.state.menu));
 
-const split = computed(() => items.value.trim().split('\n').filter(x => x.trim() !== ''));
 const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
 
 async function reloadAsk() {
@@ -55,23 +81,22 @@ async function addItem() {
 		}],
 	});
 	if (canceled) return;
-	items.value = [...split.value, item].join('\n');
+	items.value = [...items.value, item];
+}
+
+function removeItem(index: number) {
+	items.value.splice(index, 1);
 }
 
 async function save() {
-	defaultStore.set('menu', split.value);
+	defaultStore.set('menu', items.value);
 	await reloadAsk();
 }
 
 function reset() {
-	defaultStore.reset('menu');
-	items.value = defaultStore.state.menu.join('\n');
+	items.value = defaultStore.def.menu.default;
 }
 
-watch(items, async () => {
-	await save();
-});
-
 watch(menuDisplay, async () => {
 	await reloadAsk();
 });
@@ -85,3 +110,75 @@ definePageMetadata({
 	icon: 'ti ti-list',
 });
 </script>
+<style lang="scss">
+.navbar_items {
+	flex: 1;
+
+	.item {
+		position: relative;
+		display: block;
+		line-height: 2.85rem;
+		text-overflow: ellipsis;
+		overflow: hidden;
+		white-space: nowrap;
+		width: 100%;
+		text-align: left;
+		box-sizing: border-box;
+		color: var(--navFg);
+
+		.icon {
+			position: relative;
+			width: 32px;
+			margin-right: 8px;
+		}
+
+		.text {
+			position: relative;
+			font-size: 0.9em;
+		}
+
+		.navbar_item_remove {
+			position: absolute;
+			z-index: 10000;
+			width: 32px;
+			height: 32px;
+			color: #ff2a2a;
+			right: 8px;
+			opacity: 0.8;
+		}
+
+		.item_handle{
+			cursor: move;
+			width: 32px;
+			height: 32px;
+			margin: 0 8px;
+			opacity: 0.5;
+		}
+
+		&.active {
+			text-decoration: none;
+			color: var(--accent);
+
+			&:before {
+				content: "";
+				display: block;
+				height: 100%;
+				width: 100%;
+				aspect-ratio: 1;
+				margin: auto;
+				position: absolute;
+				top: 0;
+				left: 0;
+				right: 0;
+				bottom: 0;
+				border-radius: 999px;
+				background: var(--accentedBg);
+			}
+
+			> .icon, > .text {
+				opacity: 1;
+			}
+		}
+	}
+}
+</style>