diff --git a/package-lock.json b/package-lock.json index 78f56f0..1566cca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,9 +43,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -60,9 +60,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -77,9 +77,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -94,9 +94,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -111,9 +111,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -128,9 +128,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -145,9 +145,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -162,9 +162,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -179,9 +179,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -196,9 +196,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -213,9 +213,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -230,9 +230,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -247,9 +247,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -264,9 +264,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -281,9 +281,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -298,9 +298,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -315,9 +315,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -332,9 +332,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -349,9 +349,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -366,9 +366,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -383,9 +383,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -400,9 +400,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -417,9 +417,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -434,9 +434,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -451,9 +451,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -468,9 +468,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -485,28 +485,28 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.11" + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, "node_modules/@fortawesome/fontawesome-free": { @@ -611,9 +611,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", "cpu": [ "arm" ], @@ -625,9 +625,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", "cpu": [ "arm64" ], @@ -639,9 +639,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", "cpu": [ "arm64" ], @@ -653,9 +653,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -667,9 +667,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", "cpu": [ "arm64" ], @@ -681,9 +681,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -695,9 +695,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], @@ -712,9 +712,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", "cpu": [ "arm" ], @@ -729,9 +729,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], @@ -746,9 +746,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ "arm64" ], @@ -763,9 +763,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", "cpu": [ "loong64" ], @@ -780,9 +780,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", "cpu": [ "loong64" ], @@ -797,9 +797,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], @@ -814,9 +814,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", "cpu": [ "ppc64" ], @@ -831,9 +831,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", - "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ "riscv64" ], @@ -848,9 +848,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", - "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", "cpu": [ "riscv64" ], @@ -865,9 +865,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", - "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ "s390x" ], @@ -882,9 +882,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", - "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ "x64" ], @@ -899,9 +899,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", - "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], @@ -916,9 +916,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", - "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", "cpu": [ "x64" ], @@ -930,9 +930,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", - "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -944,9 +944,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", - "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ "arm64" ], @@ -958,9 +958,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", - "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ "ia32" ], @@ -972,9 +972,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", - "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -986,9 +986,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", - "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", "cpu": [ "x64" ], @@ -1006,110 +1006,110 @@ "license": "MIT" }, "node_modules/@svta/cml-608": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@svta/cml-608/-/cml-608-1.0.1.tgz", - "integrity": "sha512-Y/Ier9VPUSOBnf0bJqdDyTlPrt4dDB+jk5mYHa1bnD2kcRl8qn7KkW3PRuj4w1aVN+BS2eHmsLxodt7P2hylUg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@svta/cml-608/-/cml-608-1.0.2.tgz", + "integrity": "sha512-ZEJ68330gcLKfvVv6Qifr1HR7+GldDUxzkjqSbqRK7jHtHSLSV1JAyDwlYe8+C7ABuY1bp8MpgJ4/Gg+jl+pLQ==", "license": "Apache-2.0", "engines": { "node": ">=20" } }, "node_modules/@svta/cml-cmcd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@svta/cml-cmcd/-/cml-cmcd-1.0.1.tgz", - "integrity": "sha512-eox305g+QUJgXqOLVrbgxeQHCgl90ewwQ9O2bIoo7m+hanR8Xswu5CknFnT5qqIbLOHfw80ug+raycoAFHTQ+w==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@svta/cml-cmcd/-/cml-cmcd-2.3.2.tgz", + "integrity": "sha512-SKBBjLmci0WK8HMjuv+36tVIMktonoOoxsXblOFZmB+ePPV2zjRMTD+2ZmE/1VEPJkKHENyhSjSHgJyeOlvZ1A==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-cta": "1.0.1", - "@svta/cml-structured-field-values": "1.0.1", - "@svta/cml-utils": "1.0.1" + "@svta/cml-structured-field-values": "1.1.3", + "@svta/cml-utils": "1.5.0" } }, "node_modules/@svta/cml-cmsd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@svta/cml-cmsd/-/cml-cmsd-1.0.1.tgz", - "integrity": "sha512-+nIB8PuSfb/qw+xGaArPhNqPm84tBJUbe3H1DnPL5QUsjSUI7mUIUQwAtRV1ZdEu0+80g9i0op79woB0OIwr/g==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@svta/cml-cmsd/-/cml-cmsd-1.0.6.tgz", + "integrity": "sha512-LUORV6bb0TbU4rSC2HoPqUCix1igLrXkRQXWiIyJo2OMzb14kAK/1jsW0mzY6up6w1GrjKQcjc6OwqJdo/zd/g==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-cta": "1.0.1", - "@svta/cml-structured-field-values": "1.0.1", - "@svta/cml-utils": "1.0.1" + "@svta/cml-cta": "1.0.6", + "@svta/cml-structured-field-values": "1.1.3", + "@svta/cml-utils": "1.5.0" } }, "node_modules/@svta/cml-cta": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@svta/cml-cta/-/cml-cta-1.0.1.tgz", - "integrity": "sha512-jcXqNIPv26bmFxIOFh8/c3+6WLH4qBjKpq9qTQcggDPoHuV1YBydMsJLOnYPDeK8rNMKcAkFLbnDRvyJthu5yw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@svta/cml-cta/-/cml-cta-1.0.6.tgz", + "integrity": "sha512-l13/4myTX1EbRd38nY8J1umVY5UdR/nZO6CeKqVLXnMMIayg7/fu0TueZckjTA9Loal55MGK1xbHnXVjz7OUUw==", "license": "Apache-2.0", "peer": true, "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-structured-field-values": "1.0.1", - "@svta/cml-utils": "1.0.1" + "@svta/cml-structured-field-values": "1.1.3", + "@svta/cml-utils": "1.5.0" } }, "node_modules/@svta/cml-dash": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@svta/cml-dash/-/cml-dash-1.0.1.tgz", - "integrity": "sha512-lYnD1I7FUbbQND+xICI+kcRaRXuT+whKk27R8m8me5VMVu2sMsAMc7Yui6l9sxw2cBKt8pSETPYRm/1+n4LZkw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@svta/cml-dash/-/cml-dash-1.0.6.tgz", + "integrity": "sha512-4XtHYlPrzL/dRe/8XmRQoLnTo9S86tISgrl67eUqKl5MtNTpZBYTncuPrspslPZPZROBBWNrBuepYfYUtU9CKA==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-utils": "1.0.1" + "@svta/cml-utils": "1.5.0" } }, "node_modules/@svta/cml-id3": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@svta/cml-id3/-/cml-id3-1.0.1.tgz", - "integrity": "sha512-90fGlL1qRI88CcaB89k6NG6cC3kky4Eu2jwqU4HefqK+S5k2OASUxf8JXkGz+DsdaiY7sh51vGPYdolfBZS7ug==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@svta/cml-id3/-/cml-id3-1.0.6.tgz", + "integrity": "sha512-63j8gkAnPOmOBWlp0hIZPvsIioZttdbg6/TgwITqMYbSLYVJ+6QGa/UtIP0I84NsfstANw6QdCn7i8SS08kn1A==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-utils": "1.0.1" + "@svta/cml-utils": "1.5.0" } }, "node_modules/@svta/cml-request": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@svta/cml-request/-/cml-request-1.0.1.tgz", - "integrity": "sha512-enL19BuXUjFkDDDF9jdNwUclMNPRsagnjGAetVC7xcmpDMpEx+ZLgsDip6BFNg5p6izSEk/OyujTWW1r8bDNiA==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@svta/cml-request/-/cml-request-1.0.12.tgz", + "integrity": "sha512-4sJvnnoNpq58j2mCGP8k+MF6wVy/qa4gbt6kfT1dPIKmn3mPxw+JVfilhcWsUi+peK2yCZxOJJYyHj1cAcQE1w==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-utils": "1.0.1", - "@svta/cml-xml": "1.0.1" + "@svta/cml-cmcd": "2.3.2", + "@svta/cml-utils": "1.5.0", + "@svta/cml-xml": "1.1.4" } }, "node_modules/@svta/cml-structured-field-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@svta/cml-structured-field-values/-/cml-structured-field-values-1.0.1.tgz", - "integrity": "sha512-Kibciki59Pon3Pn/sl5uyrbJcSpZQDKqdCfDrokBvOdLoqqcd0oFrkEPsZBiuuIODX1CB80612xe8hopeFDyBA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@svta/cml-structured-field-values/-/cml-structured-field-values-1.1.3.tgz", + "integrity": "sha512-XqLzQOTznz6kh/nsSh8dy1kV7GQjL4csG+2U5EvLGhAqNuNUczbVg5EAsDobKDVg8xhdthdXx2UuwM+ZHQCxSg==", "license": "Apache-2.0", "peer": true, "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-utils": "1.0.1" + "@svta/cml-utils": "1.5.0" } }, "node_modules/@svta/cml-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@svta/cml-utils/-/cml-utils-1.0.1.tgz", - "integrity": "sha512-kso3curTJfp00I1mKFoBliBApjn4aPE+wF8cPucf7TrSDVWZDeLLuF14ASmUE9m7rnrqTTK4878VvmXaXcCCfQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@svta/cml-utils/-/cml-utils-1.5.0.tgz", + "integrity": "sha512-JMqclD7Akd+GSJiuaYNUHOP2wNtf/nauKeszlYeivSHfi0Lp3pmSW5PXDvJ2dO4aPmmSOQbF0ztTsW9Vcs2Whw==", "license": "Apache-2.0", "peer": true, "engines": { @@ -1117,15 +1117,15 @@ } }, "node_modules/@svta/cml-xml": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.0.1.tgz", - "integrity": "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.1.4.tgz", + "integrity": "sha512-jbixqjiJIc16SGxylHwiOzO+DuhkGfuP+fJ9AHeVJKdFDKnabgfCDpnp6dvZpZnjMj4nHvzVtuUV7RISPIwYXw==", "license": "Apache-2.0", "engines": { "node": ">=20" }, "peerDependencies": { - "@svta/cml-utils": "1.0.1" + "@svta/cml-utils": "1.5.0" } }, "node_modules/@tailwindcss/forms": { @@ -1142,9 +1142,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1172,9 +1172,9 @@ "license": "MIT" }, "node_modules/@yaireo/tagify": { - "version": "4.37.1", - "resolved": "https://registry.npmjs.org/@yaireo/tagify/-/tagify-4.37.1.tgz", - "integrity": "sha512-/+0ZZ55rd4sG5Nlop2QF9D2oo3kbRgn/KlsW6oN1MwKVS7DDB2mIrGnizk740M0qzXvQfE7JhbCeWDuC8+6kmA==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/@yaireo/tagify/-/tagify-4.38.0.tgz", + "integrity": "sha512-/ZyG2NWQGNcTK4ay2aPE3ka/ZQmhTo+bUKb81SqjtQlt3VrGoxCNhmvKTV/mBJbY69BOXXeBEi+a5JLrvp6wLA==", "license": "MIT", "engines": { "node": ">=22", @@ -1187,9 +1187,9 @@ } }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1222,9 +1222,9 @@ } }, "node_modules/altcha": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/altcha/-/altcha-3.0.10.tgz", - "integrity": "sha512-JMmjNb14bfXrQc2vgjfFxH5YmEEmJ4BZz/sJ1dhiBUswvtXDlzCVBtFYtu9EzDYMfrXA22hU+4hRBY6ROsP0Yw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/altcha/-/altcha-3.2.1.tgz", + "integrity": "sha512-MXsnNmgg3BGJja0DXbflv9Bp29uyGv6wUImaaT1yLxCKOj+x9QBMpi/Vdh6XbP4Uf0PykaeQwl8ivJAvIhoo0A==", "license": "MIT", "dependencies": { "hash-wasm": "^4.12.0" @@ -1263,9 +1263,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", "dev": true, "funding": [ { @@ -1283,8 +1283,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -1300,9 +1300,9 @@ } }, "node_modules/axios": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", - "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "dev": true, "license": "MIT", "dependencies": { @@ -1313,9 +1313,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.32", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", - "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.3.tgz", + "integrity": "sha512-sbT0Ui/CZwyAyy7icT1Gw5P1LKRlFaHwaF6tDCW5YHq2X5SeeZFphBuIagopSfwSSZq3sQcbmEL072yphxm7ew==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1325,21 +1325,6 @@ "node": ">=6.0.0" } }, - "node_modules/bcp-47": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", - "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/bcp-47-match": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", @@ -1350,20 +1335,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/bcp-47-normalize": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz", - "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==", - "license": "MIT", - "dependencies": { - "bcp-47": "^2.0.0", - "bcp-47-match": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1389,9 +1360,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", "dev": true, "funding": [ { @@ -1409,10 +1380,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -1446,9 +1417,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "dev": true, "funding": [ { @@ -1587,27 +1558,28 @@ "license": "MIT" }, "node_modules/dashjs": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.1.1.tgz", - "integrity": "sha512-BzNXlUgzEjhuZ5M5hlSp1qIyQHZ7NpXAR0loP9DAAFVZj/ntL1DHeZ7qp/L3bvI4rq50X5indkAZQ3zEHWJoCA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.2.0.tgz", + "integrity": "sha512-2W2KHFN53Sk7+rtdnIfSUK/3Oov+hraMTeVZwDOTSCNKM1cQtFiJdblNzRMnl59a/QDseG7JW+PC9mh/B+CMcg==", "license": "BSD-3-Clause", "dependencies": { - "@svta/cml-608": "1.0.1", - "@svta/cml-cmcd": "1.0.1", - "@svta/cml-cmsd": "1.0.1", - "@svta/cml-dash": "1.0.1", - "@svta/cml-id3": "1.0.1", - "@svta/cml-request": "1.0.1", - "@svta/cml-xml": "1.0.1", + "@svta/cml-608": "1.0.2", + "@svta/cml-cmcd": "2.3.2", + "@svta/cml-cmsd": "1.0.6", + "@svta/cml-dash": "1.0.6", + "@svta/cml-id3": "1.0.6", + "@svta/cml-request": "1.0.12", + "@svta/cml-xml": "1.1.4", "bcp-47-match": "^2.0.3", - "bcp-47-normalize": "^2.3.0", "codem-isoboxer": "0.3.10", "fast-deep-equal": "3.1.3", - "html-entities": "^2.5.2", + "html-entities": "^2.6.0", "imsc": "^1.1.5", "localforage": "^1.10.0", - "path-browserify": "^1.0.1", - "ua-parser-js": "^1.0.37" + "path-browserify": "^1.0.1" + }, + "engines": { + "node": ">=20" } }, "node_modules/debug": { @@ -1684,9 +1656,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.361", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", - "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", + "version": "1.5.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", "dev": true, "license": "ISC" }, @@ -1739,9 +1711,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1752,32 +1724,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -1867,17 +1839,17 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -1897,6 +1869,12 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fscreen": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz", + "integrity": "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2029,9 +2007,9 @@ "license": "MIT" }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2085,30 +2063,6 @@ "sax": "1.2.1" } }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2136,16 +2090,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2370,9 +2314,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -2388,9 +2332,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.46", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", - "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, "license": "MIT", "engines": { @@ -2425,9 +2369,9 @@ } }, "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz", + "integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==", "dev": true, "license": "MIT", "engines": { @@ -2505,9 +2449,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -2524,7 +2468,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2645,9 +2589,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -2724,9 +2668,9 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "peer": true, "engines": { @@ -2734,16 +2678,16 @@ } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.2.8" } }, "node_modules/react-is": { @@ -2806,13 +2750,13 @@ } }, "node_modules/rollup": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", - "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -2822,31 +2766,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.4", - "@rollup/rollup-android-arm64": "4.60.4", - "@rollup/rollup-darwin-arm64": "4.60.4", - "@rollup/rollup-darwin-x64": "4.60.4", - "@rollup/rollup-freebsd-arm64": "4.60.4", - "@rollup/rollup-freebsd-x64": "4.60.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", - "@rollup/rollup-linux-arm-musleabihf": "4.60.4", - "@rollup/rollup-linux-arm64-gnu": "4.60.4", - "@rollup/rollup-linux-arm64-musl": "4.60.4", - "@rollup/rollup-linux-loong64-gnu": "4.60.4", - "@rollup/rollup-linux-loong64-musl": "4.60.4", - "@rollup/rollup-linux-ppc64-gnu": "4.60.4", - "@rollup/rollup-linux-ppc64-musl": "4.60.4", - "@rollup/rollup-linux-riscv64-gnu": "4.60.4", - "@rollup/rollup-linux-riscv64-musl": "4.60.4", - "@rollup/rollup-linux-s390x-gnu": "4.60.4", - "@rollup/rollup-linux-x64-gnu": "4.60.4", - "@rollup/rollup-linux-x64-musl": "4.60.4", - "@rollup/rollup-openbsd-x64": "4.60.4", - "@rollup/rollup-openharmony-arm64": "4.60.4", - "@rollup/rollup-win32-arm64-msvc": "4.60.4", - "@rollup/rollup-win32-ia32-msvc": "4.60.4", - "@rollup/rollup-win32-x64-gnu": "4.60.4", - "@rollup/rollup-win32-x64-msvc": "4.60.4", + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" } }, @@ -2989,9 +2933,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -3022,9 +2966,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -3188,41 +3132,6 @@ "postcss": "^8.0.9" } }, - "node_modules/tw-elements/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/ua-parser-js": { - "version": "1.0.41", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", - "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/unplugin": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", @@ -3280,12 +3189,13 @@ "license": "MIT" }, "node_modules/vidstack": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/vidstack/-/vidstack-1.13.1.tgz", - "integrity": "sha512-ZeOaCX9cm/4pLu0K5h8LihRMGu8eJweDIfoH6besjEoPtQWdivRCFm0GuzaOlHDWjoyYO5gx2IOAih1uDVEZ3w==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/vidstack/-/vidstack-1.15.6.tgz", + "integrity": "sha512-bdPbguNooX7Og6xqhqlRoowciTL7DibntuUwFrW6Y7K2t1jcYX7yTd05k/hfEOJrTGaY2VpU5hLQpFJDNFzK4A==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.6.10", + "fscreen": "^1.2.0", "lit-html": "^2.8.0", "media-captions": "^1.0.4", "unplugin": "^1.12.0" @@ -3295,13 +3205,13 @@ } }, "node_modules/vite": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", - "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -3422,9 +3332,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -3439,6 +3349,15 @@ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" + }, + "node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } } } } diff --git a/resources/css/app.css b/resources/css/app.css index a188059..18f8604 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,4 +1,5 @@ @import "@fortawesome/fontawesome-free/css/all.css"; +@import './player.css'; @tailwind base; @tailwind components; @@ -8,47 +9,6 @@ --breakpoint-xs: 30rem; } -/* Player */ -.plyr--full-ui input[type="range"] { - color: var(--plyr-range-fill-background, var(--plyr-color-main, var(--plyr-color-main, #c61e54))) !important; -} - -.plyr__control--overlaid { - background: var(--plyr-video-control-background-hover, var(--plyr-color-main, var(--plyr-color-main, #c61e54))) !important; -} - -.plyr--video .plyr__control.plyr__tab-focus, -.plyr--video .plyr__control:hover, -.plyr--video .plyr__control[aria-expanded="true"] { - background: var(--plyr-video-control-background-hover, var(--plyr-color-main, var(--plyr-color-main, #c61e54))) !important; -} - -.plyr__menu__container .plyr__control[role="menuitemradio"][aria-checked="true"]::before { - background: var(--plyr-control-toggle-checked-background, var(--plyr-color-main, var(--plyr-color-main, #c61e54))) !important; -} - -.plyr--full-ui { - border-radius: 15px; -} - -/* Player Engagement Heatmap, overlays the progress bar track */ -.plyr__progress__heatmap { - position: absolute; - left: -6px; - right: -6px; - bottom: 40%; - height: 48px; - pointer-events: none; - z-index: 0; -} - -.plyr__progress__heatmap-canvas { - position: absolute; - inset: 0; - width: 100%; - height: 100%; -} - /* Player Ambient */ .decoy { position: absolute; @@ -68,31 +28,6 @@ input:checked~.dot { transform: translateX(100%); } -#plyr__time_skip { - background: #c61e54; - border: 0; - border-radius: 50%; - color: #fff; - left: 50%; - min-width: 60px; - width: min-content; - max-width: 100px; - max-height: 90px; - opacity: 0; - display: table-cell; - text-align: center; - vertical-align: middle; - transform: translate(-50%, -50%); - padding-top: 15px; - padding-bottom: 15px; - position: absolute; - top: 50%; - transition: 1s; - z-index: 3; - pointer-events: none; - box-shadow: 0px 0px 45px #000000; -} - /* DL Button Glow */ .hover\:glow:hover { filter: drop-shadow(0px 0px 7px rgba(255, 29, 72, 0.5)); diff --git a/resources/css/player.css b/resources/css/player.css new file mode 100644 index 0000000..e5c7c49 --- /dev/null +++ b/resources/css/player.css @@ -0,0 +1,796 @@ +/* ============================================================ + HStream Custom Video Player + Dark-theme-first with rose-red accents (#c61e54) + ============================================================ */ + +/* ---- Player Wrapper ---- */ +.hstream-player { + position: relative; + width: 100%; + aspect-ratio: 16 / 9; + background: #000; + border-radius: 12px; + overflow: visible; + cursor: pointer; + user-select: none; + -webkit-user-select: none; + font-family: 'Figtree', sans-serif; + direction: ltr; + isolation: isolate; +} + +.hstream-player * { + box-sizing: border-box; +} + +/* ---- Video Element ---- */ +.hstream-player__video { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + display: block; + background: #000; + border-radius: 12px; + overflow: hidden; + touch-action: manipulation; +} + +/* ---- Poster Overlay ---- */ +.hstream-player__poster { + position: absolute; + inset: 0; + z-index: 1; + background: #000; + border-radius: 12px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +.hstream-player__poster img { + width: 100%; + height: 100%; + object-fit: cover; + opacity: 1; + transition: opacity 0.3s ease; +} + +.hstream-player__poster--hidden { + pointer-events: none; + opacity: 0; + visibility: hidden; +} + +/* ---- Large Play Overlay ---- */ +.hstream-player__play-overlay { + position: absolute; + inset: 0; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; +} + +.hstream-player__play-overlay--visible { + opacity: 1; +} + +.hstream-player__play-btn { + width: 72px; + height: 72px; + border-radius: 50%; + background: rgba(198, 30, 84, 0.85); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + border: 2px solid rgba(255, 255, 255, 0.15); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 28px; + cursor: pointer; + pointer-events: auto; + transition: transform 0.15s ease, background 0.2s ease, box-shadow 0.2s ease; + box-shadow: 0 0 30px rgba(198, 30, 84, 0.4), 0 4px 20px rgba(0, 0, 0, 0.5); +} + +.hstream-player__play-btn:hover { + transform: scale(1.1); + background: rgba(225, 29, 72, 0.9); + box-shadow: 0 0 40px rgba(198, 30, 84, 0.6), 0 4px 25px rgba(0, 0, 0, 0.5); +} + +.hstream-player__play-btn:active { + transform: scale(0.95); +} + +/* ---- Controls Container ---- */ +.hstream-player__controls { + position: absolute; + bottom: 0; + left: 0; + right: 0; + z-index: 10; + background: linear-gradient(to top, rgba(10, 10, 10, 0.92), rgba(10, 10, 10, 0.75) 60%, transparent); + padding: 40px 12px 10px 12px; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + transition: opacity 0.15s ease, transform 0.12s ease; + border-radius: 0 0 12px 12px; +} + +.hstream-player__controls--hidden { + opacity: 0; + transform: translateY(8px); + pointer-events: none; +} + +/* ---- Progress Section (full width row) ---- */ +.hstream-player__progress-wrapper { + width: 100%; + padding: 6px 0; + cursor: pointer; + position: relative; +} + +.hstream-player__progress { + position: relative; + height: 6px; + width: 100%; + border-radius: 3px; + background: rgba(255, 255, 255, 0.12); + transition: height 0.15s ease, margin-top 0.15s ease; + overflow: visible; +} + +.hstream-player__progress:hover, +.hstream-player__progress--dragging { + height: 8px; + border-radius: 4px; +} + +.hstream-player__progress--dragging .hstream-player__progress-thumb { + transform: translate(-50%, -50%) scale(1.2); +} + +.hstream-player__progress-buffer { + position: absolute; + left: 0; + top: 0; + height: 100%; + border-radius: inherit; + background: rgba(255, 255, 255, 0.18); + transition: width 0.2s ease; + pointer-events: none; + z-index: 1; +} + +.hstream-player__progress-fill { + position: absolute; + left: 0; + top: 0; + height: 100%; + border-radius: inherit; + background: linear-gradient(to right, #c61e54, #e11d48); + transition: width 0.1s linear; + pointer-events: none; + z-index: 2; +} + +.hstream-player__progress-heatmap { + position: absolute; + left: 0; + right: 0; + bottom: 100%; + height: 48px; + pointer-events: none; + z-index: 0; +} + +.hstream-player__progress-heatmap-canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.hstream-player__progress-thumb { + position: absolute; + top: 50%; + width: 14px; + height: 14px; + border-radius: 50%; + background: #fff; + border: 2px solid #c61e54; + transform: translate(-50%, -50%) scale(0); + transition: transform 0.15s ease; + z-index: 4; + pointer-events: none; + box-shadow: 0 0 8px rgba(198, 30, 84, 0.5); +} + +.hstream-player__progress:hover .hstream-player__progress-thumb, +.hstream-player__progress-wrapper--active .hstream-player__progress-thumb { + transform: translate(-50%, -50%) scale(1); +} + +/* ---- Time Tooltip ---- */ +.hstream-player__time-tooltip { + position: absolute; + bottom: calc(100% + 8px); + transform: translateX(-50%); + background: rgba(10, 10, 10, 0.9); + color: #fff; + padding: 3px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; + z-index: 5; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.hstream-player__time-tooltip--visible { + opacity: 1; +} + +/* ---- Thumbnail Preview ---- */ +.hstream-player__thumbnail-preview { + position: absolute; + bottom: calc(100% + 16px); + transform: translateX(-50%); + background: #0a0a0a; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 8px; + overflow: hidden; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; + z-index: 5; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6); +} + +.hstream-player__thumbnail-preview--visible { + opacity: 1; +} + +.hstream-player__thumbnail-preview-img { + display: block; + object-fit: cover; + object-position: center; +} + +.hstream-player__thumbnail-preview-time { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 2px 6px; + background: rgba(0, 0, 0, 0.7); + color: #fff; + font-size: 10px; + text-align: center; + font-weight: 600; +} + +/* ---- Buttons ---- */ +.hstream-player__button { + display: inline-flex; + align-items: center; + justify-content: center; + background: transparent; + border: 0; + color: #fff; + font-size: 18px; + width: 36px; + height: 36px; + border-radius: 6px; + cursor: pointer; + padding: 0; + transition: background 0.15s ease, transform 0.1s ease, color 0.15s ease; + flex-shrink: 0; +} + +.hstream-player__button:hover { + background: rgba(255, 255, 255, 0.08); +} + +.hstream-player__button:active { + transform: scale(0.9); +} + +.hstream-player__button--active { + color: #c61e54; +} + +.hstream-player__button svg { + width: 20px; + height: 20px; + fill: currentColor; +} + +/* ---- Time Display ---- */ +.hstream-player__time-display { + color: rgba(255, 255, 255, 0.85); + font-size: 13px; + font-weight: 600; + font-variant-numeric: tabular-nums; + white-space: nowrap; + flex-shrink: 0; + padding: 0 2px; +} + +.hstream-player__time-separator { + opacity: 0.5; + margin: 0 1px; +} + +/* ---- Volume Wrapper ---- */ +.hstream-player__volume-wrapper { + display: inline-flex; + align-items: center; + gap: 2px; + flex-shrink: 0; + height: 36px; +} + +.hstream-player__volume-slider-container { + width: 0; + overflow: hidden; + transition: width 0.2s ease; + display: flex; + align-items: center; + padding: 6px 0; + margin: -6px 0; +} + +.hstream-player__volume-wrapper:hover .hstream-player__volume-slider-container { + width: 60px; +} + +.hstream-player--mobile .hstream-player__volume-slider-container { + width: 80px; + overflow: visible; +} + +.hstream-player__volume-slider { + -webkit-appearance: none !important; + appearance: none !important; + accent-color: #c61e54 !important; + width: 56px !important; + height: 4px !important; + border-radius: 2px !important; + outline: none !important; + cursor: pointer !important; + margin: 0 !important; + padding: 0 !important; + vertical-align: middle !important; + background: transparent !important; + border: none !important; +} + +.hstream-player__volume-slider:focus { + outline: none !important; +} + +.hstream-player__volume-slider::-webkit-slider-runnable-track { + height: 4px !important; + border-radius: 2px !important; + background: linear-gradient( + to right, + #c61e54 0%, + #c61e54 var(--volume-pct, 50%), + rgba(255, 255, 255, 0.15) var(--volume-pct, 50%), + rgba(255, 255, 255, 0.15) 100% + ) !important; +} + +.hstream-player__volume-slider::-webkit-slider-thumb { + -webkit-appearance: none !important; + appearance: none !important; + width: 13px !important; + height: 13px !important; + border-radius: 50% !important; + background: #c61e54 !important; + margin-top: -5px !important; + cursor: pointer !important; +} + +.hstream-player__volume-slider::-moz-range-track { + height: 4px !important; + border-radius: 2px !important; + background: rgba(255, 255, 255, 0.15) !important; + border: none !important; +} + +.hstream-player__volume-slider::-moz-range-progress { + height: 4px !important; + border-radius: 2px !important; + background: #c61e54 !important; + border: none !important; +} + +.hstream-player__volume-slider::-moz-range-thumb { + width: 13px !important; + height: 13px !important; + border-radius: 50% !important; + background: #c61e54 !important; + cursor: pointer !important; +} + +/* ---- Spacer ---- */ +.hstream-player__spacer { + flex: 1; + min-width: 4px; +} + +/* ---- Settings Menu ---- */ +.hstream-player__menu-container { + position: absolute; + right: 12px; + bottom: 100%; + margin-bottom: 2px; + min-width: 220px; + max-width: 280px; + background: rgba(20, 20, 20, 0.95); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + overflow: hidden; + z-index: 20; + opacity: 0; + transform: translateY(4px); + pointer-events: none; + transition: opacity 0.15s ease, transform 0.15s ease; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); +} + +.hstream-player__menu-container--open { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +.hstream-player__menu-panel { + display: none; +} + +.hstream-player__menu-panel--active { + display: block; +} + +.hstream-player__menu-back { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 10px 14px; + background: transparent; + border: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + color: rgba(255, 255, 255, 0.7); + font-size: 13px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + transition: background 0.15s ease; +} + +.hstream-player__menu-back:hover { + background: rgba(255, 255, 255, 0.04); + color: #fff; +} + +.hstream-player__menu-back i { + font-size: 10px; +} + +.hstream-player__menu-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 10px 14px; + background: transparent; + border: 0; + color: rgba(255, 255, 255, 0.85); + font-size: 13px; + font-weight: 500; + cursor: pointer; + font-family: inherit; + transition: background 0.15s ease, color 0.15s ease; + text-align: left; +} + +.hstream-player__menu-item:hover { + background: rgba(255, 255, 255, 0.06); + color: #fff; +} + +.hstream-player__menu-item--checked { + color: #c61e54; +} + +.hstream-player__menu-item-radio { + width: 14px; + height: 14px; + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.3); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: 8px; +} + +.hstream-player__menu-item--checked .hstream-player__menu-item-radio { + border-color: #c61e54; +} + +.hstream-player__menu-item--checked .hstream-player__menu-item-radio::after { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + background: #c61e54; +} + +.hstream-player__menu-value { + color: rgba(255, 255, 255, 0.45); + font-size: 12px; + margin-left: auto; + white-space: nowrap; +} + +.hstream-player__menu-item > span:first-of-type { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + margin-right: 8px; +} + +.hstream-player__menu-item--checked .hstream-player__menu-value { + color: #c61e54; +} + +.hstream-player__menu-badge { + display: inline-block; + background: #c61e54; + color: #fff; + font-size: 10px; + font-weight: 700; + padding: 1px 6px; + border-radius: 4px; + text-transform: uppercase; +} + +.hstream-player__menu-divider { + height: 1px; + background: rgba(255, 255, 255, 0.06); + margin: 4px 0; +} + +/* ---- Skip Overlay (Mobile) ---- */ +.hstream-player__skip-overlay { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + z-index: 3; + pointer-events: none; + opacity: 0; + transition: opacity 0.5s ease; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(198, 30, 84, 0.7); + border-radius: 50%; + width: 70px; + height: 70px; + color: #fff; + font-size: 14px; + font-weight: 700; + box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); +} + +.hstream-player__skip-overlay--visible { + opacity: 0.9; +} + +.hstream-player__skip-overlay i { + font-size: 22px; + margin-bottom: 2px; +} + +/* ---- Loading Spinner ---- */ +.hstream-player__loading { + position: absolute; + inset: 0; + z-index: 3; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; +} + +.hstream-player__loading--visible { + opacity: 1; +} + +.hstream-player__spinner { + width: 48px; + height: 48px; + border: 3px solid rgba(255, 255, 255, 0.15); + border-top-color: #c61e54; + border-radius: 50%; + animation: hstream-spin 0.8s linear infinite; +} + +@keyframes hstream-spin { + to { transform: rotate(360deg); } +} + +/* ---- Fullscreen (remove rounded borders) ---- */ +.hstream-player:fullscreen, +.hstream-player:-webkit-full-screen { + border-radius: 0; +} + +.hstream-player:fullscreen .hstream-player__video, +.hstream-player:-webkit-full-screen .hstream-player__video, +.hstream-player:fullscreen .hstream-player__poster, +.hstream-player:-webkit-full-screen .hstream-player__poster, +.hstream-player:fullscreen .hstream-player__controls, +.hstream-player:-webkit-full-screen .hstream-player__controls { + border-radius: 0; +} + +/* ---- Idle State (hide controls & cursor) ---- */ +.hstream-player--idle { + cursor: none; +} + +.hstream-player--idle .hstream-player__controls { + opacity: 0; + transform: translateY(8px); + pointer-events: none; +} + +/* ---- Mobile Responsive ---- */ +@media (max-width: 767px) { + .hstream-player { + border-radius: 0; + } + + .hstream-player__controls { + padding: 36px 4px 8px 4px; + gap: 2px; + border-radius: 0; + } + + .hstream-player__button { + width: 40px; + height: 40px; + font-size: 16px; + border-radius: 8px; + } + + .hstream-player__time-display { + font-size: 12px; + } + + .hstream-player__play-btn { + width: 60px; + height: 60px; + font-size: 24px; + } + + .hstream-player__menu-container { + right: 8px; + min-width: 200px; + max-width: 240px; + } + + .hstream-player__progress-wrapper { + padding: 8px 0; + } + + .hstream-player__progress { + height: 5px; + } + + .hstream-player__progress-thumb { + width: 16px; + height: 16px; + } + + .hstream-player__skip-overlay { + width: 60px; + height: 60px; + font-size: 12px; + } + + .hstream-player__skip-overlay i { + font-size: 18px; + } + + .hstream-player__thumbnail-preview { + display: none !important; + } + + .hstream-player__volume-slider-container { + width: 60px; + overflow: visible; + } + + .hstream-player__volume-slider { + accent-color: #c61e54 !important; + width: 56px !important; + height: 6px !important; + border-radius: 3px !important; + } + + .hstream-player__volume-slider::-webkit-slider-runnable-track { + height: 6px !important; + border-radius: 3px !important; + background: linear-gradient( + to right, + #c61e54 0%, + #c61e54 var(--volume-pct, 50%), + rgba(255, 255, 255, 0.15) var(--volume-pct, 50%), + rgba(255, 255, 255, 0.15) 100% + ) !important; + } + + .hstream-player__volume-slider::-webkit-slider-thumb { + width: 18px !important; + height: 18px !important; + margin-top: -6px !important; + } + + .hstream-player__volume-slider::-moz-range-track { + height: 6px !important; + border-radius: 3px !important; + background: rgba(255, 255, 255, 0.15) !important; + border: none !important; + } + + .hstream-player__volume-slider::-moz-range-progress { + height: 6px !important; + border-radius: 3px !important; + background: #c61e54 !important; + border: none !important; + } + + .hstream-player__volume-slider::-moz-range-thumb { + width: 18px !important; + height: 18px !important; + } +} + +@media (max-width: 380px) { + .hstream-player__time-display { + display: none; + } + + .hstream-player__button { + width: 36px; + height: 36px; + } +} diff --git a/resources/js/player.js b/resources/js/player.js index 9a6e10d..d0a436c 100644 --- a/resources/js/player.js +++ b/resources/js/player.js @@ -1,30 +1,17 @@ -// Plyr Player -import Plyr from 'plyr'; -import 'plyr/dist/plyr.css'; - -// Vidstack Player +// HStream Custom Video Player import 'vidstack/player/styles/default/theme.css'; import 'vidstack/player/styles/default/layouts/video.css'; import { VidstackPlayer, VidstackPlayerLayout } from 'vidstack/global/player'; - -// Dash Support import * as dashjs from 'dashjs'; - -// Subtitle Support import SubtitlesOctopus from '@jellyfin/libass-wasm'; - -// Custom JS -import { initMobileWidescreen } from './player-mobile'; -import { mobileDoubleClick } from './player-mobile' +import { HStreamPlayer } from './player/player-core'; +import { initMobileWidescreen, initMobileDoubleTap, isMobile } from './player/player-mobile'; import { playNextPlaylistVideo } from './playlist'; -import { addVideoTracks } from './player-data'; -import { addSubtitleTracks } from './player-data'; -import { serverSelectMenuItem, serverSelectSubmenu, serverSelectMenuClickToggle } from './player-server-select'; +import { addVideoTracks, addSubtitleTracks } from './player/player-data'; import { isIOS } from './detect-ios'; -import { startEngagementTracking, stopEngagementTracking } from './player-engagement'; -import { renderHeatmap } from './player-heatmap'; +import { startEngagementTracking, stopEngagementTracking } from './player/player-engagement'; +import { renderHeatmap } from './player/player-heatmap'; -// Variables var player = null; var av1Supported = (!!document.createElement('video').canPlayType('video/webm; codecs="av01.0.05M.08, opus"')); var dashSupported = dashjs.supportsMediaSource(); @@ -35,23 +22,23 @@ var captions = true; var lastTime = 0.0; var streamServer = ''; var streamServers = []; +var fallbackServers = []; var streamServerIndex = 0; var streamServerCount = 0; var ambientMode = true; -var serverFallback = false; var saveInterval; var watchTracked = false; +var subtitleInstance = null; -// Track that the user watched at least 10 seconds of the video function trackWatchTime() { if (watchTracked) return; - var video = document.getElementsByTagName('video')[0]; - if (video && video.currentTime >= 10) { + var videoEl = document.getElementsByTagName('video')[0]; + if (videoEl && videoEl.currentTime >= 10) { watchTracked = true; var episodeId = document.getElementById('e_id').value; window.axios.post('/watched/track', { episode_id: episodeId - }).then(function (response) { + }).then(function () { console.log('Watch tracked for episode ' + episodeId); }).catch(function (error) { console.error('Failed to track watch: ' + error); @@ -59,119 +46,24 @@ function trackWatchTime() { } } -var subtitleInstance = null; - -var controls = [ - 'play-large', // The large play button in the center - 'play', // Play/pause playback - 'progress', // The progress bar and scrubber for playback and buffering - 'current-time', // The current time of playback - 'duration', // The full duration of the media - 'mute', // Toggle mute - 'volume', // Volume control - 'captions', // Toggle captions - 'settings', // Settings menu - 'fullscreen', // Toggle fullscreen -]; - -// Load Volume from LocalStorage if (localStorage.hstreamVolume) { - volume = parseFloat(localStorage.getItem('hstreamVolume')).toFixed(2); + volume = parseFloat(localStorage.getItem('hstreamVolume')); + if (!isNaN(volume)) volume = Math.max(0, Math.min(1, volume)); console.log('Loaded Audio Volume from Local Storage: ' + volume); } -// Load Captions from LocalStorage if (localStorage.hstreamCaptions) { - captions = (localStorage.getItem('hstreamCaptions') == 'true'); + captions = (localStorage.getItem('hstreamCaptions') === 'true'); console.log('Loaded Captions Status from Local Storage: ' + captions); } -// Load Muted from LocalStorage -if (localStorage.hstreamCaptions) { - muted = (localStorage.getItem('hstreamMuted') == 'true'); +if (localStorage.hstreamMuted) { + muted = (localStorage.getItem('hstreamMuted') === 'true'); console.log('Loaded Muted Status from Local Storage: ' + muted); } -// Asia Server Fallback -if (localStorage.hstreamServerFallback) { - serverFallback = (localStorage.getItem('hstreamServerFallback') == 'true'); - console.log('Loaded Server Fallback Status from Local Storage: ' + serverFallback); -} - -// Alert User when AV1 is not supported if (!av1Supported) { - document.getElementById("av1-unsupported").classList.remove("hidden"); -} - -function initDash(data, player) { - const video = document.querySelector('video'); - - data.forEach(function (el) { - if (el.mode === 'mpd' && el.size === player.config.quality.selected) { - const dash = dashjs.MediaPlayer().create(); - dash.initialize(video, el.src, true); - // Expose player and dash so they can be used from the console - window.player = player; - window.dash = dash; - } - }); -} - -function setCanvasDimension(canvas, video) { - canvas.height = video.offsetHeight; - canvas.width = video.offsetWidth; -} - -function paintStaticVideo(ctx, video) { - if (localStorage.theme == 'light') { - return; - } - if (!ambientMode) { - return; - } - ctx.drawImage(video, 0, 0, video.offsetWidth, video.offsetHeight); -} - -function toggleAmbientMode() { - let canvas = document.getElementById("ambientVideo"), ctx = canvas.getContext("2d"), video = document.getElementsByTagName('video')[0]; - if (ambientMode) { - ambientMode = false; - localStorage.ambientMode = 'false'; - setCanvasDimension(canvas, video); - document.getElementById('ambient-mode-toggle').innerHTML = 'Ambient ModeOff'; - } else { - ambientMode = true; - localStorage.ambientMode = 'true'; - setCanvasDimension(canvas, video); - paintStaticVideo(ctx, video); - document.getElementById('ambient-mode-toggle').innerHTML = 'Ambient ModeOn'; - } -} - -function toggleAsiaServer() { - if (serverFallback) { - serverFallback = false; - localStorage.hstreamServerFallback = 'false'; - document.getElementById('server-fallback-toggle').innerHTML = 'Fallback ServerOff'; - streamServers = apiResponse.stream_domains; - } else { - serverFallback = true; - localStorage.hstreamServerFallback = 'true'; - document.getElementById('server-fallback-toggle').innerHTML = 'Fallback ServerOn'; - streamServers = apiResponse.asia_stream_domains; - } - - streamServerCount = streamServers.length; - streamServerIndex = Math.floor(Math.random() * streamServerCount); - streamServer = streamServers[streamServerIndex]; - console.log('Selected Server: ' + streamServer); - - if (player) { - clearInterval(saveInterval); - stopEngagementTracking(); - player.destroy(); - } - initPlayer(); + document.getElementById('av1-unsupported').classList.remove('hidden'); } function initSubtitles(lang) { @@ -179,32 +71,28 @@ function initSubtitles(lang) { return; } - // Dispose old instance - if (subtitleInstance != null && subtitleInstance instanceof SubtitlesOctopus) { + if (subtitleInstance !== null && subtitleInstance instanceof SubtitlesOctopus) { subtitleInstance.dispose(); } - let newSubUrl = streamServer + '/' + apiResponse.stream_url + '/'; + var newSubUrl = streamServer + '/' + apiResponse.stream_url + '/'; - if (lang != 'en') { + if (lang !== 'en') { newSubUrl += 'autotrans/' + lang + '.ass'; - } - else { - newSubUrl += 'eng.ass' + } else { + newSubUrl += 'eng.ass'; } - let subFont = '/fonts/Figtree-ExtraBold.woff2'; - // Hindi font - if (lang == 'hi') { + var subFont = '/fonts/Figtree-ExtraBold.woff2'; + if (lang === 'hi') { subFont = '/fonts/Hind-SemiBold.ttf'; } - // Subtitles var options = { - video: document.getElementsByTagName('video')[0], // HTML5 video element - subUrl: newSubUrl, // Link to subtitles - workerUrl: '/build/js/subtitles-octopus-worker.js', // Link to WebAssembly-based file "libassjs-worker.js" - legacyWorkerUrl: '/build/js/subtitles-octopus-worker-legacy.js', // Link to non-WebAssembly worker + video: document.getElementsByTagName('video')[0], + subUrl: newSubUrl, + workerUrl: '/build/js/subtitles-octopus-worker.js', + legacyWorkerUrl: '/build/js/subtitles-octopus-worker-legacy.js', fonts: [subFont], renderMode: 'wasm-blend', }; @@ -212,237 +100,146 @@ function initSubtitles(lang) { subtitleInstance = new SubtitlesOctopus(options); } +function initPlayerQualityChange(data) { + if (dashSupported && !apiResponse.legacy) { + player.on('qualitychange', function () { + initDash(data); + }); + initDash(data); + } +} + +function initDash(data) { + var videoEl = document.querySelector('video'); + var quality = player.quality; + + data.forEach(function (el) { + if (el.mode === 'mpd' && el.size === quality) { + var dash = dashjs.MediaPlayer().create(); + dash.initialize(videoEl, el.src, true); + window.dash = dash; + player.dash = dash; + } + }); +} + function initPlayer() { - player = new Plyr('#player', { - controls, - quality: { - default: 720, - options: [2161, 2160, 1081, 1080, 720] + var videoEl = document.querySelector('#player'); + var container = videoEl.parentElement; + + var data = addVideoTracks(streamServer, apiResponse, av1Supported, dashSupported); + var subtitleTracks = addSubtitleTracks(streamServer, apiResponse); + var vttThumbsUrl = streamServer + '/' + apiResponse.stream_url + '/thumbs.vtt'; + + player = new HStreamPlayer({ + container: container, + video: videoEl, + apiResponse: apiResponse, + streamServer: streamServer, + streamServers: streamServers, + fallbackServers: fallbackServers, + streamServerIndex: streamServerIndex, + av1Supported: av1Supported, + dashSupported: dashSupported, + poster: apiResponse.poster, + title: apiResponse.title, + data: data, + subtitleTracks: subtitleTracks, + volume: volume, + muted: muted, + captionsActive: captions, + captionLanguage: 'en', + ambientMode: ambientMode, + isMobile: isMobile(), + quality: parseInt(localStorage.getItem('hstreamQuality')) || 1080, + lastTime: lastTime, + subtitleInstance: subtitleInstance, + onEnded: function () { + playNextPlaylistVideo(); }, - i18n: { - qualityLabel: { - 2161: "2160p48", - 2160: "2160p", - 1081: "1080p48", - 1080: "1080p", - 720: "720p" - }, - qualityBadge: { - 2161: "UHD@48", - 1081: "FHD@48", - 1080: "FHD", - }, + onTimeUpdate: function () { + trackWatchTime(); + }, + onQualityChange: function (size) { + if (dashSupported && !apiResponse.legacy) { + initDash(data); + } + }, + onVolumeChange: function () { + localStorage.setItem('hstreamVolume', player.volume.toString()); + localStorage.setItem('hstreamMuted', player.muted.toString()); + }, + onCaptionsToggle: function (active) { + localStorage.setItem('hstreamCaptions', active.toString()); + if (subtitleInstance && subtitleInstance.canvas) { + subtitleInstance.canvas.style.visibility = active ? 'visible' : 'hidden'; + } + var libassParent = document.querySelector('.libassjs-canvas-parent'); + if (libassParent) { + libassParent.style.visibility = active ? 'visible' : 'hidden'; + } + }, + onLanguageChange: function (lang) { + initSubtitles(lang); + }, + onServerChange: function (index) { + streamServerIndex = index; + var allServers = streamServers.concat(fallbackServers); + streamServer = allServers[streamServerIndex]; + console.log('Selected Server: ' + streamServer); + + if (player) { + clearInterval(saveInterval); + stopEngagementTracking(); + player.destroy(); + } + initPlayer(); }, - fullscreen: { enabled: true, fallback: true, iosNative: true } }); - // Player Track Data - var data = addVideoTracks(streamServer, apiResponse, av1Supported, dashSupported); + window.player = player; - player.source = { - type: 'video', - title: apiResponse.title, - poster: apiResponse.poster, - previewThumbnails: { - enabled: true, - src: streamServer + '/' + apiResponse.stream_url + '/thumbs.vtt', - }, - sources: data, - tracks: addSubtitleTracks(streamServer, apiResponse) - }; - - player.volume = volume; - player.muted = muted; - //player.captions.languages = ['en']; - player.captions.language = 'en'; - player.captions.active = captions; + if (!isMobile()) { + player.initThumbnails(vttThumbsUrl); + } if (dashSupported && !apiResponse.legacy) { - player.on('qualitychange', () => { - initDash(data, player); - }); - - initDash(data, player); + initDash(data); } - // Ambient Mode - let canvas = document.getElementById("ambientVideo"), ctx = canvas.getContext("2d"), video = document.getElementsByTagName('video')[0]; - setCanvasDimension(canvas, video); - paintStaticVideo(ctx, video); + initMobileWidescreen(container, videoEl); + initMobileDoubleTap(container, videoEl, player); - var allItems = document.getElementsByClassName('plyr__control--forward'); - var lastItem = allItems[allItems.length - 1]; - lastItem.insertAdjacentHTML('afterend', ''); - document.getElementById('ambient-mode-toggle').addEventListener('click', toggleAmbientMode); + var episodeId = document.getElementById('e_id').value; + player.initHeatmap(episodeId); - if (localStorage.ambientMode == 'false') { - toggleAmbientMode(); - } - - // Server select (Asia) - lastItem = allItems[allItems.length - 1]; - let value = 'Off'; - if (serverFallback) { value = 'On'; } - lastItem.insertAdjacentHTML('afterend', ''); - document.getElementById('server-fallback-toggle').addEventListener('click', toggleAsiaServer); - - var clickedPlay = false; - - player.on('play', () => { - if (!clickedPlay) { - player.stop(); - console.log("Stopped video, because user didn't click play.") - } - - // Start engagement heatmap tracking - const episodeId = document.getElementById('e_id').value; + videoEl.addEventListener('play', function onFirstPlay() { + videoEl.removeEventListener('play', onFirstPlay); startEngagementTracking(episodeId); - - setCanvasDimension(canvas, video); - console.log('Play => Function Loop()'); - var $this = video; - (function loop() { - if (!player.paused && !player.ended && localStorage.theme == 'dark' && ambientMode) { - ctx.drawImage($this, 0, 0, $this.offsetWidth, $this.offsetHeight); - setTimeout(loop, 24000 / 1001); // drawing at 30fps - } - })(); }); - player.on('seeked', () => { - paintStaticVideo(ctx, video); - if (player.currentTime > 0) { - lastTime = player.currentTime; - } - console.log('Seeked => paintStaticVideo() at ' + player.currentTime); - }); - - window.addEventListener("resize", () => { - setCanvasDimension(canvas, video); - if (player.paused) { - paintStaticVideo(ctx, video); - } - }); - - player.on('captionsenabled', () => { - document.getElementsByClassName('libassjs-canvas-parent')[0].style.visibility = 'visible'; - localStorage.setItem('hstreamCaptions', 'true'); - console.log('Set Captions Status to Local Storage: true'); - }); - - player.on('captionsdisabled', () => { - document.getElementsByClassName('libassjs-canvas-parent')[0].style.visibility = 'hidden'; - localStorage.setItem('hstreamCaptions', 'false'); - console.log('Set Captions Status to Local Storage: false'); - }); - - player.on('volumechange', () => { - console.log('Saving Audio Volume to Local Storage: ' + player.volume); - localStorage.setItem('hstreamVolume', player.volume.toString()) - console.log('Saving Audio Muted to Local Storage: ' + player.muted.toString()); - localStorage.setItem('hstreamMuted', player.muted.toString()) - }); - - player.on('ended', () => { - playNextPlaylistVideo(); - }); - - // Track watch time after 10 seconds of playback - player.on('timeupdate', () => { - trackWatchTime(); - }); - - player.on('languagechange', (event) => { - let lang = event.detail.plyr.captions.language; - - console.log('Subtitle Event ' + lang); - initSubtitles(lang); - }); - - function playerPlayTemp() { - clickedPlay = true; - } - - document.querySelectorAll('[data-plyr="play"]').forEach(play => - play.addEventListener('click', playerPlayTemp) - ); - - document.getElementsByClassName('plyr--video')[0].addEventListener('click', playerPlayTemp); - - initMobileWidescreen(); - - // Start time setTimeout(function () { - const params = new URLSearchParams(window.location.search); - const time = parseInt(params.get("t")); + var params = new URLSearchParams(window.location.search); + var time = parseInt(params.get('t')); if (!isNaN(time)) { player.currentTime = time; - console.log("Skipping to " + time) + console.log('Skipping to ' + time); } if (lastTime > 0) { player.currentTime = lastTime; - console.log("Skipping to " + lastTime) + console.log('Skipping to ' + lastTime); } }, 500); - player.on('ready', () => { - mobileDoubleClick(player); - - // Load engagement heatmap once video duration is known - const video = document.querySelector('video'); - const episodeId = document.getElementById('e_id').value; - if (video && video.duration) { - renderHeatmap(episodeId, video.duration); - } else if (video) { - video.addEventListener('loadedmetadata', function onMeta() { - video.removeEventListener('loadedmetadata', onMeta); - renderHeatmap(episodeId, video.duration); - }); - } - }); - - // Server Select - // I hate this... - var settingElements = document.getElementsByClassName('plyr__control--forward'); - if (settingElements.length == 3) { - settingElements[2].insertAdjacentHTML('afterend', serverSelectMenuItem(streamServerIndex)); - - var settingNodes = document.getElementsByClassName('plyr__menu__container')[0].childNodes[0].childNodes; - if (settingNodes.length == 4) { - document.getElementsByClassName('plyr__menu__container')[0].childNodes[0].childNodes[3].insertAdjacentHTML('afterend', serverSelectSubmenu(streamServerIndex, streamServerCount)); - } - - // Event Listeners - document.getElementById('server-select').addEventListener('click', serverSelectMenuClickToggle); - document.getElementById('server-select-list-back-btn').addEventListener('click', serverSelectMenuClickToggle); - let serverSelects = document.getElementsByClassName('change_server'); - for (let i = 0; i < serverSelects.length; i++) { - serverSelects[i].addEventListener('click', function() { - streamServerIndex = Number(this.value); - streamServer = streamServers[streamServerIndex]; - console.log('Selected Server: ' + streamServer); - - if (player) { - clearInterval(saveInterval); - stopEngagementTracking(); - player.destroy(); - } - initPlayer(); - }); - } - } - - // Periodically save last timestamp saveInterval = setInterval(function () { lastTime = player.currentTime; - console.log("Last Player Position: " + lastTime); }, 10000); } async function initVidstackPlayer() { - const videoSource = streamServer + '/' + apiResponse.stream_url + '/x264.720p.mp4'; - const videoThumbs = streamServer + '/' + apiResponse.stream_url + '/thumbs.vtt'; - const videoCaption = streamServer + '/' + apiResponse.stream_url + '/eng.vtt'; + var videoSource = streamServer + '/' + apiResponse.stream_url + '/x264.720p.mp4'; + var videoThumbs = streamServer + '/' + apiResponse.stream_url + '/thumbs.vtt'; + var videoCaption = streamServer + '/' + apiResponse.stream_url + '/eng.vtt'; player = await VidstackPlayer.create({ target: '#player', @@ -464,57 +261,45 @@ async function initVidstackPlayer() { ] }); - // Ambient Mode - let canvas = document.getElementById("ambientVideo"), ctx = canvas.getContext("2d"), video = document.getElementsByTagName('video')[0]; - setCanvasDimension(canvas, video); - paintStaticVideo(ctx, video); + window.player = player; - player.addEventListener('play', () => { - setCanvasDimension(canvas, video); - console.log('Play => Function Loop()'); - var $this = video; - (function loop() { - if (!player.paused && !player.ended && localStorage.theme == 'dark' && ambientMode) { - ctx.drawImage($this, 0, 0, $this.offsetWidth, $this.offsetHeight); - setTimeout(loop, 24000 / 1001); // drawing at 30fps - } - })(); - }); - - // Track watch time after 10 seconds of playback - player.addEventListener('time-update', () => { + player.addEventListener('time-update', function () { trackWatchTime(); }); } -// Get Data from API window.axios.post('/player/api', { episode_id: document.getElementById('e_id').value }).then(function (response) { - if (response.status == 200) { + if (response.status === 200) { apiResponse = response.data; - streamServers = apiResponse.stream_domains; + streamServers = apiResponse.stream_domains || []; + fallbackServers = apiResponse.asia_stream_domains || []; - if (serverFallback) { - streamServers = apiResponse.asia_stream_domains; + const cdnCount = streamServers.length; + if (cdnCount > 0) { + streamServerIndex = Math.floor(Math.random() * cdnCount); + streamServer = streamServers[streamServerIndex]; + } else { + const fallbackCount = fallbackServers.length; + streamServerIndex = Math.floor(Math.random() * fallbackCount); + streamServer = fallbackServers[streamServerIndex]; } - streamServerCount = streamServers.length; - streamServerIndex = Math.floor(Math.random() * streamServerCount); - streamServer = streamServers[streamServerIndex]; + streamServerCount = streamServers.length + fallbackServers.length; console.log('Selected Server: ' + streamServer + ' with Index: ' + streamServerIndex); if (!isIOS()) { initPlayer(); - } - else { - console.log("Detected Apple Shit. Using different player.") + } else { + console.log('Detected Apple device. Using Vidstack fallback player.'); initVidstackPlayer(); } - } }).catch(function (error) { - var alert = document.getElementById("player-alert"); - alert.innerText = 'The player encountered a problem: ' + error; - alert.classList.remove("hidden"); + var alert = document.getElementById('player-alert'); + if (alert) { + alert.innerText = 'The player encountered a problem: ' + error; + alert.classList.remove('hidden'); + } }); diff --git a/resources/js/player/player-core.js b/resources/js/player/player-core.js new file mode 100644 index 0000000..25e1abf --- /dev/null +++ b/resources/js/player/player-core.js @@ -0,0 +1,1391 @@ +/** + * HStreamPlayer — Custom video player with DASH.js, SubtitlesOctopus, + * ambient mode, engagement heatmap, VTT thumbnails, and mobile gestures. + */ +import { ThumbnailPreview } from './player-thumbnails'; +import { buildServerMenu } from './player-server-select'; +import { renderHeatmap, removeHeatmap } from './player-heatmap'; + +const SKIP_SECONDS = 10; +const SAVE_INTERVAL_MS = 10000; +const IDLE_TIMEOUT_MS = 1500; + +export class HStreamPlayer { + constructor(options) { + this.container = options.container; + this.video = options.video; + this.apiResponse = options.apiResponse; + this.streamServer = options.streamServer; + this.streamServers = options.streamServers || []; + this.fallbackServers = options.fallbackServers || []; + this.streamServerIndex = options.streamServerIndex || 0; + this.av1Supported = options.av1Supported || false; + this.dashSupported = options.dashSupported || false; + this.poster = options.poster || ''; + this.title = options.title || ''; + this._data = options.data || []; + this._subtitleTracks = options.subtitleTracks || []; + + this._volume = options.volume !== undefined ? options.volume : 0.5; + this._muted = options.muted || false; + this._captionsActive = options.captionsActive !== undefined ? options.captionsActive : true; + this._captionLanguage = options.captionLanguage || 'en'; + + this._ambientMode = options.ambientMode !== undefined ? options.ambientMode : true; + this._mobileFill = options.mobileFill !== undefined ? options.mobileFill : true; + this._isMobile = options.isMobile || false; + this._quality = this._resolveQuality(options.quality || 1080); + this._lastTime = options.lastTime || 0; + + this._onEnded = options.onEnded || null; + this._onTimeUpdate = options.onTimeUpdate || null; + this._onQualityChange = options.onQualityChange || null; + this._onVolumeChange = options.onVolumeChange || null; + this._onCaptionsToggle = options.onCaptionsToggle || null; + this._onLanguageChange = options.onLanguageChange || null; + this._onServerChange = options.onServerChange || null; + + this.subtitleInstance = options.subtitleInstance || null; + this.dash = null; + this._ambientCanvas = document.getElementById('ambientVideo'); + this._ambientCtx = this._ambientCanvas ? this._ambientCanvas.getContext('2d') : null; + + this._idleTimer = null; + this._saveInterval = null; + this._controlsHidden = false; + this._userInteracted = false; + this._suppressOverlay = false; + this._menuOpen = false; + this._currentMenuPanel = 'main'; + this._thumbnailPreview = null; + + this._buildDOM(); + this._setupVideo(); + this._buildControls(); + this._setupEvents(); + this._setupKeyboard(); + this._startIdleTimer(); + this._startSaveInterval(); + this._updateUI(); + } + + // ================================================================ + // DOM Construction + // ================================================================ + + _buildDOM() { + this.container.classList.add('hstream-player'); + if (this._isMobile) { + this.container.classList.add('hstream-player--mobile'); + } + this.container.setAttribute('data-hstream-player', ''); + + this.video.classList.add('hstream-player__video'); + this.video.setAttribute('playsinline', ''); + this.video.setAttribute('crossorigin', 'anonymous'); + this.video.removeAttribute('controls'); + + this._posterEl = document.createElement('div'); + this._posterEl.className = 'hstream-player__poster'; + if (this.poster) { + const img = document.createElement('img'); + img.src = this.poster; + img.alt = this.title; + img.setAttribute('draggable', 'false'); + this._posterEl.appendChild(img); + } + this.container.appendChild(this._posterEl); + + this._playOverlay = document.createElement('div'); + this._playOverlay.className = 'hstream-player__play-overlay'; + const playBtn = document.createElement('button'); + playBtn.className = 'hstream-player__play-btn'; + playBtn.setAttribute('aria-label', 'Play'); + playBtn.innerHTML = ''; + playBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.togglePlay(); + }); + this._playOverlay.appendChild(playBtn); + this.container.appendChild(this._playOverlay); + + this._loadingEl = document.createElement('div'); + this._loadingEl.className = 'hstream-player__loading'; + this._loadingEl.innerHTML = '
'; + this.container.appendChild(this._loadingEl); + + this._skipOverlay = document.createElement('div'); + this._skipOverlay.className = 'hstream-player__skip-overlay'; + this._skipOverlay.setAttribute('aria-hidden', 'true'); + this.container.appendChild(this._skipOverlay); + + this._controlsEl = document.createElement('div'); + this._controlsEl.className = 'hstream-player__controls'; + this.container.appendChild(this._controlsEl); + + this._menuContainer = document.createElement('div'); + this._menuContainer.className = 'hstream-player__menu-container'; + this._menuContainer.setAttribute('role', 'menu'); + } + + // ================================================================ + // Controls Construction + // ================================================================ + + _buildControls() { + this._progressWrapper = document.createElement('div'); + this._progressWrapper.className = 'hstream-player__progress-wrapper'; + + this._progressEl = document.createElement('div'); + this._progressEl.className = 'hstream-player__progress'; + + this._progressBuffer = document.createElement('div'); + this._progressBuffer.className = 'hstream-player__progress-buffer'; + this._progressEl.appendChild(this._progressBuffer); + + this._progressFill = document.createElement('div'); + this._progressFill.className = 'hstream-player__progress-fill'; + this._progressEl.appendChild(this._progressFill); + + this._progressThumb = document.createElement('div'); + this._progressThumb.className = 'hstream-player__progress-thumb'; + this._progressEl.appendChild(this._progressThumb); + + this._progressWrapper.appendChild(this._progressEl); + + this._timeTooltip = document.createElement('div'); + this._timeTooltip.className = 'hstream-player__time-tooltip'; + this._timeTooltip.setAttribute('aria-hidden', 'true'); + this._progressWrapper.appendChild(this._timeTooltip); + + this._controlsEl.appendChild(this._progressWrapper); + + // Play/Pause button + this._playBtn = this._createButton('play', 'fa-solid fa-play', 'Play'); + this._playBtn.setAttribute('data-action', 'play'); + this._playBtn.addEventListener('click', (e) => { e.stopPropagation(); this.togglePlay(); }); + this._controlsEl.appendChild(this._playBtn); + + // Volume + this._volumeWrapper = document.createElement('div'); + this._volumeWrapper.className = 'hstream-player__volume-wrapper'; + + this._muteBtn = this._createButton('mute', 'fa-solid fa-volume-high', 'Mute'); + this._muteBtn.addEventListener('click', (e) => { e.stopPropagation(); this.toggleMute(); }); + this._volumeWrapper.appendChild(this._muteBtn); + + this._volumeSliderContainer = document.createElement('div'); + this._volumeSliderContainer.className = 'hstream-player__volume-slider-container'; + this._volumeSlider = document.createElement('input'); + this._volumeSlider.type = 'range'; + this._volumeSlider.className = 'hstream-player__volume-slider'; + this._volumeSlider.min = '0'; + this._volumeSlider.max = '1'; + this._volumeSlider.step = '0.05'; + this._volumeSlider.value = this._muted ? 0 : this._volume; + this._volumeSlider.addEventListener('input', (e) => { + const val = parseFloat(e.target.value); + this._updateVolumeSliderTrack(val); + this.setVolume(val); + }); + this._volumeSlider.addEventListener('click', (e) => e.stopPropagation()); + this._volumeSliderContainer.appendChild(this._volumeSlider); + this._volumeSlider.style.setProperty('--volume-pct', (this._volumeSlider.value * 100) + '%'); + this._volumeWrapper.appendChild(this._volumeSliderContainer); + this._controlsEl.appendChild(this._volumeWrapper); + + // Time display + this._timeDisplay = document.createElement('span'); + this._timeDisplay.className = 'hstream-player__time-display'; + this._timeDisplay.textContent = '0:00'; + this._controlsEl.appendChild(this._timeDisplay); + + const sep = document.createElement('span'); + sep.className = 'hstream-player__time-display hstream-player__time-separator'; + sep.textContent = '/'; + this._controlsEl.appendChild(sep); + + this._durationDisplay = document.createElement('span'); + this._durationDisplay.className = 'hstream-player__time-display'; + this._durationDisplay.textContent = '0:00'; + this._controlsEl.appendChild(this._durationDisplay); + + // Spacer + const spacer = document.createElement('div'); + spacer.className = 'hstream-player__spacer'; + this._controlsEl.appendChild(spacer); + + // Captions toggle + this._captionsBtn = this._createButton('captions', 'fa-solid fa-closed-captioning', 'Subtitles'); + this._captionsBtn.addEventListener('click', (e) => { e.stopPropagation(); this.toggleCaptions(); }); + this._captionsBtn.style.display = 'none'; + this._controlsEl.appendChild(this._captionsBtn); + + // Settings button + this._settingsBtn = this._createButton('settings', 'fa-solid fa-gear', 'Settings'); + this._settingsBtn.setAttribute('data-action', 'settings'); + this._settingsBtn.addEventListener('click', (e) => { e.stopPropagation(); this._toggleMenu(); }); + this._controlsEl.appendChild(this._settingsBtn); + + // Fullscreen button + this._fullscreenBtn = this._createButton('fullscreen', 'fa-solid fa-expand', 'Fullscreen'); + this._fullscreenBtn.setAttribute('data-action', 'fullscreen'); + this._fullscreenBtn.addEventListener('click', (e) => { e.stopPropagation(); this.toggleFullscreen(); }); + this._controlsEl.appendChild(this._fullscreenBtn); + + // Build settings menu + this._buildSettingsMenu(); + this._controlsEl.appendChild(this._menuContainer); + } + + _createButton(action, iconClass, label) { + const btn = document.createElement('button'); + btn.className = 'hstream-player__button'; + btn.type = 'button'; + btn.setAttribute('aria-label', label); + btn.setAttribute('data-action', action); + btn.innerHTML = ``; + btn.title = label; + return btn; + } + + _buildSettingsMenu() { + const mainPanel = document.createElement('div'); + mainPanel.className = 'hstream-player__menu-panel hstream-player__menu-panel--active'; + mainPanel.setAttribute('data-panel', 'main'); + + const addItem = (label, value, panelId, extraClass) => { + const item = document.createElement('button'); + item.className = 'hstream-player__menu-item' + (extraClass ? ' ' + extraClass : ''); + item.type = 'button'; + item.setAttribute('role', 'menuitem'); + item.innerHTML = `${label} ${value}`; + if (panelId) { + item.addEventListener('click', (e) => { + e.stopPropagation(); + this._showMenuPanel(panelId); + }); + } + return item; + }; + + mainPanel.appendChild(addItem('Quality', this._qualityLabel(this._quality), 'quality')); + mainPanel.appendChild(addItem('Playback Speed', this._speedLabel(this.video.playbackRate), 'speed')); + + if (this._subtitleTracks.length > 1) { + mainPanel.appendChild(addItem('Subtitles', this._captionLabel(), 'captions')); + } + + const serverLabel = this.streamServerIndex < this.streamServers.length + ? 'CDN' + (this.streamServerIndex + 1) + : 'Fallback ' + (this.streamServerIndex - this.streamServers.length + 1); + mainPanel.appendChild(addItem('Server', serverLabel, 'server')); + + const divider = document.createElement('div'); + divider.className = 'hstream-player__menu-divider'; + mainPanel.appendChild(divider); + + const ambientItem = document.createElement('button'); + ambientItem.className = 'hstream-player__menu-item'; + ambientItem.type = 'button'; + ambientItem.id = 'ambient-mode-toggle-menu'; + ambientItem.innerHTML = `Ambient Mode ${this._ambientMode ? 'On' : 'Off'}`; + ambientItem.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggleAmbientMode(); + }); + mainPanel.appendChild(ambientItem); + + this._menuContainer.appendChild(mainPanel); + + this._buildQualityPanel(); + this._buildSpeedPanel(); + this._buildCaptionsPanel(); + + const serverPanel = buildServerMenu( + this.streamServers, + this.fallbackServers, + this.streamServerIndex, + (index) => { + this._closeMenu(); + if (this._onServerChange) { + this._onServerChange(index); + } + }); + this._menuContainer.appendChild(serverPanel); + } + + _buildQualityPanel() { + const panel = document.createElement('div'); + panel.className = 'hstream-player__menu-panel'; + panel.setAttribute('data-panel', 'quality'); + + const backBtn = this._createMenuBack('Quality'); + backBtn.addEventListener('click', (e) => { e.stopPropagation(); this._showMenuPanel('main'); }); + panel.appendChild(backBtn); + + const divider = document.createElement('div'); + divider.className = 'hstream-player__menu-divider'; + panel.appendChild(divider); + + const qualities = []; + for (const src of this._data) { + if (!qualities.includes(src.size)) { + qualities.push(src.size); + } + } + qualities.sort((a, b) => a - b); + + for (const q of qualities) { + const item = document.createElement('button'); + item.className = 'hstream-player__menu-item'; + if (q === this._quality) { + item.classList.add('hstream-player__menu-item--checked'); + item.setAttribute('aria-checked', 'true'); + } else { + item.setAttribute('aria-checked', 'false'); + } + item.type = 'button'; + item.setAttribute('role', 'menuitemradio'); + item.innerHTML = `${this._qualityLabel(q)}`; + item.addEventListener('click', (e) => { + e.stopPropagation(); + this.setQuality(q); + this._closeMenu(); + }); + panel.appendChild(item); + } + + this._menuContainer.appendChild(panel); + } + + _buildSpeedPanel() { + const panel = document.createElement('div'); + panel.className = 'hstream-player__menu-panel'; + panel.setAttribute('data-panel', 'speed'); + + const backBtn = this._createMenuBack('Playback Speed'); + backBtn.addEventListener('click', (e) => { e.stopPropagation(); this._showMenuPanel('main'); }); + panel.appendChild(backBtn); + + const divider = document.createElement('div'); + divider.className = 'hstream-player__menu-divider'; + panel.appendChild(divider); + + const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; + + for (const speed of speeds) { + const item = document.createElement('button'); + item.className = 'hstream-player__menu-item'; + if (speed === this.video.playbackRate) { + item.classList.add('hstream-player__menu-item--checked'); + item.setAttribute('aria-checked', 'true'); + } else { + item.setAttribute('aria-checked', 'false'); + } + item.type = 'button'; + item.setAttribute('role', 'menuitemradio'); + item.innerHTML = `${this._speedLabel(speed)}`; + item.addEventListener('click', (e) => { + e.stopPropagation(); + this.video.playbackRate = speed; + this._updateSpeedMenus(); + this._closeMenu(); + }); + panel.appendChild(item); + } + + this._menuContainer.appendChild(panel); + } + + _buildCaptionsPanel() { + const panel = document.createElement('div'); + panel.className = 'hstream-player__menu-panel'; + panel.setAttribute('data-panel', 'captions'); + + const backBtn = this._createMenuBack('Subtitles'); + backBtn.addEventListener('click', (e) => { e.stopPropagation(); this._showMenuPanel('main'); }); + panel.appendChild(backBtn); + + const divider = document.createElement('div'); + divider.className = 'hstream-player__menu-divider'; + panel.appendChild(divider); + + const offItem = document.createElement('button'); + offItem.className = 'hstream-player__menu-item'; + if (!this._captionsActive) { + offItem.classList.add('hstream-player__menu-item--checked'); + } + offItem.type = 'button'; + offItem.setAttribute('role', 'menuitemradio'); + offItem.innerHTML = 'Off'; + offItem.addEventListener('click', (e) => { + e.stopPropagation(); + if (this._captionsActive) { + this.toggleCaptions(); + } + this._closeMenu(); + }); + panel.appendChild(offItem); + + for (const track of this._subtitleTracks) { + const item = document.createElement('button'); + item.className = 'hstream-player__menu-item'; + if (this._captionsActive && track.srclang === this._captionLanguage) { + item.classList.add('hstream-player__menu-item--checked'); + } + item.type = 'button'; + item.setAttribute('role', 'menuitemradio'); + item.innerHTML = `${track.label}`; + item.addEventListener('click', (e) => { + e.stopPropagation(); + this.setCaptionLanguage(track.srclang); + this._closeMenu(); + }); + panel.appendChild(item); + } + + this._menuContainer.appendChild(panel); + } + + _createMenuBack(label) { + const btn = document.createElement('button'); + btn.className = 'hstream-player__menu-back'; + btn.type = 'button'; + btn.innerHTML = ` ${label}`; + return btn; + } + + _showMenuPanel(panelId) { + this._menuContainer.querySelectorAll('.hstream-player__menu-panel').forEach(p => { + p.classList.remove('hstream-player__menu-panel--active'); + }); + const panel = this._menuContainer.querySelector(`[data-panel="${panelId}"]`); + if (panel) { + panel.classList.add('hstream-player__menu-panel--active'); + } + this._currentMenuPanel = panelId; + } + + _updateSpeedPanel() { + const panel = this._menuContainer.querySelector('[data-panel="speed"]'); + if (!panel) return; + panel.querySelectorAll('.hstream-player__menu-item').forEach(item => { + item.classList.remove('hstream-player__menu-item--checked'); + }); + } + + _updateSpeedMenus() { + const label = this._speedLabel(this.video.playbackRate); + + const mainPanel = this._menuContainer.querySelector('[data-panel="main"]'); + if (mainPanel) { + const items = mainPanel.querySelectorAll('.hstream-player__menu-item'); + for (const item of items) { + const text = item.textContent.trim(); + if (text.startsWith('Playback Speed')) { + const valueEl = item.querySelector('.hstream-player__menu-value'); + if (valueEl) valueEl.textContent = label; + break; + } + } + } + + const speedPanel = this._menuContainer.querySelector('[data-panel="speed"]'); + if (speedPanel) { + speedPanel.querySelectorAll('.hstream-player__menu-item').forEach(item => { + const text = item.textContent.trim(); + if (text === this._speedLabel(this.video.playbackRate)) { + item.classList.add('hstream-player__menu-item--checked'); + item.setAttribute('aria-checked', 'true'); + } else { + item.classList.remove('hstream-player__menu-item--checked'); + item.setAttribute('aria-checked', 'false'); + } + }); + } + } + + // ================================================================ + // Video Setup + // ================================================================ + + _setupVideo() { + this.video.volume = this._muted ? 0 : this._volume; + this.video.muted = this._muted; + + if (this.poster) { + this.video.poster = this.poster; + } + + if (this._data.length > 0) { + this._applySource(); + } + + this.video.addEventListener('loadedmetadata', () => { + this._durationDisplay.textContent = this._formatTime(this.video.duration); + if (this._lastTime > 0) { + this.video.currentTime = this._lastTime; + } + this._updateUI(); + }); + + this.video.addEventListener('play', () => { + this._userInteracted = true; + this._hidePoster(); + this._playBtn.innerHTML = ''; + this._playBtn.setAttribute('aria-label', 'Pause'); + this._playOverlay.classList.remove('hstream-player__play-overlay--visible'); + this._startAmbientLoop(); + }); + + this.video.addEventListener('pause', () => { + this._playBtn.innerHTML = ''; + this._playBtn.setAttribute('aria-label', 'Play'); + if (!this._userInteracted && !this._suppressOverlay) { + this._showPlayOverlay(); + } + if (this._ambientMode) { + this._paintStaticAmbient(); + } + this._resetIdleTimer(); + }); + + this.video.addEventListener('ended', () => { + this._playBtn.innerHTML = ''; + this._playBtn.setAttribute('aria-label', 'Replay'); + if (!this._suppressOverlay) { + this._showPlayOverlay(); + } + this._resetIdleTimer(); + if (this._onEnded) { + this._onEnded(); + } + }); + + this.video.addEventListener('timeupdate', () => { + this._updateProgress(); + if (this._onTimeUpdate) { + this._onTimeUpdate(); + } + }); + + this.video.addEventListener('progress', () => { + this._updateBuffer(); + }); + + this.video.addEventListener('volumechange', () => { + this._volume = this.video.volume; + this._muted = this.video.muted; + this._updateVolumeUI(); + if (this._onVolumeChange) { + this._onVolumeChange(); + } + }); + + this.video.addEventListener('waiting', () => { + this._showLoading(); + }); + + this.video.addEventListener('canplay', () => { + this._hideLoading(); + }); + + this.video.addEventListener('seeking', () => { + this._showLoading(); + }); + + this.video.addEventListener('seeked', () => { + this._hideLoading(); + if (this._ambientMode) { + this._paintStaticAmbient(); + } + }); + + this.video.addEventListener('error', () => { + this._hideLoading(); + }); + } + + _applySource() { + const selectedSource = this._data.find(s => s.size === this._quality); + if (!selectedSource && this._data.length > 0) { + this._quality = this._data[0].size; + } + + const source = selectedSource || this._data[0]; + if (!source) return; + + if (source.mode === 'mpd' && this.dashSupported) { + this._initDash(source.src); + } else { + this.video.src = source.src; + if (source.type) { + this.video.type = source.type; + } + } + } + + _initDash(mpdUrl) { + if (this.dash) { + try { this.dash.destroy(); } catch (e) { /* ignore */ } + this.dash = null; + } + + if (typeof dashjs !== 'undefined') { + this.dash = dashjs.MediaPlayer().create(); + this.dash.initialize(this.video, mpdUrl, true); + this.video.src = ''; + } + } + + // ================================================================ + // Progress Bar + // ================================================================ + + _setupProgressBar() { + let dragging = false; + + const getTimeFromEvent = (e) => { + const rect = this._progressEl.getBoundingClientRect(); + const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left; + const ratio = Math.max(0, Math.min(1, x / rect.width)); + return ratio * this.video.duration; + }; + + this._progressWrapper.addEventListener('mousedown', (e) => { + dragging = true; + this._progressWrapper.classList.add('hstream-player__progress-wrapper--active'); + this.video.currentTime = getTimeFromEvent(e); + }); + + document.addEventListener('mousemove', (e) => { + if (!dragging) return; + this.video.currentTime = getTimeFromEvent(e); + }); + + document.addEventListener('mouseup', () => { + if (dragging) { + dragging = false; + this._progressWrapper.classList.remove('hstream-player__progress-wrapper--active'); + } + }); + + this._progressWrapper.addEventListener('touchstart', (e) => { + dragging = true; + this._progressWrapper.classList.add('hstream-player__progress-wrapper--active'); + this.video.currentTime = getTimeFromEvent(e); + }, { passive: true }); + + document.addEventListener('touchmove', (e) => { + if (!dragging) return; + this.video.currentTime = getTimeFromEvent(e); + }, { passive: true }); + + document.addEventListener('touchend', () => { + if (dragging) { + dragging = false; + this._progressWrapper.classList.remove('hstream-player__progress-wrapper--active'); + } + }); + + this._progressWrapper.addEventListener('mousemove', (e) => { + const rect = this._progressEl.getBoundingClientRect(); + const x = e.clientX - rect.left; + const ratio = Math.max(0, Math.min(1, x / rect.width)); + const time = ratio * this.video.duration; + if (!isNaN(time)) { + this._timeTooltip.textContent = this._formatTime(time); + this._timeTooltip.classList.add('hstream-player__time-tooltip--visible'); + this._timeTooltip.style.left = (ratio * 100) + '%'; + } + }); + + this._progressWrapper.addEventListener('mouseleave', () => { + this._timeTooltip.classList.remove('hstream-player__time-tooltip--visible'); + }); + } + + _updateProgress() { + if (!this.video.duration || isNaN(this.video.duration)) return; + const ratio = this.video.currentTime / this.video.duration; + this._progressFill.style.width = (ratio * 100) + '%'; + this._progressThumb.style.left = (ratio * 100) + '%'; + this._timeDisplay.textContent = this._formatTime(this.video.currentTime); + } + + _updateBuffer() { + if (this.video.buffered.length === 0) return; + const bufferedEnd = this.video.buffered.end(this.video.buffered.length - 1); + if (!this.video.duration || isNaN(this.video.duration)) return; + const ratio = bufferedEnd / this.video.duration; + this._progressBuffer.style.width = (ratio * 100) + '%'; + } + + // ================================================================ + // Volume UI + // ================================================================ + + _updateVolumeUI() { + const vol = this._muted ? 0 : this._volume; + this._volumeSlider.value = vol; + this._updateVolumeSliderTrack(vol); + + if (this._muted || vol === 0) { + this._muteBtn.innerHTML = ''; + this._muteBtn.setAttribute('aria-label', 'Unmute'); + } else if (vol < 0.5) { + this._muteBtn.innerHTML = ''; + this._muteBtn.setAttribute('aria-label', 'Mute'); + } else { + this._muteBtn.innerHTML = ''; + this._muteBtn.setAttribute('aria-label', 'Mute'); + } + } + + _updateVolumeSliderTrack(vol) { + this._volumeSlider.style.setProperty('--volume-pct', (vol * 100) + '%'); + } + + // ================================================================ + // Event System + // ================================================================ + + _setupEvents() { + this._setupProgressBar(); + + this.container.addEventListener('click', (e) => { + if (this._isMobile) return; + if ( + e.target.closest('.hstream-player__controls') || + e.target.closest('.hstream-player__menu-container') || + e.target.closest('.hstream-player__play-overlay') + ) { + return; + } + this.togglePlay(); + }); + + this.container.addEventListener('dblclick', (e) => { + if ( + e.target.closest('.hstream-player__controls') || + e.target.closest('.hstream-player__menu-container') + ) { + return; + } + this.toggleFullscreen(); + }); + + this.container.addEventListener('mousemove', () => this._resetIdleTimer()); + this.container.addEventListener('touchstart', () => this._resetIdleTimer(), { passive: true }); + + document.addEventListener('fullscreenchange', () => this._onFullscreenChange()); + document.addEventListener('webkitfullscreenchange', () => this._onFullscreenChange()); + + document.addEventListener('click', (e) => { + if (this._menuOpen && !this.container.contains(e.target)) { + this._closeMenu(); + } + }); + + window.addEventListener('resize', () => { + if (this._ambientMode) { + this._updateAmbientCanvasSize(); + if (this.video.paused) { + this._paintStaticAmbient(); + } + } + }); + } + + _setupKeyboard() { + this._keyHandler = (e) => { + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return; + + switch (e.key.toLowerCase()) { + case ' ': + case 'k': + e.preventDefault(); + this.togglePlay(); + break; + case 'arrowleft': + e.preventDefault(); + this.video.currentTime = Math.max(0, this.video.currentTime - 5); + break; + case 'arrowright': + e.preventDefault(); + this.video.currentTime = Math.min(this.video.duration, this.video.currentTime + 5); + break; + case 'arrowup': + e.preventDefault(); + this.setVolume(Math.min(1, this._volume + 0.05)); + break; + case 'arrowdown': + e.preventDefault(); + this.setVolume(Math.max(0, this._volume - 0.05)); + break; + case 'f': + e.preventDefault(); + this.toggleFullscreen(); + break; + case 'm': + e.preventDefault(); + this.toggleMute(); + break; + case 'c': + e.preventDefault(); + this.toggleCaptions(); + break; + } + }; + document.addEventListener('keydown', this._keyHandler); + } + + // ================================================================ + // Public API + // ================================================================ + + play() { + this.video.play().catch(() => {}); + } + + pause() { + this.video.pause(); + } + + togglePlay() { + if (this.video.paused) { + this.play(); + } else { + this.pause(); + } + } + + stop() { + this.video.pause(); + this.video.currentTime = 0; + } + + seek(time) { + this.video.currentTime = Math.max(0, Math.min(this.video.duration || Infinity, time)); + } + + forward() { + this.video.currentTime = Math.min(this.video.duration, this.video.currentTime + SKIP_SECONDS); + } + + rewind() { + this.video.currentTime = Math.max(0, this.video.currentTime - SKIP_SECONDS); + } + + get currentTime() { + return this.video.currentTime; + } + + set currentTime(t) { + this.video.currentTime = t; + } + + get duration() { + return this.video.duration; + } + + get paused() { + return this.video.paused; + } + + get ended() { + return this.video.ended; + } + + get volume() { + return this._volume; + } + + get muted() { + return this._muted; + } + + setVolume(vol) { + this._volume = Math.max(0, Math.min(1, vol)); + this.video.volume = this._volume; + if (this._volume > 0 && this._muted) { + this._muted = false; + this.video.muted = false; + } + this._saveLocalState(); + } + + toggleMute() { + this._muted = !this._muted; + this.video.muted = this._muted; + if (!this._muted && this._volume === 0) { + this._volume = 0.5; + this.video.volume = 0.5; + } + this._saveLocalState(); + } + + toggleFullscreen() { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + const el = this.container; + if (el.requestFullscreen) { + el.requestFullscreen(); + } else if (el.webkitRequestFullscreen) { + el.webkitRequestFullscreen(); + } + } + } + + _onFullscreenChange() { + const isFullscreen = !!document.fullscreenElement; + if (isFullscreen) { + this._fullscreenBtn.innerHTML = ''; + this._fullscreenBtn.setAttribute('aria-label', 'Exit fullscreen'); + } else { + this._fullscreenBtn.innerHTML = ''; + this._fullscreenBtn.setAttribute('aria-label', 'Fullscreen'); + } + } + + toggleCaptions() { + this._captionsActive = !this._captionsActive; + + if (this.subtitleInstance && this.subtitleInstance.canvas) { + this.subtitleInstance.canvas.style.visibility = this._captionsActive ? 'visible' : 'hidden'; + } + const libassParent = document.querySelector('.libassjs-canvas-parent'); + if (libassParent) { + libassParent.style.visibility = this._captionsActive ? 'visible' : 'hidden'; + } + + if (this._captionsActive) { + this._captionsBtn.classList.add('hstream-player__button--active'); + } else { + this._captionsBtn.classList.remove('hstream-player__button--active'); + } + + localStorage.setItem('hstreamCaptions', this._captionsActive.toString()); + if (this._onCaptionsToggle) { + this._onCaptionsToggle(this._captionsActive); + } + } + + setCaptionLanguage(lang) { + this._captionLanguage = lang; + if (!this._captionsActive) { + this._captionsActive = true; + this._captionsBtn.classList.add('hstream-player__button--active'); + localStorage.setItem('hstreamCaptions', 'true'); + } + if (this._onLanguageChange) { + this._onLanguageChange(lang); + } + } + + get captionLanguage() { + return this._captionLanguage; + } + + get captionsActive() { + return this._captionsActive; + } + + setQuality(size) { + if (size === this._quality) return; + const prevTime = this.video.currentTime; + const wasPlaying = !this.video.paused; + this._suppressOverlay = true; + + this._quality = size; + this._saveLocalState(); + this._applySource(); + + if (prevTime > 0) { + const restorePosition = () => { + this._suppressOverlay = false; + if (wasPlaying) { + this.video.currentTime = prevTime; + this.video.play().catch(() => {}); + } else { + this.video.currentTime = prevTime; + } + }; + this.video.addEventListener('canplay', restorePosition, { once: true }); + setTimeout(() => { + this.video.removeEventListener('canplay', restorePosition); + this._suppressOverlay = false; + if (Math.abs(this.video.currentTime - prevTime) > 1 && prevTime > 0) { + this.video.currentTime = prevTime; + } + }, 3000); + } else { + this._suppressOverlay = false; + } + + if (this._onQualityChange) { + this._onQualityChange(size); + } + this._updateQualityMenus(); + } + + _updateQualityMenus() { + const label = this._qualityLabel(this._quality); + + const mainPanel = this._menuContainer.querySelector('[data-panel="main"]'); + if (mainPanel) { + const items = mainPanel.querySelectorAll('.hstream-player__menu-item'); + for (const item of items) { + const text = item.textContent.trim(); + if (text.startsWith('Quality')) { + const valueEl = item.querySelector('.hstream-player__menu-value'); + if (valueEl) valueEl.textContent = label; + break; + } + } + } + + const qualityPanel = this._menuContainer.querySelector('[data-panel="quality"]'); + if (qualityPanel) { + qualityPanel.querySelectorAll('.hstream-player__menu-item').forEach(item => { + const text = item.textContent.trim(); + if (text === this._qualityLabel(this._quality)) { + item.classList.add('hstream-player__menu-item--checked'); + item.setAttribute('aria-checked', 'true'); + } else { + item.classList.remove('hstream-player__menu-item--checked'); + item.setAttribute('aria-checked', 'false'); + } + }); + } + } + + get quality() { + return this._quality; + } + + get isFullscreen() { + return !!document.fullscreenElement; + } + + toggleAmbientMode() { + this._ambientMode = !this._ambientMode; + localStorage.setItem('ambientMode', this._ambientMode.toString()); + + const menuItem = document.getElementById('ambient-mode-toggle-menu'); + if (menuItem) { + menuItem.querySelector('.hstream-player__menu-value').textContent = this._ambientMode ? 'On' : 'Off'; + } + + if (this._ambientMode) { + this._updateAmbientCanvasSize(); + this._paintStaticAmbient(); + if (!this.video.paused && !this.video.ended && localStorage.getItem('theme') !== 'light') { + this._startAmbientLoop(); + } + } else { + this._clearAmbientCanvas(); + } + } + + _clearAmbientCanvas() { + if (!this._ambientCanvas || !this._ambientCtx) return; + this._ambientCtx.clearRect(0, 0, this._ambientCanvas.width, this._ambientCanvas.height); + } + + toggleMobileFill() { + this._mobileFill = !this._mobileFill; + this.video.style.objectFit = this._mobileFill ? 'cover' : 'contain'; + } + + destroy() { + this._clearIdleTimer(); + if (this._saveInterval) { + clearInterval(this._saveInterval); + this._saveInterval = null; + } + if (this._keyHandler) { + document.removeEventListener('keydown', this._keyHandler); + this._keyHandler = null; + } + if (this.dash) { + try { this.dash.destroy(); } catch (e) { /* ignore */ } + this.dash = null; + } + if (this._thumbnailPreview) { + this._thumbnailPreview.destroy(); + this._thumbnailPreview = null; + } + removeHeatmap(); + + const dynSelectors = [ + '.hstream-player__poster', + '.hstream-player__play-overlay', + '.hstream-player__loading', + '.hstream-player__skip-overlay', + '.hstream-player__controls', + '.hstream-player__thumbnail-preview', + ]; + dynSelectors.forEach(sel => { + const el = this.container.querySelector(sel); + if (el) el.remove(); + }); + + if (this.video && this.video.parentNode === this.container) { + this.video.classList.remove('hstream-player__video'); + this.video.removeAttribute('playsinline'); + this.video.removeAttribute('crossorigin'); + const cleanVideo = this.video.cloneNode(false); + this.video.replaceWith(cleanVideo); + } + + this.container.classList.remove('hstream-player', 'hstream-player--idle', 'hstream-player--mobile'); + this.container.removeAttribute('data-hstream-player'); + } + + // ================================================================ + // Ambient Mode + // ================================================================ + + _startAmbientLoop() { + if (!this._ambientCanvas || !this._ambientCtx) return; + if (!this._ambientMode) return; + if (localStorage.getItem('theme') === 'light') return; + + this._updateAmbientCanvasSize(); + + const loop = () => { + if (!this.video.paused && !this.video.ended && this._ambientMode) { + if (localStorage.getItem('theme') !== 'light') { + this._ambientCtx.drawImage( + this.video, + 0, 0, + this._ambientCanvas.width, + this._ambientCanvas.height + ); + } + setTimeout(loop, 24); + } + }; + loop(); + } + + _paintStaticAmbient() { + if (!this._ambientCanvas || !this._ambientCtx) return; + if (!this._ambientMode) return; + if (localStorage.getItem('theme') === 'light') return; + + this._updateAmbientCanvasSize(); + this._ambientCtx.drawImage( + this.video, + 0, 0, + this._ambientCanvas.width, + this._ambientCanvas.height + ); + } + + _updateAmbientCanvasSize() { + if (!this._ambientCanvas) return; + this._ambientCanvas.width = this.video.offsetWidth; + this._ambientCanvas.height = this.video.offsetHeight; + } + + // ================================================================ + // UI Helpers + // ================================================================ + + _updateUI() { + this._updateVolumeUI(); + + if (this._captionsActive) { + this._captionsBtn.classList.add('hstream-player__button--active'); + } + if (this._subtitleTracks.length > 1) { + this._captionsBtn.style.display = ''; + } + + if (isNaN(this.video.duration)) { + this._durationDisplay.textContent = '0:00'; + } else { + this._durationDisplay.textContent = this._formatTime(this.video.duration); + } + + if (!this._userInteracted && this.video.currentTime === 0) { + this._showPlayOverlay(); + } + } + + _hidePoster() { + this._posterEl.classList.add('hstream-player__poster--hidden'); + } + + _showPlayOverlay() { + this._playOverlay.classList.add('hstream-player__play-overlay--visible'); + } + + _showLoading() { + this._loadingEl.classList.add('hstream-player__loading--visible'); + } + + _hideLoading() { + this._loadingEl.classList.remove('hstream-player__loading--visible'); + } + + _toggleMenu() { + if (this._menuOpen) { + this._closeMenu(); + } else { + this._openMenu(); + } + } + + _openMenu() { + this._menuOpen = true; + this._showMenuPanel('main'); + this._menuContainer.classList.add('hstream-player__menu-container--open'); + } + + _closeMenu() { + this._menuOpen = false; + this._menuContainer.classList.remove('hstream-player__menu-container--open'); + } + + // ================================================================ + // Idle Timer (auto-hide controls) + // ================================================================ + + _startIdleTimer() { + this._resetIdleTimer(); + } + + _resetIdleTimer() { + this._clearIdleTimer(); + this.container.classList.remove('hstream-player--idle'); + this._controlsEl.classList.remove('hstream-player__controls--hidden'); + + this._idleTimer = setTimeout(() => { + if (!this.video.paused && !this._menuOpen) { + this.container.classList.add('hstream-player--idle'); + } + }, IDLE_TIMEOUT_MS); + } + + _clearIdleTimer() { + if (this._idleTimer) { + clearTimeout(this._idleTimer); + this._idleTimer = null; + } + } + + // ================================================================ + // Save Interval + // ================================================================ + + _startSaveInterval() { + this._saveInterval = setInterval(() => { + if (!this.video.paused) { + this._lastTime = this.video.currentTime; + } + }, SAVE_INTERVAL_MS); + } + + getLastTime() { + return this._lastTime; + } + + _saveLocalState() { + localStorage.setItem('hstreamVolume', this._volume.toString()); + localStorage.setItem('hstreamMuted', this._muted.toString()); + localStorage.setItem('hstreamQuality', this._quality.toString()); + } + + _resolveQuality(preferredSize) { + const sizes = this._data.map(s => s.size); + if (sizes.includes(preferredSize)) { + return preferredSize; + } + if (preferredSize === 2161) { + if (sizes.includes(2160)) return 2160; + if (sizes.includes(1080)) return 1080; + if (sizes.includes(720)) return 720; + } + if (preferredSize === 1081) { + if (sizes.includes(1080)) return 1080; + if (sizes.includes(720)) return 720; + } + if (sizes.includes(720)) return 720; + return sizes[0] || 720; + } + + // ================================================================ + // Thumbnails + // ================================================================ + + initThumbnails(vttUrl) { + if (!vttUrl) return; + this._thumbnailPreview = new ThumbnailPreview(this._progressWrapper, this.video); + this._thumbnailPreview.load(vttUrl); + } + + initHeatmap(episodeId) { + const video = this.video; + if (video && video.duration && !isNaN(video.duration)) { + renderHeatmap(episodeId, video.duration); + } else if (video) { + const onMeta = () => { + video.removeEventListener('loadedmetadata', onMeta); + if (video.duration && !isNaN(video.duration)) { + renderHeatmap(episodeId, video.duration); + } + }; + video.addEventListener('loadedmetadata', onMeta); + } + } + + // ================================================================ + // Formatting + // ================================================================ + + _formatTime(seconds) { + if (isNaN(seconds) || !isFinite(seconds)) return '0:00'; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + if (h > 0) { + return h + ':' + m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0'); + } + return m + ':' + s.toString().padStart(2, '0'); + } + + _qualityLabel(size) { + const labels = { + 2161: '2160p48', + 2160: '2160p', + 1081: '1080p48', + 1080: '1080p', + 720: '720p', + }; + return labels[size] || size + 'p'; + } + + _speedLabel(speed) { + if (speed === 1) return 'Normal'; + return speed + 'x'; + } + + _captionLabel() { + if (!this._captionsActive) return 'Off'; + const track = this._subtitleTracks.find(t => t.srclang === this._captionLanguage); + return track ? track.label : 'English'; + } + + // ================================================================ + // Expose transcript for menu entry point reuse + // ================================================================ + + getVideoElement() { + return this.video; + } +} diff --git a/resources/js/player/player-data.js b/resources/js/player/player-data.js new file mode 100644 index 0000000..8bce867 --- /dev/null +++ b/resources/js/player/player-data.js @@ -0,0 +1,94 @@ +export function addVideoTracks(streamServer, apiResponse, av1Supported, dashSupported) { + if (dashSupported) { + return addDashTracks(streamServer, apiResponse, av1Supported); + } + + return addLegacyTracks(streamServer, apiResponse, av1Supported); +} + + +function addDashTracks(streamServer, apiResponse, av1Supported) { + var data = []; + + // 720p + data.push({ + src: streamServer + '/' + apiResponse.stream_url + '/720/manifest.mpd', + size: 720, + mode: 'mpd', + }); + + if (av1Supported) { + // 1080p + data.push({ + src: streamServer + '/' + apiResponse.stream_url + '/1080/manifest.mpd', + size: 1080, + mode: 'mpd', + }); + + // 2160p + data.push({ + src: streamServer + '/' + apiResponse.stream_url + '/2160/manifest.mpd', + size: 2160, + mode: 'mpd', + }); + + if (apiResponse.interpolated == 1) { + // 1080p Interpolated + data.push({ + src: streamServer + '/' + apiResponse.stream_url + '/1080i/manifest.mpd', + size: 1081, + mode: 'mpd', + }); + } + + if (apiResponse.interpolated_uhd == 1) { + // 2160p Interpolated + data.push({ + src: streamServer + '/' + apiResponse.stream_url + '/2160i/manifest.mpd', + size: 2161, + mode: 'mpd', + }); + } + } + + return data; +} + +function addLegacyTracks(streamServer, apiResponse, av1Supported) { + var data = []; + + // 720p + data.push({ + src: streamServer + '/' + apiResponse.stream_url + '/x264.720p.mp4', + type: 'video/mp4', + size: 720, + }); + + return data; +} + + +export function addSubtitleTracks(streamServer, apiResponse) { + var data = []; + + // Default + data.push({ + kind: 'captions', + label: 'English', + srclang: 'en', + src: '', + default: true, + }); + + for (var key in apiResponse.extra_subtitles) { + data.push({ + kind: 'captions', + label: apiResponse.extra_subtitles[key] + ' (Auto Transl.)', + srclang: key, + src: '', + default: false, + }); + } + + return data; +} diff --git a/resources/js/player/player-engagement.js b/resources/js/player/player-engagement.js new file mode 100644 index 0000000..7280c7d --- /dev/null +++ b/resources/js/player/player-engagement.js @@ -0,0 +1,72 @@ +// Engagement heatmap tracking +// Samples the user's current time while playing and sends batched segment data to the server. +// Only tracks segment >= 1 (excludes 0-10s). +// Only calls the endpoint when the user is logged in. + +let engagementInterval; +let engagementSegments = new Set(); +let engagementReportInterval; +const SEGMENT_DURATION = 10; // seconds per segment +const SAMPLE_INTERVAL = 5000; // sample every 5s +const REPORT_INTERVAL = 15000; // send batch every 15s + +function isAuthenticated() { + const el = document.getElementById('auth_check'); + return el && el.value === '1'; +} + +function sendEngagement(episodeId, segments) { + if (!isAuthenticated()) return; + + window.axios.post('/player/engagement', { + episode_id: episodeId, + segments: segments, + }).catch(() => { + // Fire-and-forget: silently ignore network errors + }); +} + +export function startEngagementTracking(episodeId) { + engagementSegments.clear(); + + // Sample current time while playing + engagementInterval = setInterval(() => { + const video = document.querySelector('video'); + if (!video || video.paused) return; + + const segment = Math.floor(video.currentTime / SEGMENT_DURATION); + // Skip segment 0 (0-10s) — no need to track the very start + if (segment >= 1) { + engagementSegments.add(segment); + } + }, SAMPLE_INTERVAL); + + // Batch report to server + engagementReportInterval = setInterval(() => { + if (engagementSegments.size === 0) return; + + const segments = Array.from(engagementSegments); + engagementSegments.clear(); + + sendEngagement(episodeId, segments); + }, REPORT_INTERVAL); + + // Flush remaining segments & cleanup on page unload + const cleanup = () => { + clearInterval(engagementInterval); + clearInterval(engagementReportInterval); + + if (engagementSegments.size > 0) { + const segments = Array.from(engagementSegments); + engagementSegments.clear(); + sendEngagement(episodeId, segments); + } + }; + + window.addEventListener('beforeunload', cleanup); +} + +export function stopEngagementTracking() { + if (engagementInterval) clearInterval(engagementInterval); + if (engagementReportInterval) clearInterval(engagementReportInterval); +} \ No newline at end of file diff --git a/resources/js/player/player-heatmap.js b/resources/js/player/player-heatmap.js new file mode 100644 index 0000000..85fa05b --- /dev/null +++ b/resources/js/player/player-heatmap.js @@ -0,0 +1,214 @@ +// Engagement heatmap display +// Fetches aggregated watch data and renders vertical bar heatmap directly on the Plyr progress bar track. + +let heatmapContainer = null; +let heatmapCanvas = null; +let heatmapResizeObserver = null; + +/** + * Fetch engagement data from the server and render the heatmap. + * @param {string} episodeId - The episode ID. + * @param {number} duration - Video duration in seconds. + */ +export async function renderHeatmap(episodeId, duration) { + try { + const response = await window.axios.get(`/player/engagement/${episodeId}`); + const data = response.data; + + if (!data || Object.keys(data).length === 0) { + return; + } + + drawHeatmapCurve(data, duration); + } catch (error) { + console.error('Failed to load engagement data:', error); + } +} + +/** + * Draw a smooth area chart on a canvas element above the progress bar. + * @param {Object} data - Key-value map of segment -> watch_count. + * @param {number} duration - Video duration in seconds. + */ +function drawHeatmapCurve(data, duration) { + const SEGMENT_DURATION = 10; + const totalSegments = Math.ceil(duration / SEGMENT_DURATION); + + // Build raw counts array, filling gaps with 0 + const rawCounts = []; + for (let i = 0; i < totalSegments; i++) { + rawCounts.push(data[i] || 0); + } + + // Apply weighted moving average to smooth individual spikes + const counts = smoothData(rawCounts); + + const maxCount = Math.max(...counts, 1); + + // Remove existing heatmap if present + if (heatmapContainer) { + if (heatmapResizeObserver) heatmapResizeObserver.disconnect(); + heatmapContainer.remove(); + heatmapCanvas = null; + } + + const progressBar = document.querySelector('.hstream-player__progress'); + if (!progressBar) return; + + heatmapContainer = document.createElement('div'); + heatmapContainer.className = 'hstream-player__progress-heatmap'; + heatmapContainer.setAttribute('aria-hidden', 'true'); + + heatmapCanvas = document.createElement('canvas'); + heatmapCanvas.className = 'hstream-player__progress-heatmap-canvas'; + heatmapContainer.appendChild(heatmapCanvas); + + // Insert as first child of the progress bar so it sits behind the scrubber + progressBar.insertBefore(heatmapContainer, progressBar.firstChild); + + // Defer drawing to get container dimensions + requestAnimationFrame(() => { + requestAnimationFrame(() => drawCurve(heatmapCanvas, counts, maxCount)); + }); + + // Redraw on resize + heatmapResizeObserver = new ResizeObserver(() => { + drawCurve(heatmapCanvas, counts, maxCount); + }); + heatmapResizeObserver.observe(heatmapContainer); +} + +/** + * Apply two passes of a 5-point weighted (Gaussian) moving average. + * Near edges falls back to a 3-point average. + * Preserves the first and last points. + */ +function smoothData(data) { + if (data.length <= 2) return [...data]; + + let result = data; + + for (let pass = 0; pass < 2; pass++) { + const smoothed = [result[0]]; + + for (let i = 1; i < result.length - 1; i++) { + if (result.length > 4 && i >= 2 && i <= result.length - 3) { + smoothed.push( + (result[i - 2] * 1 + result[i - 1] * 2 + result[i] * 4 + + result[i + 1] * 2 + result[i + 2] * 1) / 10 + ); + } else { + smoothed.push((result[i - 1] + result[i] + result[i + 1]) / 3); + } + } + + smoothed.push(result[result.length - 1]); + result = smoothed; + } + + return result; +} + +/** + * Render vertical bar heatmap directly on the progress bar track. + * Each bar represents a time segment; taller bars = more engagement. + */ +function drawCurve(canvas, counts, maxCount) { + const parent = canvas.parentElement; + if (!parent) return; + + const rect = parent.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) return; + + const dpr = window.devicePixelRatio || 1; + const w = rect.width; + const h = rect.height; + + canvas.width = Math.round(w * dpr); + canvas.height = Math.round(h * dpr); + canvas.style.width = w + 'px'; + canvas.style.height = h + 'px'; + + const ctx = canvas.getContext('2d'); + ctx.scale(dpr, dpr); + ctx.clearRect(0, 0, w, h); + + if (counts.length === 0 || maxCount === 0) return; + + const paddingX = 1; + const paddingY = 2; + const drawW = w - paddingX * 2; + const drawH = h - paddingY * 2; + const n = counts.length; + + const pts = []; + for (let i = 0; i < n; i++) { + const x = paddingX + (i / (n - 1 || 1)) * drawW; + const ratio = Math.min(counts[i] / maxCount, 1); + const y = paddingY + (1 - ratio) * drawH; + pts.push({ x, y }); + } + + if (pts.length < 2) return; + + // Build the smooth path using quadratic bezier curves through midpoints + const path = [{ x: pts[0].x, y: pts[0].y }]; + + for (let i = 0; i < pts.length - 1; i++) { + const midX = (pts[i].x + pts[i + 1].x) / 2; + const midY = (pts[i].y + pts[i + 1].y) / 2; + path.push({ x: midX, y: midY, cp: { x: pts[i].x, y: pts[i].y } }); + } + path.push({ x: pts[pts.length - 1].x, y: pts[pts.length - 1].y }); + + // --- Draw a subtle glow behind the line --- + ctx.beginPath(); + ctx.moveTo(path[0].x, path[0].y); + for (let i = 1; i < path.length; i++) { + const prev = path[i - 1]; + const curr = path[i]; + if (curr.cp) { + ctx.quadraticCurveTo(curr.cp.x, curr.cp.y, curr.x, curr.y); + } else { + ctx.lineTo(curr.x, curr.y); + } + } + ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; + ctx.lineWidth = 3.0; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.stroke(); + + // --- Draw the main waveform line --- + ctx.beginPath(); + ctx.moveTo(path[0].x, path[0].y); + for (let i = 1; i < path.length; i++) { + const prev = path[i - 1]; + const curr = path[i]; + if (curr.cp) { + ctx.quadraticCurveTo(curr.cp.x, curr.cp.y, curr.x, curr.y); + } else { + ctx.lineTo(curr.x, curr.y); + } + } + ctx.strokeStyle = 'rgba(255, 255, 255, 0.55)'; + ctx.lineWidth = 1.5; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.stroke(); +} + +/** + * Remove the heatmap from the DOM. + */ +export function removeHeatmap() { + if (heatmapResizeObserver) { + heatmapResizeObserver.disconnect(); + heatmapResizeObserver = null; + } + if (heatmapContainer) { + heatmapContainer.remove(); + heatmapContainer = null; + heatmapCanvas = null; + } +} \ No newline at end of file diff --git a/resources/js/player/player-mobile.js b/resources/js/player/player-mobile.js new file mode 100644 index 0000000..21f5b0d --- /dev/null +++ b/resources/js/player/player-mobile.js @@ -0,0 +1,151 @@ +/** + * Mobile-specific player features: + * - Double-tap left/right to skip ±10s + * - Object-fit toggle button for widescreen fill + */ + +export function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +} + +export function initMobileWidescreen(playerWrapper, video) { + if (!isMobile()) { + return; + } + + const controls = playerWrapper.querySelector('.hstream-player__controls'); + if (!controls) { + return; + } + + const btn = document.createElement('button'); + btn.className = 'hstream-player__button hstream-player__mobile-fill-btn'; + btn.type = 'button'; + btn.setAttribute('aria-label', 'Toggle screen fill'); + btn.innerHTML = ''; + btn.title = 'Fill Screen'; + + const fullscreenBtn = controls.querySelector('[data-action="fullscreen"]'); + if (fullscreenBtn) { + fullscreenBtn.insertAdjacentElement('beforebegin', btn); + } else { + controls.appendChild(btn); + } + + let fillEnabled = true; + video.style.objectFit = 'cover'; + + btn.addEventListener('click', (e) => { + e.stopPropagation(); + if (fillEnabled) { + video.style.objectFit = 'contain'; + fillEnabled = false; + btn.classList.remove('hstream-player__button--active'); + } else { + video.style.objectFit = 'cover'; + fillEnabled = true; + btn.classList.add('hstream-player__button--active'); + } + }); + + btn.classList.add('hstream-player__button--active'); +} + +export function initMobileDoubleTap(playerWrapper, video, player) { + if (!isMobile()) { + return; + } + + const skipOverlay = playerWrapper.querySelector('.hstream-player__skip-overlay'); + if (!skipOverlay) { + return; + } + + class MultiClickCounter { + constructor() { + this.timers = []; + this.count = 0; + this.reseted = 0; + this.lastSide = null; + } + + clicked() { + this.count += 1; + const xcount = this.count; + this.timers.push(setTimeout(() => this.reset(xcount), 500)); + return this.count; + } + + resetCount(n) { + this.reseted = this.count; + this.count = n; + this.timers.forEach(t => clearTimeout(t)); + this.timers = []; + } + + reset(xcount) { + if (this.count > xcount) return; + this.count = 0; + this.lastSide = null; + this.reseted = 0; + skipOverlay.classList.remove('hstream-player__skip-overlay--visible'); + this.timers = []; + } + } + + const counter = new MultiClickCounter(); + + const handleTap = (e) => { + e.preventDefault(); + + const count = counter.clicked(); + if (count < 2) return; + + const rect = e.target.getBoundingClientRect(); + const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left; + const perc = (x / rect.width) * 100; + + let shouldReset = true; + const lastSide = counter.lastSide; + + if (lastSide === null) { + shouldReset = false; + } + + if (perc < 40) { + if (player.currentTime === 0) return; + counter.lastSide = 'L'; + if (shouldReset && lastSide !== 'L') { + counter.resetCount(1); + return; + } + const skipSeconds = (count - 1) * 10; + player.currentTime = Math.max(0, player.currentTime - skipSeconds); + skipOverlay.innerHTML = '' + skipSeconds + 's'; + skipOverlay.classList.add('hstream-player__skip-overlay--visible'); + setTimeout(() => skipOverlay.classList.remove('hstream-player__skip-overlay--visible'), 800); + } else if (perc > 60) { + if (player.currentTime >= player.duration) return; + counter.lastSide = 'R'; + if (shouldReset && lastSide !== 'R') { + counter.resetCount(1); + return; + } + const skipSeconds = (count - 1) * 10; + player.currentTime = Math.min(player.duration, player.currentTime + skipSeconds); + skipOverlay.innerHTML = '' + skipSeconds + 's'; + skipOverlay.classList.add('hstream-player__skip-overlay--visible'); + setTimeout(() => skipOverlay.classList.remove('hstream-player__skip-overlay--visible'), 800); + } else { + player.togglePlay(); + counter.lastSide = 'C'; + } + }; + + playerWrapper.addEventListener('click', handleTap); + + video.addEventListener('dblclick', (e) => { + e.preventDefault(); + e.stopPropagation(); + }); +} diff --git a/resources/js/player/player-server-select.js b/resources/js/player/player-server-select.js new file mode 100644 index 0000000..7d9f917 --- /dev/null +++ b/resources/js/player/player-server-select.js @@ -0,0 +1,69 @@ +/** + * Builds the server/CDN selector submenu panel for the settings menu. + * @param {string[]} streamServers - Regular CDN server URLs + * @param {string[]} fallbackServers - Fallback server URLs + * @param {number} selectedIndex - Index in the combined server list + * @param {function} onSelect - Callback receiving the combined index + */ +export function buildServerMenu(streamServers, fallbackServers, selectedIndex, onSelect) { + const panel = document.createElement('div'); + panel.className = 'hstream-player__menu-panel'; + panel.setAttribute('data-panel', 'server'); + + const backBtn = document.createElement('button'); + backBtn.className = 'hstream-player__menu-back'; + backBtn.type = 'button'; + backBtn.innerHTML = ' Server'; + backBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const menuContainer = panel.closest('.hstream-player__menu-container'); + if (menuContainer) { + menuContainer.querySelectorAll('.hstream-player__menu-panel').forEach(p => p.classList.remove('hstream-player__menu-panel--active')); + const mainPanel = menuContainer.querySelector('[data-panel="main"]'); + if (mainPanel) mainPanel.classList.add('hstream-player__menu-panel--active'); + } + }); + panel.appendChild(backBtn); + + const addServerItems = (servers, labelPrefix, startIndex) => { + for (let i = 0; i < servers.length; i++) { + const index = startIndex + i; + const item = document.createElement('button'); + item.className = 'hstream-player__menu-item'; + item.type = 'button'; + item.setAttribute('role', 'menuitemradio'); + + if (index === selectedIndex) { + item.classList.add('hstream-player__menu-item--checked'); + item.setAttribute('aria-checked', 'true'); + } else { + item.setAttribute('aria-checked', 'false'); + } + + const num = i + 1; + item.innerHTML = `${labelPrefix} ${num} ${labelPrefix}${num}`; + + item.addEventListener('click', (e) => { + e.stopPropagation(); + onSelect(index); + }); + panel.appendChild(item); + } + }; + + const divider = document.createElement('div'); + divider.className = 'hstream-player__menu-divider'; + panel.appendChild(divider); + + addServerItems(streamServers, 'Server', 0); + + if (fallbackServers && fallbackServers.length > 0) { + const fbDivider = document.createElement('div'); + fbDivider.className = 'hstream-player__menu-divider'; + panel.appendChild(fbDivider); + + addServerItems(fallbackServers, 'Fallback', streamServers.length); + } + + return panel; +} diff --git a/resources/js/player/player-thumbnails.js b/resources/js/player/player-thumbnails.js new file mode 100644 index 0000000..9113c3c --- /dev/null +++ b/resources/js/player/player-thumbnails.js @@ -0,0 +1,218 @@ +/** + * VTT-based sprite thumbnail preview. + * Parses WEBVTT cues with Media Fragment URIs (#xywh=x,y,w,h) and renders + * a floating preview image above the progress bar on hover. + */ + +export class ThumbnailPreview { + constructor(progressWrapper, video) { + this.progressWrapper = progressWrapper; + this.video = video; + this.cues = []; + this.spriteImg = null; + this.thumbnailWidth = 160; + this.thumbnailHeight = 90; + this.visible = false; + + this.el = document.createElement('div'); + this.el.className = 'hstream-player__thumbnail-preview'; + this.el.setAttribute('aria-hidden', 'true'); + + this.imgEl = document.createElement('div'); + this.imgEl.className = 'hstream-player__thumbnail-preview-img'; + this.el.appendChild(this.imgEl); + + this.timeEl = document.createElement('div'); + this.timeEl.className = 'hstream-player__thumbnail-preview-time'; + this.el.appendChild(this.timeEl); + + this.el.style.display = 'none'; + this.progressWrapper.appendChild(this.el); + + this._onMove = this._onMove.bind(this); + this._onLeave = this._onLeave.bind(this); + } + + /** + * Fetch and parse the thumbnail VTT file. + * @param {string} vttUrl + */ + async load(vttUrl) { + try { + const response = await fetch(vttUrl); + if (!response.ok) { + throw new Error('Failed to fetch VTT: ' + response.status); + } + const text = await response.text(); + const baseDir = vttUrl.substring(0, vttUrl.lastIndexOf('/') + 1); + this.cues = this._parseVTT(text, baseDir); + if (this.cues.length > 0) { + this.spriteImg = new Image(); + this.spriteImg.crossOrigin = 'anonymous'; + this.spriteImg.src = this.cues[0].spriteUrl; + await new Promise((resolve, reject) => { + this.spriteImg.onload = resolve; + this.spriteImg.onerror = reject; + }); + } + this._attach(); + } catch (err) { + console.warn('[ThumbnailPreview] Could not load thumbnails:', err); + } + } + + /** + * Parse WEBVTT text extracting cues with sprite coordinates. + */ + _parseVTT(text, baseDir) { + const cues = []; + const lines = text.split(/\r?\n/); + const cueRegex = /^(\d{2}:\d{2}:\d{2}\.\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}\.\d{3})/; + const xywhRegex = /#xywh=(\d+),(\d+),(\d+),(\d+)/; + + const resolveUrl = (maybeRelative) => { + if (!baseDir || maybeRelative.startsWith('http://') || maybeRelative.startsWith('https://') || maybeRelative.startsWith('data:') || maybeRelative.startsWith('/')) { + return maybeRelative; + } + try { + return new URL(maybeRelative, baseDir).href; + } catch (e) { + return baseDir + maybeRelative; + } + }; + + let i = 0; + while (i < lines.length) { + const line = lines[i].trim(); + const match = line.match(cueRegex); + if (match) { + const startTime = this._timeToSeconds(match[1]); + const endTime = this._timeToSeconds(match[2]); + i++; + while (i < lines.length) { + const payload = lines[i].trim(); + if (payload === '' || payload.match(cueRegex)) { + break; + } + const xywh = payload.match(xywhRegex); + if (xywh) { + const rawUrl = payload.substring(0, xywh.index); + cues.push({ + startTime, + endTime, + spriteUrl: resolveUrl(rawUrl), + x: parseInt(xywh[1], 10), + y: parseInt(xywh[2], 10), + w: parseInt(xywh[3], 10), + h: parseInt(xywh[4], 10), + }); + break; + } + const noteMatch = payload.match(/^NOTE/); + if (!noteMatch) { + const urlMatch = payload.match(/^(\S+)/); + if (urlMatch) { + cues.push({ startTime, endTime, spriteUrl: resolveUrl(urlMatch[1]), x: 0, y: 0, w: 0, h: 0 }); + break; + } + } + i++; + } + } + i++; + } + return cues; + } + + _timeToSeconds(timestamp) { + const [h, m, s] = timestamp.split(':'); + return parseFloat(h) * 3600 + parseFloat(m) * 60 + parseFloat(s); + } + + _attach() { + this.progressWrapper.addEventListener('mousemove', this._onMove); + this.progressWrapper.addEventListener('mouseleave', this._onLeave); + this.progressWrapper.addEventListener('touchmove', this._onMove, { passive: true }); + this.progressWrapper.addEventListener('touchend', this._onLeave); + } + + _onMove(e) { + const rect = this.progressWrapper.getBoundingClientRect(); + const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left; + const ratio = Math.max(0, Math.min(1, x / rect.width)); + const time = ratio * this.video.duration; + + const cue = this._findCue(time); + if (!cue) { + this._hide(); + return; + } + + this._show(cue, time, rect, x); + } + + _findCue(time) { + for (let i = 0; i < this.cues.length; i++) { + if (time >= this.cues[i].startTime && time <= this.cues[i].endTime) { + return this.cues[i]; + } + } + return null; + } + + _show(cue, time, progressRect, mouseX) { + this.imgEl.style.backgroundImage = `url(${cue.spriteUrl})`; + this.imgEl.style.width = cue.w + 'px'; + this.imgEl.style.height = cue.h + 'px'; + this.imgEl.style.backgroundPosition = `-${cue.x}px -${cue.y}px`; + + const mins = Math.floor(time / 60); + const secs = Math.floor(time % 60); + this.timeEl.textContent = `${mins}:${secs.toString().padStart(2, '0')}`; + + const containerWidth = this.progressWrapper.offsetWidth; + const halfW = cue.w / 2; + let left = mouseX; + if (left < halfW + 4) left = halfW + 4; + if (left > containerWidth - halfW - 4) left = containerWidth - halfW - 4; + + this.el.style.left = left + 'px'; + this.el.style.display = ''; + + const timeTooltip = this.progressWrapper.querySelector('.hstream-player__time-tooltip'); + if (timeTooltip) { + timeTooltip.classList.remove('hstream-player__time-tooltip--visible'); + } + + if (!this.visible) { + this.visible = true; + requestAnimationFrame(() => { + this.el.classList.add('hstream-player__thumbnail-preview--visible'); + }); + } + } + + _hide() { + this.visible = false; + this.el.classList.remove('hstream-player__thumbnail-preview--visible'); + setTimeout(() => { + if (!this.visible) { + this.el.style.display = 'none'; + } + }, 150); + } + + _onLeave() { + this._hide(); + } + + destroy() { + this.progressWrapper.removeEventListener('mousemove', this._onMove); + this.progressWrapper.removeEventListener('mouseleave', this._onLeave); + this.progressWrapper.removeEventListener('touchmove', this._onMove); + this.progressWrapper.removeEventListener('touchend', this._onLeave); + if (this.el.parentNode) { + this.el.parentNode.removeChild(this.el); + } + } +} diff --git a/resources/views/stream/partials/player.blade.php b/resources/views/stream/partials/player.blade.php index d381b0f..134d815 100644 --- a/resources/views/stream/partials/player.blade.php +++ b/resources/views/stream/partials/player.blade.php @@ -8,6 +8,6 @@ @endif
- +
diff --git a/vite.config.js b/vite.config.js index 80f511a..7441663 100644 --- a/vite.config.js +++ b/vite.config.js @@ -12,8 +12,6 @@ export default defineConfig({ 'resources/js/app.js', 'resources/js/modals-playlist.js', 'resources/js/theme.js', - 'resources/js/player-mobile.js', - 'resources/js/player-data.js', 'resources/js/player.js', 'resources/js/playlist.js', 'resources/js/upload.js',