refactor: ページの追加を容易にするためにめっちゃ変えた

This commit is contained in:
kakkokari-gtyih 2024-08-12 17:06:34 +09:00
parent f84d9fdcc8
commit d5ea00846a
11 changed files with 259 additions and 137 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -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) {

View file

@ -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);

View file

@ -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;