big ui refractor

This commit is contained in:
Rene Kievits
2025-12-23 02:23:44 +01:00
parent 4428a2b7be
commit eaed23a678
62 changed files with 2662 additions and 815 deletions

View File

@@ -1,143 +1,171 @@
<template lang="pug">
v-app.zen-app
v-navigation-drawer(
v-model="drawer"
temporary
location="right"
color="#1e1e24"
class="border-none"
width="280"
v-app.zen-app.overflow-hidden
.parallax-bg(
:style="parallaxStyle"
:class="{ 'zooming': viewState === 'zooming' }"
)
.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') }}
.bg-gradient
.bg-grid
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') }}
.kanji-layer
span.bg-kanji(
v-for="(k, i) in backgroundKanjis"
:key="i"
:style="k.style"
) {{ k.char }}
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') }}
.orb.orb-1(:class="{ 'zooming': viewState === 'zooming' }")
.orb.orb-2(:class="{ 'zooming': viewState === 'zooming' }")
.orb.orb-3(:class="{ 'zooming': viewState === 'zooming' }")
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') }}
v-app-bar.px-2.app-bar-blur.safe-area-header(
flat
color="rgba(30, 30, 36, 0.8)"
border="b"
.app-content-wrapper(
v-if="store.token || viewState === 'zooming'"
:class="{ 'app-entering': viewState === 'zooming', 'app-visible': viewState === 'app' }"
)
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-navigation-drawer(
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-spacer
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') }}
template(v-if="store.token")
.d-none.d-md-flex.align-center
v-btn.mx-1(
to="/"
:active="false"
variant="text"
:color="$route.path === '/' ? '#00cec9' : 'grey'"
) {{ $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-btn.mx-1(
to="/collection"
:active="false"
variant="text"
:color="$route.path === '/collection' ? '#00cec9' : 'grey'"
) {{ $t('nav.collection') }}
v-divider.my-4.border-subtle
v-divider.mx-2.my-auto(vertical length="20" color="grey-darken-2")
.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-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-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-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-btn.justify-start.px-4.text-red-lighten-2(
variant="text"
block
@click="handleLogout"
)
v-icon(start icon="mdi-logout")
| {{ $t('nav.logout') }}
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"
)
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
.d-flex.d-md-none
v-btn(
icon="mdi-menu"
variant="text"
color="grey-lighten-1"
@click="drawer = !drawer"
)
v-spacer
v-main
.fill-height.d-flex.align-center.justify-center.px-4(v-if="!store.token")
v-card.pa-6.rounded-lg.elevation-10.text-center.border-subtle(
color="#1e1e24"
width="100%"
max-width="400"
)
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') }}
@@ -151,7 +179,8 @@
color="white"
hide-details
density="comfortable"
@keyup.enter="handleLogin"
@keyup.enter="triggerLogin"
:disabled="viewState === 'authenticating' || viewState === 'zooming'"
)
v-alert.mb-4.text-left.text-caption(
@@ -161,16 +190,14 @@
variant="tonal"
) {{ errorMsg }}
v-btn.text-black.font-weight-bold.mt-4(
v-btn.text-black.font-weight-bold.mt-4.login-btn(
block
color="#00cec9"
height="44"
:loading="loggingIn"
@click="handleLogin"
:loading="viewState === 'authenticating'"
@click="triggerLogin"
) {{ $t('login.button') }}
router-view(v-else)
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') }}
@@ -205,15 +232,15 @@
span {{ $t('settings.loose') }} (20)
.text-caption.text-grey.mb-2 {{ $t('settings.language') }}
v-btn-toggle.d-flex.w-100.border-subtle(
v-model="$i18n.locale"
mandatory
rounded="lg"
color="#00cec9"
)
v-btn.flex-grow-1(value="en") EN
v-btn.flex-grow-1(value="de") DE
v-btn.flex-grow-1(value="ja") JA
.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(
@@ -256,7 +283,9 @@
<script setup>
/* eslint-disable no-unused-vars */
import { ref, watch, onMounted } from 'vue';
import {
ref, watch, onMounted, computed, onUnmounted,
} from 'vue';
import { useI18n } from 'vue-i18n';
import { useAppStore } from '@/stores/appStore';
import logo from '@/assets/icon.svg';
@@ -265,8 +294,10 @@ 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 loggingIn = ref(false);
const syncing = ref(false);
const errorMsg = ref('');
const showSettings = ref(false);
@@ -276,16 +307,114 @@ 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(() => {
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;
}
});
@@ -305,58 +434,223 @@ async function manualSync() {
}
}
async function handleLogin() {
async function triggerLogin() {
if (!inputKey.value) return;
loggingIn.value = true;
viewState.value = 'authenticating';
errorMsg.value = '';
try {
const result = await store.login(inputKey.value.trim());
if (result.user && !result.user.lastSync) {
manualSync();
}
// Wait for authentication state to settle
setTimeout(() => {
// Step 1: Start the zoom animation on the login card/background
viewState.value = 'zooming';
// Step 2: Only after a significant part of the zoom has happened,
// let the app content mount and start its own internal fade-in
setTimeout(() => {
viewState.value = 'app';
if (result.user && !result.user.lastSync) {
manualSync();
}
}, 1000); // Increased delay to ensure zoom is the primary visual focus first
}, 800);
} catch (e) {
console.error(e);
errorMsg.value = e.message || t('login.failed');
} finally {
loggingIn.value = false;
viewState.value = 'login';
}
}
function saveSettings() {
store.saveSettings({
batchSize: tempBatchSize.value,
drawingAccuracy: tempDrawingAccuracy.value,
});
localStorage.setItem('zen_locale', locale.value);
showSettings.value = false;
store.fetchQueue();
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" src="@/styles/pages/_app.scss"></style>
<style lang="scss">
.zen-app {
position: relative;
background-color: #121212 !important;
}
<style lang="scss" scoped>
.safe-area-header {
padding-top: env(safe-area-inset-top);
.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;
height: auto !important;
&.zooming {
transition: transform 1.2s cubic-bezier(0.7, 0, 0.3, 1);
}
:deep(.v-toolbar__content) {
min-height: 64px;
align-items: center;
.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);
/* Increased blur for a smoother transition */
transition: all 1.2s cubic-bezier(0.4, 0, 0.2, 1);
/* Match the zoom ease */
pointer-events: none;
/* Prevent clicks during transition */
&.app-entering {
opacity: 0;
/* Keep hidden initially even if class is present */
}
&.app-visible {
opacity: 1;
transform: scale(1);
filter: none;
pointer-events: auto;
}
}
</style>