654 lines
17 KiB
Vue
654 lines
17 KiB
Vue
<template lang="pug">
|
|
v-app.zen-app.overflow-hidden
|
|
v-navigation-drawer(
|
|
v-if="store.token || viewState === 'zooming'"
|
|
v-model="drawer"
|
|
temporary
|
|
location="right"
|
|
color="#1e1e24"
|
|
class="border-none"
|
|
width="280"
|
|
)
|
|
.d-flex.flex-column.h-100.pa-4
|
|
.d-flex.align-center.mb-6.px-2
|
|
img.mr-3(:src="logo" height="32" width="32" alt="Logo")
|
|
span.text-h6.font-weight-bold {{ $t('nav.menu') }}
|
|
|
|
v-list.pa-0(nav bg-color="transparent")
|
|
v-list-item.mb-2(
|
|
to="/"
|
|
rounded="lg"
|
|
:active="$route.path === '/'"
|
|
)
|
|
template(v-slot:prepend)
|
|
v-icon(color="#00cec9" icon="mdi-view-dashboard")
|
|
v-list-item-title.font-weight-bold {{ $t('nav.dashboard') }}
|
|
|
|
v-list-item.mb-2(
|
|
to="/collection"
|
|
rounded="lg"
|
|
:active="$route.path === '/collection'"
|
|
)
|
|
template(v-slot:prepend)
|
|
v-icon(color="#00cec9" icon="mdi-bookshelf")
|
|
v-list-item-title.font-weight-bold {{ $t('nav.collection') }}
|
|
|
|
v-divider.my-4.border-subtle
|
|
|
|
.d-flex.flex-column.gap-2
|
|
v-btn.justify-start.px-4(
|
|
variant="text"
|
|
block
|
|
color="grey-lighten-1"
|
|
@click="showSettings = true; drawer = false"
|
|
)
|
|
v-icon(start icon="mdi-cog")
|
|
| {{ $t('nav.settings') }}
|
|
|
|
v-btn.justify-start.px-4(
|
|
variant="text"
|
|
block
|
|
color="grey-lighten-1"
|
|
:loading="syncing"
|
|
@click="manualSync"
|
|
)
|
|
v-icon(start icon="mdi-sync")
|
|
| {{ $t('nav.sync') }}
|
|
|
|
v-btn.justify-start.px-4.text-red-lighten-2(
|
|
variant="text"
|
|
block
|
|
@click="handleLogout"
|
|
)
|
|
v-icon(start icon="mdi-logout")
|
|
| {{ $t('nav.logout') }}
|
|
.parallax-bg(
|
|
:style="parallaxStyle"
|
|
:class="{ 'zooming': viewState === 'zooming' }"
|
|
)
|
|
.bg-gradient
|
|
.bg-grid
|
|
|
|
.kanji-layer
|
|
span.bg-kanji(
|
|
v-for="(k, i) in backgroundKanjis"
|
|
:key="i"
|
|
:style="k.style"
|
|
) {{ k.char }}
|
|
|
|
.orb.orb-1(:class="{ 'zooming': viewState === 'zooming' }")
|
|
.orb.orb-2(:class="{ 'zooming': viewState === 'zooming' }")
|
|
.orb.orb-3(:class="{ 'zooming': viewState === 'zooming' }")
|
|
|
|
.app-content-wrapper(
|
|
v-if="store.token || viewState === 'zooming'"
|
|
:class="{ 'app-entering': viewState === 'zooming', 'app-visible': viewState === 'app' }"
|
|
)
|
|
|
|
v-app-bar.px-2.app-bar-blur.safe-area-header(
|
|
flat
|
|
color="rgba(30, 30, 36, 0.8)"
|
|
border="b"
|
|
)
|
|
v-app-bar-title.font-weight-bold.text-h6(style="min-width: fit-content;")
|
|
.d-flex.align-center.cursor-pointer.logo-hover(@click="$router.push('/')")
|
|
img(:src="logo" height="32" width="32" alt="Zen Kanji Logo")
|
|
span.ml-3.tracking-tight Zen Kanji
|
|
|
|
v-spacer
|
|
|
|
template(v-if="store.token")
|
|
.d-none.d-md-flex.align-center
|
|
v-btn.mx-1(
|
|
to="/"
|
|
variant="text"
|
|
:color="$route.path === '/' ? '#00cec9' : 'grey'"
|
|
) {{ $t('nav.dashboard') }}
|
|
|
|
v-btn.mx-1(
|
|
to="/collection"
|
|
variant="text"
|
|
:color="$route.path === '/collection' ? '#00cec9' : 'grey'"
|
|
) {{ $t('nav.collection') }}
|
|
|
|
v-divider.mx-2.my-auto(vertical length="20" color="grey-darken-2")
|
|
|
|
v-tooltip(:text="$t('nav.settings')" location="bottom")
|
|
template(v-slot:activator="{ props }")
|
|
v-btn(
|
|
v-bind="props"
|
|
icon="mdi-cog"
|
|
variant="text"
|
|
color="grey-lighten-1"
|
|
@click="showSettings = true"
|
|
)
|
|
|
|
v-tooltip(:text="$t('nav.sync')" location="bottom")
|
|
template(v-slot:activator="{ props }")
|
|
v-btn(
|
|
v-bind="props"
|
|
icon="mdi-sync"
|
|
variant="text"
|
|
:loading="syncing"
|
|
color="grey-lighten-1"
|
|
@click="manualSync"
|
|
)
|
|
|
|
v-tooltip(:text="$t('nav.logout')" location="bottom")
|
|
template(v-slot:activator="{ props }")
|
|
v-btn(
|
|
v-bind="props"
|
|
icon="mdi-logout"
|
|
variant="text"
|
|
color="grey-darken-1"
|
|
@click="handleLogout"
|
|
)
|
|
|
|
.d-flex.d-md-none
|
|
v-btn(
|
|
icon="mdi-menu"
|
|
variant="text"
|
|
color="grey-lighten-1"
|
|
@click="drawer = !drawer"
|
|
)
|
|
|
|
v-main.fill-height
|
|
router-view(v-slot="{ Component }")
|
|
transition(name="page" mode="out-in")
|
|
component(:is="Component")
|
|
|
|
.login-overlay(
|
|
v-if="!store.token || viewState === 'zooming' || viewState === 'authenticating'"
|
|
:class="{ 'zooming': viewState === 'zooming' }"
|
|
)
|
|
v-card.login-card.pa-6.rounded-lg.elevation-10.text-center.border-subtle(
|
|
color="#1e1e24"
|
|
width="100%"
|
|
max-width="400"
|
|
)
|
|
.login-content
|
|
img.mb-4(:src="logo" height="64" width="64")
|
|
|
|
h1.text-h5.font-weight-bold.mb-2 {{ $t('hero.welcome') }}
|
|
p.text-grey-lighten-1.text-body-2.mb-6 {{ $t('login.instruction') }}
|
|
|
|
v-text-field.mb-2(
|
|
v-model="inputKey"
|
|
:placeholder="$t('login.placeholder')"
|
|
variant="solo-filled"
|
|
bg-color="#2f3542"
|
|
color="white"
|
|
hide-details
|
|
density="comfortable"
|
|
@keyup.enter="triggerLogin"
|
|
:disabled="viewState === 'authenticating' || viewState === 'zooming'"
|
|
)
|
|
|
|
v-alert.mb-4.text-left.text-caption(
|
|
v-if="errorMsg"
|
|
type="error"
|
|
density="compact"
|
|
variant="tonal"
|
|
) {{ errorMsg }}
|
|
|
|
v-btn.text-black.font-weight-bold.mt-4.login-btn(
|
|
block
|
|
color="#00cec9"
|
|
height="44"
|
|
:loading="viewState === 'authenticating'"
|
|
@click="triggerLogin"
|
|
) {{ $t('login.button') }}
|
|
|
|
v-dialog(v-model="showSettings" max-width="340")
|
|
v-card.rounded-xl.pa-4.border-subtle(color="#1e1e24")
|
|
v-card-title.text-center.font-weight-bold {{ $t('settings.title') }}
|
|
|
|
v-card-text.pt-4
|
|
.text-caption.text-grey.mb-2 {{ $t('settings.batchSize') }}
|
|
v-slider(
|
|
v-model="tempBatchSize"
|
|
:min="5"
|
|
:max="100"
|
|
:step="5"
|
|
thumb-label
|
|
color="#00cec9"
|
|
track-color="grey-darken-3"
|
|
)
|
|
.text-center.text-h5.font-weight-bold.text-teal-accent-3.mb-6
|
|
| {{ tempBatchSize }} {{ $t('settings.items') }}
|
|
|
|
.text-caption.text-grey.mb-2 {{ $t('settings.drawingTolerance') }}
|
|
v-slider(
|
|
v-model="tempDrawingAccuracy"
|
|
:min="1"
|
|
:max="20"
|
|
:step="1"
|
|
thumb-label
|
|
color="#00cec9"
|
|
track-color="grey-darken-3"
|
|
)
|
|
.d-flex.justify-space-between.text-caption.text-grey-lighten-1.mb-6.px-1
|
|
span {{ $t('settings.strict') }} (5)
|
|
span.font-weight-bold.text-body-1(color="#00cec9") {{ tempDrawingAccuracy }}
|
|
span {{ $t('settings.loose') }} (20)
|
|
|
|
.text-caption.text-grey.mb-2 {{ $t('settings.language') }}
|
|
|
|
.lang-switch-wrapper.border-subtle.mb-2
|
|
.lang-glider(:style="{ transform: `translateX(${localeIndex * 100}%)` }")
|
|
button.lang-btn(
|
|
v-for="l in availableLocales"
|
|
:key="l"
|
|
:class="{ 'active': tempLocale === l }"
|
|
@click="tempLocale = l"
|
|
) {{ l.toUpperCase() }}
|
|
|
|
v-card-actions.mt-2
|
|
v-btn.text-white(
|
|
block
|
|
color="#2f3542"
|
|
@click="saveSettings"
|
|
) {{ $t('settings.save') }}
|
|
|
|
v-dialog(v-model="showLogoutDialog" max-width="320")
|
|
v-card.rounded-xl.pa-4.border-subtle(color="#1e1e24")
|
|
.d-flex.flex-column.align-center.text-center.pt-2
|
|
v-avatar.mb-3(color="red-darken-4" size="48")
|
|
v-icon(icon="mdi-logout" size="24" color="red-lighten-1")
|
|
|
|
h3.text-h6.font-weight-bold.mb-2 {{ $t('nav.logout') }}?
|
|
|
|
p.text-body-2.text-grey-lighten-1.px-2.mb-4
|
|
| {{ $t('alerts.logoutConfirm') }}
|
|
|
|
v-card-actions.d-flex.gap-2.pa-0.mt-2
|
|
v-btn.flex-grow-1.text-capitalize(
|
|
variant="tonal"
|
|
color="grey"
|
|
height="40"
|
|
@click="showLogoutDialog = false"
|
|
) {{ $t('common.cancel') }}
|
|
|
|
v-btn.flex-grow-1.text-capitalize(
|
|
color="red-accent-2"
|
|
variant="flat"
|
|
height="40"
|
|
@click="confirmLogout"
|
|
) {{ $t('nav.logout') }}
|
|
|
|
v-snackbar(v-model="snackbar.show" :color="snackbar.color" timeout="3000")
|
|
| {{ snackbar.text }}
|
|
template(v-slot:actions)
|
|
v-btn(variant="text" @click="snackbar.show = false") {{ $t('common.close') }}
|
|
</template>
|
|
|
|
<script setup>
|
|
/* eslint-disable no-unused-vars */
|
|
import {
|
|
ref, watch, onMounted, computed, onUnmounted,
|
|
} from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { App as CapacitorApp } from '@capacitor/app';
|
|
import { useAppStore } from '@/stores/appStore';
|
|
import { checkForUpdates } from '@/utils/autoUpdate';
|
|
import logo from '@/assets/icon.svg';
|
|
|
|
const drawer = ref(false);
|
|
const { locale, t } = useI18n();
|
|
const store = useAppStore();
|
|
|
|
const viewState = ref('login');
|
|
const mousePos = ref({ x: 0, y: 0 });
|
|
|
|
const inputKey = ref('');
|
|
const syncing = ref(false);
|
|
const errorMsg = ref('');
|
|
const showSettings = ref(false);
|
|
const showLogoutDialog = ref(false);
|
|
const snackbar = ref({ show: false, text: '', color: 'success' });
|
|
|
|
const tempBatchSize = ref(store.batchSize);
|
|
const tempDrawingAccuracy = ref(store.drawingAccuracy);
|
|
|
|
const availableLocales = ['en', 'de', 'ja'];
|
|
const tempLocale = ref(locale.value);
|
|
const localeIndex = computed(() => availableLocales.indexOf(tempLocale.value));
|
|
|
|
const backgroundKanjis = [
|
|
{
|
|
char: '禅',
|
|
style: {
|
|
top: '15%', left: '10%', fontSize: '8rem', opacity: 0.03,
|
|
},
|
|
},
|
|
{
|
|
char: '空',
|
|
style: {
|
|
top: '60%', left: '85%', fontSize: '10rem', opacity: 0.03,
|
|
},
|
|
},
|
|
{
|
|
char: '無',
|
|
style: {
|
|
top: '30%', left: '60%', fontSize: '12rem', opacity: 0.02,
|
|
},
|
|
},
|
|
{
|
|
char: '心',
|
|
style: {
|
|
top: '80%', left: '20%', fontSize: '6rem', opacity: 0.04,
|
|
},
|
|
},
|
|
{
|
|
char: '道',
|
|
style: {
|
|
top: '10%', left: '70%', fontSize: '5rem', opacity: 0.05,
|
|
},
|
|
},
|
|
{
|
|
char: '学',
|
|
style: {
|
|
top: '50%', left: '5%', fontSize: '4rem', opacity: 0.04,
|
|
},
|
|
},
|
|
{
|
|
char: '夢',
|
|
style: {
|
|
top: '90%', left: '60%', fontSize: '7rem', opacity: 0.03,
|
|
},
|
|
},
|
|
{
|
|
char: '力',
|
|
style: {
|
|
top: '25%', left: '35%', fontSize: '3rem', opacity: 0.05,
|
|
},
|
|
},
|
|
{
|
|
char: '月',
|
|
style: {
|
|
top: '75%', left: '90%', fontSize: '4rem', opacity: 0.04,
|
|
},
|
|
},
|
|
{
|
|
char: '水',
|
|
style: {
|
|
top: '40%', left: '80%', fontSize: '5rem', opacity: 0.03,
|
|
},
|
|
},
|
|
{
|
|
char: '火',
|
|
style: {
|
|
top: '70%', left: '50%', fontSize: '9rem', opacity: 0.02,
|
|
},
|
|
},
|
|
];
|
|
|
|
const handleMouseMove = (e) => {
|
|
mousePos.value = {
|
|
x: (e.clientX / window.innerWidth) * 2 - 1,
|
|
y: (e.clientY / window.innerHeight) * 2 - 1,
|
|
};
|
|
};
|
|
|
|
onMounted(() => {
|
|
CapacitorApp.addListener('appStateChange', ({ isActive }) => {
|
|
if (isActive) checkForUpdates();
|
|
});
|
|
if (store.token) {
|
|
viewState.value = 'app';
|
|
store.fetchStats();
|
|
} else {
|
|
viewState.value = 'login';
|
|
}
|
|
window.addEventListener('mousemove', handleMouseMove);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
});
|
|
|
|
const parallaxStyle = computed(() => {
|
|
if (viewState.value === 'app') return {};
|
|
const x = mousePos.value.x * -20;
|
|
const y = mousePos.value.y * -20;
|
|
return {
|
|
transform: `translate(${x}px, ${y}px) scale(1.1)`,
|
|
};
|
|
});
|
|
|
|
watch(showSettings, (isOpen) => {
|
|
if (isOpen) {
|
|
tempBatchSize.value = store.batchSize;
|
|
tempDrawingAccuracy.value = store.drawingAccuracy;
|
|
tempLocale.value = locale.value;
|
|
}
|
|
});
|
|
|
|
async function manualSync() {
|
|
syncing.value = true;
|
|
try {
|
|
const result = await store.sync();
|
|
snackbar.value = { show: true, text: t('alerts.syncSuccess', { count: result.count }), color: 'success' };
|
|
await store.fetchQueue();
|
|
await store.fetchStats();
|
|
await store.fetchCollection();
|
|
} catch (e) {
|
|
console.error(e);
|
|
snackbar.value = { show: true, text: t('alerts.syncFailed'), color: 'error' };
|
|
} finally {
|
|
syncing.value = false;
|
|
}
|
|
}
|
|
|
|
async function triggerLogin() {
|
|
if (!inputKey.value) return;
|
|
viewState.value = 'authenticating';
|
|
errorMsg.value = '';
|
|
|
|
try {
|
|
const result = await store.login(inputKey.value.trim());
|
|
|
|
setTimeout(() => {
|
|
viewState.value = 'zooming';
|
|
setTimeout(() => {
|
|
viewState.value = 'app';
|
|
if (result.user && !result.user.lastSync) {
|
|
manualSync();
|
|
}
|
|
}, 1000);
|
|
}, 800);
|
|
} catch (e) {
|
|
console.error(e);
|
|
errorMsg.value = e.message || t('login.failed');
|
|
viewState.value = 'login';
|
|
}
|
|
}
|
|
|
|
function saveSettings() {
|
|
showSettings.value = false;
|
|
|
|
setTimeout(() => {
|
|
if (locale.value !== tempLocale.value) {
|
|
locale.value = tempLocale.value;
|
|
localStorage.setItem('zen_locale', locale.value);
|
|
}
|
|
|
|
store.saveSettings({
|
|
batchSize: tempBatchSize.value,
|
|
drawingAccuracy: tempDrawingAccuracy.value,
|
|
});
|
|
|
|
store.fetchQueue();
|
|
}, 300);
|
|
}
|
|
|
|
function handleLogout() {
|
|
showLogoutDialog.value = true;
|
|
drawer.value = false;
|
|
}
|
|
|
|
function confirmLogout() {
|
|
store.logout();
|
|
inputKey.value = '';
|
|
showLogoutDialog.value = false;
|
|
viewState.value = 'login';
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.zen-app {
|
|
position: relative;
|
|
background-color: #121212 !important;
|
|
}
|
|
|
|
.parallax-bg {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
z-index: 0;
|
|
transition: transform 0.2s ease-out;
|
|
background: radial-gradient(circle at 50% 50%, #1a1a20 0%, #0a0a0c 100%);
|
|
overflow: hidden;
|
|
|
|
&.zooming {
|
|
transition: transform 1.2s cubic-bezier(0.7, 0, 0.3, 1);
|
|
}
|
|
|
|
.bg-grid {
|
|
position: absolute;
|
|
inset: 0;
|
|
background-image: radial-gradient(rgba(0, 206, 201, 0.15) 1px, transparent 1px);
|
|
background-size: 40px 40px;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.kanji-layer {
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.bg-kanji {
|
|
position: absolute;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
font-weight: 900;
|
|
font-family: "Noto Serif JP", serif;
|
|
user-select: none;
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
}
|
|
|
|
.orb {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
filter: blur(80px);
|
|
mix-blend-mode: screen;
|
|
opacity: 0.6;
|
|
animation: blob 10s infinite alternate;
|
|
transition: all 1.2s ease-in-out;
|
|
z-index: 0;
|
|
|
|
&.zooming {
|
|
transform: scale(5);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.orb-1 {
|
|
top: 20%;
|
|
left: 20%;
|
|
width: 400px;
|
|
height: 400px;
|
|
background: rgba(0, 206, 201, 0.15);
|
|
}
|
|
|
|
.orb-2 {
|
|
top: 30%;
|
|
right: 20%;
|
|
width: 350px;
|
|
height: 350px;
|
|
background: rgba(162, 155, 254, 0.15);
|
|
animation-delay: 2s;
|
|
}
|
|
|
|
.orb-3 {
|
|
bottom: 10%;
|
|
left: 30%;
|
|
width: 300px;
|
|
height: 300px;
|
|
background: rgba(30, 144, 255, 0.1);
|
|
animation-delay: 4s;
|
|
}
|
|
}
|
|
|
|
@keyframes blob {
|
|
0% {
|
|
transform: translate(0, 0) scale(1);
|
|
}
|
|
|
|
50% {
|
|
transform: translate(30px, -50px) scale(1.1);
|
|
}
|
|
|
|
100% {
|
|
transform: translate(-20px, 20px) scale(0.9);
|
|
}
|
|
}
|
|
|
|
.login-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 50;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 1.2s cubic-bezier(0.7, 0, 0.3, 1);
|
|
transform-origin: center center;
|
|
|
|
&.zooming {
|
|
transform: scale(15);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.login-card {
|
|
position: relative;
|
|
background: rgba(30, 30, 36, 0.6) !important;
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
transition: transform 0.5s ease;
|
|
|
|
&:hover {
|
|
transform: scale(1.01);
|
|
}
|
|
}
|
|
}
|
|
|
|
.app-content-wrapper {
|
|
position: relative;
|
|
z-index: 10;
|
|
width: 100%;
|
|
height: 100%;
|
|
opacity: 0;
|
|
transform: scale(0.95);
|
|
filter: blur(10px);
|
|
transition: all 1.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
pointer-events: none;
|
|
|
|
&.app-entering {
|
|
opacity: 0;
|
|
}
|
|
|
|
&.app-visible {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
filter: none;
|
|
pointer-events: auto;
|
|
}
|
|
}
|
|
</style>
|