From e83d28f96f13a8c2fdb97d64189e600c038da8ee Mon Sep 17 00:00:00 2001
From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Mon, 26 Feb 2024 18:08:37 +0900
Subject: [PATCH] =?UTF-8?q?=E3=82=AD=E3=83=A3=E3=83=B3=E3=82=BB=E3=83=AB?=
 =?UTF-8?q?=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92=E5=AE=9F=E8=A3=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../frontend/src/components/MkTutorial.vue    |  11 +-
 packages/frontend/src/pages/onboarding.vue    | 107 ++++++++++++++----
 2 files changed, 95 insertions(+), 23 deletions(-)

diff --git a/packages/frontend/src/components/MkTutorial.vue b/packages/frontend/src/components/MkTutorial.vue
index b7874926d4..b051dcffde 100644
--- a/packages/frontend/src/components/MkTutorial.vue
+++ b/packages/frontend/src/components/MkTutorial.vue
@@ -131,6 +131,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 </div>
 </template>
 
+<script lang="ts">
+
+// チュートリアルの枚数を増やしたら必ず変更すること!!
+export const MAX_PAGE = 9;
+
+</script>
+
 <script lang="ts" setup>
 import { ref, computed, watch } from 'vue';
 import MkButton from '@/components/MkButton.vue';
