215 lines
7.0 KiB
Vue
215 lines
7.0 KiB
Vue
<template lang="pug">
|
|
v-container.page-container-center.fill-height
|
|
.d-flex.flex-column.justify-center.align-center.fill-height(v-if="loading")
|
|
v-progress-circular(indeterminate color="primary" size="64")
|
|
.text-body-1.text-grey.mt-4 {{ $t('review.loading') }}
|
|
|
|
v-card.lesson-card(
|
|
v-else-if="currentItem"
|
|
)
|
|
.d-flex.align-center.pa-4.border-b-subtle
|
|
v-btn(icon="mdi-close" variant="text" to="/dashboard" color="grey")
|
|
v-spacer
|
|
.text-caption.text-grey-darken-1.font-weight-bold
|
|
| {{ sessionDone + 1 }} / {{ sessionTotal }}
|
|
v-spacer
|
|
v-chip.font-weight-bold(
|
|
size="small"
|
|
:color="phaseColor"
|
|
variant="tonal"
|
|
) {{ phaseLabel }}
|
|
|
|
v-window.flex-grow-1(v-model="phase")
|
|
v-window-item(value="primer")
|
|
.d-flex.flex-column.align-center.pa-6
|
|
.hero-wrapper.mb-6
|
|
KanjiSvgViewer(:char="currentItem.char" mode="hero")
|
|
.text-h4.font-weight-bold.text-white.mb-2 {{ currentItem.meaning }}
|
|
.text-body-1.text-grey.text-center.mb-8(
|
|
v-if="currentItem.mnemonic") "{{ currentItem.mnemonic }}"
|
|
.radical-section(v-if="currentItem.radicals?.length")
|
|
.text-caption.text-grey-darken-1.text-uppercase.mb-3.font-weight-bold
|
|
|{{ $t('lesson.components') }}
|
|
.d-flex.flex-wrap.gap-3.justify-center
|
|
v-sheet.radical-chip(
|
|
v-for="rad in currentItem.radicals" :key="rad.meaning")
|
|
.d-flex.align-center.gap-2.px-3.py-1
|
|
v-avatar(size="24" rounded="0")
|
|
img(v-if="rad.image" :src="rad.image")
|
|
span.text-h6.font-weight-bold.text-white(v-else) {{ rad.char }}
|
|
.text-caption.text-grey-lighten-1 {{ rad.meaning }}
|
|
.readings-grid
|
|
.reading-box
|
|
.label {{ $t('collection.onyomi') }}
|
|
.val.text-cyan-lighten-3 {{ currentItem.onyomi[0] || '-' }}
|
|
.reading-box
|
|
.label {{ $t('collection.kunyomi') }}
|
|
.val.text-pink-lighten-3 {{ currentItem.kunyomi[0] || '-' }}
|
|
|
|
v-window-item(value="demo")
|
|
.d-flex.flex-column.align-center.justify-center.h-100.pa-6
|
|
.text-subtitle-1.text-grey.mb-6 {{ $t('lesson.observe') }}
|
|
.canvas-wrapper.lesson-canvas-wrapper
|
|
KanjiSvgViewer(ref="demoViewer" :char="currentItem.char" mode="animate")
|
|
|
|
v-window-item(value="drawing")
|
|
.d-flex.flex-column.align-center.justify-center.h-100.pa-6
|
|
.text-subtitle-1.text-grey.mb-6
|
|
span(v-if="subPhase === 'guided'") {{ $t('lesson.trace') }}
|
|
span(v-else) Draw ({{ practiceCount }}/3)
|
|
|
|
.transition-opacity(
|
|
:style="{ opacity: canvasOpacity }"
|
|
style="transition: opacity 0.3s ease;"
|
|
)
|
|
KanjiCanvas(
|
|
ref="canvas"
|
|
:char="currentItem.char"
|
|
:auto-hint="subPhase === 'guided'"
|
|
:size="300"
|
|
@complete="handleDrawComplete"
|
|
@mistake="handleMistake"
|
|
)
|
|
|
|
.pa-6
|
|
v-btn.action-btn(
|
|
v-if="phase === 'primer'"
|
|
@click="phase = 'demo'"
|
|
color="cyan" rounded="pill" size="large" block
|
|
) {{ $t('lesson.understand') }}
|
|
v-btn.action-btn(
|
|
v-if="phase === 'demo'"
|
|
@click="startDrawing"
|
|
color="cyan" rounded="pill" size="large" block
|
|
) {{ $t('lesson.ready') }}
|
|
.d-flex.justify-center.gap-4.mt-2(v-if="phase === 'drawing' && subPhase === 'practice'")
|
|
v-btn(
|
|
variant="text" color="amber" @click="useHint"
|
|
) {{ $t('lesson.hintAction') }}
|
|
v-btn(variant="text" color="grey" @click="phase='demo'") {{ $t('lesson.watchAgain') }}
|
|
|
|
v-card.text-center.pa-8.lesson-card(
|
|
v-else
|
|
)
|
|
v-icon.mb-6(size="80" color="purple-accent-2") mdi-check-decagram
|
|
.text-h4.font-weight-bold.text-white.mb-2 {{ $t('lesson.completeTitle') }}
|
|
.text-body-1.text-grey.mb-8 {{ $t('lesson.learned', { n: sessionDone }) }}
|
|
v-btn.glow-btn.text-black.font-weight-bold(
|
|
to="/dashboard"
|
|
block
|
|
color="cyan"
|
|
height="50"
|
|
rounded="pill"
|
|
) {{ $t('lesson.backToDashboard') }}
|
|
|
|
</template>
|
|
|
|
<script setup>
|
|
/* eslint-disable no-unused-vars */
|
|
|
|
import {
|
|
ref, computed, onMounted, nextTick, watch,
|
|
} from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useAppStore } from '@/stores/appStore';
|
|
import KanjiCanvas from '@/components/kanji/KanjiCanvas.vue';
|
|
import KanjiSvgViewer from '@/components/kanji/KanjiSvgViewer.vue';
|
|
|
|
const { t } = useI18n();
|
|
const store = useAppStore();
|
|
const currentItem = ref(null);
|
|
const phase = ref('primer');
|
|
const subPhase = ref('guided');
|
|
const practiceCount = ref(0);
|
|
const sessionDone = ref(0);
|
|
const sessionTotal = ref(0);
|
|
const canvasOpacity = ref(1);
|
|
const loading = ref(true);
|
|
|
|
const canvas = ref(null);
|
|
const demoViewer = ref(null);
|
|
|
|
const phaseLabel = computed(() => {
|
|
if (phase.value === 'primer') return t('lesson.phasePrimer');
|
|
if (phase.value === 'demo') return t('lesson.phaseDemo');
|
|
return subPhase.value === 'guided' ? t('lesson.phaseGuided') : t('lesson.phasePractice');
|
|
});
|
|
|
|
const phaseColor = computed(() => (phase.value === 'primer' ? 'blue' : 'purple'));
|
|
|
|
watch(phase, (val) => {
|
|
if (val === 'demo') nextTick(() => demoViewer.value?.playAnimation());
|
|
});
|
|
|
|
function loadNext() {
|
|
if (store.lessonQueue.length === 0) {
|
|
currentItem.value = null;
|
|
return;
|
|
}
|
|
[currentItem.value] = store.lessonQueue;
|
|
phase.value = 'primer';
|
|
subPhase.value = 'guided';
|
|
practiceCount.value = 0;
|
|
}
|
|
|
|
function startDrawing() {
|
|
phase.value = 'drawing';
|
|
subPhase.value = 'guided';
|
|
nextTick(() => canvas.value?.reset());
|
|
}
|
|
|
|
async function handleDrawComplete() {
|
|
await new Promise((r) => { setTimeout(r, 400); });
|
|
|
|
if (subPhase.value === 'guided') {
|
|
canvasOpacity.value = 0;
|
|
await new Promise((r) => { setTimeout(r, 300); });
|
|
|
|
subPhase.value = 'practice';
|
|
await nextTick();
|
|
canvas.value?.reset();
|
|
|
|
canvasOpacity.value = 1;
|
|
} else {
|
|
practiceCount.value += 1;
|
|
|
|
if (practiceCount.value < 3) {
|
|
canvasOpacity.value = 0;
|
|
await new Promise((r) => { setTimeout(r, 300); });
|
|
canvas.value?.reset();
|
|
canvasOpacity.value = 1;
|
|
} else {
|
|
await store.submitLesson(currentItem.value.wkSubjectId);
|
|
sessionDone.value += 1;
|
|
store.lessonQueue.shift();
|
|
loadNext();
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleMistake() {
|
|
if (subPhase.value === 'practice') practiceCount.value = 0;
|
|
}
|
|
|
|
function useHint() {
|
|
canvas.value?.showHint();
|
|
if (subPhase.value === 'practice') {
|
|
practiceCount.value = 0;
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
loading.value = true;
|
|
await store.fetchLessonQueue();
|
|
sessionTotal.value = store.lessonQueue.length;
|
|
loadNext();
|
|
loading.value = false;
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.page-container-center {
|
|
min-height: 100%;
|
|
}
|
|
</style>
|