mirror of
https://github.com/misskey-dev/misskey.git
synced 2025-01-01 07:06:21 +01:00
refactor: ページの追加を容易にするためにめっちゃ変えた
This commit is contained in:
parent
f84d9fdcc8
commit
d5ea00846a
11 changed files with 259 additions and 137 deletions
|
@ -39,6 +39,7 @@ import { i18n } from '@/i18n.js';
|
|||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import XUser from '@/components/MkTutorial.FollowUsers.UserCard.vue';
|
||||
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const pinnedUsers: Paging = {
|
||||
endpoint: 'pinned-users',
|
||||
|
@ -56,4 +57,8 @@ const popularUsers: Paging = {
|
|||
sort: '+follower',
|
||||
},
|
||||
};
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: true,
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -22,25 +22,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
|
||||
<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
|
||||
<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
|
||||
<div v-else><b :class="$style.actionWaitText">{{ i18n.ts._initialTutorial._reaction.reactToContinue }}</b></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { $i } from '@/account.js';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
phase: 'aboutNote' | 'howToReact';
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'reacted'): void;
|
||||
}>();
|
||||
|
||||
const exampleNote = reactive<Misskey.entities.Note>({
|
||||
id: '0000000000',
|
||||
createdAt: '2019-04-14T17:30:49.181Z',
|
||||
|
@ -74,11 +72,20 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
|||
replyId: null,
|
||||
renoteId: null,
|
||||
});
|
||||
|
||||
const onceReacted = ref<boolean>(false);
|
||||
|
||||
const canContinue = computed(() => {
|
||||
if (props.phase === 'aboutNote') {
|
||||
return true;
|
||||
} else if (props.phase === 'howToReact') {
|
||||
return onceReacted.value;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
function addReaction(emoji) {
|
||||
onceReacted.value = true;
|
||||
emit('reacted');
|
||||
exampleNote.reactions[emoji] = 1;
|
||||
exampleNote.myReaction = emoji;
|
||||
|
||||
|
@ -108,6 +115,10 @@ function removeReaction(emoji) {
|
|||
delete exampleNote.reactions[emoji];
|
||||
exampleNote.myReaction = undefined;
|
||||
}
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -127,4 +138,8 @@ function removeReaction(emoji) {
|
|||
margin: 0 auto;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.actionWaitText {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -42,6 +42,7 @@ import MkNote from '@/components/MkNote.vue';
|
|||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import MkFormSection from '@/components/form/section.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const exampleCWNote = reactive<Misskey.entities.Note>({
|
||||
id: '0000000000',
|
||||
|
@ -76,6 +77,10 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
|
|||
replyId: null,
|
||||
renoteId: null,
|
||||
});
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -30,6 +30,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
|||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
|
@ -48,6 +49,10 @@ watch([isLocked, publicReactions, hideOnlineStatus, noCrawle, preventAiLearning]
|
|||
preventAiLearning: !!preventAiLearning.value,
|
||||
});
|
||||
});
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -40,6 +40,7 @@ import MkInfo from '@/components/MkInfo.vue';
|
|||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import * as os from '@/os.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
|
@ -86,6 +87,10 @@ function setAvatar(ev: MouseEvent) {
|
|||
$i.avatarUrl = i.avatarUrl;
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@fileChangeSensitive="doSucceeded"
|
||||
></MkPostForm>
|
||||
<div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div>
|
||||
<div v-else><b :class="$style.actionWaitText">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.doItToContinue }}</b></div>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.previewNoteText }}</template>
|
||||
<MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote>
|
||||
|
@ -32,17 +33,13 @@ import MkFolder from '@/components/MkFolder.vue';
|
|||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'succeeded'): void;
|
||||
}>();
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
const onceSucceeded = ref<boolean>(false);
|
||||
|
||||
function doSucceeded(fileId: string, to: boolean) {
|
||||
if (fileId === exampleNote.fileIds?.[0] && to) {
|
||||
onceSucceeded.value = true;
|
||||
emit('succeeded');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,6 +84,9 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
|||
renoteId: null,
|
||||
});
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: onceSucceeded,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -143,4 +143,8 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
|||
position: relative;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.actionWaitText {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -27,6 +27,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script setup lang="ts">
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
|
||||
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||
|
||||
defineExpose<TutorialPageCommonExpose>({
|
||||
canContinue: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="showProgressbar" :class="$style.progressBar">
|
||||
<div :class="$style.progressBarValue" :style="{ width: `${(page / MAX_PAGE) * 100}%` }"></div>
|
||||
</div>
|
||||
<div v-if="showProgressbar && page !== 0 && page !== MAX_PAGE" :class="$style.progressText">{{ page }}/{{ MAX_PAGE - 1 }}</div>
|
||||
<div v-if="showProgressbar && page !== 0 && page !== MAX_PAGE" :class="$style.progressText">{{ page }}/{{ MAX_PAGE }}</div>
|
||||
<div :class="$style.tutorialMain">
|
||||
<Transition
|
||||
mode="out-in"
|
||||
|
@ -16,6 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
|
||||
@beforeLeave="areButtonsLocked = true"
|
||||
@afterEnter="areButtonsLocked = false"
|
||||
>
|
||||
<slot v-if="page === 0" key="tutorialPage_0" name="welcome" :close="() => emit('close', true)" :next="next">
|
||||
<div :class="$style.centerPage">
|
||||
|
@ -31,71 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkSpacer>
|
||||
</div>
|
||||
</slot>
|
||||
<div v-else-if="page === 1" key="tutorialPage_1" :class="$style.pageContainer">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<XProfileSettings/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="page === 2" key="tutorialPage_2" :class="$style.pageContainer">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<XNote phase="aboutNote"/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="page === 3" key="tutorialPage_3" :class="$style.pageContainer">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_gaps">
|
||||
<XNote phase="howToReact" @reacted="isReactionTutorialPushed = true"/>
|
||||
<b v-if="!isReactionTutorialPushed" :class="$style.actionWaitText">{{ i18n.ts._initialTutorial._reaction.reactToContinue }}</b>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="page === 4" key="tutorialPage_4" :class="$style.pageContainer">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<XTimeline/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="page === 5" key="tutorialPage_5" :class="$style.pageContainer">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<XFollowUsers/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="page === 6" key="tutorialPage_6" :class="$style.pageContainer">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<XPostNote/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="page === 7" key="tutorialPage_7" :class="$style.pageContainer">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_gaps">
|
||||
<XSensitive @succeeded="isSensitiveTutorialSucceeded = true"/>
|
||||
<b v-if="!isSensitiveTutorialSucceeded" :class="$style.actionWaitText">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.doItToContinue }}</b>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="page === 8" key="tutorialPage_8" :class="$style.pageContainer">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<div class="_gaps">
|
||||
<XPrivacySettings/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
<slot v-else-if="page === 9" key="tutorialPage_9" name="finish" :close="() => emit('close')" :prev="prev">
|
||||
<slot v-else-if="page === MAX_PAGE" :key="`tutorialPage_${MAX_PAGE}`" name="finish" :close="() => emit('close')" :prev="prev">
|
||||
<div :class="$style.centerPage">
|
||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
|
@ -110,18 +49,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div style="text-align: center;">{{ i18n.ts._initialTutorial._done.youCanReferTutorialBy }}</div>
|
||||
<div style="text-align: center;">{{ i18n.tsx._initialTutorial._done.haveFun({ name: instance.name ?? host }) }}</div>
|
||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
||||
<MkButton v-if="initialPage !== 4" rounded @click="prev"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton rounded @click="prev"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton rounded primary gradate @click="emit('close')">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</slot>
|
||||
<div v-else :key="`tutorialPage_${page}`" :class="$style.pageContainer">
|
||||
<div :class="$style.pageRoot">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
|
||||
<component
|
||||
:is="componentsDef[page - 1].component"
|
||||
ref="tutorialPageEl"
|
||||
v-bind="componentsDef[page - 1].props"
|
||||
/>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<div :class="[$style.pageFooter, { [$style.pageFooterShown]: (page > 0 && page < MAX_PAGE) }]">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton v-if="initialPage !== page" rounded @click="prev"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton v-if="initialPage !== page" :disabled="areButtonsLocked" rounded @click="prev"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||
<MkButton primary rounded gradate :disabled="!canContinue" data-cy-user-setup-continue @click="next">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -129,14 +79,76 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
// チュートリアルの枚数を増やしたら必ず変更すること!!
|
||||
export const MAX_PAGE = 9;
|
||||
/**
|
||||
* 【ページの足し方】
|
||||
*
|
||||
* 1. ページコンポーネントを作成
|
||||
* このとき、TutorialPageCommonExposeを実装すること
|
||||
* (canContinueを変化させることで、次へボタンが押されるのをブロックできます。ギミックがないページはtrueでOK。)
|
||||
* 2. tutorialBodyPagesDefにページのアイコン・タイトル・区分を追加
|
||||
* (区分がsetupの場合はwithSetup == falseのときにスキップされます)
|
||||
* 3. componentsDefにページのコンポーネントを追加(順番を対応させること)
|
||||
*/
|
||||
|
||||
/** チュートリアルページ用Expose */
|
||||
export type TutorialPageCommonExpose = {
|
||||
canContinue: boolean | Ref<boolean>;
|
||||
};
|
||||
|
||||
/** ページ メタデータ */
|
||||
export type TutorialPage = {
|
||||
icon?: string;
|
||||
type: 'tutorial' | 'setup';
|
||||
title: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* はじめと終わり以外のページ メタデータ
|
||||
*
|
||||
* (コンポーネントはsetup内で定義しています)
|
||||
*/
|
||||
export const tutorialBodyPagesDef = [{
|
||||
icon: 'ti ti-user',
|
||||
type: 'setup',
|
||||
title: i18n.ts._initialTutorial._profileSettings.title,
|
||||
}, {
|
||||
icon: 'ti ti-pencil',
|
||||
type: 'tutorial',
|
||||
title: i18n.ts._initialTutorial._note.title,
|
||||
}, {
|
||||
icon: 'ti ti-mood-smile',
|
||||
type: 'tutorial',
|
||||
title: i18n.ts._initialTutorial._reaction.title,
|
||||
}, {
|
||||
icon: 'ti ti-home',
|
||||
type: 'tutorial',
|
||||
title: i18n.ts._initialTutorial._timeline.title,
|
||||
}, {
|
||||
icon: 'ti ti-user-add',
|
||||
type: 'setup',
|
||||
title: i18n.ts.follow,
|
||||
}, {
|
||||
icon: 'ti ti-pencil-plus',
|
||||
type: 'tutorial',
|
||||
title: i18n.ts._initialTutorial._postNote.title,
|
||||
}, {
|
||||
icon: 'ti ti-eye-exclamation',
|
||||
type: 'tutorial',
|
||||
title: i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title,
|
||||
}, {
|
||||
icon: 'ti ti-lock',
|
||||
type: 'setup',
|
||||
title: i18n.ts._initialTutorial._privacySettings.title,
|
||||
}] as const satisfies TutorialPage[];
|
||||
|
||||
export const MAX_PAGE = tutorialBodyPagesDef.length + 1; // 0始まりにするために +2 - 1 = +1
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { ref, shallowRef, isRef, computed, watch } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import XProfileSettings from '@/components/MkTutorial.ProfileSettings.vue';
|
||||
import XNote from '@/components/MkTutorial.Note.vue';
|
||||
|
@ -146,11 +158,13 @@ import XPostNote from '@/components/MkTutorial.PostNote.vue';
|
|||
import XSensitive from '@/components/MkTutorial.Sensitive.vue';
|
||||
import XPrivacySettings from '@/components/MkTutorial.PrivacySettings.vue';
|
||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { host } from '@/config.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
|
||||
import type { Component } from 'vue';
|
||||
import type { Tuple } from '@/type.js';
|
||||
|
||||
const props = defineProps<{
|
||||
initialPage?: number;
|
||||
showProgressbar?: boolean;
|
||||
|
@ -166,35 +180,95 @@ const emit = defineEmits<{
|
|||
// テストの場合は全インタラクションをスキップする
|
||||
const isTest = (import.meta.env.MODE === 'test');
|
||||
|
||||
type ComponentDef = {
|
||||
component: Component;
|
||||
props?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* はじめと終わり以外のページ コンポーネント
|
||||
*
|
||||
* (メタデータは上の方で定義しています)
|
||||
*/
|
||||
const componentsDef: Tuple<ComponentDef, typeof tutorialBodyPagesDef.length> = [
|
||||
{ component: XProfileSettings },
|
||||
{ component: XNote, props: { phase: 'aboutNote' } },
|
||||
{ component: XNote, props: { phase: 'howToReact' } },
|
||||
{ component: XTimeline },
|
||||
{ component: XFollowUsers },
|
||||
{ component: XPostNote },
|
||||
{ component: XSensitive },
|
||||
{ component: XPrivacySettings },
|
||||
] as const satisfies ComponentDef[];
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||
const page = ref(props.initialPage ?? 0);
|
||||
|
||||
const currentPageDef = computed(() => {
|
||||
if (page.value > 0 && page.value < MAX_PAGE - 1) {
|
||||
return tutorialBodyPagesDef[page.value - 1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(page, (to) => {
|
||||
if (to === MAX_PAGE) {
|
||||
claimAchievement('tutorialCompleted');
|
||||
}
|
||||
});
|
||||
|
||||
const isReactionTutorialPushed = ref<boolean>(isTest);
|
||||
const isSensitiveTutorialSucceeded = ref<boolean>(isTest);
|
||||
// ページコンポーネントのexposeを受け取る
|
||||
const tutorialPageEl = shallowRef<TutorialPageCommonExpose | null>(null);
|
||||
|
||||
// トランジション中に連打されて進んじゃうのを防ぐ
|
||||
const areButtonsLocked = ref(false);
|
||||
|
||||
const canContinue = computed(() => {
|
||||
if (page.value === 3) {
|
||||
return isReactionTutorialPushed.value;
|
||||
} else if (page.value === 7) {
|
||||
return isSensitiveTutorialSucceeded.value;
|
||||
if (isTest) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (areButtonsLocked.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tutorialPageEl.value) {
|
||||
if (isRef(tutorialPageEl.value.canContinue)) {
|
||||
return tutorialPageEl.value.canContinue.value;
|
||||
} else {
|
||||
return tutorialPageEl.value.canContinue;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
function next() {
|
||||
if (page.value === 0 && !props.withSetup) {
|
||||
page.value += 2;
|
||||
} else if (page.value === 4 && !props.withSetup) {
|
||||
page.value += 2;
|
||||
} else if (page.value === 7 && !props.withSetup) {
|
||||
page.value += 2;
|
||||
if (areButtonsLocked.value) {
|
||||
return;
|
||||
} else {
|
||||
areButtonsLocked.value = true;
|
||||
}
|
||||
|
||||
const bodyPagesDefIndex = page.value - 1;
|
||||
|
||||
if (!props.withSetup && tutorialBodyPagesDef[bodyPagesDefIndex + 1].type === 'setup') {
|
||||
function findNextTutorialPage() {
|
||||
for (let i = bodyPagesDefIndex + 1; i < tutorialBodyPagesDef.length; i++) {
|
||||
if (tutorialBodyPagesDef[i] == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (tutorialBodyPagesDef[i].type === 'tutorial') {
|
||||
return i + 1; // はじめの1ページ分足す
|
||||
}
|
||||
}
|
||||
|
||||
return MAX_PAGE;
|
||||
}
|
||||
|
||||
page.value = findNextTutorialPage();
|
||||
} else {
|
||||
page.value++;
|
||||
}
|
||||
|
@ -203,18 +277,41 @@ function next() {
|
|||
}
|
||||
|
||||
function prev() {
|
||||
if (page.value === 2 && !props.withSetup) {
|
||||
page.value -= 2;
|
||||
} else if (page.value === 6 && !props.withSetup) {
|
||||
page.value -= 2;
|
||||
} else if (page.value === 9 && !props.withSetup) {
|
||||
page.value -= 2;
|
||||
if (areButtonsLocked.value) {
|
||||
return;
|
||||
} else {
|
||||
areButtonsLocked.value = true;
|
||||
}
|
||||
|
||||
const bodyPagesDefIndex = page.value - 1;
|
||||
|
||||
if (!props.withSetup && tutorialBodyPagesDef[bodyPagesDefIndex - 1].type === 'setup') {
|
||||
function findPrevTutorialPage() {
|
||||
for (let i = bodyPagesDefIndex - 1; i >= 0; i--) {
|
||||
if (tutorialBodyPagesDef[i] == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (tutorialBodyPagesDef[i].type === 'tutorial') {
|
||||
return i + 1; // はじめの1ページ分足す
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
page.value = findPrevTutorialPage();
|
||||
} else {
|
||||
page.value--;
|
||||
}
|
||||
|
||||
emit('pageChanged', page.value);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
page,
|
||||
currentPageDef,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -311,10 +408,6 @@ function prev() {
|
|||
margin-bottom: 56px;
|
||||
}
|
||||
|
||||
.actionWaitText {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.pageFooter {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
|
|
|
@ -11,17 +11,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@close="close(true)"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template v-if="page === 2" #header><i class="ti ti-pencil"></i> {{ i18n.ts._initialTutorial._note.title }}</template>
|
||||
<template v-else-if="page === 3" #header><i class="ti ti-mood-smile"></i> {{ i18n.ts._initialTutorial._reaction.title }}</template>
|
||||
<template v-else-if="page === 4" #header><i class="ti ti-home"></i> {{ i18n.ts._initialTutorial._timeline.title }}</template>
|
||||
<template v-else-if="page === 6" #header><i class="ti ti-pencil-plus"></i> {{ i18n.ts._initialTutorial._postNote.title }}</template>
|
||||
<template v-else-if="page === 7" #header><i class="ti ti-eye-exclamation"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</template>
|
||||
<template v-if="tutorialEl?.currentPageDef" #header>
|
||||
<i v-if="tutorialEl.currentPageDef.icon" :class="tutorialEl.currentPageDef.icon"></i>
|
||||
{{ tutorialEl.currentPageDef.title }}
|
||||
</template>
|
||||
<template v-else #header>{{ i18n.ts._initialTutorial.title }}</template>
|
||||
|
||||
<XTutorial
|
||||
ref="tutorialEl"
|
||||
:initialPage="initialPage"
|
||||
:skippable="true"
|
||||
@pageChanged="handlePageChange"
|
||||
@close="close"
|
||||
/>
|
||||
</MkModalWindow>
|
||||
|
@ -34,7 +33,7 @@ import XTutorial from '@/components/MkTutorial.vue';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
initialPage?: number;
|
||||
}>();
|
||||
|
||||
|
@ -44,12 +43,7 @@ const emit = defineEmits<{
|
|||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const page = ref(props.initialPage ?? 0);
|
||||
|
||||
function handlePageChange(to: number) {
|
||||
page.value = to;
|
||||
}
|
||||
const tutorialEl = shallowRef<InstanceType<typeof XTutorial>>();
|
||||
|
||||
async function close(skip?: boolean) {
|
||||
if (skip) {
|
||||
|
|
|
@ -7,17 +7,11 @@ 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.tutorialTitle, { [$style.showing]: (tutorialEl?.page !== 0) }]">
|
||||
<div :class="$style.text">
|
||||
<span v-if="page === 1"><i class="ti ti-user-edit"></i> {{ i18n.ts._initialTutorial._profileSettings.title }}</span>
|
||||
<span v-else-if="page === 2"><i class="ti ti-pencil"></i> {{ i18n.ts._initialTutorial._note.title }}</span>
|
||||
<span v-else-if="page === 3"><i class="ti ti-mood-smile"></i> {{ i18n.ts._initialTutorial._reaction.title }}</span>
|
||||
<span v-else-if="page === 4"><i class="ti ti-home"></i> {{ i18n.ts._initialTutorial._timeline.title }}</span>
|
||||
<span v-else-if="page === 5"><i class="ti ti-user-plus"></i> {{ i18n.ts.follow }}</span>
|
||||
<span v-else-if="page === 6"><i class="ti ti-pencil-plus"></i> {{ i18n.ts._initialTutorial._postNote.title }}</span>
|
||||
<span v-else-if="page === 7"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</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-if="tutorialEl?.currentPageDef">
|
||||
<i v-if="tutorialEl.currentPageDef.icon" :class="tutorialEl.currentPageDef.icon"></i> {{ tutorialEl.currentPageDef.title }}
|
||||
</span>
|
||||
<span v-else>{{ i18n.ts._initialTutorial.title }}</span>
|
||||
</div>
|
||||
<div v-if="instance.canSkipInitialTutorial" :class="$style.closeButton">
|
||||
|
@ -25,15 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<MkTutorial
|
||||
ref="tutorialEl"
|
||||
: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 -->
|
||||
<!-- 大規模サーバーの管理者さんへ: このスライドの内容をサーバー独自でアレンジすると良さそうなのでやってみてね -->
|
||||
<div ref="welcomePageRootEl" :class="$style.welcomePageRoot">
|
||||
<canvas ref="confettiEl" :class="$style.welcomePageConfetti"></canvas>
|
||||
<div
|
||||
|
@ -131,15 +123,11 @@ 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, { MAX_PAGE } from '@/components/MkTutorial.vue';
|
||||
import MkTutorial from '@/components/MkTutorial.vue';
|
||||
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
|
||||
const page = ref(0);
|
||||
|
||||
function pageChangeHandler(to: number) {
|
||||
page.value = to;
|
||||
}
|
||||
const tutorialEl = shallowRef<InstanceType<typeof MkTutorial> | null>(null);
|
||||
|
||||
// See: @/_boot_/common.ts L123 for details
|
||||
const query = new URLSearchParams(location.search);
|
||||
|
|
|
@ -6,3 +6,6 @@
|
|||
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||
|
||||
export type WithNonNullable<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]> };
|
||||
|
||||
type _TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N ? R : _TupleOf<T, N, [T, ...R]>;
|
||||
export type Tuple<T, N extends number> = N extends N ? number extends N ? T[] : _TupleOf<T, N, []> : never;
|
||||
|
|
Loading…
Reference in a new issue