@@ -162,9 +169,6 @@ const emit = defineEmits<{
 // eslint-disable-next-line vue/no-setup-props-destructure
 const page = ref(props.initialPage ?? 0);
 
-// チュートリアルの枚数を増やしたら必ず変更すること!!
-const MAX_PAGE = 9;
-
 watch(page, (to) => {
 	if (to === MAX_PAGE) {
 		claimAchievement('tutorialCompleted');
@@ -211,6 +215,7 @@ function prev() {
 
 <style lang="scss" module>
 .tutorialRoot {
+	position: relative;
 	box-sizing: border-box;
 	overflow: hidden;
 	width: 100%;
diff --git a/packages/frontend/src/pages/onboarding.vue b/packages/frontend/src/pages/onboarding.vue
index 5bff51e9e8..6b3e0f1503 100644
--- a/packages/frontend/src/pages/onboarding.vue
+++ b/packages/frontend/src/pages/onboarding.vue
@@ -7,10 +7,29 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div :class="[$style.onboardingRoot, { [$style.ready]: animationPhase >= 1 }]">
 	<MkAnimBg :class="$style.onboardingBg"/>
 	<div :class="[$style.onboardingContainer]">
+		<div :class="[$style.tutorialTitle, { [$style.showing]: (page !== 0) }]">
+			<div :class="$style.text">
+				<span v-if="page === 1"><i class="ti ti-pencil"></i> {{ i18n.ts._initialTutorial._note.title }}</span>
+				<span v-else-if="page === 2"><i class="ti ti-mood-smile"></i> {{ i18n.ts._initialTutorial._reaction.title }}</span>
+				<span v-else-if="page === 3"><i class="ti ti-home"></i> {{ i18n.ts._initialTutorial._timeline.title }}</span>
+				<span v-else-if="page === 4"><i class="ti ti-user-plus"></i> {{ i18n.ts.follow }}</span>
+				<span v-else-if="page === 5"><i class="ti ti-pencil-plus"></i> {{ i18n.ts._initialTutorial._postNote.title }}</span>
+				<span v-else-if="page === 6"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</span>
+				<span v-else-if="page === 7"><i class="ti ti-bell"></i> {{ i18n.ts.pushNotification }}</span>
+				<span v-else-if="page === 8"><i class="ti ti-lock"></i> {{ i18n.ts.privacy }}</span>
+				<span v-else-if="page === MAX_PAGE"><!-- なんもなし --></span>
+				<span v-else>{{ i18n.ts._initialTutorial.title }}</span>
+			</div>
+			<div v-if="instance.canSkipInitialTutorial" :class="$style.closeButton">
+				<button class="_button" @click="cancel"><i class="ti ti-x"></i></button>
+			</div>
+		</div>
 		<MkTutorial
+			:class="$style.tutorialRoot"
 			:showProgressbar="true"
 			:skippable="false"
 			:withSetup="true"
+			@pageChanged="pageChangeHandler"
 		>
 			<template #welcome="{ next }">
 				<!-- Tips for large-scale server admins: you should customize this slide for better branding -->
@@ -36,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 								</div>
 								<div>{{ i18n.tsx._initialTutorial._onboardingLanding.description({ name: instance.name ?? host }) }}</div>
 								<MkButton large primary rounded gradate style="margin: 16px auto 0;" @click="next">{{ i18n.ts.start }} <i class="ti ti-arrow-right"></i></MkButton>
-								<MkButton v-if="instance.canSkipInitialTutorial" transparent rounded style="margin: 0 auto;" @click="cancel">{{ i18n.ts.cancel }}</MkButton>
+								<MkButton v-if="instance.canSkipInitialTutorial" transparent rounded style="margin: 0 auto;" @click="cancel">{{ i18n.ts.later }}</MkButton>
 								<MkInfo style="width: fit-content; margin: 0 auto; text-align: start; white-space: pre-wrap;">{{ i18n.tsx._initialTutorial._onboardingLanding.takesAbout({ min: 3 }) }}</MkInfo>
 							</div>
 						</MkSpacer>
@@ -111,15 +130,41 @@ import { confirm as osConfirm } from '@/os.js';
 import MkAnimBg from '@/components/MkAnimBg.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import MkTutorial from '@/components/MkTutorial.vue';
+import MkTutorial, { MAX_PAGE } from '@/components/MkTutorial.vue';
 
 import FormLink from '@/components/form/link.vue';
 
+const page = ref(0);
+
+function pageChangeHandler(to: number) {
+	page.value = to;
+}
+
+// See: @/_boot_/common.ts L123 for details
+const query = new URLSearchParams(location.search);
+const originalPath = query.get('redirected_from');
+
+async function cancel() {
+	const confirm = await osConfirm({
+		type: 'question',
+		title: i18n.ts._initialTutorial.skipAreYouSure,
+		text: i18n.ts._initialTutorial._done.youCanReferTutorialBy,
+		okText: i18n.ts.yes,
+		cancelText: i18n.ts.no,
+	});
+
+	if (confirm.canceled) return;
+
+	location.href = '/';
+}
+
+// #region デフォルトオープニング画面のアニメーション
 const confettiEl = shallowRef<HTMLCanvasElement | null>(null);
 const welcomePageRootEl = shallowRef<HTMLDivElement | null>(null);
 const instanceIconEl = shallowRef<HTMLImageElement | null>(null);
 const instanceIconY = ref(0);
-const instanceIconYPx = computed(() => `${instanceIconY.value - 30}px`);
+// 30pxは文字が上がってくる距離、40pxは上部ヘッダの高さ
+const instanceIconYPx = computed(() => `${instanceIconY.value - 30 + 40}px`);
 
 /**
  * 0 … なにもしない
@@ -130,23 +175,6 @@ const instanceIconYPx = computed(() => `${instanceIconY.value - 30}px`);
  */
 const animationPhase = ref(0);
 
-// See: @/_boot_/common.ts L123 for details
-const query = new URLSearchParams(location.search);
-const originalPath = query.get('redirected_from');
-
-async function cancel() {
-	const confirm = await osConfirm({
-		type: 'question',
-		text: i18n.ts._initialTutorial.skipAreYouSure,
-		okText: i18n.ts.yes,
-		cancelText: i18n.ts.no,
-	});
-
-	if (confirm.canceled) return;
-
-	location.href = '/';
-}
-
 // 画面上部に表示されるアイコンの中心Y座標を取得
 function getIconY(instanceIconEl: HTMLImageElement, welcomePageRootEl: HTMLDivElement) {
 	const instanceIconElRect = instanceIconEl.getBoundingClientRect();
@@ -201,6 +229,8 @@ onMounted(() => {
 	});
 });
 
+// #endregion
+
 definePageMetadata(() => ({
 	title: 'Onboarding',
 	description: 'Welcome to Misskey!',
@@ -240,6 +270,43 @@ definePageMetadata(() => ({
 	container-type: inline-size;
 }
 
+.tutorialTitle {
+	position: absolute;
+	box-sizing: border-box;
+	top: 0;
+	left: 0;
+	width: 100%;
+	font-size: 14px;
+	line-height: 40px;
+	height: 40px;
+	padding: 0 var(--margin);
+	background: var(--panelHighlight);
+	display: flex;
+	transition: transform 0.5s ease;
+	transform: translateY(-100%);
+
+	&.showing {
+		transform: translateY(0);
+	}
+
+	.text {
+		font-weight: 700;
+	}
+
+	.closeButton {
+		margin-left: auto;
+
+		>._button {
+			padding: 8px;
+		}
+	}
+}
+
+.tutorialRoot {
+	margin-top: 40px;
+	height: calc(100% - 40px);
+}
+
 .ready {
 	& .onboardingBg {
 		opacity: 1;