From ef0ddb6fb53748abd71f2b6a8d11fceea78cf335 Mon Sep 17 00:00:00 2001 From: Julian Bilcke Date: Sun, 28 Jul 2024 19:41:03 +0200 Subject: [PATCH] add flying window mode --- package-lock.json | 645 +----------------- package.json | 6 +- src/app/embed/embed.tsx | 7 +- src/app/layout.tsx | 2 - src/app/main.tsx | 307 ++++++--- src/components/core/timeline/index.tsx | 1 + src/components/core/tree/index.tsx | 2 +- .../dialogs/iframe-warning/index.tsx | 7 +- src/components/dialogs/loader/index.tsx | 5 - src/components/monitor/index.tsx | 31 +- src/components/toolbars/bottom-bar/index.tsx | 6 +- .../toolbars/bottom-bar/tasks/index.tsx | 13 +- src/components/toolbars/top-menu/index.tsx | 73 +- .../toolbars/top-menu/view/index.tsx | 21 + src/components/windows/index.tsx | 415 +++++++++++ src/lib/hooks/useFullscreenStatus.ts | 89 ++- src/services/debug.ts | 2 + src/services/ui/getDefaultUIState.ts | 2 + src/services/ui/theme.ts | 100 ++- src/services/ui/useUI.ts | 4 + src/services/windows/types.ts | 30 + src/services/windows/useWindows.ts | 116 ++++ 22 files changed, 1014 insertions(+), 870 deletions(-) create mode 100644 src/components/windows/index.tsx create mode 100644 src/services/windows/types.ts create mode 100644 src/services/windows/useWindows.ts diff --git a/package-lock.json b/package-lock.json index 7534d46d..bec92c5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "clapper", - "version": "0.0.5", + "version": "0.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "clapper", - "version": "0.0.5", + "version": "0.0.6", "license": "GPL-3.0-only", "dependencies": { "@aitube/broadway": "0.1.2", "@aitube/clap": "0.1.2", - "@aitube/clapper-services": "0.1.2-4", + "@aitube/clapper-services": "0.1.2-7", "@aitube/engine": "0.1.2", - "@aitube/timeline": "0.1.2-0", + "@aitube/timeline": "0.1.2-1", "@fal-ai/serverless-client": "^0.13.0", "@ffmpeg/ffmpeg": "^0.12.10", "@ffmpeg/util": "^0.12.1", @@ -184,12 +184,12 @@ } }, "node_modules/@aitube/clapper-services": { - "version": "0.1.2-4", - "resolved": "https://registry.npmjs.org/@aitube/clapper-services/-/clapper-services-0.1.2-4.tgz", - "integrity": "sha512-Y/GqpTGOWRCgm5lFsRkjAXRZT0RabxH+dUevarRLFmY1+HhYMz3Mh7hu8GnIHKWdKetAJADKasF4mfIhYZaj8g==", + "version": "0.1.2-7", + "resolved": "https://registry.npmjs.org/@aitube/clapper-services/-/clapper-services-0.1.2-7.tgz", + "integrity": "sha512-xqt8XG8R7SHG+7bBb1+5yHzbfkpndyGiJOV8Agz5wfcuOKHuSk8EOrcKc7oB2t/AVrnkQSp9ph+6Gnf1mOXLpg==", "peerDependencies": { "@aitube/clap": "0.1.2", - "@aitube/timeline": "0.1.2-0", + "@aitube/timeline": "0.1.2-1", "@monaco-editor/react": "4.6.0", "monaco-editor": "0.50.0", "react": "*", @@ -215,9 +215,9 @@ } }, "node_modules/@aitube/timeline": { - "version": "0.1.2-0", - "resolved": "https://registry.npmjs.org/@aitube/timeline/-/timeline-0.1.2-0.tgz", - "integrity": "sha512-6KuiwBMgEt8U9xQZs+v4SQuy3RgcHOMz2HrLy2V4qERtFzNbyQnRHXx4u0iGn9Z/BbvVTBPbSLiJOhAMteHeeg==", + "version": "0.1.2-1", + "resolved": "https://registry.npmjs.org/@aitube/timeline/-/timeline-0.1.2-1.tgz", + "integrity": "sha512-7Y4ExWf2GA2p7a0W8Yt6pfIEa79I0oXiyhGWY+b2BN0SXKHOwEYYnlNlDBQhTDILsHsYgKnvvABt2iQdnt4xxA==", "dependencies": { "date-fns": "^3.6.0", "react-virtualized-auto-sizer": "^1.0.24" @@ -2806,15 +2806,6 @@ "node": ">=14.14" } }, - "node_modules/@emnapi/runtime": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", - "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emotion/is-prop-valid": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz", @@ -2838,70 +2829,6 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", @@ -2918,294 +2845,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3599,27 +3238,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", - "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-libvips-linux-x64": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", @@ -3751,31 +3369,6 @@ "@img/sharp-libvips-linux-arm64": "1.0.2" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", - "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "glibc": ">=2.31", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.2" - } - }, "node_modules/@img/sharp-linux-x64": { "version": "0.33.4", "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", @@ -3851,27 +3444,6 @@ "@img/sharp-libvips-linuxmusl-x64": "1.0.2" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", - "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", - "cpu": [ - "wasm32" - ], - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.1.1" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-win32-ia32": { "version": "0.33.4", "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", @@ -6536,32 +6108,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz", - "integrity": "sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz", - "integrity": "sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.19.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz", @@ -6575,175 +6121,6 @@ "darwin" ] }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz", - "integrity": "sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz", - "integrity": "sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz", - "integrity": "sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz", - "integrity": "sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz", - "integrity": "sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz", - "integrity": "sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz", - "integrity": "sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz", - "integrity": "sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz", - "integrity": "sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz", - "integrity": "sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz", - "integrity": "sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz", - "integrity": "sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz", - "integrity": "sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", diff --git a/package.json b/package.json index 1cb04190..22bde703 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clapper", - "version": "0.0.5", + "version": "0.0.6", "private": true, "description": "🎬 Clapper", "license": "GPL-3.0-only", @@ -38,9 +38,9 @@ "dependencies": { "@aitube/broadway": "0.1.2", "@aitube/clap": "0.1.2", - "@aitube/clapper-services": "0.1.2-4", + "@aitube/clapper-services": "0.1.2-7", "@aitube/engine": "0.1.2", - "@aitube/timeline": "0.1.2-0", + "@aitube/timeline": "0.1.2-1", "@fal-ai/serverless-client": "^0.13.0", "@ffmpeg/ffmpeg": "^0.12.10", "@ffmpeg/util": "^0.12.1", diff --git a/src/app/embed/embed.tsx b/src/app/embed/embed.tsx index 8310ee7c..b32005d3 100644 --- a/src/app/embed/embed.tsx +++ b/src/app/embed/embed.tsx @@ -11,10 +11,12 @@ import { Monitor } from '@/components/monitor' import { SettingsDialog } from '@/components/settings' import { LoadingDialog } from '@/components/dialogs/loader/LoadingDialog' import { TopBar } from '@/components/toolbars/top-bar' +import { useTheme } from '@/services' export function Embed() { const ref = useRef(null) const isEmpty = useTimeline((s) => s.isEmpty) + const theme = useTheme() return ( @@ -23,10 +25,7 @@ export function Embed() { className={cn( `dark fixed flex h-screen w-screen select-none flex-col items-center justify-center overflow-hidden font-light text-stone-900/90 dark:text-stone-100/90` )} - style={{ - backgroundImage: - 'repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #37353455, #373534 )', - }} + style={{ backgroundImage: theme.wallpaperBgImage }} >
{children} diff --git a/src/app/main.tsx b/src/app/main.tsx index d65d40b9..82e6bccb 100644 --- a/src/app/main.tsx +++ b/src/app/main.tsx @@ -5,6 +5,7 @@ import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex' import { useSearchParams } from 'next/navigation' import { DndProvider, useDrop } from 'react-dnd' import { HTML5Backend, NativeTypes } from 'react-dnd-html5-backend' +import { UIWindowLayout } from '@aitube/clapper-services' import { Toaster } from '@/components/ui/sonner' import { cn } from '@/lib/utils' @@ -20,6 +21,11 @@ import { ChatView } from '@/components/assistant/ChatView' import { Editors } from '@/components/editors/Editors' import { useTheme } from '@/services/ui/useTheme' import { BottomToolbar } from '@/components/toolbars/bottom-bar' +import { FruityDesktop, FruityWindow } from '@/components/windows' +import { ScriptEditor } from '@/components/editors/ScriptEditor' +import { SegmentEditor } from '@/components/editors/SegmentEditor' +import { EntityEditor } from '@/components/editors/EntityEditor' +import { WorkflowEditor } from '@/components/editors/WorkflowEditor' type DroppableThing = { files: File[] } @@ -33,6 +39,7 @@ function MainContent() { const theme = useTheme() const openFiles = useIO((s) => s.openFiles) const isTopMenuOpen = useUI((s) => s.isTopMenuOpen) + const windowLayout = useUI((s) => s.windowLayout) const [{ isOver, canDrop }, connectFileDrop] = useDrop({ accept: [NativeTypes.FILE], @@ -56,133 +63,219 @@ function MainContent() { setHasBetaAccess(hasBetaAccess) }, [hasBetaAccess, setHasBetaAccess]) - return ( + const gridLayout = ( + + + + + + {showExplorer && ( + + + + )} + {showExplorer && showVideoPlayer && } + {showVideoPlayer && ( + + + + )} + + + + + + + + + + {showAssistant && } + {showAssistant && ( + + + + )} + + ) + + const flyingLayout = ( + + + + + + + + + + + + + + {hasBetaAccess && ( + + + + )} + + + + + + + + + + ) + + const welcomeScreen = (
-
- - - - - - {showExplorer && ( - - - - )} - {showExplorer && showVideoPlayer && } - {showVideoPlayer && ( - - - - )} - - - - - - - - - - {showAssistant && } - {showAssistant && ( - - - +
+ > + +
+
+ +
+
+

+ Welcome to{' '} + + Clapper + + . +

+
+

A free and open-source AI video editor,

+

designed for the age of generative filmmaking.

+
+
+
+ ) + return ( +
+
-
-
- -
-
- -
-
-

- Welcome to{' '} - - Clapper - - . -

-
-

A free and open-source AI video editor,

-

designed for the age of generative filmmaking.

-
-
-
+ {windowLayout === UIWindowLayout.FLYING ? flyingLayout : gridLayout}
+ {welcomeScreen} + - + {windowLayout === UIWindowLayout.GRID && }
) } diff --git a/src/components/core/timeline/index.tsx b/src/components/core/timeline/index.tsx index 2a0371f1..1be0aec6 100644 --- a/src/components/core/timeline/index.tsx +++ b/src/components/core/timeline/index.tsx @@ -18,6 +18,7 @@ export function Timeline() { const setJumpAt = useTimeline((s) => s.setJumpAt) const setIsPlaying = useTimeline((s) => s.setIsPlaying) const setTogglePlayback = useTimeline((s) => s.setTogglePlayback) + const isEmpty = useTimeline((s) => s.isEmpty) const startLoop = useResolver((s) => s.startLoop) diff --git a/src/components/core/tree/index.tsx b/src/components/core/tree/index.tsx index 2791764c..500a507d 100644 --- a/src/components/core/tree/index.tsx +++ b/src/components/core/tree/index.tsx @@ -157,7 +157,7 @@ export function Node({ width={2} xmlns="http://www.w3.org/2000/svg" // if you want to display vertical lines, tweak the stroke-gray-900/100 - className="absolute bottom-0 left-3.5 top-[31px] z-[-1] h-[calc(100%-30px)] -translate-x-1/2 transform stroke-gray-900/100" + className="absolute bottom-0 left-3.5 top-[31px] z-[-1] h-[calc(100%-30px)] -translate-x-1/2 transform" key={node.id + 'line'} stroke="currentColor" > diff --git a/src/components/dialogs/iframe-warning/index.tsx b/src/components/dialogs/iframe-warning/index.tsx index 0245c0a3..89668ca0 100644 --- a/src/components/dialogs/iframe-warning/index.tsx +++ b/src/components/dialogs/iframe-warning/index.tsx @@ -2,10 +2,12 @@ import { APP_DOMAIN, APP_LINK, APP_NAME } from '@/lib/core/constants' import { cn } from '@/lib/utils' +import { useTheme } from '@/services' import { useEffect, useState } from 'react' export function IframeWarning() { const [showWarning, setShowWarning] = useState(false) + const theme = useTheme() useEffect(() => { setShowWarning(window.self !== window.top) @@ -21,10 +23,7 @@ export function IframeWarning() { ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0' )} - style={{ - backgroundImage: - 'repeating-radial-gradient( circle at 0 0, transparent 0, #000000 7px ), repeating-linear-gradient( #37353455, #373534 )', - }} + style={{ backgroundImage: theme.wallpaperBgImage }} >

s.windowLayout) return (

-
-
setFullscreen()} - className={cn( - `cursor-pointer rounded-full p-2`, - `transition-all duration-100 ease-in-out`, - isFullscreen - ? `opacity-0` - : `scale-95 opacity-70 hover:scale-100 hover:opacity-100` - )} - > - {/**/} - + {windowLayout === UIWindowLayout.GRID && ( +
+
setFullscreen()} + className={cn( + `cursor-pointer rounded-full p-2`, + `transition-all duration-100 ease-in-out`, + isFullscreen + ? `opacity-0` + : `scale-95 opacity-70 hover:scale-100 hover:opacity-100` + )} + > + +
-
+ )}
) } diff --git a/src/components/toolbars/bottom-bar/index.tsx b/src/components/toolbars/bottom-bar/index.tsx index ebc97112..8c9175e1 100644 --- a/src/components/toolbars/bottom-bar/index.tsx +++ b/src/components/toolbars/bottom-bar/index.tsx @@ -15,7 +15,7 @@ export function BottomToolbar() { `items-center justify-between`, `left-0 right-0 h-7`, `px-3`, - `text-xs font-light text-stone-400` + `text-xs font-light text-white/40` )} style={{ borderTop: 'solid 1px rgba(255,255,255,0.3)', @@ -27,8 +27,8 @@ export function BottomToolbar() { >
- app version: - {APP_REVISION} + app version: + {APP_REVISION}
{/* diff --git a/src/components/toolbars/bottom-bar/tasks/index.tsx b/src/components/toolbars/bottom-bar/tasks/index.tsx index 9daeac68..3b0d917f 100644 --- a/src/components/toolbars/bottom-bar/tasks/index.tsx +++ b/src/components/toolbars/bottom-bar/tasks/index.tsx @@ -33,11 +33,18 @@ export function Tasks() { return (
- - + + {nbRunningBackgroundTasks || 'no'} - pending tasks + pending tasks
diff --git a/src/components/toolbars/top-menu/index.tsx b/src/components/toolbars/top-menu/index.tsx index 87b719ff..93d8acfb 100644 --- a/src/components/toolbars/top-menu/index.tsx +++ b/src/components/toolbars/top-menu/index.tsx @@ -22,6 +22,8 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { ToggleView } from './ToggleView' +import { UIWindowLayout } from '@aitube/clapper-services' +import { Tasks } from '../bottom-bar/tasks' export function TopMenu() { const isBusyResolving = useResolver((s) => s.isBusyResolving) @@ -35,7 +37,7 @@ export function TopMenu() { const showAssistant = useUI((s) => s.showAssistant) const setShowAssistant = useUI((s) => s.setShowAssistant) const setIsTopMenuOpen = useUI((s) => s.setIsTopMenuOpen) - + const windowLayout = useUI((s) => s.windowLayout) const hasBetaAccess = useUI((s) => s.hasBetaAccess) return ( @@ -64,37 +66,46 @@ export function TopMenu() { { // clap?.meta?.title || "Untitled" } - - Toggle Explorer - - {/* - - Toggle Timeline - - */} - - Toggle Monitor - - - Toggle Assistant - + {windowLayout === UIWindowLayout.GRID ? ( + <> + + Toggle Explorer + + {/* + + Toggle Timeline + + */} + + + Toggle Monitor + + + Toggle Assistant + + + ) : ( + <> + + + )} diff --git a/src/components/toolbars/top-menu/view/index.tsx b/src/components/toolbars/top-menu/view/index.tsx index 3ffb64ae..7696bc16 100644 --- a/src/components/toolbars/top-menu/view/index.tsx +++ b/src/components/toolbars/top-menu/view/index.tsx @@ -12,6 +12,7 @@ import { import { useFullscreenStatus } from '@/lib/hooks' import { useUI } from '@/services/ui' import { ThemeList } from '../lists/ThemeList' +import { UIWindowLayout } from '@aitube/clapper-services' export function TopMenuView() { const [isFullscreen, setFullscreen, ref] = useFullscreenStatus() @@ -24,6 +25,9 @@ export function TopMenuView() { } }, [ref]) + const windowLayout = useUI((s) => s.windowLayout) + const setWindowLayout = useUI((s) => s.setWindowLayout) + const showTimeline = useUI((s) => s.showTimeline) const setShowTimeline = useUI((s) => s.setShowTimeline) @@ -63,6 +67,23 @@ export function TopMenuView() { Toggle fullscreen + { + setWindowLayout( + windowLayout === UIWindowLayout.FLYING + ? UIWindowLayout.GRID + : UIWindowLayout.FLYING + ) + + e.stopPropagation() + e.preventDefault() + return false + }} + > + 🥭 Fruity mode (experimental) + + {/* = ({ className = '', children, style }) => { + return ( +
+ {children} +
+ ) +} + +// FruityWindow component +export const FruityWindow: React.FC<{ + id: string + title?: string | JSX.Element + defaultWidth?: number | string + minWidth?: number | string + defaultHeight?: number | string + minHeight?: number | string + defaultX?: number + defaultY?: number + canBeReduced?: boolean + canBeClosed?: boolean + canBeFullScreen?: boolean + toolbar?: (props: { isFocused: boolean }) => JSX.Element + children?: React.ReactNode +}> = ({ + id, + title = 'Untitled', + defaultWidth = 800, + minWidth = 160, + defaultHeight = 600, + minHeight = 100, + defaultX, + defaultY, + canBeReduced = true, + canBeClosed = true, + canBeFullScreen = true, + toolbar, + children, +}) => { + const theme = useTheme() + const windowRef = useRef(null) + const headerRef = useRef(null) + const [isEditing, setIsEditing] = useState(false) + const [isDragging, setIsDragging] = useState(false) + const [isResizing, setIsResizing] = useState(false) + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) + const [resizeDirection, setResizeDirection] = useState('') + + const window = useWindows(useCallback((state) => state.windows[id], [id])) + const addWindow = useWindows((state) => state.addWindow) + const updateWindow = useWindows((state) => state.updateWindow) + const focusWindow = useWindows((state) => state.focusWindow) + const removeWindow = useWindows((state) => state.removeWindow) + + const [isFullscreen, setFullscreen, ref] = useFullscreenStatus() + + const parseSize = (size: number | string): number => { + if (typeof size === 'number') return size + if (typeof size === 'string') { + const parsed = parseInt(size, 10) + return isNaN(parsed) ? 0 : parsed + } + return 0 + } + + useEffect(() => { + if (!window) { + addWindow({ + id, + title, + isVisible: true, + width: parseSize(defaultWidth), + height: parseSize(defaultHeight), + x: defaultX, + y: defaultY, + canBeReduced, + canBeClosed, + }) + } + }, [ + addWindow, + canBeClosed, + canBeReduced, + defaultHeight, + defaultWidth, + defaultX, + defaultY, + id, + title, + window, + ]) + + useEffect(() => { + const handleMouseDown = (e: MouseEvent) => { + if (windowRef.current && windowRef.current.contains(e.target as Node)) { + focusWindow(id) + } + } + + document.addEventListener('mousedown', handleMouseDown) + return () => { + document.removeEventListener('mousedown', handleMouseDown) + } + }, [focusWindow, id]) + + const handleDragStart = useCallback( + (e: React.MouseEvent) => { + if (window) { + setIsDragging(true) + setDragOffset({ + x: e.clientX - window.x, + y: e.clientY - window.y, + }) + } + }, + [window] + ) + + const handleDrag = useCallback( + (e: MouseEvent) => { + if (isDragging && window) { + const newY = Math.max(0, e.clientY - dragOffset.y) // Ensure y is at least 32px from the top + updateWindow(id, { + x: e.clientX - dragOffset.x, + y: newY, + }) + } + }, + [dragOffset.x, dragOffset.y, id, isDragging, updateWindow, window] + ) + const handleDragEnd = useCallback(() => { + setIsDragging(false) + }, []) + + useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', handleDrag) + document.addEventListener('mouseup', handleDragEnd) + } else { + document.removeEventListener('mousemove', handleDrag) + document.removeEventListener('mouseup', handleDragEnd) + } + return () => { + document.removeEventListener('mousemove', handleDrag) + document.removeEventListener('mouseup', handleDragEnd) + } + }, [isDragging, handleDrag, handleDragEnd]) + + const handleResizeStart = useCallback( + (e: React.MouseEvent, direction: string) => { + e.preventDefault() + e.stopPropagation() + if (window) { + setIsResizing(true) + setResizeDirection(direction) + setDragOffset({ + x: e.clientX, + y: e.clientY, + }) + } + }, + [window] + ) + + const handleResize = useCallback( + (e: MouseEvent) => { + if (isResizing && window) { + const dx = e.clientX - dragOffset.x + const dy = e.clientY - dragOffset.y + let newWidth = window.width + let newHeight = window.height + let newX = window.x + let newY = window.y + + const parsedMinWidth = parseSize(minWidth) + const parsedMinHeight = parseSize(minHeight) + + if (resizeDirection.includes('w')) { + newWidth = Math.max(parsedMinWidth, window.width - dx) + newX = window.x + window.width - newWidth + } + if (resizeDirection.includes('e')) { + newWidth = Math.max(parsedMinWidth, window.width + dx) + } + if (resizeDirection.includes('n')) { + newHeight = Math.max(parsedMinHeight, window.height - dy) + newY = Math.max(0, window.y + window.height - newHeight) + newHeight = window.y + window.height - newY // Adjust height based on the new Y position + } + if (resizeDirection.includes('s')) { + newHeight = Math.max(parsedMinHeight, window.height + dy) + } + + // Ensure the window doesn't go above the limit + if (newY < 0) { + newHeight = newHeight - (0 - newY) + newY = 0 + } + + updateWindow(id, { + width: newWidth, + height: newHeight, + x: newX, + y: newY, + }) + setDragOffset({ x: e.clientX, y: e.clientY }) + } + }, + [ + dragOffset.x, + dragOffset.y, + id, + isResizing, + minHeight, + minWidth, + resizeDirection, + updateWindow, + window, + ] + ) + + const handleResizeEnd = useCallback(() => { + setIsResizing(false) + setResizeDirection('') + }, []) + + useEffect(() => { + if (isResizing) { + document.addEventListener('mousemove', handleResize) + document.addEventListener('mouseup', handleResizeEnd) + } else { + document.removeEventListener('mousemove', handleResize) + document.removeEventListener('mouseup', handleResizeEnd) + } + return () => { + document.removeEventListener('mousemove', handleResize) + document.removeEventListener('mouseup', handleResizeEnd) + } + }, [isResizing, handleResize, handleResizeEnd]) + + const toggleReduce = useCallback(() => { + if (window) { + updateWindow(id, { isReduced: !window.isReduced }) + } + }, [id, updateWindow, window]) + + const handleHeaderDoubleClick = useCallback( + (e: React.MouseEvent) => { + // Check if the double-click occurred on the header background + if (e.target === headerRef.current) { + toggleReduce() + } + }, + [toggleReduce] + ) + + if (!window) return null + + return ( +
+ {!isFullscreen && ( +
+ {isEditing ? ( + updateWindow(id, { title: e.target.value })} + onBlur={() => setIsEditing(false)} + onKeyDown={(e) => { + if (e.key === 'Enter') setIsEditing(false) + }} + className="rounded-none bg-neutral-950/80 px-0 text-sm text-white/60" + autoFocus + /> + ) : ( +
setIsEditing(true)}> + {typeof window.title === 'string' + ? window.title + : (window.title as any)({ isFocused: window.isFocused })} +
+ )} +
+ {toolbar && toolbar({ isFocused: window.isFocused })} + {canBeFullScreen && ( + + )} + {canBeReduced && ( + + )} + {canBeClosed && ( + + )} +
+
+ )} + {!window.isReduced && ( +
+ {children} +
+ )} + {!isFullscreen && !window.isReduced && ( + <> +
handleResizeStart(e, 'w')} + /> +
handleResizeStart(e, 'e')} + /> +
handleResizeStart(e, 'n')} + /> +
handleResizeStart(e, 's')} + /> +
handleResizeStart(e, 'nw')} + /> +
handleResizeStart(e, 'ne')} + /> +
handleResizeStart(e, 'sw')} + /> +
handleResizeStart(e, 'se')} + /> + + )} +
+ ) +} diff --git a/src/lib/hooks/useFullscreenStatus.ts b/src/lib/hooks/useFullscreenStatus.ts index 18de0061..ba1b0363 100644 --- a/src/lib/hooks/useFullscreenStatus.ts +++ b/src/lib/hooks/useFullscreenStatus.ts @@ -5,6 +5,7 @@ import React, { useLayoutEffect, useRef, MutableRefObject, + useCallback, } from 'react' interface FullscreenElement { @@ -24,67 +25,65 @@ export function useFullscreenStatus(): [ MutableRefObject, ] { const elRef = useRef(null) - const [isFullscreen, setIsFullscreen] = useState( - typeof document !== 'undefined' + const [isFullscreen, setIsFullscreen] = useState(false) + + const getFullscreenStatus = useCallback(() => { + return typeof document !== 'undefined' ? Boolean( (document as FullscreenElement)[getBrowserFullscreenElementProp()] ) : false - ) + }, []) - const setFullscreen = (maybeValue?: boolean) => { - if (!elRef.current) return + const updateFullscreenStatus = useCallback(() => { + setIsFullscreen(getFullscreenStatus()) + }, [getFullscreenStatus]) - const isFullScreen = - typeof document !== 'undefined' - ? Boolean( - (document as FullscreenElement)[getBrowserFullscreenElementProp()] - ) - : false + const setFullscreen = useCallback( + (maybeValue?: boolean) => { + if (!elRef.current) return - const shouldBeFullScreen = - typeof maybeValue === 'boolean' ? maybeValue : !isFullScreen + const isFullScreen = getFullscreenStatus() + const shouldBeFullScreen = + typeof maybeValue === 'boolean' ? maybeValue : !isFullScreen - // nothing to do - if (isFullScreen === shouldBeFullScreen) { - return - } + if (isFullScreen === shouldBeFullScreen) { + return + } - const operation = shouldBeFullScreen - ? elRef.current.requestFullscreen() - : typeof document !== 'undefined' - ? document.exitFullscreen() - : Promise.resolve(true) - - operation - .then(() => { - setIsFullscreen(shouldBeFullScreen) - }) - .catch(() => { - if (typeof document !== 'undefined') { - // make sure we grab a fresh value here - const isFullScreen = Boolean( - (document as FullscreenElement)[getBrowserFullscreenElementProp()] - ) - setIsFullscreen(isFullScreen) - } - }) - } + const operation = shouldBeFullScreen + ? elRef.current.requestFullscreen() + : typeof document !== 'undefined' + ? document.exitFullscreen() + : Promise.resolve(true) + + operation.then(updateFullscreenStatus).catch(updateFullscreenStatus) + }, + [getFullscreenStatus, updateFullscreenStatus] + ) useLayoutEffect(() => { + updateFullscreenStatus() + + const fullscreenChangeHandler = () => { + updateFullscreenStatus() + } + if (typeof document !== 'undefined') { - document.onfullscreenchange = () => - setIsFullscreen( - Boolean( - (document as FullscreenElement)[getBrowserFullscreenElementProp()] - ) - ) + document.addEventListener('fullscreenchange', fullscreenChangeHandler) + + // Polling mechanism + const intervalId = setInterval(updateFullscreenStatus, 100) return () => { - document.onfullscreenchange = null + document.removeEventListener( + 'fullscreenchange', + fullscreenChangeHandler + ) + clearInterval(intervalId) } } - }, []) + }, [updateFullscreenStatus]) return [isFullscreen, setFullscreen, elRef] } diff --git a/src/services/debug.ts b/src/services/debug.ts index 6cf1c14c..3a2dee33 100644 --- a/src/services/debug.ts +++ b/src/services/debug.ts @@ -18,6 +18,7 @@ import { useResolver } from './resolver/useResolver' import { useSettings } from './settings/useSettings' import { useSimulator } from './simulator/useSimulator' import { useUI } from './ui/useUI' +import { useWindows } from './windows/useWindows' // those are just used for developer convenience // to help debug things in the chrome developer console @@ -41,4 +42,5 @@ if (typeof window !== 'undefined') { w.useSettings = useSettings w.useSimulator = useSimulator w.useUI = useUI + w.useWindows = useWindows } diff --git a/src/services/ui/getDefaultUIState.ts b/src/services/ui/getDefaultUIState.ts index 64465f3a..9338fb08 100644 --- a/src/services/ui/getDefaultUIState.ts +++ b/src/services/ui/getDefaultUIState.ts @@ -2,6 +2,7 @@ import { ProjectCreationWizardStep, SettingsCategory, UIState, + UIWindowLayout, } from '@aitube/clapper-services' export function getDefaultUIState(): UIState { @@ -20,6 +21,7 @@ export function getDefaultUIState(): UIState { showFPS: false, followCursor: false, editorFontSize: 12, + windowLayout: UIWindowLayout.GRID, projectCreationWizardStep: ProjectCreationWizardStep.NONE, } diff --git a/src/services/ui/theme.ts b/src/services/ui/theme.ts index 08279047..2e25f870 100644 --- a/src/services/ui/theme.ts +++ b/src/services/ui/theme.ts @@ -82,6 +82,10 @@ export const backstage: UITheme = { author: 'Clapper', colorMode: UIThemeColorMode.DARK, description: '', + + wallpaperBgImage: `repeating-radial-gradient(circle at 0 0, transparent 0, #000000 7px ), + repeating-linear-gradient( #37353455, #373534 )`, + defaultBgColor: '#1c1917', defaultTextColor: '#d6d3d1', defaultPrimaryColor: '#FACC15', @@ -105,6 +109,11 @@ export const backstage: UITheme = { formInputRadius: '8px', + windowHeaderBgColor: '#1c1917', + windowBodyBgColor: '#292524', + windowBorderColor: 'rgba(255, 255, 255, 0.05)', + windowBorderRadius: '8px', + timeline: { topBarTimeScale: { backgroundColor: '#7d7c78', @@ -166,6 +175,10 @@ export const midnight: UITheme = { author: 'Clapper', colorMode: UIThemeColorMode.DARK, description: '', + + wallpaperBgImage: `repeating-radial-gradient(circle at -10% -10%, #000819 0, #020410 7px ), + repeating-linear-gradient( #01012210, #121224 )`, + defaultBgColor: '#101e2d', defaultTextColor: '#b8bdc3', defaultPrimaryColor: '#5A9DF7', @@ -188,6 +201,11 @@ export const midnight: UITheme = { formInputRadius: '8px', + windowHeaderBgColor: '#000e1d', + windowBodyBgColor: '#011222', + windowBorderColor: 'rgba(255, 255, 255, 0.05)', + windowBorderRadius: '8px', + timeline: { topBarTimeScale: { backgroundColor: '#32506b', @@ -236,6 +254,10 @@ export const lavender: UITheme = { author: 'Clapper', colorMode: UIThemeColorMode.DARK, description: '', + + wallpaperBgImage: `repeating-radial-gradient(circle at 0 0, #161520 0, #160510 7px ), + repeating-linear-gradient( #16152055, #161520 )`, + defaultBgColor: '#32294b', defaultTextColor: '#dbd0fd', defaultPrimaryColor: '#A78BFA', @@ -258,6 +280,11 @@ export const lavender: UITheme = { formInputRadius: '8px', + windowHeaderBgColor: '#1a1526', + windowBodyBgColor: '#211b30', + windowBorderColor: 'rgba(255, 255, 255, 0.05)', + windowBorderRadius: '8px', + timeline: { topBarTimeScale: { backgroundColor: '#6E5A7F', @@ -317,21 +344,25 @@ export const flix: UITheme = { author: 'Clapper', colorMode: UIThemeColorMode.DARK, description: 'A new DVD rent-by-mail business', - defaultBgColor: '#000000', + + wallpaperBgImage: `repeating-radial-gradient(circle at 0 0, transparent 0, #000000 6px ), + repeating-linear-gradient( #37353455, #373534 )`, + + defaultBgColor: '#131313', defaultTextColor: '#D22F27', defaultPrimaryColor: '#D22F27', defaultBorderColor: '#152639', logoColor: '#D22F27', - editorBgColor: '#000000', + editorBgColor: '#131313', editorCursorColor: '#D22F27', editorTextColor: '#ffeeee', editorMenuBgColor: '#000000', editorMenuTextColor: 'ffeeee', editorBorderColor: '#232323', - monitorBgColor: '#000000', + monitorBgColor: '#131313', monitorSecondaryTextColor: '#D6D3D1', monitorPrimaryTextColor: '#D22F27', - assistantBgColor: '#000000', + assistantBgColor: '#131313', assistantUserBgColor: '#075985', assistantUserTextColor: '#bae6fd', assistantRobotBgColor: '#3730a3', @@ -339,6 +370,11 @@ export const flix: UITheme = { formInputRadius: '8px', + windowHeaderBgColor: '#121212', + windowBodyBgColor: '#131313', + windowBorderColor: 'rgba(255, 255, 255, 0.05)', + windowBorderRadius: '8px', + timeline: { topBarTimeScale: { backgroundColor: 'rgb(80,80,80)', @@ -399,6 +435,10 @@ export const lore: UITheme = { author: 'Clapper', colorMode: UIThemeColorMode.DARK, description: '', + + wallpaperBgImage: `repeating-radial-gradient(circle at 0 0, #101520 0, #100510 7px ), + repeating-linear-gradient( #10152055, #101520 )`, + defaultBgColor: '#151520', defaultTextColor: '#f6d6d8', defaultPrimaryColor: '#DE4A80', @@ -421,6 +461,11 @@ export const lore: UITheme = { formInputRadius: '8px', + windowHeaderBgColor: '#151520', + windowBodyBgColor: '#151520', + windowBorderColor: 'rgba(255, 255, 255, 0.05)', + windowBorderRadius: '8px', + timeline: { topBarTimeScale: { backgroundColor: 'rgb(98,94,111)', @@ -480,22 +525,26 @@ export const gordon: UITheme = { author: 'Clapper', colorMode: UIThemeColorMode.DARK, description: '', + + wallpaperBgImage: `repeating-radial-gradient(circle at 100% 0, #232323 0, #333333 5px ), + repeating-linear-gradient( #43434355, #434343 )`, + defaultBgColor: '#525252', defaultTextColor: '#e0e0e0', defaultPrimaryColor: '#ff8400', defaultBorderColor: '#2e2e2e', logoColor: '#ff8400', - editorBgColor: '#434343', + editorBgColor: '#383838', editorCursorColor: '#ffffff', - editorTextColor: '#e0e0e0', + editorTextColor: '#c0c0c0', editorMenuBgColor: '#323232', editorMenuTextColor: 'e0e0e0', editorBorderColor: '#2e2e2e', - monitorBgColor: '#333333', + monitorBgColor: '#404040', monitorSecondaryTextColor: '#e0e0e0', monitorPrimaryTextColor: '#ff8400', - assistantBgColor: '#535353', + assistantBgColor: '#404040', assistantUserBgColor: '#ff8400', assistantUserTextColor: '#ffffff', assistantRobotBgColor: '#6b6b6b', @@ -503,6 +552,11 @@ export const gordon: UITheme = { formInputRadius: '8px', + windowHeaderBgColor: '#323232', + windowBodyBgColor: '#434343', + windowBorderColor: 'rgba(255, 255, 255, 0.05)', + windowBorderRadius: '8px', + timeline: { topBarTimeScale: { backgroundColor: '#7b7b7b', @@ -563,21 +617,25 @@ export const silent: UITheme = { author: 'Clapper', colorMode: UIThemeColorMode.DARK, description: 'Monochrome theme inspired by classic films', - defaultBgColor: '#1a1a1a', + + wallpaperBgImage: `repeating-radial-gradient(circle at 50% 50%, transparent 0, #000000 6px ), + repeating-linear-gradient( #00000055, #37373760 )`, + + defaultBgColor: '#1f1f1f', defaultTextColor: '#d0d0d0', defaultPrimaryColor: '#ffffff', defaultBorderColor: '#2a2a2a', logoColor: '#ffffff', - editorBgColor: '#1a1a1a', + editorBgColor: '#1f1f1f', editorCursorColor: '#ffffff', editorTextColor: '#d0d0d0', - editorMenuBgColor: '#1a1a1a', + editorMenuBgColor: '#121212', editorMenuTextColor: 'd0d0d0', editorBorderColor: '#2a2a2a', - monitorBgColor: '#0f0f0f', + monitorBgColor: '#1f1f1f', monitorSecondaryTextColor: '#b0b0b0', monitorPrimaryTextColor: '#ffffff', - assistantBgColor: '#1a1a1a', + assistantBgColor: '#1f1f1f', assistantUserBgColor: '#3a3a3a', assistantUserTextColor: '#ffffff', assistantRobotBgColor: '#2a2a2a', @@ -585,6 +643,11 @@ export const silent: UITheme = { formInputRadius: '32px', + windowHeaderBgColor: '#121212', + windowBodyBgColor: '#1f1f1f', + windowBorderColor: 'rgba(255, 255, 255, 0.05)', + windowBorderRadius: '8px', + timeline: { topBarTimeScale: { backgroundColor: '#3a3a3a', @@ -692,7 +755,7 @@ export const silent: UITheme = { }, }, workflow: { - bgColor: '#100e0e', + bgColor: '#191919', node: { bgColor: '#121212', borderColor: '#3a3a3a', @@ -713,6 +776,10 @@ export const ripley: UITheme = { author: 'Clapper', colorMode: UIThemeColorMode.DARK, description: 'Inspired by a famous Weyland-Yutani employee.', + + wallpaperBgImage: `repeating-radial-gradient(circle at 0 0, #151515 0, #050905 7px ), + repeating-linear-gradient( #15181555, #151815 )`, + defaultBgColor: '#292929', defaultTextColor: '#E0E7E0', defaultPrimaryColor: '#bcc227', @@ -735,6 +802,11 @@ export const ripley: UITheme = { formInputRadius: '32px', + windowHeaderBgColor: '#383838', + windowBodyBgColor: '#1E1E1E', + windowBorderColor: 'rgba(255, 255, 255, 0.05)', + windowBorderRadius: '10px', + timeline: { topBarTimeScale: { backgroundColor: '#797979', diff --git a/src/services/ui/useUI.ts b/src/services/ui/useUI.ts index c09d1d8d..d5c21cb7 100644 --- a/src/services/ui/useUI.ts +++ b/src/services/ui/useUI.ts @@ -11,6 +11,7 @@ import { UIStore, UITheme, UIThemeName, + UIWindowLayout, } from '@aitube/clapper-services' import { getDefaultUIState } from './getDefaultUIState' import { themes } from './theme' @@ -84,6 +85,9 @@ export const useUI = create()( setEditorFontSize: (editorFontSize: number) => { set({ editorFontSize }) }, + setWindowLayout: (windowLayout: UIWindowLayout) => { + set({ windowLayout }) + }, setProjectCreationWizardStep: ( projectCreationWizardStep: ProjectCreationWizardStep ) => { diff --git a/src/services/windows/types.ts b/src/services/windows/types.ts new file mode 100644 index 00000000..682aa093 --- /dev/null +++ b/src/services/windows/types.ts @@ -0,0 +1,30 @@ +// Define the window state type +export type WindowState = { + id: string + title: string | JSX.Element + isVisible: boolean + zIndex: number + width: number + height: number + x: number + y: number + isFocused: boolean + isReduced: boolean + canBeReduced: boolean + canBeClosed: boolean +} + +// Define the store type +export type WindowsStore = { + windows: Record + getNextPosition: (width: number, height: number) => { x: number; y: number } + addWindow: ( + window: Omit< + WindowState, + 'zIndex' | 'isFocused' | 'isReduced' | 'x' | 'y' + > & { x?: number; y?: number } + ) => void + updateWindow: (id: string, updates: Partial) => void + removeWindow: (id: string) => void + focusWindow: (id: string) => void +} diff --git a/src/services/windows/useWindows.ts b/src/services/windows/useWindows.ts new file mode 100644 index 00000000..a7b34668 --- /dev/null +++ b/src/services/windows/useWindows.ts @@ -0,0 +1,116 @@ +import { create } from 'zustand' + +import { WindowsStore, WindowState } from './types' +import { useCallback } from 'react' + +// Create the Zustand store +export const useWindows = create((set, get) => ({ + windows: {}, + getNextPosition: (width: number, height: number) => { + const state = get() + const existingWindows = Object.values(state.windows) + const defaultOffset = 30 // Offset for cascading windows + let newX = 0 + let newY = 0 + let maxIterations = 100 // Prevent infinite loop + + const isOverlapping = (x: number, y: number) => { + return existingWindows.some( + (w) => + x < w.x + w.width && + x + width > w.x && + y < w.y + w.height && + y + height > w.y + ) + } + + while (maxIterations > 0) { + if (!isOverlapping(newX, newY)) { + break + } + + newX += defaultOffset + newY += defaultOffset + + // Reset position if it goes too far + if (newX > 300 || newY > 300) { + newX = Math.floor(Math.random() * 100) + newY = Math.floor(Math.random() * 100) + } + + maxIterations-- + } + + return { x: newX, y: newY } + }, + addWindow: (window) => + set((state) => { + const maxZIndex = Math.max( + 0, + ...Object.values(state.windows).map((w) => w.zIndex) + ) + const { x: newX, y: newY } = state.getNextPosition( + window.width, + window.height + ) + return { + windows: { + ...state.windows, + [window.id]: { + ...window, + zIndex: maxZIndex + 1, + isFocused: true, + isReduced: false, + x: window.x !== undefined ? window.x : newX, + y: window.y !== undefined ? window.y : newY, + width: window.width, + height: window.height, + }, + }, + } + }), + updateWindow: (id, updates) => + set((state) => ({ + windows: { + ...state.windows, + [id]: { ...state.windows[id], ...updates }, + }, + })), + removeWindow: (id) => + set((state) => { + const { [id]: _, ...rest } = state.windows + return { windows: rest } + }), + focusWindow: (id) => + set((state) => { + const maxZIndex = Math.max( + ...Object.values(state.windows).map((w) => w.zIndex) + ) + return { + windows: Object.fromEntries( + Object.entries(state.windows).map(([key, window]) => [ + key, + { + ...window, + zIndex: key === id ? maxZIndex + 1 : window.zIndex, + isFocused: key === id, + }, + ]) + ), + } + }), +})) + +// Custom hook to use window state in other components +export const useWindow = (id: string) => { + return useWindows( + useCallback( + (state) => ({ + ...state.windows[id], + updateWindow: (updates: Partial) => + state.updateWindow(id, updates), + }), + [id] + ) + ) +}