mirror of
https://github.com/misskey-dev/misskey.git
synced 2025-01-19 13:33:27 +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 MkFolder from '@/components/MkFolder.vue';
|
||||||
import XUser from '@/components/MkTutorial.FollowUsers.UserCard.vue';
|
import XUser from '@/components/MkTutorial.FollowUsers.UserCard.vue';
|
||||||
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||||
|
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||||
|
|
||||||
const pinnedUsers: Paging = {
|
const pinnedUsers: Paging = {
|
||||||
endpoint: 'pinned-users',
|
endpoint: 'pinned-users',
|
||||||
|
@ -56,4 +57,8 @@ const popularUsers: Paging = {
|
||||||
sort: '+follower',
|
sort: '+follower',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
defineExpose<TutorialPageCommonExpose>({
|
||||||
|
canContinue: true,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -22,25 +22,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
|
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
|
||||||
<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
|
<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-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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { ref, reactive } from 'vue';
|
import { ref, reactive, computed } from 'vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { globalEvents } from '@/events.js';
|
import { globalEvents } from '@/events.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import MkNote from '@/components/MkNote.vue';
|
import MkNote from '@/components/MkNote.vue';
|
||||||
|
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
phase: 'aboutNote' | 'howToReact';
|
phase: 'aboutNote' | 'howToReact';
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: 'reacted'): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const exampleNote = reactive<Misskey.entities.Note>({
|
const exampleNote = reactive<Misskey.entities.Note>({
|
||||||
id: '0000000000',
|
id: '0000000000',
|
||||||
createdAt: '2019-04-14T17:30:49.181Z',
|
createdAt: '2019-04-14T17:30:49.181Z',
|
||||||
|
@ -74,11 +72,20 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
||||||
replyId: null,
|
replyId: null,
|
||||||
renoteId: null,
|
renoteId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onceReacted = ref<boolean>(false);
|
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) {
|
function addReaction(emoji) {
|
||||||
onceReacted.value = true;
|
onceReacted.value = true;
|
||||||
emit('reacted');
|
|
||||||
exampleNote.reactions[emoji] = 1;
|
exampleNote.reactions[emoji] = 1;
|
||||||
exampleNote.myReaction = emoji;
|
exampleNote.myReaction = emoji;
|
||||||
|
|
||||||
|
@ -108,6 +115,10 @@ function removeReaction(emoji) {
|
||||||
delete exampleNote.reactions[emoji];
|
delete exampleNote.reactions[emoji];
|
||||||
exampleNote.myReaction = undefined;
|
exampleNote.myReaction = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose<TutorialPageCommonExpose>({
|
||||||
|
canContinue,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -127,4 +138,8 @@ function removeReaction(emoji) {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionWaitText {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -42,6 +42,7 @@ import MkNote from '@/components/MkNote.vue';
|
||||||
import MkPostForm from '@/components/MkPostForm.vue';
|
import MkPostForm from '@/components/MkPostForm.vue';
|
||||||
import MkFormSection from '@/components/form/section.vue';
|
import MkFormSection from '@/components/form/section.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||||
|
|
||||||
const exampleCWNote = reactive<Misskey.entities.Note>({
|
const exampleCWNote = reactive<Misskey.entities.Note>({
|
||||||
id: '0000000000',
|
id: '0000000000',
|
||||||
|
@ -76,6 +77,10 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
|
||||||
replyId: null,
|
replyId: null,
|
||||||
renoteId: null,
|
renoteId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineExpose<TutorialPageCommonExpose>({
|
||||||
|
canContinue: true,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -30,6 +30,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { signinRequired } from '@/account.js';
|
import { signinRequired } from '@/account.js';
|
||||||
|
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
|
@ -48,6 +49,10 @@ watch([isLocked, publicReactions, hideOnlineStatus, noCrawle, preventAiLearning]
|
||||||
preventAiLearning: !!preventAiLearning.value,
|
preventAiLearning: !!preventAiLearning.value,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineExpose<TutorialPageCommonExpose>({
|
||||||
|
canContinue: true,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -40,6 +40,7 @@ import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { selectFile } from '@/scripts/select-file.js';
|
import { selectFile } from '@/scripts/select-file.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { signinRequired } from '@/account.js';
|
import { signinRequired } from '@/account.js';
|
||||||
|
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
|
@ -86,6 +87,10 @@ function setAvatar(ev: MouseEvent) {
|
||||||
$i.avatarUrl = i.avatarUrl;
|
$i.avatarUrl = i.avatarUrl;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose<TutorialPageCommonExpose>({
|
||||||
|
canContinue: true,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@fileChangeSensitive="doSucceeded"
|
@fileChangeSensitive="doSucceeded"
|
||||||
></MkPostForm>
|
></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-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>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.previewNoteText }}</template>
|
<template #label>{{ i18n.ts.previewNoteText }}</template>
|
||||||
<MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote>
|
<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 MkInfo from '@/components/MkInfo.vue';
|
||||||
import MkNote from '@/components/MkNote.vue';
|
import MkNote from '@/components/MkNote.vue';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: 'succeeded'): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const onceSucceeded = ref<boolean>(false);
|
const onceSucceeded = ref<boolean>(false);
|
||||||
|
|
||||||
function doSucceeded(fileId: string, to: boolean) {
|
function doSucceeded(fileId: string, to: boolean) {
|
||||||
if (fileId === exampleNote.fileIds?.[0] && to) {
|
if (fileId === exampleNote.fileIds?.[0] && to) {
|
||||||
onceSucceeded.value = true;
|
onceSucceeded.value = true;
|
||||||
emit('succeeded');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +84,9 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
||||||
renoteId: null,
|
renoteId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineExpose<TutorialPageCommonExpose>({
|
||||||
|
canContinue: onceSucceeded,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -143,4 +143,8 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionWaitText {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -27,6 +27,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
|
import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
|
||||||
|
import type { TutorialPageCommonExpose } from '@/components/MkTutorial.vue';
|
||||||
|
|
||||||
|
defineExpose<TutorialPageCommonExpose>({
|
||||||
|
canContinue: true,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="showProgressbar" :class="$style.progressBar">
|
<div v-if="showProgressbar" :class="$style.progressBar">
|
||||||
<div :class="$style.progressBarValue" :style="{ width: `${(page / MAX_PAGE) * 100}%` }"></div>
|
<div :class="$style.progressBarValue" :style="{ width: `${(page / MAX_PAGE) * 100}%` }"></div>
|
||||||
</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">
|
<div :class="$style.tutorialMain">
|
||||||
<Transition
|
<Transition
|
||||||
mode="out-in"
|
mode="out-in"
|
||||||
|
@ -16,6 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||||
:enterFromClass="$style.transition_x_enterFrom"
|
:enterFromClass="$style.transition_x_enterFrom"
|
||||||
:leaveToClass="$style.transition_x_leaveTo"
|
: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">
|
<slot v-if="page === 0" key="tutorialPage_0" name="welcome" :close="() => emit('close', true)" :next="next">
|
||||||
<div :class="$style.centerPage">
|
<div :class="$style.centerPage">
|
||||||
|
@ -31,71 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
<div v-else-if="page === 1" key="tutorialPage_1" :class="$style.pageContainer">
|
<slot v-else-if="page === MAX_PAGE" :key="`tutorialPage_${MAX_PAGE}`" name="finish" :close="() => emit('close')" :prev="prev">
|
||||||
<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">
|
|
||||||
<div :class="$style.centerPage">
|
<div :class="$style.centerPage">
|
||||||
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
|
||||||
<MkSpacer :marginMin="20" :marginMax="28">
|
<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.ts._initialTutorial._done.youCanReferTutorialBy }}</div>
|
||||||
<div style="text-align: center;">{{ i18n.tsx._initialTutorial._done.haveFun({ name: instance.name ?? host }) }}</div>
|
<div style="text-align: center;">{{ i18n.tsx._initialTutorial._done.haveFun({ name: instance.name ?? host }) }}</div>
|
||||||
<div class="_buttonsCenter" style="margin-top: 16px;">
|
<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>
|
<MkButton rounded primary gradate @click="emit('close')">{{ i18n.ts.close }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</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>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
<div :class="[$style.pageFooter, { [$style.pageFooterShown]: (page > 0 && page < MAX_PAGE) }]">
|
<div :class="[$style.pageFooter, { [$style.pageFooterShown]: (page > 0 && page < MAX_PAGE) }]">
|
||||||
<div class="_buttonsCenter">
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,14 +79,76 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 MkButton from '@/components/MkButton.vue';
|
||||||
import XProfileSettings from '@/components/MkTutorial.ProfileSettings.vue';
|
import XProfileSettings from '@/components/MkTutorial.ProfileSettings.vue';
|
||||||
import XNote from '@/components/MkTutorial.Note.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 XSensitive from '@/components/MkTutorial.Sensitive.vue';
|
||||||
import XPrivacySettings from '@/components/MkTutorial.PrivacySettings.vue';
|
import XPrivacySettings from '@/components/MkTutorial.PrivacySettings.vue';
|
||||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { host } from '@/config.js';
|
import { host } from '@/config.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
|
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
import type { Tuple } from '@/type.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialPage?: number;
|
initialPage?: number;
|
||||||
showProgressbar?: boolean;
|
showProgressbar?: boolean;
|
||||||
|
@ -166,35 +180,95 @@ const emit = defineEmits<{
|
||||||
// テストの場合は全インタラクションをスキップする
|
// テストの場合は全インタラクションをスキップする
|
||||||
const isTest = (import.meta.env.MODE === 'test');
|
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
|
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||||
const page = ref(props.initialPage ?? 0);
|
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) => {
|
watch(page, (to) => {
|
||||||
if (to === MAX_PAGE) {
|
if (to === MAX_PAGE) {
|
||||||
claimAchievement('tutorialCompleted');
|
claimAchievement('tutorialCompleted');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const isReactionTutorialPushed = ref<boolean>(isTest);
|
// ページコンポーネントのexposeを受け取る
|
||||||
const isSensitiveTutorialSucceeded = ref<boolean>(isTest);
|
const tutorialPageEl = shallowRef<TutorialPageCommonExpose | null>(null);
|
||||||
|
|
||||||
|
// トランジション中に連打されて進んじゃうのを防ぐ
|
||||||
|
const areButtonsLocked = ref(false);
|
||||||
|
|
||||||
const canContinue = computed(() => {
|
const canContinue = computed(() => {
|
||||||
if (page.value === 3) {
|
if (isTest) {
|
||||||
return isReactionTutorialPushed.value;
|
return true;
|
||||||
} else if (page.value === 7) {
|
}
|
||||||
return isSensitiveTutorialSucceeded.value;
|
|
||||||
|
if (areButtonsLocked.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tutorialPageEl.value) {
|
||||||
|
if (isRef(tutorialPageEl.value.canContinue)) {
|
||||||
|
return tutorialPageEl.value.canContinue.value;
|
||||||
|
} else {
|
||||||
|
return tutorialPageEl.value.canContinue;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function next() {
|
function next() {
|
||||||
if (page.value === 0 && !props.withSetup) {
|
if (areButtonsLocked.value) {
|
||||||
page.value += 2;
|
return;
|
||||||
} else if (page.value === 4 && !props.withSetup) {
|
} else {
|
||||||
page.value += 2;
|
areButtonsLocked.value = true;
|
||||||
} else if (page.value === 7 && !props.withSetup) {
|
}
|
||||||
page.value += 2;
|
|
||||||
|
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 {
|
} else {
|
||||||
page.value++;
|
page.value++;
|
||||||
}
|
}
|
||||||
|
@ -203,18 +277,41 @@ function next() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function prev() {
|
function prev() {
|
||||||
if (page.value === 2 && !props.withSetup) {
|
if (areButtonsLocked.value) {
|
||||||
page.value -= 2;
|
return;
|
||||||
} else if (page.value === 6 && !props.withSetup) {
|
} else {
|
||||||
page.value -= 2;
|
areButtonsLocked.value = true;
|
||||||
} else if (page.value === 9 && !props.withSetup) {
|
}
|
||||||
page.value -= 2;
|
|
||||||
|
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 {
|
} else {
|
||||||
page.value--;
|
page.value--;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('pageChanged', page.value);
|
emit('pageChanged', page.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
page,
|
||||||
|
currentPageDef,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -311,10 +408,6 @@ function prev() {
|
||||||
margin-bottom: 56px;
|
margin-bottom: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionWaitText {
|
|
||||||
color: var(--error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pageFooter {
|
.pageFooter {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
|
@ -11,17 +11,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@close="close(true)"
|
@close="close(true)"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template v-if="page === 2" #header><i class="ti ti-pencil"></i> {{ i18n.ts._initialTutorial._note.title }}</template>
|
<template v-if="tutorialEl?.currentPageDef" #header>
|
||||||
<template v-else-if="page === 3" #header><i class="ti ti-mood-smile"></i> {{ i18n.ts._initialTutorial._reaction.title }}</template>
|
<i v-if="tutorialEl.currentPageDef.icon" :class="tutorialEl.currentPageDef.icon"></i>
|
||||||
<template v-else-if="page === 4" #header><i class="ti ti-home"></i> {{ i18n.ts._initialTutorial._timeline.title }}</template>
|
{{ tutorialEl.currentPageDef.title }}
|
||||||
<template v-else-if="page === 6" #header><i class="ti ti-pencil-plus"></i> {{ i18n.ts._initialTutorial._postNote.title }}</template>
|
</template>
|
||||||
<template v-else-if="page === 7" #header><i class="ti ti-eye-exclamation"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</template>
|
|
||||||
<template v-else #header>{{ i18n.ts._initialTutorial.title }}</template>
|
<template v-else #header>{{ i18n.ts._initialTutorial.title }}</template>
|
||||||
|
|
||||||
<XTutorial
|
<XTutorial
|
||||||
|
ref="tutorialEl"
|
||||||
:initialPage="initialPage"
|
:initialPage="initialPage"
|
||||||
:skippable="true"
|
:skippable="true"
|
||||||
@pageChanged="handlePageChange"
|
|
||||||
@close="close"
|
@close="close"
|
||||||
/>
|
/>
|
||||||
</MkModalWindow>
|
</MkModalWindow>
|
||||||
|
@ -34,7 +33,7 @@ import XTutorial from '@/components/MkTutorial.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
initialPage?: number;
|
initialPage?: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -44,12 +43,7 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
|
||||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
const tutorialEl = shallowRef<InstanceType<typeof XTutorial>>();
|
||||||
const page = ref(props.initialPage ?? 0);
|
|
||||||
|
|
||||||
function handlePageChange(to: number) {
|
|
||||||
page.value = to;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function close(skip?: boolean) {
|
async function close(skip?: boolean) {
|
||||||
if (skip) {
|
if (skip) {
|
||||||
|
|
|
@ -7,17 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="[$style.onboardingRoot, { [$style.ready]: animationPhase >= 1 }]">
|
<div :class="[$style.onboardingRoot, { [$style.ready]: animationPhase >= 1 }]">
|
||||||
<MkAnimBg :class="$style.onboardingBg"/>
|
<MkAnimBg :class="$style.onboardingBg"/>
|
||||||
<div :class="[$style.onboardingContainer]">
|
<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">
|
<div :class="$style.text">
|
||||||
<span v-if="page === 1"><i class="ti ti-user-edit"></i> {{ i18n.ts._initialTutorial._profileSettings.title }}</span>
|
<span v-if="tutorialEl?.currentPageDef">
|
||||||
<span v-else-if="page === 2"><i class="ti ti-pencil"></i> {{ i18n.ts._initialTutorial._note.title }}</span>
|
<i v-if="tutorialEl.currentPageDef.icon" :class="tutorialEl.currentPageDef.icon"></i> {{ tutorialEl.currentPageDef.title }}
|
||||||
<span v-else-if="page === 3"><i class="ti ti-mood-smile"></i> {{ i18n.ts._initialTutorial._reaction.title }}</span>
|
</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-else>{{ i18n.ts._initialTutorial.title }}</span>
|
<span v-else>{{ i18n.ts._initialTutorial.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="instance.canSkipInitialTutorial" :class="$style.closeButton">
|
<div v-if="instance.canSkipInitialTutorial" :class="$style.closeButton">
|
||||||
|
@ -25,15 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkTutorial
|
<MkTutorial
|
||||||
|
ref="tutorialEl"
|
||||||
:class="$style.tutorialRoot"
|
:class="$style.tutorialRoot"
|
||||||
:showProgressbar="true"
|
:showProgressbar="true"
|
||||||
:skippable="false"
|
:skippable="false"
|
||||||
:withSetup="true"
|
:withSetup="true"
|
||||||
@pageChanged="pageChangeHandler"
|
|
||||||
>
|
>
|
||||||
<template #welcome="{ next }">
|
<template #welcome="{ next }">
|
||||||
<!-- Tips for large-scale server admins: you should customize this slide for better branding -->
|
|
||||||
<!-- 大規模サーバーの管理者さんへ: このスライドの内容をサーバー独自でアレンジすると良さそうなのでやってみてね -->
|
|
||||||
<div ref="welcomePageRootEl" :class="$style.welcomePageRoot">
|
<div ref="welcomePageRootEl" :class="$style.welcomePageRoot">
|
||||||
<canvas ref="confettiEl" :class="$style.welcomePageConfetti"></canvas>
|
<canvas ref="confettiEl" :class="$style.welcomePageConfetti"></canvas>
|
||||||
<div
|
<div
|
||||||
|
@ -131,15 +123,11 @@ import { confirm as osConfirm } from '@/os.js';
|
||||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInfo from '@/components/MkInfo.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';
|
import FormLink from '@/components/form/link.vue';
|
||||||
|
|
||||||
const page = ref(0);
|
const tutorialEl = shallowRef<InstanceType<typeof MkTutorial> | null>(null);
|
||||||
|
|
||||||
function pageChangeHandler(to: number) {
|
|
||||||
page.value = to;
|
|
||||||
}
|
|
||||||
|
|
||||||
// See: @/_boot_/common.ts L123 for details
|
// See: @/_boot_/common.ts L123 for details
|
||||||
const query = new URLSearchParams(location.search);
|
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 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]> };
|
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