From 96f8d5093be797f7fb9f56897e56d388012efa85 Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Mon, 29 Dec 2025 19:47:01 +0100 Subject: [PATCH] feat: remove query based routing --- .../BottomScreen/Menus/Clock/Menu.vue | 18 +++- .../Settings/BottomScreen/Menus/Menus.vue | 95 ++++++++++--------- .../BottomScreen/Menus/Options/Menu.vue | 18 +++- .../BottomScreen/Menus/TouchScreen/Menu.vue | 17 +++- .../BottomScreen/Menus/User/Color.vue | 35 ++++++- .../Settings/BottomScreen/Menus/User/Menu.vue | 18 +++- .../Settings/TopScreen/Notifications.vue | 36 +++---- app/pages/index.vue | 19 ++-- app/stores/app.ts | 47 +++++---- app/stores/contact.ts | 5 +- app/stores/home.ts | 6 +- app/stores/projects.ts | 5 +- app/stores/settings.ts | 27 +++--- app/types/app.d.ts | 1 + 14 files changed, 207 insertions(+), 140 deletions(-) create mode 100644 app/types/app.d.ts diff --git a/app/components/Settings/BottomScreen/Menus/Clock/Menu.vue b/app/components/Settings/BottomScreen/Menus/Clock/Menu.vue index 4e99dc2..9bdfbdc 100644 --- a/app/components/Settings/BottomScreen/Menus/Clock/Menu.vue +++ b/app/components/Settings/BottomScreen/Menus/Clock/Menu.vue @@ -5,13 +5,25 @@ const props = defineProps<{ }>(); const settingsStore = useSettingsStore(); +const menusContext = inject<{ + isSubmenuSelected: ComputedRef; + selectedSubmenuParent: ComputedRef; +}>("menusContext")!; const { assets } = useAssets(); -const isOpen = computed(() => settingsStore.isMenuOpen("clock")); -const isAnyOtherMenuOpen = computed(() => - settingsStore.isAnyOtherMenuOpen("clock"), +const isOpen = computed( + () => settingsStore.currentMenu === "clock" && settingsStore.menuExpanded, ); +const isAnyOtherMenuOpen = computed(() => { + if (settingsStore.currentSubMenu) { + return !settingsStore.currentSubMenu.startsWith("clock"); + } + if (menusContext.isSubmenuSelected.value) { + return menusContext.selectedSubmenuParent.value !== "clock"; + } + return false; +}); const animation = useMenuAnimation("clock", isOpen); diff --git a/app/components/Settings/BottomScreen/Menus/Menus.vue b/app/components/Settings/BottomScreen/Menus/Menus.vue index 2c98be4..43e5a57 100644 --- a/app/components/Settings/BottomScreen/Menus/Menus.vue +++ b/app/components/Settings/BottomScreen/Menus/Menus.vue @@ -11,8 +11,27 @@ import UserMenu from "./User/Menu.vue"; import TouchScreenMenu from "./TouchScreen/Menu.vue"; import Selector from "~/components/Common/ButtonSelector.vue"; +const app = useAppStore(); const settingsStore = useSettingsStore(); +type Menu = "options" | "clock" | "user" | "touchScreen"; + +const MAIN_MENUS: Menu[] = ["options", "clock", "user", "touchScreen"]; + +const isMainMenu = (button: string): button is Menu => { + return MAIN_MENUS.includes(button as Menu); +}; + +const isSubMenu = (button: string): boolean => { + return /^(options|clock|user|touchScreen)[A-Z]/.test(button); +}; + +const getParentMenu = (submenu: string): Menu => { + const match = submenu.match(/^(options|clock|user|touchScreen)/); + if (!match?.[1]) throw new Error(`Invalid submenu: '${submenu}'`); + return match[1] as Menu; +}; + const { selectedButton: selected, selectorPosition } = useButtonNavigation({ buttons: { options: [31, 119, 49, 49], @@ -35,13 +54,8 @@ const { selectedButton: selected, selectorPosition } = useButtonNavigation({ }, initialButton: "options", onButtonClick: (buttonName: string) => { - if (isSubmenu(buttonName)) { - router.push({ - query: { - screen: "settings", - menu: buttonName, - }, - }); + if (isSubMenu(buttonName)) { + settingsStore.openSubMenu(buttonName); } }, navigation: { @@ -118,51 +132,27 @@ const { selectedButton: selected, selectorPosition } = useButtonNavigation({ disabled: computed(() => settingsStore.currentSubMenu !== null), }); -const isSubmenu = (buttonName: string) => { - return ( - /^(options|clock|user|touchScreen)[A-Z]/.test(buttonName) && - !["options", "clock", "user", "touchScreen"].includes(buttonName) - ); -}; +const isSubmenuSelected = computed(() => isSubMenu(selected.value)); +const selectedSubmenuParent = computed(() => + isSubmenuSelected.value ? getParentMenu(selected.value) : null, +); -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); - } - } +provide("menusContext", { + isSubmenuSelected, + selectedSubmenuParent, }); watch( selected, (newSelected) => { if (settingsStore.currentSubMenu === null) { - if (isSubmenu(newSelected)) { - router.push({ - query: { - screen: "settings", - menu: newSelected.split(/[A-Z]/)[0], - }, - }); - } else { - router.push({ query: { screen: "settings", menu: undefined } }); + if (isMainMenu(newSelected)) { + settingsStore.openMenu(newSelected, false); + } else if (isSubMenu(newSelected)) { + const parentMenu = getParentMenu(newSelected); + if (parentMenu) { + settingsStore.openMenu(parentMenu, true); + } } } }, @@ -186,6 +176,21 @@ const viewComponents: Record = { + + + diff --git a/app/components/Settings/BottomScreen/Menus/Options/Menu.vue b/app/components/Settings/BottomScreen/Menus/Options/Menu.vue index 341e28b..410f230 100644 --- a/app/components/Settings/BottomScreen/Menus/Options/Menu.vue +++ b/app/components/Settings/BottomScreen/Menus/Options/Menu.vue @@ -5,13 +5,25 @@ const props = defineProps<{ }>(); const settingsStore = useSettingsStore(); +const menusContext = inject<{ + isSubmenuSelected: ComputedRef; + selectedSubmenuParent: ComputedRef; +}>("menusContext")!; const { assets } = useAssets(); -const isOpen = computed(() => settingsStore.isMenuOpen("options")); -const isAnyOtherMenuOpen = computed(() => - settingsStore.isAnyOtherMenuOpen("options"), +const isOpen = computed( + () => settingsStore.currentMenu === "options" && settingsStore.menuExpanded, ); +const isAnyOtherMenuOpen = computed(() => { + if (settingsStore.currentSubMenu) { + return !settingsStore.currentSubMenu.startsWith("options"); + } + if (menusContext.isSubmenuSelected.value) { + return menusContext.selectedSubmenuParent.value !== "options"; + } + return false; +}); const animation = useMenuAnimation("options", isOpen); diff --git a/app/components/Settings/BottomScreen/Menus/TouchScreen/Menu.vue b/app/components/Settings/BottomScreen/Menus/TouchScreen/Menu.vue index c68bcff..e5112b2 100644 --- a/app/components/Settings/BottomScreen/Menus/TouchScreen/Menu.vue +++ b/app/components/Settings/BottomScreen/Menus/TouchScreen/Menu.vue @@ -5,12 +5,23 @@ const props = defineProps<{ }>(); const settingsStore = useSettingsStore(); +const menusContext = inject<{ + isSubmenuSelected: ComputedRef; + selectedSubmenuParent: ComputedRef; +}>("menusContext")!; const { assets } = useAssets(); -const isAnyOtherMenuOpen = computed(() => - settingsStore.isAnyOtherMenuOpen("touchScreen"), -); +// TODO: i don't like this +const isAnyOtherMenuOpen = computed(() => { + if (settingsStore.currentSubMenu) { + return !settingsStore.currentSubMenu.startsWith("touchScreen"); + } + if (menusContext.isSubmenuSelected.value) { + return menusContext.selectedSubmenuParent.value !== "touchScreen"; + } + return false; +}); useRender((ctx) => { if (isAnyOtherMenuOpen.value) { diff --git a/app/components/Settings/BottomScreen/Menus/User/Color.vue b/app/components/Settings/BottomScreen/Menus/User/Color.vue index c1a9f8e..75ef6a6 100644 --- a/app/components/Settings/BottomScreen/Menus/User/Color.vue +++ b/app/components/Settings/BottomScreen/Menus/User/Color.vue @@ -7,8 +7,12 @@ const SPACING = 16; const ANIMATION_SPEED = 475; const app = useAppStore(); +const store = useSettingsStore(); const { assets } = useAssets(); +const originalSelectedCol = app.color.col; +const originalSelectedRow = app.color.row; + let selectedCol = app.color.col; let selectedRow = app.color.row; let selectorX = GRID_START_X + selectedCol * (CELL_SIZE + SPACING) - 4; @@ -94,7 +98,32 @@ useRender((ctx, deltaTime) => { ctx.fillRect(192, 96, 32, 32); }); -defineOptions({ - render: () => null, -}); +const handleCancel = () => { + select(originalSelectedCol, originalSelectedRow); + store.closeSubMenu(); +}; + +const { open: openModal, close: closeModal } = useConfirmationModal(); + +const handleConfirm = () => { + app.save(); + openModal({ + text: "hey", + showButtons: false, + }); + setTimeout(() => { + closeModal(); + store.closeSubMenu(); + }, 2000); +}; + + diff --git a/app/components/Settings/BottomScreen/Menus/User/Menu.vue b/app/components/Settings/BottomScreen/Menus/User/Menu.vue index 88c5498..936d151 100644 --- a/app/components/Settings/BottomScreen/Menus/User/Menu.vue +++ b/app/components/Settings/BottomScreen/Menus/User/Menu.vue @@ -5,13 +5,25 @@ const props = defineProps<{ }>(); const settingsStore = useSettingsStore(); +const menusContext = inject<{ + isSubmenuSelected: ComputedRef; + selectedSubmenuParent: ComputedRef; +}>("menusContext")!; const { assets } = useAssets(); -const isOpen = computed(() => settingsStore.isMenuOpen("user")); -const isAnyOtherMenuOpen = computed(() => - settingsStore.isAnyOtherMenuOpen("user"), +const isOpen = computed( + () => settingsStore.currentMenu === "user" && settingsStore.menuExpanded, ); +const isAnyOtherMenuOpen = computed(() => { + if (settingsStore.currentSubMenu) { + return !settingsStore.currentSubMenu.startsWith("user"); + } + if (menusContext.isSubmenuSelected.value) { + return menusContext.selectedSubmenuParent.value !== "user"; + } + return false; +}); const animation = useMenuAnimation("user", isOpen); diff --git a/app/components/Settings/TopScreen/Notifications.vue b/app/components/Settings/TopScreen/Notifications.vue index ca62273..24bb733 100644 --- a/app/components/Settings/TopScreen/Notifications.vue +++ b/app/components/Settings/TopScreen/Notifications.vue @@ -30,36 +30,24 @@ const mainNotification = computed(() => ({ description: $t("settings.description"), })); +// TODO: this could be computed instead +const MENU_IMAGES: Record = { + options: assets.settings.topScreen.options.options, + clock: assets.settings.topScreen.clock.clock, + user: assets.settings.topScreen.user.user, + touchScreen: assets.settings.topScreen.touchScreen.touchScreen, +}; + const menuNotification = computed(() => { - if (!store.currentMenu) return null; - - let image: HTMLImageElement | null = null; - let id = ""; - - if (/^options[A-Z]/.test(store.currentMenu)) { - image = assets.settings.topScreen.options.options; - id = "options"; - } else if (/^clock[A-Z]/.test(store.currentMenu)) { - image = assets.settings.topScreen.clock.clock; - id = "clock"; - } else if (/^user[A-Z]/.test(store.currentMenu)) { - image = assets.settings.topScreen.user.user; - id = "user"; - } else if (/^touchScreen[A-Z]/.test(store.currentMenu)) { - image = assets.settings.topScreen.touchScreen.touchScreen; - id = "touchScreen"; - } - - if (!image) return null; + if (!store.currentMenu || !store.menuExpanded) return null; return { - image, - title: $t(`settings.${id}.title`), - description: $t(`settings.${id}.description`), + image: MENU_IMAGES[store.currentMenu]!, + title: $t(`settings.${store.currentMenu}.title`), + description: $t(`settings.${store.currentMenu}.description`), }; }); -// TODO: this could be computed instead const IMAGES_MAP: Record = { optionsStartUp: assets.settings.topScreen.options.startUp, optionsLanguage: assets.settings.topScreen.options.language, diff --git a/app/pages/index.vue b/app/pages/index.vue index 84f4321..eff0564 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -8,8 +8,7 @@ type ScreenInstance = InstanceType; const { isReady } = useAssets(); -const route = useRoute(); -const screen = computed(() => route.query.screen as string | undefined); +const app = useAppStore(); const topScreen = useTemplateRef("topScreen"); const bottomScreen = useTemplateRef("bottomScreen"); @@ -76,18 +75,18 @@ useKeyUp((key) => {
- - - - + + + +
- - - - + + + +
diff --git a/app/stores/app.ts b/app/stores/app.ts index a203fe1..9e00fc2 100644 --- a/app/stores/app.ts +++ b/app/stores/app.ts @@ -15,36 +15,35 @@ const defaultSettings = (): Settings => ({ color: { col: 0, row: 0 }, }); -const loadSettings = (): Settings => { - const stored = localStorage.getItem(STORAGE_ID); - try { - const result = settingsSchema.safeParse(JSON.parse(stored ?? "")); - if (result.success) { - return result.data; - } - } catch { - // JSON.parse failed - } - - const settings = defaultSettings(); - saveSettings(settings); - return settings; -}; - -const saveSettings = (settings: Settings) => { - localStorage.setItem(STORAGE_ID, JSON.stringify(settings)); -}; - export const useAppStore = defineStore("app", { - state: () => ({ - booted: false, - settings: loadSettings(), - }), + state: () => { + let settings: Settings; + const stored = localStorage.getItem(STORAGE_ID); + try { + settings = settingsSchema.parse(JSON.parse(stored ?? "")); + } catch { + settings = defaultSettings(); + } + + return { + booted: false, + settings, + screen: "home" as AppScreen, + }; + }, actions: { setColor(col: number, row: number) { this.settings.color = { col, row }; }, + + navigateTo(screen: AppScreen) { + this.screen = screen; + }, + + save() { + localStorage.setItem(STORAGE_ID, JSON.stringify(this.settings)); + }, }, getters: { diff --git a/app/stores/contact.ts b/app/stores/contact.ts index 586b175..388ea12 100644 --- a/app/stores/contact.ts +++ b/app/stores/contact.ts @@ -90,8 +90,9 @@ export const useContactStore = defineStore("contact", { const timeline = gsap.timeline({ onComplete: () => { setTimeout(() => { - const router = useRouter(); - router.push({ query: {} }); + this.isOutro = false; + const app = useAppStore(); + app.navigateTo("home"); }, 2000); }, }); diff --git a/app/stores/home.ts b/app/stores/home.ts index 436bed3..fcf5489 100644 --- a/app/stores/home.ts +++ b/app/stores/home.ts @@ -54,15 +54,15 @@ export const useHomeStore = defineStore("home", { ); }, - animateOutro(to: "contact" | "projects" | "theme" | "settings" | "alarm") { + animateOutro(to: AppScreen) { this.isOutro = true; this.outro.animateTop = to !== "settings"; const timeline = gsap.timeline({ onComplete: () => { this.isOutro = true; - const router = useRouter(); - router.push({ query: { screen: to } }); + const app = useAppStore(); + app.navigateTo(to); }, }); diff --git a/app/stores/projects.ts b/app/stores/projects.ts index 811c0f2..784af40 100644 --- a/app/stores/projects.ts +++ b/app/stores/projects.ts @@ -141,8 +141,9 @@ export const useProjectsStore = defineStore("projects", { ease: "none", onComplete: () => { setTimeout(() => { - const router = useRouter(); - router.push({ query: {} }); + this.isOutro = false; + const app = useAppStore(); + app.navigateTo("home"); }, 3000); }, }, diff --git a/app/stores/settings.ts b/app/stores/settings.ts index bef5c8d..1d97460 100644 --- a/app/stores/settings.ts +++ b/app/stores/settings.ts @@ -1,28 +1,25 @@ +type Menu = "options" | "clock" | "user" | "touchScreen"; + export const useSettingsStore = defineStore("settings", { state: () => ({ - currentMenu: null as string | null, + currentMenu: null as Menu | null, currentSubMenu: null as string | null, + menuExpanded: false, }), - getters: { - isMenuOpen: (state) => (menu: string) => { - if (!state.currentMenu) return false; - return new RegExp(`^${menu}[A-Z]`).test(state.currentMenu); - }, - isAnyOtherMenuOpen: (state) => (excludeMenu: string) => { - if (!state.currentMenu) return false; - return ["options", "clock", "user", "touchScreen"] - .filter((m) => m !== excludeMenu) - .some((m) => new RegExp(`^${m}[A-Z]`).test(state.currentMenu!)); - }, - }, actions: { - setActiveMenu(menu: string | null) { + openMenu(menu: Menu, expanded: boolean = false) { this.currentMenu = menu; + this.menuExpanded = expanded; + this.currentSubMenu = null; }, - setCurrentSubMenu(submenu: string | null) { + openSubMenu(submenu: string) { this.currentSubMenu = submenu; }, + + closeSubMenu() { + this.currentSubMenu = null; + }, }, }); diff --git a/app/types/app.d.ts b/app/types/app.d.ts new file mode 100644 index 0000000..0f87c2b --- /dev/null +++ b/app/types/app.d.ts @@ -0,0 +1 @@ +type AppScreen = "home" | "contact" | "projects" | "settings";