Skip to content

Commit

Permalink
Merge branch 'develop' into 33-special-transcription-characters
Browse files Browse the repository at this point in the history
  • Loading branch information
Lev Z Király authored and Lev Z Király committed Nov 24, 2023
2 parents 18556aa + 44aa07f commit 129c63a
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 64 deletions.
63 changes: 1 addition & 62 deletions components/app-navigation-menu.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<script lang="ts" setup>
import { AppWindowIcon, CheckIcon } from "lucide-vue-next";
import type { ItemType, MainItemType } from "@/lib/api-client";
const props = defineProps<{
Expand All @@ -13,13 +11,6 @@ const emit = defineEmits<{
const { menus } = toRefs(props);
const router = useRouter();
const route = useRoute();
const windowsStore = useWindowsStore();
const { setWindowArrangement } = windowsStore;
const { arrangement: currentArrangement, registry } = storeToRefs(windowsStore);
const currentMenu = ref("");
function close() {
Expand Down Expand Up @@ -58,58 +49,6 @@ onScopeDispose(() => {
</template>
</MenubarContent>
</MenubarMenu>

<MenubarMenu>
<MenubarTrigger aria-label="Windows" class="ml-auto">
<AppWindowIcon class="h-6 w-6" />
</MenubarTrigger>
<MenubarContent align="end">
<template v-if="registry.size === 0">
<MenubarLabel>No windows open</MenubarLabel>
</template>
<template v-else>
<MenubarLabel>Windows ({{ registry.size }})</MenubarLabel>
<MenubarSeparator />
<MenubarItem
v-for="[id, item] of registry"
:key="id"
@select="
() => {
// @ts-expect-error Property missing in upstream types.
if (item.winbox.min) {
// @ts-expect-error Method missing in upstream types.
item.winbox.restore();
}
item.winbox.focus();
/** Windows are only displayed on `/`. */
if (route.path !== '/') {
void router.push('/');
}
}
"
>
{{ item.winbox.title }}
</MenubarItem>
<MenubarSeparator />
<MenubarLabel>Arrangement</MenubarLabel>
<MenubarSeparator />
<MenubarItem
v-for="(arrangement, id) of arrangements"
:key="id"
class="justify-between"
@select="
() => {
setWindowArrangement(id);
}
"
>
{{ arrangement.label }}
<CheckIcon v-if="id === currentArrangement" class="h-4 w-4" />
</MenubarItem>
</template>
</MenubarContent>
</MenubarMenu>
<WindowListDropdown :isMobile="false" />
</Menubar>
</template>
3 changes: 3 additions & 0 deletions components/app-navigation-mobile-menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ onScopeDispose(() => {

<template>
<Sheet v-model:open="isSidepanelOpen">
<Menubar class="w-full border-none">
<WindowListDropdown :isMobile="true" />
</Menubar>
<SheetTrigger aria-label="Toggle menu" class="cursor-default">
<MenuIcon class="mx-3 my-1.5 h-5 w-5" />
</SheetTrigger>
Expand Down
69 changes: 69 additions & 0 deletions components/window-list-dropdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { AppWindowIcon, CheckIcon } from "lucide-vue-next";
const props = defineProps(["isMobile"]);
const router = useRouter();
const route = useRoute();
const windowsStore = useWindowsStore();
const { setWindowArrangement } = windowsStore;
const { arrangement: currentArrangement, registry } = storeToRefs(windowsStore);
</script>

<template>
<MenubarMenu>
<MenubarTrigger aria-label="Windows" class="ml-auto">
<AppWindowIcon class="h-6 w-6" />
</MenubarTrigger>
<MenubarContent align="end">
<template v-if="registry.size === 0">
<MenubarLabel>No windows open</MenubarLabel>
</template>
<template v-else>
<MenubarLabel>Windows ({{ registry.size }})</MenubarLabel>
<MenubarSeparator />
<MenubarItem
v-for="[id, item] of registry"
:key="id"
@select="
() => {
// @ts-expect-error Property missing in upstream types.
if (item.winbox.min) {
// @ts-expect-error Method missing in upstream types.
item.winbox.restore();
}
item.winbox.focus();
/** Windows are only displayed on `/`. */
if (route.path !== '/') {
void router.push('/');
}
}
"
>
{{ item.winbox.title }}
</MenubarItem>
<template v-if="$props.isMobile === false">
<MenubarSeparator />
<MenubarLabel>Arrangement</MenubarLabel>
<MenubarSeparator />
<MenubarItem
v-for="(arrangement, id) of arrangements"
:key="id"
class="justify-between"
@select="
() => {
setWindowArrangement(id);
}
"
>
{{ arrangement.label }}
<CheckIcon v-if="id === currentArrangement" class="h-4 w-4" />
</MenubarItem>
</template>
</template>
</MenubarContent>
</MenubarMenu>
</template>
2 changes: 2 additions & 0 deletions components/window-manager.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const rootElement = ref<HTMLElement | null>(null);
const debouncedArrangeWindows = debounce(arrangeWindows, 150);
useResizeObserver(rootElement, debouncedArrangeWindows);
onMounted(windowsStore.restoreState);
</script>

<template>
Expand Down
140 changes: 138 additions & 2 deletions stores/use-windows-store.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { nanoid } from "nanoid";
import WinBox from "winbox";
import { z } from "zod";

import type { QueryDescription } from "@/lib/api-client";
import * as arrange from "@/utils/window-arrangement";

import { useToastsStore } from "./use-toasts-store";

interface WindowItemBase {
id: string;
winbox: WinBox;
Expand Down Expand Up @@ -96,18 +99,98 @@ export const arrangements = {

export type WindowArrangement = keyof typeof arrangements;

const WindowState = z.object({
x: z.number(),
y: z.number(),
z: z.number(),
width: z.number(),
height: z.number(),
kind: z.string(),
title: z.string(),
params: z.unknown(),
});
type WindowStateInferred = z.infer<typeof WindowState>;

export const useWindowsStore = defineStore("windows", () => {
const registry = ref<WindowRegistry>(new Map());
const arrangement = ref<WindowArrangement>("smart-tile");

const router = useRouter();
const route = useRoute();

const toasts = useToastsStore();

async function initializeScreen() {
await navigateTo({
path: "/",
query: { w: btoa("[]"), a: arrangement.value },
});
}

const restoreState = async () => {
if (!route.query.w || !route.query.a) {
await initializeScreen();
return;
}
const w = atob(route.query.w);

let windowStates: Array<WindowStateInferred>;
try {
windowStates = JSON.parse(w) as Array<WindowStateInferred>;
} catch (e) {
toasts.addToast({
title: "RestoreState Error: JSON parse failed",
description: e instanceof Error ? e.message : "Unknown error, check console",
});
console.error(e);
await initializeScreen();
return;
}

if (!Array.isArray(windowStates)) {
toasts.addToast({
title: "RestoreState Error: Window list is not array",
description: "Window list parameter must be an array",
});
await initializeScreen();
return;
}

await nextTick();
windowStates.forEach((w) => {
try {
WindowState.parse(w);
addWindow({
title: w.title,
kind: w.kind as WindowItemKind,
params: w.params,
x: String(w.x) + "%",
y: String(w.y) + "%",
zIndex: w.z,
height: String(w.height) + "%",
width: String(w.width) + "%",
});
} catch (e) {
toasts.addToast({
title: "RestoreState Error: WindowState parse failed",
description: e instanceof Error ? e.message : "Unknown error, check console",
});
console.error(e);
}
});
setWindowArrangement(route.query.a as WindowArrangement);
};

function addWindow<Kind extends WindowItemKind>(params: {
id?: string | null;
title: string;
kind: Kind;
params: WindowItemMap[Kind]["params"];
x?: number | string; // string support added for "px" and "%" typed values
y?: number | string;
width?: number | string;
height?: number | string;
zIndex?: number;
}) {
const rootElement = document.getElementById(windowRootId);
if (rootElement == null) return;
Expand All @@ -128,8 +211,20 @@ export const useWindowsStore = defineStore("windows", () => {
const winbox = new WinBox({
id,
title,
x: "center",
y: "center",
index: params.zIndex ? params.zIndex : undefined,
x: params.x ? params.x : "center",
y: params.y ? params.y : "center",
width: params.width,
height: params.height,
onfocus() {
updateUrl();
},
onresize() {
updateUrl();
},
onmove() {
updateUrl();
},
onclose() {
registry.value.delete(id);
return false;
Expand Down Expand Up @@ -187,9 +282,50 @@ export const useWindowsStore = defineStore("windows", () => {

watch([() => registry.value.size, arrangement], () => {
arrangeWindows();
updateUrl();
});

function serializeWindowStates() {
const windowStates: Array<WindowStateInferred> = [];

const rootElement = document.getElementById(windowRootId);
if (rootElement == null) return;
const viewport = rootElement.getBoundingClientRect();

function viewportPercentageWith2DigitPrecision(x: number, dir: "height" | "width") {
return Math.floor((10000 * x) / viewport[dir]) / 100;
}

registry.value.forEach((w) => {
windowStates.push({
x: viewportPercentageWith2DigitPrecision(w.winbox.x as number, "width"),
y: viewportPercentageWith2DigitPrecision(w.winbox.y as number, "height"),
z: w.winbox.index,
width: viewportPercentageWith2DigitPrecision(w.winbox.width as number, "width"),
height: viewportPercentageWith2DigitPrecision(w.winbox.height as number, "height"),
kind: w.kind,
title: w.winbox.title,
params: w.params,
} as WindowStateInferred);
});
console.log(JSON.stringify(windowStates));
return windowStates;
}

function updateUrl() {
const windowStates = serializeWindowStates();
// TODO: check url length, it may be too long. Note: shortest limit is 2047 (MS Edge) https://serpstat.com/blog/how-long-should-be-the-page-url-length-for-seo/
void navigateTo({
path: "/",
query: {
w: btoa(JSON.stringify(windowStates)),
a: arrangement.value,
},
});
}

return {
restoreState,
addWindow,
removeWindow,
registry,
Expand Down
1 change: 1 addition & 0 deletions vicav-app-api
Submodule vicav-app-api added at 7ae96b

0 comments on commit 129c63a

Please sign in to comment.