mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-10 04:22:13 +10:00
partial: mutationobserver
This commit is contained in:
10
main/app.vue
10
main/app.vue
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<NuxtLoadingIndicator color="#2563eb" />
|
||||
<NuxtLayout class="select-none w-screen h-screen">
|
||||
<NuxtLoadingIndicator color="#2563eb" />
|
||||
<NuxtLayout ref="rootNode" class="select-none w-screen h-screen">
|
||||
<NuxtPage />
|
||||
<ModalStack />
|
||||
</NuxtLayout>
|
||||
@ -15,6 +15,7 @@ import {
|
||||
initialNavigation,
|
||||
setupHooks,
|
||||
} from "./composables/state-navigation.js";
|
||||
import { createTVNavigator } from "./composables/tvmode.js";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@ -41,6 +42,11 @@ router.beforeEach(async () => {
|
||||
await fetchState();
|
||||
});
|
||||
|
||||
const rootNode = ref<HTMLElement>();
|
||||
onMounted(() => {
|
||||
const navigator = createTVNavigator(rootNode);
|
||||
});
|
||||
|
||||
setupHooks();
|
||||
initialNavigation(state);
|
||||
|
||||
|
||||
176
main/composables/tvmode.ts
Normal file
176
main/composables/tvmode.ts
Normal file
@ -0,0 +1,176 @@
|
||||
const NAVIGATE_MODIFIED_PROP = "tvnav-id";
|
||||
const NAVIGATE_INTERACT_ID = "tvnav-iid";
|
||||
|
||||
const Directions = ["left", "right", "up", "down"] as const;
|
||||
type Direction = (typeof Directions)[number];
|
||||
|
||||
const NAVIGATE_LEFT_ID = "tvnav-left";
|
||||
const NAVIGATE_RIGHT_ID = "tvnav-right";
|
||||
const NAVIGATE_UP_ID = "tvnav-up";
|
||||
const NAVIGATE_DOWN_ID = "tvnav-down";
|
||||
|
||||
interface NavigationJump {
|
||||
distance: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
class TVModeNavigator {
|
||||
private rootNode: Ref<HTMLElement | undefined>;
|
||||
private navigationNodes: Map<string, Array<Element>> = new Map();
|
||||
|
||||
constructor(rootNode: Ref<HTMLElement | undefined>) {
|
||||
this.rootNode = rootNode;
|
||||
|
||||
const thisRef = this;
|
||||
const observer = new MutationObserver((v, k) => {
|
||||
this.onMutation(thisRef, v, k);
|
||||
});
|
||||
observer.observe(document.getRootNode(), {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
recursivelyFindInteractable(element: Element): Array<Element> {
|
||||
const elements = [];
|
||||
for (const child of element.children) {
|
||||
if (!child) continue;
|
||||
if (child instanceof HTMLAnchorElement) {
|
||||
elements.push(child);
|
||||
continue;
|
||||
}
|
||||
if (child instanceof HTMLButtonElement) {
|
||||
elements.push(child);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save ourselves a function call
|
||||
if (child.children.length > 0) {
|
||||
elements.push(...this.recursivelyFindInteractable(child));
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
getInteractionId(element: Element) {
|
||||
const id = element.getAttribute(NAVIGATE_INTERACT_ID);
|
||||
if (id) return id;
|
||||
const newId = crypto.randomUUID();
|
||||
element.setAttribute(NAVIGATE_INTERACT_ID, newId);
|
||||
return newId;
|
||||
}
|
||||
|
||||
private getNavJumpKey(direction: Direction) {
|
||||
switch (direction) {
|
||||
case "down":
|
||||
return NAVIGATE_DOWN_ID;
|
||||
case "left":
|
||||
return NAVIGATE_LEFT_ID;
|
||||
case "right":
|
||||
return NAVIGATE_RIGHT_ID;
|
||||
case "up":
|
||||
return NAVIGATE_UP_ID;
|
||||
}
|
||||
|
||||
throw "Invalid direction";
|
||||
}
|
||||
|
||||
getNavJump(
|
||||
element: Element,
|
||||
direction: Direction
|
||||
): NavigationJump | undefined {
|
||||
const key = this.getNavJumpKey(direction);
|
||||
const value = element.getAttribute(key);
|
||||
if (!value) return undefined;
|
||||
const [id, distance] = value.split("/");
|
||||
return {
|
||||
distance: parseFloat(distance),
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
onMutation(
|
||||
self: TVModeNavigator,
|
||||
mutationlist: Array<MutationRecord>,
|
||||
observer: unknown
|
||||
) {
|
||||
for (const mutation of mutationlist) {
|
||||
for (const node of mutation.removedNodes) {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
||||
const el = node as Element;
|
||||
const id = el.getAttribute(NAVIGATE_MODIFIED_PROP);
|
||||
if (id) {
|
||||
self.navigationNodes.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (!node) continue;
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
||||
const el = node as Element;
|
||||
|
||||
const existingId = el.getAttribute(NAVIGATE_MODIFIED_PROP);
|
||||
if (existingId) {
|
||||
self.navigationNodes.delete(existingId);
|
||||
}
|
||||
|
||||
const interactiveNodes = self.recursivelyFindInteractable(el);
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
el.setAttribute(NAVIGATE_MODIFIED_PROP, id);
|
||||
|
||||
self.navigationNodes.set(id, interactiveNodes);
|
||||
}
|
||||
}
|
||||
|
||||
const interactiveElements = this.navigationNodes.values().toArray().flat();
|
||||
for (const element of interactiveElements) {
|
||||
const currentId = self.getInteractionId(element);
|
||||
const directionJumps: Map<Direction, NavigationJump> = new Map();
|
||||
for (const direction of Directions) {
|
||||
const jump = self.getNavJump(element, direction);
|
||||
if (jump) {
|
||||
directionJumps.set(direction, jump);
|
||||
}
|
||||
}
|
||||
|
||||
const lowerX = element.clientLeft;
|
||||
const upperX = element.clientLeft + element.clientWidth;
|
||||
const lowerY = element.clientTop;
|
||||
const upperY = element.clientTop + element.clientHeight;
|
||||
|
||||
for (const otherElement of interactiveElements) {
|
||||
const otherId = self.getInteractionId(otherElement);
|
||||
if (otherId == currentId) continue; // Skip us
|
||||
|
||||
const otherLowerX = otherElement.clientLeft;
|
||||
const otherUpperX = otherElement.clientLeft + otherElement.clientWidth;
|
||||
const otherLowerY = otherElement.clientTop;
|
||||
const otherUpperY = otherElement.clientTop + otherElement.clientHeight;
|
||||
|
||||
for (const direction of Directions) {
|
||||
let jump;
|
||||
switch (direction) {
|
||||
case "down":
|
||||
const leeway =
|
||||
0.1 * (element.clientWidth + otherElement.clientWidth);
|
||||
if (
|
||||
lowerX - leeway < otherUpperX ||
|
||||
upperX + leeway > otherLowerX
|
||||
)
|
||||
break;
|
||||
jump = {
|
||||
id: otherId,
|
||||
distance: upperY - otherUpperY,
|
||||
} satisfies NavigationJump;
|
||||
console.log(jump);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createTVNavigator = (rootNode: Ref<HTMLElement | undefined>) =>
|
||||
new TVModeNavigator(rootNode);
|
||||
24
tvmode/.gitignore
vendored
Normal file
24
tvmode/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
75
tvmode/README.md
Normal file
75
tvmode/README.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
3
tvmode/app.vue
Normal file
3
tvmode/app.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
3
tvmode/composables/nav.ts
Normal file
3
tvmode/composables/nav.ts
Normal file
@ -0,0 +1,3 @@
|
||||
class TVModeNavigator {
|
||||
|
||||
};
|
||||
5
tvmode/nuxt.config.ts
Normal file
5
tvmode/nuxt.config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true }
|
||||
})
|
||||
17
tvmode/package.json
Normal file
17
tvmode/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "^4.1.2",
|
||||
"vue": "^3.5.21",
|
||||
"vue-router": "^4.5.1"
|
||||
}
|
||||
}
|
||||
BIN
tvmode/public/favicon.ico
Normal file
BIN
tvmode/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
tvmode/public/robots.txt
Normal file
2
tvmode/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
18
tvmode/tsconfig.json
Normal file
18
tvmode/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
4839
tvmode/yarn.lock
Normal file
4839
tvmode/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user