feat(settings): more specific notification handling, and put menu in query to allow user to go back in history to go back in menus
This commit is contained in:
@@ -1,44 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Background from "./Background.vue";
|
import Background from "./Background.vue";
|
||||||
import Menus from "./Menus/Menus.vue";
|
import Menus from "./Menus/Menus.vue";
|
||||||
import OptionsStartUp from "./Menus/Options/StartUp.vue";
|
|
||||||
import OptionsLanguage from "./Menus/Options/Language.vue";
|
|
||||||
import OptionsGbaMode from "./Menus/Options/GbaMode.vue";
|
|
||||||
|
|
||||||
const store = useSettingsStore();
|
|
||||||
|
|
||||||
const viewComponents: Record<string, Component> = {
|
|
||||||
optionsStartUp: OptionsStartUp,
|
|
||||||
optionsLanguage: OptionsLanguage,
|
|
||||||
optionsGbaMode: OptionsGbaMode,
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentViewComponent = computed(() => {
|
|
||||||
if (store.currentView === "menu" || !store.currentView) return null;
|
|
||||||
return viewComponents[store.currentView] || null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleBackNavigation = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === "Escape" || event.key === "Backspace") {
|
|
||||||
if (store.navigationStack.length > 0) {
|
|
||||||
store.popNavigation();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
store.$reset();
|
|
||||||
window.addEventListener("keydown", handleBackNavigation);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener("keydown", handleBackNavigation);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Background />
|
<Background />
|
||||||
<Menus v-if="store.currentView === 'menu'" />
|
<Menus />
|
||||||
<component :is="currentViewComponent" v-else-if="currentViewComponent" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const isAnyOtherMenuOpen = computed(() =>
|
|||||||
settingsStore.isAnyOtherMenuOpen("clock"),
|
settingsStore.isAnyOtherMenuOpen("clock"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const animation = useMenuAnimation(isOpen);
|
const animation = useMenuAnimation("clock", isOpen);
|
||||||
|
|
||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
ctx.translate(props.x, props.y);
|
ctx.translate(props.x, props.y);
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import OptionsMenu from "./Options/Menu.vue";
|
import OptionsMenu from "./Options/Menu.vue";
|
||||||
|
import OptionsStartUp from "./Options/StartUp.vue";
|
||||||
|
import OptionsLanguage from "./Options/Language.vue";
|
||||||
|
import OptionsGbaMode from "./Options/GbaMode.vue";
|
||||||
|
|
||||||
import ClockMenu from "./Clock/Menu.vue";
|
import ClockMenu from "./Clock/Menu.vue";
|
||||||
import UserMenu from "./User/Menu.vue";
|
import UserMenu from "./User/Menu.vue";
|
||||||
import TouchScreenMenu from "./TouchScreen/Menu.vue";
|
import TouchScreenMenu from "./TouchScreen/Menu.vue";
|
||||||
import Selector from "~/components/Common/ButtonSelector.vue";
|
import Selector from "~/components/Common/ButtonSelector.vue";
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
const { selectedButton: selected, selectorPosition } = useButtonNavigation({
|
const { selectedButton: selected, selectorPosition } = useButtonNavigation({
|
||||||
buttons: {
|
buttons: {
|
||||||
options: [31, 119, 49, 49],
|
options: [31, 119, 49, 49],
|
||||||
@@ -28,7 +34,11 @@ const { selectedButton: selected, selectorPosition } = useButtonNavigation({
|
|||||||
initialButton: "options",
|
initialButton: "options",
|
||||||
onButtonClick: (buttonName: string) => {
|
onButtonClick: (buttonName: string) => {
|
||||||
if (isSubmenu(buttonName)) {
|
if (isSubmenu(buttonName)) {
|
||||||
settingsStore.pushNavigation(buttonName);
|
router.push({
|
||||||
|
query: {
|
||||||
|
menu: buttonName,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
@@ -104,8 +114,6 @@ const { selectedButton: selected, selectorPosition } = useButtonNavigation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const settingsStore = useSettingsStore();
|
|
||||||
|
|
||||||
const isSubmenu = (buttonName: string) => {
|
const isSubmenu = (buttonName: string) => {
|
||||||
return (
|
return (
|
||||||
/^(options|clock|user|touchScreen)[A-Z]/.test(buttonName) &&
|
/^(options|clock|user|touchScreen)[A-Z]/.test(buttonName) &&
|
||||||
@@ -113,20 +121,64 @@ const isSubmenu = (buttonName: string) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
onBeforeRouteUpdate((to, from) => {
|
||||||
|
const fromMenu = from.query.menu?.toString();
|
||||||
|
const toMenu = to.query.menu?.toString();
|
||||||
|
|
||||||
|
if (!fromMenu && toMenu) {
|
||||||
|
settingsStore.setActiveMenu(selected.value);
|
||||||
|
} else if (fromMenu && !toMenu) {
|
||||||
|
if (fromMenu === "options" || fromMenu === "clock" || fromMenu === "user") {
|
||||||
|
selected.value = fromMenu;
|
||||||
|
settingsStore.setActiveMenu(null);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unreachable");
|
||||||
|
}
|
||||||
|
} else if (fromMenu && toMenu) {
|
||||||
|
if (toMenu === "options" || toMenu === "clock" || toMenu === "user") {
|
||||||
|
settingsStore.setCurrentSubMenu(null);
|
||||||
|
settingsStore.setActiveMenu(selected.value);
|
||||||
|
} else {
|
||||||
|
settingsStore.setCurrentSubMenu(toMenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
selected,
|
selected,
|
||||||
(newSelected) => {
|
(newSelected) => {
|
||||||
settingsStore.setActiveMenu(newSelected);
|
if (settingsStore.currentSubMenu === null) {
|
||||||
|
if (isSubmenu(newSelected)) {
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
menu: newSelected.split(/[A-Z]/)[0],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
router.push({ query: { menu: undefined } });
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const viewComponents: Record<string, Component> = {
|
||||||
|
optionsStartUp: OptionsStartUp,
|
||||||
|
optionsLanguage: OptionsLanguage,
|
||||||
|
optionsGbaMode: OptionsGbaMode,
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<template v-if="!settingsStore.currentSubMenu">
|
||||||
<OptionsMenu :x="33" :y="121" />
|
<OptionsMenu :x="33" :y="121" />
|
||||||
<ClockMenu :x="81" :y="121" />
|
<ClockMenu :x="81" :y="121" />
|
||||||
<UserMenu :x="129" :y="121" />
|
<UserMenu :x="129" :y="121" />
|
||||||
<TouchScreenMenu :x="177" :y="121" :opacity="1" />
|
<TouchScreenMenu :x="177" :y="121" :opacity="1" />
|
||||||
|
|
||||||
<Selector :rect="selectorPosition" :opacity="1" />
|
<Selector :rect="selectorPosition" :opacity="1" />
|
||||||
|
</template>
|
||||||
|
<component :is="viewComponents[settingsStore.currentSubMenu]" v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const isAnyOtherMenuOpen = computed(() =>
|
|||||||
settingsStore.isAnyOtherMenuOpen("options"),
|
settingsStore.isAnyOtherMenuOpen("options"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const animation = useMenuAnimation(isOpen);
|
const animation = useMenuAnimation("options", isOpen);
|
||||||
|
|
||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
ctx.translate(props.x, props.y);
|
ctx.translate(props.x, props.y);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const isAnyOtherMenuOpen = computed(() =>
|
|||||||
settingsStore.isAnyOtherMenuOpen("user"),
|
settingsStore.isAnyOtherMenuOpen("user"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const animation = useMenuAnimation(isOpen);
|
const animation = useMenuAnimation("user", isOpen);
|
||||||
|
|
||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
ctx.translate(props.x, props.y);
|
ctx.translate(props.x, props.y);
|
||||||
|
|||||||
@@ -33,25 +33,6 @@ const [
|
|||||||
GBA_MODE_IMAGE,
|
GBA_MODE_IMAGE,
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeMenu = computed(() => {
|
|
||||||
if (!store.activeMenu) return null;
|
|
||||||
|
|
||||||
if (/^options[A-Z]/.test(store.activeMenu)) {
|
|
||||||
return { id: "options", image: optionsImage };
|
|
||||||
}
|
|
||||||
if (/^clock[A-Z]/.test(store.activeMenu)) {
|
|
||||||
return { id: "clock", image: clockImage };
|
|
||||||
}
|
|
||||||
if (/^user[A-Z]/.test(store.activeMenu)) {
|
|
||||||
return { id: "user", image: userImage };
|
|
||||||
}
|
|
||||||
if (/^touchScreen[A-Z]/.test(store.activeMenu)) {
|
|
||||||
return { id: "touchScreen", image: touchScreenImage };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderNotification = (
|
const renderNotification = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
image: HTMLImageElement,
|
image: HTMLImageElement,
|
||||||
@@ -73,50 +54,100 @@ const renderNotification = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const notificationStack = computed(() => {
|
const mainNotification = computed(() => ({
|
||||||
const stack: Array<{ id: string; image: HTMLImageElement }> = [
|
image: settingsImage!,
|
||||||
{ id: "main", image: settingsImage! },
|
title: $t("settings.title"),
|
||||||
];
|
description: $t("settings.description"),
|
||||||
|
}));
|
||||||
|
|
||||||
if (activeMenu.value) {
|
const menuNotification = computed(() => {
|
||||||
stack.push({ id: activeMenu.value.id, image: activeMenu.value.image! });
|
if (!store.currentMenu) return null;
|
||||||
|
|
||||||
|
let image: HTMLImageElement | null = null;
|
||||||
|
let id = "";
|
||||||
|
|
||||||
|
if (/^options[A-Z]/.test(store.currentMenu)) {
|
||||||
|
image = optionsImage!;
|
||||||
|
id = "options";
|
||||||
|
} else if (/^clock[A-Z]/.test(store.currentMenu)) {
|
||||||
|
image = clockImage!;
|
||||||
|
id = "clock";
|
||||||
|
} else if (/^user[A-Z]/.test(store.currentMenu)) {
|
||||||
|
image = userImage!;
|
||||||
|
id = "user";
|
||||||
|
} else if (/^touchScreen[A-Z]/.test(store.currentMenu)) {
|
||||||
|
image = touchScreenImage!;
|
||||||
|
id = "touchScreen";
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const view of store.navigationStack) {
|
if (!image) return null;
|
||||||
const menuMatch = view.match(/^(options|clock|user|touchScreen)(.+)$/);
|
|
||||||
if (menuMatch) {
|
return {
|
||||||
const [, menu, submenu] = menuMatch;
|
image,
|
||||||
const submenuKey = submenu!.charAt(0).toLowerCase() + submenu!.slice(1);
|
title: $t(`settings.${id}.title`),
|
||||||
const imageMap: Record<string, HTMLImageElement> = {
|
description: $t(`settings.${id}.description`),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const IMAGES_MAP: Record<string, HTMLImageElement> = {
|
||||||
optionsStartUp: startUpImage!,
|
optionsStartUp: startUpImage!,
|
||||||
optionsLanguage: languageImage!,
|
optionsLanguage: languageImage!,
|
||||||
optionsGbaMode: gbaModeImage!,
|
optionsGbaMode: gbaModeImage!,
|
||||||
};
|
};
|
||||||
if (imageMap[view]) {
|
|
||||||
stack.push({ id: `${menu}.${submenuKey}`, image: imageMap[view]! });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stack;
|
const submenuNotification = computed(() => {
|
||||||
|
if (!store.currentSubMenu) return null;
|
||||||
|
|
||||||
|
const image = IMAGES_MAP[store.currentSubMenu];
|
||||||
|
if (!image) return null;
|
||||||
|
|
||||||
|
const menuMatch = store.currentSubMenu.match(
|
||||||
|
/^(options|clock|user|touchScreen)(.+)$/,
|
||||||
|
);
|
||||||
|
if (!menuMatch) return null;
|
||||||
|
|
||||||
|
const [, menu, submenu] = menuMatch;
|
||||||
|
const submenuKey = submenu!.charAt(0).toLowerCase() + submenu!.slice(1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
image,
|
||||||
|
title: $t(`settings.${menu}.${submenuKey}.title`),
|
||||||
|
description: $t(`settings.${menu}.${submenuKey}.description`),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
useRender((ctx) => {
|
useRender((ctx) => {
|
||||||
const stackSize = notificationStack.value.length;
|
let count = 1;
|
||||||
|
if (menuNotification.value) count++;
|
||||||
|
if (submenuNotification.value) count++;
|
||||||
|
|
||||||
ctx.translate(0, 144 - (stackSize - 1) * 16);
|
ctx.translate(0, 144 - (count - 1) * 16);
|
||||||
|
|
||||||
for (let i = 0; i < stackSize; i++) {
|
|
||||||
const notification = notificationStack.value[i]!;
|
|
||||||
|
|
||||||
renderNotification(
|
renderNotification(
|
||||||
ctx,
|
ctx,
|
||||||
notification.image,
|
mainNotification.value.image,
|
||||||
$t(`settings.${notification.id}.title`),
|
mainNotification.value.title,
|
||||||
$t(`settings.${notification.id}.description`),
|
mainNotification.value.description,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (menuNotification.value) {
|
||||||
ctx.translate(0, 16);
|
ctx.translate(0, 16);
|
||||||
|
renderNotification(
|
||||||
|
ctx,
|
||||||
|
menuNotification.value.image,
|
||||||
|
menuNotification.value.title,
|
||||||
|
menuNotification.value.description,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submenuNotification.value) {
|
||||||
|
ctx.translate(0, 16);
|
||||||
|
renderNotification(
|
||||||
|
ctx,
|
||||||
|
submenuNotification.value.image,
|
||||||
|
submenuNotification.value.title,
|
||||||
|
submenuNotification.value.description,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,49 @@
|
|||||||
import gsap from "gsap";
|
import gsap from "gsap";
|
||||||
|
|
||||||
export const useMenuAnimation = (isOpen: Ref<boolean>) => {
|
export const useMenuAnimation = (key: string, isOpen: Ref<boolean>) => {
|
||||||
const animation = reactive({
|
const animation = useState(`animation-${key}`, () => ({
|
||||||
playing: false,
|
playing: false,
|
||||||
stage1Offset: 48,
|
stage1Offset: 48,
|
||||||
stage2Offset: 48,
|
stage2Offset: 48,
|
||||||
});
|
}));
|
||||||
|
|
||||||
watch(isOpen, (current, previous) => {
|
watch(isOpen, (current, previous) => {
|
||||||
const duration = 0.1;
|
const duration = 0.1;
|
||||||
const timeline = gsap.timeline({
|
const timeline = gsap.timeline({
|
||||||
onStart: () => {
|
onStart: () => {
|
||||||
animation.playing = true;
|
animation.value.playing = true;
|
||||||
},
|
},
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
animation.playing = false;
|
animation.value.playing = false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (current === true && previous === false) {
|
if (current === true && previous === false) {
|
||||||
timeline
|
timeline
|
||||||
.fromTo(animation, { stage1Offset: 48 }, { stage1Offset: 0, duration })
|
.fromTo(
|
||||||
.fromTo(animation, { stage2Offset: 48 }, { stage2Offset: 0, duration });
|
animation.value,
|
||||||
|
{ stage1Offset: 48 },
|
||||||
|
{ stage1Offset: 0, duration },
|
||||||
|
)
|
||||||
|
.fromTo(
|
||||||
|
animation.value,
|
||||||
|
{ stage2Offset: 48 },
|
||||||
|
{ stage2Offset: 0, duration },
|
||||||
|
);
|
||||||
} else if (current === false && previous === true) {
|
} else if (current === false && previous === true) {
|
||||||
timeline
|
timeline
|
||||||
.fromTo(animation, { stage2Offset: 0 }, { stage2Offset: 48, duration })
|
.fromTo(
|
||||||
.fromTo(animation, { stage1Offset: 0 }, { stage1Offset: 48, duration });
|
animation.value,
|
||||||
|
{ stage2Offset: 0 },
|
||||||
|
{ stage2Offset: 48, duration },
|
||||||
|
)
|
||||||
|
.fromTo(
|
||||||
|
animation.value,
|
||||||
|
{ stage1Offset: 0 },
|
||||||
|
{ stage1Offset: 48, duration },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return animation;
|
return animation.value;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,37 +1,28 @@
|
|||||||
export const useSettingsStore = defineStore("settings", {
|
export const useSettingsStore = defineStore("settings", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
activeMenu: null as string | null,
|
currentMenu: null as string | null,
|
||||||
navigationStack: [] as string[],
|
currentSubMenu: null as string | null,
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
isMenuOpen: (state) => (menu: string) => {
|
isMenuOpen: (state) => (menu: string) => {
|
||||||
if (!state.activeMenu) return false;
|
if (!state.currentMenu) return false;
|
||||||
return new RegExp(`^${menu}[A-Z]`).test(state.activeMenu);
|
return new RegExp(`^${menu}[A-Z]`).test(state.currentMenu);
|
||||||
},
|
},
|
||||||
|
|
||||||
isAnyOtherMenuOpen: (state) => (excludeMenu: string) => {
|
isAnyOtherMenuOpen: (state) => (excludeMenu: string) => {
|
||||||
if (!state.activeMenu) return false;
|
if (!state.currentMenu) return false;
|
||||||
return ["options", "clock", "user", "touchScreen"]
|
return ["options", "clock", "user", "touchScreen"]
|
||||||
.filter((m) => m !== excludeMenu)
|
.filter((m) => m !== excludeMenu)
|
||||||
.some((m) => new RegExp(`^${m}[A-Z]`).test(state.activeMenu!));
|
.some((m) => new RegExp(`^${m}[A-Z]`).test(state.currentMenu!));
|
||||||
},
|
|
||||||
|
|
||||||
currentView: (state) => {
|
|
||||||
if (state.navigationStack.length === 0) return "menu";
|
|
||||||
return state.navigationStack[state.navigationStack.length - 1];
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setActiveMenu(menu: string | null) {
|
setActiveMenu(menu: string | null) {
|
||||||
this.activeMenu = menu;
|
this.currentMenu = menu;
|
||||||
},
|
},
|
||||||
|
|
||||||
pushNavigation(view: string) {
|
setCurrentSubMenu(submenu: string | null) {
|
||||||
this.navigationStack.push(view);
|
this.currentSubMenu = submenu;
|
||||||
},
|
|
||||||
|
|
||||||
popNavigation() {
|
|
||||||
this.navigationStack.pop();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
{
|
{
|
||||||
"settings": {
|
"settings": {
|
||||||
"main": {
|
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"description": "Change system settings here. Select\nthe settings you'd like to change."
|
"description": "Change system settings here. Select\nthe settings you'd like to change.",
|
||||||
},
|
|
||||||
|
|
||||||
"options": {
|
"options": {
|
||||||
"title": "Options",
|
"title": "Options",
|
||||||
|
|||||||
Reference in New Issue
Block a user