diff --git a/.gitignore b/.gitignore index ad6b3898..c22dde42 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,6 @@ public/assets .blinko/* dump.sql public/sw* +public/plugins/* public/workbox*.* -seed.js \ No newline at end of file +seed.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 1929ecde..a339e1be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,113 @@ +## [0.40.5](https://github.com/blinko-space/blinko/compare/v0.40.4...v0.40.5) (2025-02-21) + + +### Bug Fixes + +* Improve global configuration retrieval logic ([5627ef9](https://github.com/blinko-space/blinko/commit/5627ef9e20f424371b5e221f5f312b8d2450fb7d)) + +## [0.40.4](https://github.com/blinko-space/blinko/compare/v0.40.3...v0.40.4) (2025-02-21) + + +### Bug Fixes + +* next.config.js ([2afa8aa](https://github.com/blinko-space/blinko/commit/2afa8aac1fe1bfbb7b83e69d321b0635f9376b57)) + +## [0.40.3](https://github.com/blinko-space/blinko/compare/v0.40.2...v0.40.3) (2025-02-21) + + +### Bug Fixes + +* Move plugin storage to .blinko directory ([37d63c2](https://github.com/blinko-space/blinko/commit/37d63c28e4437351273e035ac989c4867fa6c929)) + +## [0.40.2](https://github.com/blinko-space/blinko/compare/v0.40.1...v0.40.2) (2025-02-20) + + +### Bug Fixes + +* Add global clipboard copy function and update build configurations ([147d8c7](https://github.com/blinko-space/blinko/commit/147d8c7a9cc2b7e6c56281f443a4282c6908c258)) +* plugin issues ([4cb83f4](https://github.com/blinko-space/blinko/commit/4cb83f4373e4f06a7fc922d3aece9c4c07bcebfe)) + +## [0.40.1](https://github.com/blinko-space/blinko/compare/v0.40.0...v0.40.1) (2025-02-20) + + +### Bug Fixes + +* **config:** Update plugin route to use API endpoint for serving plugins ([f893873](https://github.com/blinko-space/blinko/commit/f893873c6795fed965b0c1b5c94e9b5bf9b1280f)) + +# [0.40.0](https://github.com/blinko-space/blinko/compare/v0.39.3...v0.40.0) (2025-02-20) + + +### Bug Fixes + +* Improve plugin installation and error handling ([9b99200](https://github.com/blinko-space/blinko/commit/9b992001c4d6c12109a0fe18095a8dd749519b7c)) + + +### Features + +* **config:** Add rewrite rule for serving plugins from public directory ([6db57d0](https://github.com/blinko-space/blinko/commit/6db57d0445361fcdfdce92b680f9fb7a11e924a9)) + +## [0.39.3](https://github.com/blinko-space/blinko/compare/v0.39.2...v0.39.3) (2025-02-20) + + +### Bug Fixes + +* update ([47beb64](https://github.com/blinko-space/blinko/commit/47beb64d557dc983289a221586ac1d7dd70eb543)) + +## [0.39.2](https://github.com/blinko-space/blinko/compare/v0.39.1...v0.39.2) (2025-02-20) + + +### Bug Fixes + +* **plugin:** Hardcode dev plugin name for settings configuration ([96baf02](https://github.com/blinko-space/blinko/commit/96baf0278c0887ec3b42378039b63c1a90237a3a)) + +## [0.39.1](https://github.com/blinko-space/blinko/compare/v0.39.0...v0.39.1) (2025-02-20) + + +### Bug Fixes + +* Remove debug logging of plugin menu length in right-click menu ([f6028e4](https://github.com/blinko-space/blinko/commit/f6028e4157788aafcc615b82ef69e8585ce59792)) + +# [0.39.0](https://github.com/blinko-space/blinko/compare/v0.38.6...v0.39.0) (2025-02-20) + + +### Bug Fixes + +* add TypeScript type definitions and enhance plugin architecture ([cd7db9c](https://github.com/blinko-space/blinko/commit/cd7db9cc7caeb4989b375d167c18745737447566)) +* **plugin:** improve plugin ([d62a1ef](https://github.com/blinko-space/blinko/commit/d62a1ef087b42d3e9509f90c01ae5ec513c1b755)) + + +### Features + +* enhance plugin system with Alpine.js and custom toolbar icons ([7499ba7](https://github.com/blinko-space/blinko/commit/7499ba7591396451afc8206e1650dbacc82bc893)) +* integrate custom plugin loading in MyApp component ([7a81b81](https://github.com/blinko-space/blinko/commit/7a81b81c451722c45088312b58d5b92e5d477049)) +* integrate SystemJS for dynamic plugin loading ([3ef8db6](https://github.com/blinko-space/blinko/commit/3ef8db62905d1659604bf693f81dec40406d44f9)) +* **plugin:** Add settings panel support for installed plugins ([2c71ebf](https://github.com/blinko-space/blinko/commit/2c71ebf100976b2f213f9e5a446f5679b08e5976)) +* **plugin:** Enhance plugin system with advanced features and UI improvements ([819ddcb](https://github.com/blinko-space/blinko/commit/819ddcbb9a9fa091af8d4204520808cc7794728b)) +* **plugin:** Enhance plugin system with dynamic toolbar rendering and global Blinko interface ([df2ef70](https://github.com/blinko-space/blinko/commit/df2ef70d3513644812f4cbba85ddd969d9817e9a)) +* **plugin:** Implement comprehensive plugin management system ([f76c451](https://github.com/blinko-space/blinko/commit/f76c45117d6a637a3b4569c0a973af64cb4b1ea9)) +* **plugin:** Modify dev plugin loading to support settings panel detection ([1428bbb](https://github.com/blinko-space/blinko/commit/1428bbb93aa36ff2ab66beff32e63a12d494b13a)) + +## [0.38.6](https://github.com/blinko-space/blinko/compare/v0.38.5...v0.38.6) (2025-02-17) + + +### Bug Fixes + +* Add multi-select functionality and delete confirmation for resources [#492](https://github.com/blinko-space/blinko/issues/492) ([7375b6f](https://github.com/blinko-space/blinko/commit/7375b6ff2d89ede3f50158a1743fe45f0a70637d)) + +## [0.38.5](https://github.com/blinko-space/blinko/compare/v0.38.4...v0.38.5) (2025-02-12) + + +### Bug Fixes + +* globalConfig get issue ([a745602](https://github.com/blinko-space/blinko/commit/a745602e2338ffd2de65fe7745f7d10d032db264)) + +## [0.38.4](https://github.com/blinko-space/blinko/compare/v0.38.3...v0.38.4) (2025-02-12) + + +### Bug Fixes + +* webhookEndpoint get issue ([6f751e0](https://github.com/blinko-space/blinko/commit/6f751e0539dc67a733634feb0149341daec8d6ef)) + ## [0.38.3](https://github.com/blinko-space/blinko/compare/v0.38.2...v0.38.3) (2025-02-06) diff --git a/blinko-types/package.json b/blinko-types/package.json new file mode 100644 index 00000000..69205724 --- /dev/null +++ b/blinko-types/package.json @@ -0,0 +1,24 @@ +{ + "name": "blinko", + "access": "public", + "version": "0.0.11", + "description": "TypeScript type definitions for Blinko", + "private": false, + "types": "dist/types/src/store/plugin/index.d.ts", + "files": [ + "dist/types" + ], + "scripts": { + "build": "cd .. && pnpm run build:types" + }, + "keywords": [ + "typescript", + "types", + "blinko" + ], + "repository": { + "type": "git", + "url": "https://github.com/blinko-space/blinko.git" + }, + "license": "MIT" +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index 51a45336..82d3b829 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,5 @@ const isProduction = process.env.NODE_ENV === 'production'; -const isVercel = process.env.VERCEL === '1'; + const withPWA = require('next-pwa')({ dest: 'public', register: true, @@ -106,7 +106,7 @@ module.exports = withPWA({ }, ], source: '/logo-dark.png', - }, + } ]; }, webpack: (config, { dev,isServer }) => { @@ -126,10 +126,19 @@ module.exports = withPWA({ }) return config; }, - outputFileTracing: isVercel? false : true, + outputFileTracing: true, reactStrictMode: isProduction? true : false, swcMinify: true, eslint: { ignoreDuringBuilds: true, }, + async rewrites() { + return [ + { + source: '/plugins/:path*', + destination: '/api/serve-plugin/:path*', + }, + ] + } + }) \ No newline at end of file diff --git a/package.json b/package.json index ce6f6f01..4ad764ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blinko", - "version": "0.38.3", + "version": "0.40.5", "repository": "https://github.com/blinko-space/blinko.git", "private": true, "browser": { @@ -23,13 +23,15 @@ "dev": "next dev -p 1111", "dev:https": "ngrok http 1111", "build": "set NODE_ENV=production & next build", + "build:dev": "set NODE_ENV=development & next build", "build:debug": "set DEBUG=* & next build", "build:max": "set NODE_OPTIONS=--max-old-space-size=16384 & set DEBUG=* & next build", "start": "prisma migrate & prisma db seed & next start -p 1111", "lint": "next lint", "analyze": "ANALYZE=true next build", "postinstall": "pnpm generate", - "docker:reset:password": "node prisma/resetpassword.js" + "docker:reset:password": "node prisma/resetpassword.js", + "build:types": "tsc -p tsconfig.types.json" }, "dependencies": { "@ashwamegh/react-link-preview": "^1.0.1", @@ -51,6 +53,7 @@ "@nextui-org/popover": "^2.1.29", "@nextui-org/react": "^2.6.5", "@nextui-org/theme": "^2.4.1", + "@preact/compat": "^18.3.1", "@prisma/client": "^5.21.1", "@radix-ui/colors": "^3.0.0", "@radix-ui/react-dialog": "^1.1.4", @@ -64,6 +67,7 @@ "@trpc/server": "11.0.0-rc.553", "@types/mime-types": "^2.1.4", "adm-zip": "^0.5.16", + "alpinejs": "^3.14.8", "archiver": "^7.0.1", "axios": "^1.7.7", "boring-avatars": "^1.11.2", @@ -121,6 +125,7 @@ "pg-connection-string": "^2.7.0", "pg-dump-restore": "^1.0.12", "postcss-import": "^16.1.0", + "preact": "^10.26.1", "qrcode.react": "^4.1.0", "rctx-contextmenu": "^1.4.1", "react": "^18.2.0", @@ -156,6 +161,7 @@ "superjson": "^2.2.1", "swagger-ui-react": "^5.17.14", "swiper": "^11.1.14", + "systemjs": "^6.15.1", "tailwind-merge": "^1.14.0", "three": "^0.171.0", "three-stdlib": "^2.35.2", @@ -185,6 +191,7 @@ "@types/react": "18.2.8", "@types/react-dom": "^18.2.4", "@types/react-grid-layout": "^1.3.2", + "@types/systemjs": "^6.15.1", "@types/three": "^0.171.0", "@types/uuid": "^9.0.1", "autoprefixer": "^10.4.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c373c136..8c08ad06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: '@nextui-org/theme': specifier: ^2.4.1 version: 2.4.1(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.2.5)(typescript@5.6.3))) + '@preact/compat': + specifier: ^18.3.1 + version: 18.3.1(preact@10.26.1) '@prisma/client': specifier: ^5.21.1 version: 5.22.0(prisma@5.21.1) @@ -107,6 +110,9 @@ importers: adm-zip: specifier: ^0.5.16 version: 0.5.16 + alpinejs: + specifier: ^3.14.8 + version: 3.14.8 archiver: specifier: ^7.0.1 version: 7.0.1 @@ -278,6 +284,9 @@ importers: postcss-import: specifier: ^16.1.0 version: 16.1.0(postcss@8.4.47) + preact: + specifier: ^10.26.1 + version: 10.26.1 qrcode.react: specifier: ^4.1.0 version: 4.1.0(react@18.3.1) @@ -383,6 +392,9 @@ importers: swiper: specifier: ^11.1.14 version: 11.1.14 + systemjs: + specifier: ^6.15.1 + version: 6.15.1 tailwind-merge: specifier: ^1.14.0 version: 1.14.0 @@ -465,6 +477,9 @@ importers: '@types/react-grid-layout': specifier: ^1.3.2 version: 1.3.5 + '@types/systemjs': + specifier: ^6.15.1 + version: 6.15.1 '@types/three': specifier: ^0.171.0 version: 0.171.0 @@ -2942,6 +2957,11 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@preact/compat@18.3.1': + resolution: {integrity: sha512-Kog4PSRxtT4COtOXjsuQPV1vMXpUzREQfv+6Dmcy9/rMk0HOPK0HTE9fspFjAmY8R80T/T8gtgmZ68u5bOSngw==} + peerDependencies: + preact: '*' + '@prisma/client@5.22.0': resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} engines: {node: '>=16.13'} @@ -4565,6 +4585,9 @@ packages: '@types/stats.js@0.17.3': resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==} + '@types/systemjs@6.15.1': + resolution: {integrity: sha512-MfDFIN+jRQOX1JRBrbbb72tsFJnK0n7mtLC+L2Y3t7As/vFxJiFGA/09FE+6ssFheHAibd8Q3gs959c+Sgf/9A==} + '@types/three@0.171.0': resolution: {integrity: sha512-oLuT1SAsT+CUg/wxUTFHo0K3NtJLnx9sJhZWQJp/0uXqFpzSk1hRHmvWvpaAWSfvx2db0lVKZ5/wV0I0isD2mQ==} @@ -4592,6 +4615,12 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@vue/reactivity@3.1.5': + resolution: {integrity: sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==} + + '@vue/shared@3.1.5': + resolution: {integrity: sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==} + '@webassemblyjs/ast@1.12.1': resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} @@ -4739,6 +4768,9 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + alpinejs@3.14.8: + resolution: {integrity: sha512-wT2fuP2DXpGk/jKaglwy7S/IJpm1FD+b7U6zUrhwErjoq5h27S4dxkJEXVvhbdwyPv9U+3OkUuNLkZT4h2Kfrg==} + amdefine@1.0.1: resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} engines: {node: '>=0.4.2'} @@ -8745,8 +8777,8 @@ packages: peerDependencies: preact: '>=10' - preact@10.24.3: - resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} + preact@10.26.1: + resolution: {integrity: sha512-K5aMG0NdGHZ8yV1GfGtGA4JwnWxe/HIDzyr9svdo2DeokLUJ/+W8MpeuPrfOytu5rHHgYQrvGxUoW83sapJZnw==} prebuild-install@7.1.2: resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} @@ -9882,6 +9914,9 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + systemjs@6.15.1: + resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==} + tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -14129,6 +14164,10 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@preact/compat@18.3.1(preact@10.26.1)': + dependencies: + preact: 10.26.1 + '@prisma/client@5.22.0(prisma@5.21.1)': optionalDependencies: prisma: 5.21.1 @@ -16671,6 +16710,8 @@ snapshots: '@types/stats.js@0.17.3': {} + '@types/systemjs@6.15.1': {} + '@types/three@0.171.0': dependencies: '@tweenjs/tween.js': 23.1.3 @@ -16696,6 +16737,12 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@vue/reactivity@3.1.5': + dependencies: + '@vue/shared': 3.1.5 + + '@vue/shared@3.1.5': {} + '@webassemblyjs/ast@1.12.1': dependencies: '@webassemblyjs/helper-numbers': 1.11.6 @@ -16871,6 +16918,10 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + alpinejs@3.14.8: + dependencies: + '@vue/reactivity': 3.1.5 + amdefine@1.0.1: optional: true @@ -20429,8 +20480,8 @@ snapshots: next: 14.2.21(@babel/core@7.25.8)(@opentelemetry/api@1.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) oauth: 0.9.15 openid-client: 5.7.0 - preact: 10.24.3 - preact-render-to-string: 5.2.6(preact@10.24.3) + preact: 10.26.1 + preact-render-to-string: 5.2.6(preact@10.26.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) uuid: 8.3.2 @@ -21254,12 +21305,12 @@ snapshots: potpack@1.0.2: {} - preact-render-to-string@5.2.6(preact@10.24.3): + preact-render-to-string@5.2.6(preact@10.26.1): dependencies: - preact: 10.24.3 + preact: 10.26.1 pretty-format: 3.8.0 - preact@10.24.3: {} + preact@10.26.1: {} prebuild-install@7.1.2: dependencies: @@ -22675,6 +22726,8 @@ snapshots: symbol-tree@3.2.4: optional: true + systemjs@6.15.1: {} + tabbable@6.2.0: {} tailwind-merge@1.14.0: {} diff --git a/prisma/migrations/20250219083523_0_39_0/migration.sql b/prisma/migrations/20250219083523_0_39_0/migration.sql new file mode 100644 index 00000000..fe5a92eb --- /dev/null +++ b/prisma/migrations/20250219083523_0_39_0/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "plugin" ( + "id" SERIAL NOT NULL, + "metadata" JSON NOT NULL, + "path" VARCHAR NOT NULL, + "isUse" BOOLEAN NOT NULL DEFAULT true, + "isDev" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "plugin_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c7df0f46..31ecbcdf 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -187,3 +187,13 @@ model cache { createdAt DateTime @default(now()) @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6) } + +model plugin { + id Int @id @default(autoincrement()) + metadata Json @db.Json + path String @db.VarChar + isUse Boolean @default(true) + isDev Boolean @default(false) + createdAt DateTime @default(now()) @db.Timestamptz(6) + updatedAt DateTime @updatedAt @db.Timestamptz(6) +} diff --git a/public/fallback-He2c-SngEtMyzg7eS51ua.js b/public/fallback-M6zu_8kcos4ZIMgAVOKwv.js similarity index 100% rename from public/fallback-He2c-SngEtMyzg7eS51ua.js rename to public/fallback-M6zu_8kcos4ZIMgAVOKwv.js diff --git a/public/locales/ar/translation.json b/public/locales/ar/translation.json index 5ac3c06b..704b458b 100644 --- a/public/locales/ar/translation.json +++ b/public/locales/ar/translation.json @@ -488,5 +488,20 @@ "has-todo": "يجب القيام به", "reference-by": "بالمرجعية", "hide-notification": "إخفاء الإشعارات", - "search-settings": "إعدادات البحث..." + "search-settings": "إعدادات البحث...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "سيقوم هذا الإجراء بحذف الملفات المحددة ولا يمكن استعادتها، يرجى تأكيد.", + "plugin-settings": "إعداد الإضافة", + "installed-plugins": "تم التثبيت", + "marketplace": "السوق", + "local-development": "التنمية المحلية", + "add-local-plugin": "أضف الإضافة المحلية", + "local-plugin": "الإضافة المحلية", + "uninstall": "إلغاء التثبيت", + "install": "تثبيت", + "downloads": "تنزيلات", + "plugin-updated": "تم تحديث الإضافة", + "plugin-update-failed": "فشل تحديث الإضافة", + "plugin-connection-failed": "فشل الاتصال بالإضافة", + "disconnect": "افصل", + "local-development-description": "أضف إضافة تطوير محلية وقم بتصحيح الأخطاء فيها." } diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 46b67df0..51b5bdde 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -474,5 +474,20 @@ "has-todo": "Hat ZU TUN", "reference-by": "Referenz Durch", "hide-notification": "Benachrichtigung ausblenden", - "search-settings": "Sucheinstellungen..." + "search-settings": "Sucheinstellungen...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "Dieser Vorgang wird die ausgewählten Dateien löschen und kann nicht wiederhergestellt werden. Bitte bestätigen Sie.", + "plugin-settings": "Plugin-Einstellung", + "installed-plugins": "Installiert", + "marketplace": "Marktplatz", + "local-development": "Lokale Entwicklung", + "add-local-plugin": "Füge lokales Plugin hinzu", + "local-plugin": "Lokales Plugin", + "uninstall": "Deinstallieren", + "install": "Install\n\nInstallation", + "downloads": "Downloads\n\nHerunterladen", + "plugin-updated": "Plugin aktualisiert", + "plugin-update-failed": "Plugin-Update fehlgeschlagen.", + "plugin-connection-failed": "Plugin-Verbindung fehlgeschlagen", + "disconnect": "Trennen", + "local-development-description": "Fügen Sie ein lokales Entwicklungs-Plugin hinzu und debuggen Sie es." } diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index e6683d50..91ac97e7 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -491,5 +491,20 @@ "has-todo": "Has TO-DO", "reference-by": "Reference By", "hide-notification": "Hide Notification", - "search-settings": "Search Settings..." + "search-settings": "Search Settings...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "This operation will delete the selected files and cannot be restored,Please confirm", + "plugin-settings": "Plugin Setting", + "installed-plugins": "Installed", + "marketplace": "Marketplace", + "local-development": "Local Development", + "local-plugin": "Local Plugin", + "add-local-plugin": "Add Local Plugin", + "uninstall": "Uninstall", + "install": "Install", + "downloads": "Downloads", + "plugin-updated": "Plugin Updated", + "plugin-update-failed": "Plugin Update Failed", + "plugin-connection-failed": "Plugin Connection Failed", + "disconnect": "Disconnect", + "local-development-description": "Add a local development plug-in and debug it." } diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 637b0b7c..4b876f0e 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -467,5 +467,20 @@ "has-todo": "Tiene TODO", "reference-by": "Referencia Por", "hide-notification": "Ocultar notificación", - "search-settings": "Configuración de búsqueda..." + "search-settings": "Configuración de búsqueda...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "Esta operación eliminará los archivos seleccionados y no se podrá restaurar. Por favor, confirme.", + "plugin-settings": "Ajuste del plugin", + "installed-plugins": "Instalado", + "marketplace": "Mercado", + "local-development": "Desarrollo Local", + "add-local-plugin": "Agregar complemento local", + "local-plugin": "Plugin local", + "uninstall": "Desinstalar", + "install": "Instalar", + "downloads": "Descargas", + "plugin-updated": "Plugin actualizado", + "plugin-update-failed": "Error al actualizar el plugin", + "plugin-connection-failed": "Conexión del complemento fallida", + "disconnect": "Desconectar", + "local-development-description": "Agregar un complemento de desarrollo local y depurarlo." } diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index a6e34c6d..c6965563 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -463,5 +463,20 @@ "has-todo": "A faire", "reference-by": "Référence par", "hide-notification": "Masquer la notification", - "search-settings": "Paramètres de recherche..." + "search-settings": "Paramètres de recherche...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "Cette opération supprimera les fichiers sélectionnés et ne pourra pas être restaurée. Veuillez confirmer.", + "plugin-settings": "Paramètres du plugin", + "installed-plugins": "Installé", + "marketplace": "Place de marché", + "local-development": "Développement local", + "add-local-plugin": "Ajouter le plugin local", + "local-plugin": "Plugin local", + "uninstall": "Désinstaller", + "install": "Installer", + "downloads": "Téléchargements", + "plugin-updated": "Extension mise à jour", + "plugin-update-failed": "Mise à jour du plugin échouée", + "plugin-connection-failed": "Échec de la connexion du plug-in", + "disconnect": "Déconnecter", + "local-development-description": "Ajoutez un plug-in de développement local et déboguez-le." } diff --git a/public/locales/ja/translation.json b/public/locales/ja/translation.json index 3f6b4dd6..acf3a21b 100644 --- a/public/locales/ja/translation.json +++ b/public/locales/ja/translation.json @@ -447,5 +447,21 @@ "has-todo": "「やるべきこと」", "reference-by": "言及者", "hide-notification": "通知を非表示", - "search-settings": "検索設定..." + "search-settings": "検索設定...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "この操作により選択したファイルが削除され、元に戻すことはできません。ご確認ください。", + "plugin-settings": "プラグインの設定", + "installed-plugins": "インストール済み", + "marketplace": "マーケットプレイス", + "local-development": "地域開発", + "add-local-plugin": "ローカルプラグインを追加します。", + "local-plugin": "ローカル プラグイン", + "uninstall": "アンインストール", + "install": "インストールします。", + "downloads": "ダウンロード", + "plugin-updated": "プラグインが更新されました", + "plugin-update-failed": "プラグインの更新に失敗しました", + "plugin-connection-failed": "プラグインの接続に失敗しました", + "disconnect": "切断", + "local-development-description": "ローカル開発プラグインを追加してデバッグします。", + "setting": "" } diff --git a/public/locales/ka/translation.json b/public/locales/ka/translation.json index 34baef39..62b52b43 100644 --- a/public/locales/ka/translation.json +++ b/public/locales/ka/translation.json @@ -490,5 +490,20 @@ "has-todo": "გაკეთე!", "reference-by": "მისათი Reference By", "hide-notification": "შეფia Notification", - "search-settings": "ძიების პარამეტრო ・・・" + "search-settings": "ძიების პარამეტრო ・・・", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "ეს ოპერაცია წოდ შო (delete) 5,000-5000-784138-taxiga png. Please confirm", + "plugin-settings": "პლაგინის პარ", + "installed-plugins": "დაყენებუ̆ ხო", + "marketplace": "მაღრაცხოვ (Marketplace)", + "local-development": "ლოკალური განვითGeorgia: , TifliadTranslation resultfirebase-UI-.foregroundColor不能为nil。", + "add-local-plugin": "დაამატე ლოკალურ პლa�in‌\u001e", + "local-plugin": "ლოკალურ პლაგინი", + "uninstall": "წაშლის", + "install": "დაიყენეთ", + "downloads": "ჩამოტვირთე", + "plugin-updated": "პლაგინი განახლდა", + "plugin-update-failed": "პლაგინი განოსupdate-  fileet !", + "plugin-connection-failed": "პლაგინი დაუკონე􀳦\bͼ ಬ›􁊥¡⅕程失败", + "disconnect": "წყლულ გათო", + "local-development-description": "დაამატეთ ლოკალურ გ­Plug-in-`i,  ~_18877gDebug ~t_vÂ?ab_eg." } diff --git a/public/locales/ko/translation.json b/public/locales/ko/translation.json index 08cd0461..3d393ea6 100644 --- a/public/locales/ko/translation.json +++ b/public/locales/ko/translation.json @@ -470,5 +470,20 @@ "has-todo": "할 일", "reference-by": "참조자", "hide-notification": "알림 숨기기", - "search-settings": "검색 설정..." + "search-settings": "검색 설정...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "이 작업은 선택한 파일을 삭제하며 복원할 수 없습니다. 확인해 주십시오.", + "plugin-settings": "플러그인 설정", + "installed-plugins": "설치됨", + "marketplace": "시장", + "local-development": "현지 개발", + "add-local-plugin": "로컬 플러그인 추가", + "local-plugin": "로컬 플러그인", + "uninstall": "제거하기", + "install": "설치하다", + "downloads": "다운로드", + "plugin-updated": "플러그인이 업데이트되었습니다.", + "plugin-update-failed": "플러그인 업데이트 실패", + "plugin-connection-failed": "플러그인 연결 실패", + "disconnect": "연결 끊기", + "local-development-description": "로컬 개발 플러그인을 추가하고 디버깅하세요." } diff --git a/public/locales/pl/translation.json b/public/locales/pl/translation.json index 23eb2894..5a2ce519 100644 --- a/public/locales/pl/translation.json +++ b/public/locales/pl/translation.json @@ -491,5 +491,20 @@ "has-todo": "Do wykonania", "reference-by": "Odnośnik do", "hide-notification": "Ukryj powiadomienie", - "search-settings": "Ustawienia wyszukiwania..." + "search-settings": "Ustawienia wyszukiwania...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "To działanie spowoduje usunięcie wybranych plików i nie będzie możliwe ich przywrócenie. Proszę potwierdzić.", + "plugin-settings": "Ustawienia wtyczki", + "installed-plugins": "Zainstalowany", + "marketplace": "Rynek", + "local-development": "Rozwój lokalny", + "add-local-plugin": "Dodaj lokalny wtyczkę", + "local-plugin": "Lokalny dodatek", + "uninstall": "Odinstaluj", + "install": "Zainstaluj", + "downloads": "Pobrania", + "plugin-updated": "Wtyczka zaktualizowana", + "plugin-update-failed": "Aktualizacja wtyczki nie powiodła się", + "plugin-connection-failed": "Wtyczka nieudane połączenie", + "disconnect": "Rozłączać", + "local-development-description": "Dodaj wtyczkę do lokalnego środowiska programistycznego i ją zdebuguj." } diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index 8a7e1638..319f7a3c 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -399,5 +399,20 @@ "has-todo": "Tem PARA FAZER", "reference-by": "Referência por", "hide-notification": "Ocultar notificação", - "search-settings": "Configurações de pesquisa..." + "search-settings": "Configurações de pesquisa...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "Esta operação irá apagar os arquivos selecionados e não poderá ser restaurada, por favor confirme.", + "plugin-settings": "Configuração do Plugin", + "installed-plugins": "Instalado", + "marketplace": "Mercado", + "local-development": "Desenvolvimento Local", + "add-local-plugin": "Adicionar Plugin Local", + "local-plugin": "Plugin Local", + "uninstall": "Desinstalar", + "install": "Instalar", + "downloads": "Baixar", + "plugin-updated": "Plugin Atualizado", + "plugin-update-failed": "Falha na Atualização do Plugin", + "plugin-connection-failed": "Falha na conexão do plugin", + "disconnect": "Desconectar", + "local-development-description": "Adicione um plug-in de desenvolvimento local e depure-o." } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index bc35672d..322ace2f 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -407,5 +407,20 @@ "has-todo": "Есть ДЛЯ-СДЕЛКИ", "reference-by": "Ссылкой от", "hide-notification": "Скрыть уведомление", - "search-settings": "Настройки поиска..." + "search-settings": "Настройки поиска...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "Эта операция удалит выбранные файлы и не может быть восстановлена. Подтвердите, пожалуйста.", + "plugin-settings": "Настройка плагина", + "installed-plugins": "Установлено", + "marketplace": "Торговая площадь", + "local-development": "Местное развитие", + "add-local-plugin": "Добавить локальный плагин", + "local-plugin": "Локальный плагин", + "uninstall": "Удалить", + "install": "Установить", + "downloads": "Загрузки", + "plugin-updated": "Плагин обновлен", + "plugin-update-failed": "Обновление плагина не удалось", + "plugin-connection-failed": "Ошибка соединения с плагином", + "disconnect": "Отключить", + "local-development-description": "Добавьте локальный плагин разработки и отладите его." } diff --git a/public/locales/tr/translation.json b/public/locales/tr/translation.json index 3fb8e3fe..730dcfe5 100644 --- a/public/locales/tr/translation.json +++ b/public/locales/tr/translation.json @@ -416,5 +416,20 @@ "has-todo": "YAPILACAKLAR", "reference-by": "Referans Tarafından", "hide-notification": "Bildirimi Gizle", - "search-settings": "Arama Ayarları..." + "search-settings": "Arama Ayarları...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "Bu işlem seçilen dosyaları silecek ve geri alınamaz, Lütfen onaylayın.", + "plugin-settings": "Eklenti Ayarı", + "installed-plugins": "Yüklendi", + "marketplace": "Pazar yeri", + "local-development": "Yerel Kalkınma", + "add-local-plugin": "Yerel Eklenti Ekle", + "local-plugin": "Yerel Eklenti", + "uninstall": "Kaldır", + "install": "Yüklemek", + "downloads": "İndirmeler", + "plugin-updated": "Eklenti Güncellendi.", + "plugin-update-failed": "Eklenti Güncelleme Başarısız oldu", + "plugin-connection-failed": "Eklenti Bağlantısı Başarısız oldu", + "disconnect": "Bağlantıyı kesmek", + "local-development-description": "Yerel geliştirme eklentisi ekleyin ve hata ayıklamayı yapın." } diff --git a/public/locales/zh-TW/translation.json b/public/locales/zh-TW/translation.json index e4a4ff13..7273a4ef 100644 --- a/public/locales/zh-TW/translation.json +++ b/public/locales/zh-TW/translation.json @@ -503,5 +503,20 @@ "has-todo": "有待辦事項", "reference-by": "被引用", "hide-notification": "隱藏通知", - "search-settings": "搜尋設定..." + "search-settings": "搜尋設定...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "這個操作將刪除選定的檔案,無法復原,請確認。", + "plugin-settings": "外掛程式設定", + "installed-plugins": "已安裝", + "marketplace": "市場", + "local-development": "本地開發", + "add-local-plugin": "新增本地插件", + "local-plugin": "本地插件", + "uninstall": "解除安裝", + "install": "安裝", + "downloads": "下載", + "plugin-updated": "插件已更新", + "plugin-update-failed": "插件更新失敗", + "plugin-connection-failed": "外掛程式連線失敗", + "disconnect": "中文(台灣):斷開連線", + "local-development-description": "新增本地開發外掛程式並進行除錯。" } diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index 266391cd..8adf61af 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -493,5 +493,20 @@ "has-todo": "有待完成", "reference-by": "被引用", "hide-notification": "隐藏通知", - "search-settings": "搜索设置..." + "search-settings": "搜索设置...", + "this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm": "此操作将删除所选文件,且无法恢复,请确认。", + "plugin-settings": "插件设置", + "installed-plugins": "已安装", + "marketplace": "市场", + "local-development": "本地开发", + "add-local-plugin": "添加本地插件", + "local-plugin": "本地插件", + "uninstall": "卸载", + "install": "安装", + "downloads": "下载", + "plugin-updated": "插件已更新", + "plugin-update-failed": "插件更新失败。", + "plugin-connection-failed": "插件连接失败", + "disconnect": "断开连接", + "local-development-description": "添加本地开发插件并对其进行调试。" } diff --git a/src/components/BlinkoMultiSelectPop/index.tsx b/src/components/BlinkoMultiSelectPop/index.tsx index 8766a94d..44abc4bd 100644 --- a/src/components/BlinkoMultiSelectPop/index.tsx +++ b/src/components/BlinkoMultiSelectPop/index.tsx @@ -1,73 +1,37 @@ - -import { _ } from '@/lib/lodash'; import { observer } from 'mobx-react-lite'; import { RootStore } from '@/store'; -import { motion } from "motion/react" -import { Icon } from '@iconify/react'; import { useTranslation } from 'react-i18next'; import { ToastPlugin } from '@/store/module/Toast/Toast'; import { ShowUpdateTagDialog } from '../Common/UpdateTagPop'; import { showTipsDialog } from '../Common/TipsDialog'; -import { DialogStore } from '@/store/module/Dialog'; import { BlinkoStore } from '@/store/blinkoStore'; import { api } from '@/lib/trpc'; -import { DeleteItem } from '../BlinkoRightClickMenu'; import { DialogStandaloneStore } from '@/store/module/DialogStandalone'; - - -const SelectBox = `select-none multi-select-toolbar flex fixed w-[80%] md:w-fit h-[50px] -p-4 rounded-xl font-bold items-center justify-center -left-1/2 -translate-x-1/2 top-10 md:top-auto md:bottom-10 -bg-primary text-primary-foreground z-[-999] gap-4 shadow-lg opacity-0` - -const SelectItems = "flex items-center justify-center gap-1 cursor-pointer hover:opacity-80 transition-all text-xs md:text-base" +import { MultiSelectToolbar } from '../Common/MultiSelectToolbar'; export const BlinkoMultiSelectPop = observer(() => { - const { t } = useTranslation() - const blinko = RootStore.Get(BlinkoStore) - return -
- { - blinko.curMultiSelectIds = blinko.noteList.value?.map(i => i.id).filter(i => i !== undefined) || [] - }} - className='cursor-pointer hover:opacity-80 transition-all' icon="fluent:select-all-on-16-filled" width="20" height="20" /> - {blinko.noteList.value?.length}/{blinko.curMultiSelectIds.length} {t('items')}
- -
+ const { t } = useTranslation(); + const blinko = RootStore.Get(BlinkoStore); -
{ + const actions = [ + { + icon: "eva:archive-outline", + text: t('archive'), + onClick: async () => { await RootStore.Get(ToastPlugin).promise( api.notes.updateMany.mutate({ ids: blinko.curMultiSelectIds, isArchived: true }), { loading: t('in-progress'), success: {t('your-changes-have-been-saved')}, error: {t('operation-failed')}, - }) - blinko.onMultiSelectRest() - }}> - -
{t('archive')}
-
- -
{ + }); + blinko.onMultiSelectRest(); + } + }, + { + icon: "solar:tag-outline", + text: t('add-tag'), + onClick: () => { ShowUpdateTagDialog({ type: 'select', onSave: async (tagName) => { @@ -77,17 +41,17 @@ export const BlinkoMultiSelectPop = observer(() => { loading: t('in-progress'), success: {t('your-changes-have-been-saved')}, error: {t('operation-failed')}, - }) - blinko.onMultiSelectRest() + }); + blinko.onMultiSelectRest(); } - }) - }}> - -
{t('add-tag')}
-
- -
{ + }); + } + }, + { + icon: "mingcute:delete-2-line", + text: t('delete'), + isDeleteButton: true, + onClick: () => { showTipsDialog({ title: t('confirm-to-delete'), content: t('this-operation-removes-the-associated-label-and-cannot-be-restored-please-confirm'), @@ -98,24 +62,21 @@ export const BlinkoMultiSelectPop = observer(() => { loading: t('in-progress'), success: {t('your-changes-have-been-saved')}, error: {t('operation-failed')}, - }) - blinko.curMultiSelectIds.map(i => api.ai.embeddingDelete.mutate({ id: i })) - blinko.onMultiSelectRest() - RootStore.Get(DialogStandaloneStore).close() + }); + blinko.curMultiSelectIds.map(i => api.ai.embeddingDelete.mutate({ id: i })); + blinko.onMultiSelectRest(); + RootStore.Get(DialogStandaloneStore).close(); } - }) - - }} className={SelectItems + ' text-red-500'}> - -
{t('delete')}
-
- -
{ - blinko.onMultiSelectRest() - }}> - -
+ }); + } + } + ]; -
-}) \ No newline at end of file + return ( + blinko.onMultiSelectRest()} + /> + ); +}); \ No newline at end of file diff --git a/src/components/BlinkoNotification/index.tsx b/src/components/BlinkoNotification/index.tsx index 894d04f2..35d6bf3f 100644 --- a/src/components/BlinkoNotification/index.tsx +++ b/src/components/BlinkoNotification/index.tsx @@ -12,6 +12,7 @@ import { Notifications, NotificationType } from '@/lib/prismaZodType'; import { ShowCommentDialog } from '../BlinkoCard/commentButton'; import { BlinkoStore } from '@/store/blinkoStore'; + export const BlinkoNotification = observer(() => { const { t } = useTranslation(); const blinko = RootStore.Get(BlinkoStore) @@ -79,7 +80,7 @@ export const BlinkoNotification = observer(() => { size="sm" > { + const { t } = useTranslation(); + const resourceStore = RootStore.Get(ResourceStore); + + const actions = [ + { + icon: "mingcute:delete-2-line", + text: t('delete'), + isDeleteButton: true, + onClick: () => { + showTipsDialog({ + title: t('confirm-to-delete'), + content: t('this-operation-will-delete-the-selected-files-and-cannot-be-restored-please-confirm'), + onConfirm: async () => { + const selectedIds = Array.from(resourceStore.selectedItems); + if (selectedIds.length === 0) return; + + await RootStore.Get(ToastPlugin).promise( + api.attachments.deleteMany.mutate({ ids: selectedIds }), + { + loading: t('in-progress'), + success: {t('your-changes-have-been-saved')}, + error: {t('operation-failed')}, + } + ); + resourceStore.clearSelection(); + resourceStore.loadResources(resourceStore.currentFolder || undefined); + RootStore.Get(DialogStandaloneStore).close(); + } + }); + } + } + ]; + + return ( + 0} + actions={actions} + onClose={() => resourceStore.clearSelection()} + /> + ); +}); diff --git a/src/components/BlinkoRightClickMenu/index.tsx b/src/components/BlinkoRightClickMenu/index.tsx index 907c6897..00024138 100644 --- a/src/components/BlinkoRightClickMenu/index.tsx +++ b/src/components/BlinkoRightClickMenu/index.tsx @@ -19,6 +19,7 @@ import { parseAbsoluteToLocal } from "@internationalized/date"; import i18n from "@/lib/i18n"; import { BlinkoShareDialog } from "../BlinkoShareDialog"; import { BaseStore } from "@/store/baseStore"; +import { PluginApiStore } from "@/store/plugin/pluginApiStore"; export const ShowEditTimeModel = () => { const blinko = RootStore.Get(BlinkoStore) @@ -270,6 +271,7 @@ export const BlinkoRightClickMenu = observer(() => { const [isDetailPage, setIsDetailPage] = useState(false) const router = useRouter() const blinko = RootStore.Get(BlinkoStore) + const pluginApi = RootStore.Get(PluginApiStore) useEffect(() => { setIsDetailPage(router.pathname.includes('/detail')) @@ -310,9 +312,14 @@ export const BlinkoRightClickMenu = observer(() => { ) : <>} - - - + {pluginApi.customRightClickMenus.map((menu) => ( + menu.onClick(blinko.curSelectedNote!)} disabled={menu.disabled}> +
+ {menu.icon && } +
{menu.label}
+
+
+ ))} {!blinko.curSelectedNote?.isRecycle ? ( @@ -332,6 +339,7 @@ export const LeftCickMenu = observer(({ onTrigger, className }: { onTrigger: () const [isDetailPage, setIsDetailPage] = useState(false) const router = useRouter() const blinko = RootStore.Get(BlinkoStore) + const pluginApi = RootStore.Get(PluginApiStore) useEffect(() => { setIsDetailPage(router.pathname.includes('/detail')) @@ -362,6 +370,23 @@ export const LeftCickMenu = observer(({ onTrigger, className }: { onTrigger: () ) : <>} + { + pluginApi.customRightClickMenus.length > 0 ? + <> + { + pluginApi.customRightClickMenus.map((menu) => ( + menu.onClick(blinko.curSelectedNote!)}> +
+ {menu.icon && } +
{menu.label}
+
+
+ )) + } + : + <> + } + {!blinko.curSelectedNote?.isRecycle ? ( @@ -373,6 +398,7 @@ export const LeftCickMenu = observer(({ onTrigger, className }: { onTrigger: () ) : <>} + }) \ No newline at end of file diff --git a/src/components/BlinkoSettings/AiSetting.tsx b/src/components/BlinkoSettings/AiSetting.tsx index 9fa185b0..0ed15d98 100644 --- a/src/components/BlinkoSettings/AiSetting.tsx +++ b/src/components/BlinkoSettings/AiSetting.tsx @@ -57,7 +57,7 @@ export const AiSetting = observer(() => { return ( { + const { t } = useTranslation(); + return ( + +
+ + +
+
+
+
+
+
+

url && window.open(url, '_blank')}> + {displayName?.[i18n.language] || displayName?.default} +

+ {url && ( + window.open(url, '_blank')} + /> + )} +
+
+ + v{version} + + {author && ( + + by {author} + + )} +
+
+
+
+
+ {actionButton} +
+
+ +

+ {description?.[i18n.language] || description?.default} +

+ + {!!downloads && downloads > 0 && ( +
+
+ + {downloads.toLocaleString()} {t('downloads')} +
+
+ )} +
+
+ + ); +}; + +const InstalledPlugins = observer(() => { + const { t } = useTranslation(); + const pluginManager = RootStore.Get(PluginManagerStore); + + const handleUninstall = async (id: number) => { + await PromiseCall(pluginManager.uninstallPlugin(id)); + }; + + return ( +
+ + {pluginManager.installedPlugins.value?.map((plugin) => { + const metadata = plugin.metadata as { + name: string; + version: string; + displayName: { default: string; zh_CN: string }; + description: { default: string; zh_CN: string }; + withSettingPanel?: boolean; + }; + console.log('metadata', metadata); + return ( + + {pluginManager.isIntalledPluginWithSettingPanel(metadata.name) && ( + +
+ } + /> + ); + })} +
+ ); +}); + +const AllPlugins = observer(() => { + const { t } = useTranslation(); + const pluginManager = RootStore.Get(PluginManagerStore); + const [loadingPluginName, setLoadingPluginName] = useState(null); + + const handleInstall = async (plugin: PluginInfo) => { + setLoadingPluginName(plugin.name); + try { + await PromiseCall(pluginManager.installPlugin(plugin), { autoAlert: true }); + pluginManager.loadAllPlugins(); + } finally { + setLoadingPluginName(null); + } + }; + + return ( +
+ + + {pluginManager.marketplacePlugins.value?.map((plugin) => ( + } + onPress={() => handleInstall(plugin)} + > + {t('install')} + + } + /> + ))} +
+ ); +}); + +const AddLocalPluginDialog = observer(({ onClose }: { onClose: () => void }) => { + const { t } = useTranslation(); + const [url, setUrl] = useState(''); + + const handleConfirm = () => { + RootStore.Get(PluginManagerStore).connectDevPlugin(url); + onClose(); + }; + + return ( +
+ setUrl(e.target.value)} + startContent={ + + } + className="w-full" + /> +
+ + +
+
+ ); +}); + +const LocalDevelopment = observer(() => { + const { t } = useTranslation(); + const dialog = RootStore.Get(DialogStandaloneStore); + const pluginManager = RootStore.Get(PluginManagerStore); + + const handleAddLocalPlugin = () => { + dialog.setData({ + isOpen: true, + title: t('add-local-plugin'), + size: 'md', + content: dialog.close()} /> + }); + }; + + const handleDisconnect = () => { + pluginManager.disconnectDevPlugin(); + }; + + return ( +
+
+
+ {t('local-development-description')} +
+ +
+ + {pluginManager.devWebscoketUrl.value && ( + +
+
+
+
+
+
+
+ {pluginManager.devPluginMetadata?.displayName?.[i18n.language] || + pluginManager.devPluginMetadata?.displayName?.default || + t('local-plugin')} +
+
+ {pluginManager.devPluginMetadata?.description?.[i18n.language] || + pluginManager.devPluginMetadata?.description?.default} +
+
+
+ + {pluginManager.devWebscoketUrl.value} +
+ +
+
+
+ {pluginManager.devPluginMetadata?.withSettingPanel && ( + + )} +
+ + )} +
+ ); +}); + +export const PluginSetting = observer(() => { + const { t } = useTranslation(); + useEffect(() => { + RootStore.Get(PluginManagerStore).loadAllPlugins(); + }, []); + return ( + + + + + + + + + + + + + + ); +}); \ No newline at end of file diff --git a/src/components/Common/Editor/Toolbar/AIWriteButton/index.tsx b/src/components/Common/Editor/Toolbar/AIWriteButton/index.tsx index c03a6639..8739570c 100644 --- a/src/components/Common/Editor/Toolbar/AIWriteButton/index.tsx +++ b/src/components/Common/Editor/Toolbar/AIWriteButton/index.tsx @@ -14,6 +14,7 @@ import { Icon } from '@iconify/react'; import { SendIcon } from '@/components/Common/Icons'; import { MarkdownRender } from '@/components/Common/MarkdownRender'; import { eventBus } from '@/lib/event'; +import { PluginApiStore } from '@/store/plugin/pluginApiStore'; interface Props { store: EditorStore; @@ -26,6 +27,7 @@ export const AIWriteButton = observer(({ store, content }: Props) => { const blinko = RootStore.Get(BlinkoStore); const ai = RootStore.Get(AiStore); const scrollRef = useRef(null); + const pluginApi = RootStore.Get(PluginApiStore); const localStore = RootStore.Local(() => ({ show: false, @@ -91,6 +93,24 @@ export const AIWriteButton = observer(({ store, content }: Props) => {
+
+ {pluginApi.customAiPrompts.map((prompt, index) => ( + + ))} +
+ {ai.writingResponseText && ( { }}> {ai.isLoading ? diff --git a/src/components/Common/Editor/Toolbar/IconButton/index.tsx b/src/components/Common/Editor/Toolbar/IconButton/index.tsx index d0d24c88..c55307db 100644 --- a/src/components/Common/Editor/Toolbar/IconButton/index.tsx +++ b/src/components/Common/Editor/Toolbar/IconButton/index.tsx @@ -17,7 +17,11 @@ export const IconButton = observer(({ tooltip, icon, onClick, classNames, childr { onClick?.(e) }}> - + {typeof icon === 'string' && icon.includes('svg') ? ( +
+ ) : ( + + )} {children} diff --git a/src/components/Common/Editor/index.tsx b/src/components/Common/Editor/index.tsx index fc56989d..0f2c1489 100644 --- a/src/components/Common/Editor/index.tsx +++ b/src/components/Common/Editor/index.tsx @@ -11,7 +11,7 @@ import { _ } from '@/lib/lodash'; import { useTranslation } from 'react-i18next'; import { useMediaQuery } from 'usehooks-ts'; import { type Attachment } from '@/server/types'; -import { Card } from '@nextui-org/react'; +import { Card, Popover, PopoverTrigger, PopoverContent } from '@nextui-org/react'; import { AttachmentsRender, ReferenceRender } from '../AttachmentRender'; import { UploadButtons } from './Toolbar/UploadButtons'; import { ReferenceButton } from './Toolbar/ReferenceButton'; @@ -29,6 +29,10 @@ import { EditorStore } from "./editorStore"; import { AIWriteButton } from "./Toolbar/AIWriteButton"; import { FullScreenButton } from "./Toolbar/FullScreenButton"; import { eventBus } from "@/lib/event"; +import { Icon } from "@iconify/react"; +import { PluginApiStore } from "@/store/plugin/pluginApiStore"; +import { PluginRender } from '@/store/plugin/pluginRender'; +import { IconButton } from "./Toolbar/IconButton"; //https://ld246.com/guide/markdown type IProps = { @@ -48,6 +52,7 @@ const Editor = observer(({ content, onChange, onSend, isSendLoading, originFiles const cardRef = React.useRef(null) const isPc = useMediaQuery('(min-width: 768px)') const store = useLocalObservable(() => new EditorStore()) + const pluginApi = RootStore.Get(PluginApiStore) const blinko = RootStore.Get(BlinkoStore) const { t } = useTranslation() @@ -128,6 +133,18 @@ const Editor = observer(({ content, onChange, onSend, isSendLoading, originFiles open={open} onFileUpload={store.uploadFiles} /> + {pluginApi.customToolbarIcons.map((item) => ( + + +
+ +
+
+ + + +
+ ))} )}
@@ -142,5 +159,4 @@ const Editor = observer(({ content, onChange, onSend, isSendLoading, originFiles ); }); -export default Editor - +export default Editor \ No newline at end of file diff --git a/src/components/Common/LoadingAndEmpty.tsx b/src/components/Common/LoadingAndEmpty.tsx index cb9085d3..e033bdd6 100644 --- a/src/components/Common/LoadingAndEmpty.tsx +++ b/src/components/Common/LoadingAndEmpty.tsx @@ -5,21 +5,23 @@ interface LoadingAndEmptyProps { isLoading?: boolean; isEmpty?: boolean; emptyMessage?: string; + className?: string; + isAbsolute?: boolean; } -export const LoadingAndEmpty = ({ isLoading, isEmpty, emptyMessage }: LoadingAndEmptyProps) => { +export const LoadingAndEmpty = ({ isLoading, isEmpty, emptyMessage, className, isAbsolute = true }: LoadingAndEmptyProps) => { const { t } = useTranslation(); - + return ( -
- + {isEmpty && ( -
+
{emptyMessage || t('no-data-here-well-then-time-to-write-a-note')} diff --git a/src/components/Common/MarkdownRender/ListItem.tsx b/src/components/Common/MarkdownRender/ListItem.tsx index 7a0e0040..0d44105a 100644 --- a/src/components/Common/MarkdownRender/ListItem.tsx +++ b/src/components/Common/MarkdownRender/ListItem.tsx @@ -8,6 +8,80 @@ interface ListItemProps { className?: string; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getNodeAsText = (node: any): string => { + if (!node) return ''; + if (typeof node === 'string') return node; + + const { type, props } = node; + switch (type) { + case 'strong': + return `**${getNodesAsText(props.children)}**`; + case 'em': + return `*${props.children}*`; + case 'del': + return `~~${props.children}~~`; + } + + const name = type?.name; + const children = props?.children; + switch (name) { + case 'a': + return `[${children}](${props.href})`; + } + + return Array.isArray(children) ? getNodesAsText(children) : ''; +} +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getNodesAsText = (nodes: any[]): string => { + return Array.isArray(nodes) ? + nodes.filter(v => { + if (v === null || v.type === 'input' || v === '\n') return false; + if (v.type === 'ul' + && v.props?.className?.includes('contains-task-list')) { + return false; + } + return true; + }).map(getNodeAsText).join('') : + getNodeAsText(nodes); +} + +const replaceTaskMark = ( + content: string, isChecked: boolean, taskText: string, + newContent: string, targetState: boolean +): string => { + const key = `]${taskText}`; + const index = content.indexOf(key); + if (index === -1) return newContent; + + const start = index - 4; + if (start < 0) return newContent; + if (isChecked === (content.charAt(index - 1) === ' ')) return newContent; + + const oldMark = content.slice(start, index + key.length); + const targetMark = `${oldMark.charAt(0)} [${targetState ? 'x' : ' '}]${taskText}`; + return newContent.replace(oldMark, targetMark); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getChildTasks = (nodes: any[]): { text: string; checked: boolean }[] => { + return nodes.map(node => { + const className = node?.props?.className; + if (!className) return null; + + if (className.includes('task-list-item')) { + const children = node.props.children; + const childCheckbox = children.find(child => child?.type === 'input'); + const text = getNodeAsText(node); + return [{ text, checked: childCheckbox?.props?.checked ?? false }, ...getChildTasks(children)]; + } + if (className.includes('contains-task-list')) { + return getChildTasks(node.props.children); + } + return null; + }).filter(v => v !== null).flat(); +}; + export const ListItem: React.FC = ({ children, content, onChange, className }) => { if (!className?.includes('task-list-item')) { return
  • {children}
  • ; @@ -17,42 +91,7 @@ export const ListItem: React.FC = ({ children, content, onChange, const checkbox = childArray.find((child: any) => child?.type === 'input') as any; const isChecked = checkbox?.props?.checked ?? false; - const getTaskText = (nodes: any[]): string => { - return nodes - .filter((node: any) => node?.type !== 'input') - .map((node: any) => { - if (typeof node === 'string') return node; - if (node?.props?.href) return `[${node.props.children}](${node.props.href})`; - if (node?.props?.children) { - return Array.isArray(node.props.children) - ? getTaskText(node.props.children) - : node.props.children; - } - return ''; - }) - .join(''); - }; - - const getChildTasks = (nodes: any[]): { text: string; checked: boolean }[] => { - //@ts-ignore - return nodes - .filter((node: any) => node?.type !== 'input') - .map((node: any) => { - if (node?.props?.className?.includes('task-list-item')) { - const childCheckbox = node.props.children.find((child: any) => child?.type === 'input'); - const text = getTaskText([node]); - return { text, checked: childCheckbox?.props?.checked ?? false }; - } - if (node?.props?.children) { - return getChildTasks(Array.isArray(node.props.children) ? node.props.children : [node.props.children]).flat(); - } - return null; - }) - .filter(Boolean) - .flat(); - }; - - const taskText = getTaskText(childArray); + const taskText = getNodesAsText(childArray); const textContent = childArray.map((child: any) => { if (child?.type === 'input') return null; if (child?.props?.children?.[0]?.props?.type === 'checkbox') { @@ -75,26 +114,12 @@ export const ListItem: React.FC = ({ children, content, onChange, if (!onChange) return; e.stopPropagation(); - let newContent = content; const targetState = hasChildren ? !allChildrenChecked : !isChecked; - const oldMark = isChecked ? - content.includes('- [x]') ? `- [x]${taskText}` : `* [x]${taskText}` : - content.includes('- [ ]') ? `- [ ]${taskText}` : `* [ ]${taskText}`; - const newMark = targetState ? - content.includes('- [') ? `- [x]${taskText}` : `* [x]${taskText}` : - content.includes('- [') ? `- [ ]${taskText}` : `* [ ]${taskText}`; - newContent = newContent.replace(oldMark, newMark); - + let newContent = replaceTaskMark(content, isChecked, taskText, content, targetState); if (hasChildren) { childTasks.forEach(task => { - const oldChildMark = task.checked ? - content.includes('- [x]') ? `- [x]${task.text}` : `* [x]${task.text}` : - content.includes('- [ ]') ? `- [ ]${task.text}` : `* [ ]${task.text}`; - const newChildMark = targetState ? - content.includes('- [') ? `- [x]${task.text}` : `* [x]${task.text}` : - content.includes('- [') ? `- [ ]${task.text}` : `* [ ]${task.text}`; - newContent = newContent.replace(oldChildMark, newChildMark); + newContent = replaceTaskMark(content, task.checked, task.text, newContent, targetState); }); } diff --git a/src/components/Common/MultiSelectToolbar.tsx b/src/components/Common/MultiSelectToolbar.tsx new file mode 100644 index 00000000..1d44884a --- /dev/null +++ b/src/components/Common/MultiSelectToolbar.tsx @@ -0,0 +1,46 @@ +import { Button } from "@nextui-org/react"; +import { Icon } from "@iconify/react"; + +interface Action { + icon: string; + text: string; + isDeleteButton?: boolean; + onClick: () => void; +} + +interface MultiSelectToolbarProps { + show: boolean; + actions: Action[]; + onClose: () => void; +} + +export const MultiSelectToolbar = ({ show, actions, onClose }: MultiSelectToolbarProps) => { + if (!show) return null; + + return ( +
    +
    + {actions.map((action, index) => ( + + ))} +
    +
    + ); +}; \ No newline at end of file diff --git a/src/lib/prismaZodType.ts b/src/lib/prismaZodType.ts index 4b86df60..e6acdc3a 100644 --- a/src/lib/prismaZodType.ts +++ b/src/lib/prismaZodType.ts @@ -251,3 +251,21 @@ export const cacheSchema = z.object({ value: z.any(), }) + +///////////////////////////////////////// +// PLUGIN SCHEMA +// ///////////////////////////////////////// + +export const pluginSchema = z.object({ + id: z.number().int(), + metadata: z.any(), + path: z.string(), + isUse: z.boolean(), + isDev: z.boolean(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), +}) + +export type plugin = z.infer + + diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 73820aed..db9d53a6 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -18,6 +18,9 @@ import { motion } from 'motion/react'; import { BlinkoMultiSelectPop } from '@/components/BlinkoMultiSelectPop'; import { BlinkoMusicPlayer } from '@/components/BlinkoMusicPlayer'; import { LoadingPage } from '@/components/Common/LoadingPage'; +import { PluginManagerStore } from '@/store/plugin/pluginManagerStore'; +import { RootStore } from '@/store'; +import { api } from '@/lib/trpc'; const MyApp = ({ Component, pageProps }) => { const [isLoading, setIsLoading] = useState(true); @@ -28,6 +31,7 @@ const MyApp = ({ Component, pageProps }) => { const timer = setTimeout(() => { setIsLoading(false); }, 500); + RootStore.Get(PluginManagerStore).initInstalledPlugins(); return () => clearTimeout(timer); }, []); diff --git a/src/pages/api/serve-plugin/[...path].ts b/src/pages/api/serve-plugin/[...path].ts new file mode 100644 index 00000000..2e51fb4c --- /dev/null +++ b/src/pages/api/serve-plugin/[...path].ts @@ -0,0 +1,17 @@ +import { createReadStream } from 'fs'; +import { join } from 'path'; +import { NextApiRequest, NextApiResponse } from 'next'; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { path } = req.query; + const pathArray = Array.isArray(path) ? path : [path || ''].filter(Boolean); + const filePath = join('.blinko', 'plugins', ...pathArray); + + try { + const stream = createReadStream(filePath); + res.setHeader('Content-Type', 'application/javascript'); + stream.pipe(res); + } catch (error) { + res.status(404).json({ error: 'Plugin not found' }); + } +} \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 148d7c4e..2ca44765 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -34,6 +34,7 @@ const Home = observer(() => { maxWidth: blinko.config.value?.maxHomePageWidth ? `${blinko.config.value?.maxHomePageWidth}px` : '100%' }} className={`md:p-0 relative h-full flex flex-col-reverse md:flex-col mx-auto`}> + {store.showEditor && isPc &&
    { if (!isPc) return diff --git a/src/pages/resources.tsx b/src/pages/resources.tsx index 4a97dc05..c4dd0ccb 100644 --- a/src/pages/resources.tsx +++ b/src/pages/resources.tsx @@ -9,6 +9,7 @@ import { DragDropContext, Droppable } from 'react-beautiful-dnd-next'; import { toJS } from "mobx"; import { useRouter } from 'next/router'; import { MemoizedResourceItem } from "@/components/BlinkoResource/ResourceItem"; +import { ResourceMultiSelectPop } from "@/components/BlinkoResource/ResourceMultiSelectpop"; import { Breadcrumbs, BreadcrumbItem, Button } from "@nextui-org/react"; import { AnimatePresence, motion } from "framer-motion"; import { LoadingAndEmpty } from "@/components/Common/LoadingAndEmpty"; @@ -44,76 +45,76 @@ const Page = observer(() => { resourceStore.use(router) return ( - - -
    -
    - - {resourceStore.currentFolder && ( - - - {folderBreadcrumbs.map((folder, index) => ( - { - if (index === 0) { - resourceStore.navigateBack(router); - } else { - const currentPathSegments = resourceStore.currentFolder?.split('/') || []; - const clickedPathLevel = index; - const stepsToGoBack = currentPathSegments.length - clickedPathLevel; - - for (let i = 0; i < stepsToGoBack; i++) { + <> + + +
    +
    + + {resourceStore.currentFolder && ( + + + {folderBreadcrumbs.map((folder, index) => ( + { + if (index === 0) { resourceStore.navigateBack(router); + } else { + const currentPathSegments = resourceStore.currentFolder?.split('/') || []; + const clickedPathLevel = index; + const stepsToGoBack = currentPathSegments.length - clickedPathLevel; + + for (let i = 0; i < stepsToGoBack; i++) { + resourceStore.navigateBack(router); + } } - } - }} - > - {folder} - - ))} - - - )} - -
    + }} + > + {folder} + + ))} + + + )} + +
    -
    - - - + + - {selectedItems.size > 0 && resourceStore.currentFolder && resourceStore.currentFolder !== 'Root' && ( { }} > - )} -
    -
    - - - {resources.length > 0 && ( - - {(provided, snapshot) => ( -
    0 && resourceStore.currentFolder && resourceStore.currentFolder !== 'Root' && ( + - {resources.map((item, index) => ( - resourceStore.navigateToFolder(folder, router)} - /> - ))} - {provided.placeholder} -
    + + )} -
    - )} +
    +
    + + + + {resources.length > 0 && ( + + {(provided, snapshot) => ( +
    + {resources.map((item, index) => ( + resourceStore.navigateToFolder(folder, router)} + /> + ))} + {provided.placeholder} +
    + )} +
    + )} -
    + - - + + + + ); }); diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index da8998e9..afbb12c8 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -18,6 +18,7 @@ import { JSX } from "react"; import { ScrollableTabs, TabItem } from "@/components/Common/ScrollableTabs"; import { useState } from "react"; import { BlinkoStore } from "@/store/blinkoStore"; +import { PluginSetting } from "@/components/BlinkoSettings/PluginSetting"; type SettingItem = { key: string; @@ -70,7 +71,7 @@ const Page = observer(() => { { key: "ai", title: 'AI', - icon: "tabler:brain", + icon: "mingcute:ai-line", component: , requireAdmin: true, keywords: ['ai', 'artificial intelligence', '人工智能'] @@ -123,6 +124,14 @@ const Page = observer(() => { requireAdmin: false, keywords: ['export', 'data', '导出', '数据导出'] }, + { + key: "plugin", + title: t('plugin-settings'), + icon: "mingcute:plugin-line", + component: , + requireAdmin: true, + keywords: ['plugin', 'plugins', '插件', '插件设置'] + }, { key: "about", title: t('about'), diff --git a/src/server/plugins/ai.ts b/src/server/plugins/ai.ts index c3acb586..94ed4525 100644 --- a/src/server/plugins/ai.ts +++ b/src/server/plugins/ai.ts @@ -45,10 +45,10 @@ export class AiService { case filePath.endsWith('.txt'): loader = new TextLoader(filePath); break; - // case filePath.endsWith('.csv'): - // console.log('load csv') - // loader = new CSVLoader(filePath); - // break; + case filePath.endsWith('.csv'): + console.log('load csv') + loader = new CSVLoader(filePath); + break; default: loader = new UnstructuredLoader(filePath); } diff --git a/src/server/routers/_app.ts b/src/server/routers/_app.ts index cd941bc5..ef68b1bf 100644 --- a/src/server/routers/_app.ts +++ b/src/server/routers/_app.ts @@ -16,6 +16,7 @@ import { analyticsRouter } from './analytics'; import { commentRouter } from './comment'; import { followsRouter } from './follows'; import { notificationRouter } from './notification'; +import { pluginRouter } from './plugin'; export const appRouter = router({ ai: aiRouter, @@ -29,7 +30,8 @@ export const appRouter = router({ analytics: analyticsRouter, comments: commentRouter, follows: followsRouter, - notifications: notificationRouter + notifications: notificationRouter, + plugin: pluginRouter }); export const createCaller = t.createCallerFactory(appRouter); diff --git a/src/server/routers/attachment.ts b/src/server/routers/attachment.ts index 9981ec8f..f34c020a 100644 --- a/src/server/routers/attachment.ts +++ b/src/server/routers/attachment.ts @@ -5,7 +5,7 @@ import { Prisma } from '@prisma/client'; import path from 'path'; import { FileService } from '../plugins/files'; -interface AttachmentResult { +export interface AttachmentResult { id: number | null; path: string; name: string; @@ -420,4 +420,17 @@ export const attachmentsRouter = router({ } }); }), + deleteMany: authProcedure + .input(z.object({ + ids: z.array(z.number()), + })) + .mutation(async ({ input, ctx }) => { + const { ids } = input; + await prisma.attachments.deleteMany({ + where: { + id: { in: ids } + } + }); + return { success: true, message: 'Files deleted successfully' }; + }), }); diff --git a/src/server/routers/config.ts b/src/server/routers/config.ts index 4b39981c..b884b4a4 100644 --- a/src/server/routers/config.ts +++ b/src/server/routers/config.ts @@ -12,8 +12,9 @@ export const getGlobalConfig = async ({ ctx, useAdmin = false }: { ctx?: Context const globalConfig = configs.reduce((acc, item) => { const config = item.config as { type: string, value: any }; - if (item.key === 'isUseAI' - || item.key == 'isCloseBackgroundAnimation' + //If not login return the frist config + if ( + item.key == 'isCloseBackgroundAnimation' || item.key == 'isAllowRegister' || item.key == 'language' || item.key == 'theme' @@ -22,8 +23,16 @@ export const getGlobalConfig = async ({ ctx, useAdmin = false }: { ctx?: Context || item.key == 'maxHomePageWidth' || item.key == 'customBackgroundUrl' ) { + //if user not login, then use frist find config + if (!userId) { + acc[item.key] = config.value; + return acc; + } + } + //always return isUseAI config + if (item.key == 'isUseAI') { acc[item.key] = config.value; - return acc; + return acc } if (!isSuperAdmin && !item.userId) { return acc; @@ -75,4 +84,45 @@ export const configRouter = router({ return await prisma.config.create({ data: { key, config: { type: typeof value, value } } }) } }), + + setPluginConfig: authProcedure + .meta({ openapi: { method: 'POST', path: '/v1/config/setPluginConfig', summary: 'Set plugin config', protect: true, tags: ['Config'] } }) + .input(z.object({ + pluginName: z.string(), + key: z.string(), + value: z.any() + })) + .output(z.any()) + .mutation(async function ({ input, ctx }) { + const userId = Number(ctx.id) + const { pluginName, key, value } = input + const hasKey = await prisma.config.findFirst({ where: { userId, key: `plugin_config_${pluginName}_${key}` } }) + if (hasKey) { + return await prisma.config.update({ where: { id: hasKey.id }, data: { config: { type: typeof value, value } } }) + } + return await prisma.config.create({ data: { userId, key: `plugin_config_${pluginName}_${key}`, config: { type: typeof value, value } } }) + }), + getPluginConfig: authProcedure + .meta({ openapi: { method: 'GET', path: '/v1/config/getPluginConfig', summary: 'Get plugin config', protect: true, tags: ['Config'] } }) + .input(z.object({ + pluginName: z.string() + })) + .output(z.any()) + .query(async function ({ input, ctx }) { + const userId = Number(ctx.id) + const { pluginName } = input + const configs = await prisma.config.findMany({ + where: { + userId, + key: { + contains: `plugin_config_${pluginName}_` + } + } + }) + return configs.reduce((acc, item) => { + const key = item.key.replace(`plugin_config_${pluginName}_`, ''); + acc[key] = (item.config as { value: any }).value; + return acc; + }, {} as Record); + }) }) diff --git a/src/server/routers/helper/index.ts b/src/server/routers/helper/index.ts index 4bee068b..abe4901f 100644 --- a/src/server/routers/helper/index.ts +++ b/src/server/routers/helper/index.ts @@ -11,7 +11,8 @@ import { encode, getToken as getNextAuthToken } from 'next-auth/jwt'; export const SendWebhook = async (data: any, webhookType: string, ctx: { id: string }) => { try { - const globalConfig = await getGlobalConfig({ useAdmin: true }) + //@ts-ignore + const globalConfig = await getGlobalConfig({ ctx }) if (globalConfig.webhookEndpoint) { await axios.post(globalConfig.webhookEndpoint, { data, webhookType, activityType: `blinko.note.${webhookType}` }) } diff --git a/src/server/routers/plugin.ts b/src/server/routers/plugin.ts new file mode 100644 index 00000000..61fb34eb --- /dev/null +++ b/src/server/routers/plugin.ts @@ -0,0 +1,215 @@ +import { router, authProcedure } from '../trpc'; +import { z } from 'zod'; +import { prisma } from '../prisma'; +import fs from 'fs/promises'; +import path from 'path'; +import axios from 'axios'; +import yauzl from 'yauzl-promise'; +import { createWriteStream } from 'fs'; +import { pluginInfoSchema, installPluginSchema } from '../types'; +import { pluginSchema } from '@/lib/prismaZodType'; +import { cache } from '@/lib/cache'; +import { existsSync } from 'fs'; + +const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes cache duration +const MAX_RETRIES = 3; +const RETRY_DELAY = 1000; // 1 second + +const ensurePluginDir = async () => { + const dir = path.join(process.cwd(), '.blinko', 'plugins'); + if (!existsSync(dir)) { + await fs.mkdir(dir, { recursive: true }); + } +}; + +async function downloadWithRetry(url: string, filePath: string, retries = MAX_RETRIES): Promise { + try { + const response = await axios.get(url, { + responseType: 'arraybuffer', + timeout: 30000, // 30 seconds timeout + maxContentLength: 50 * 1024 * 1024 // 50MB max + }); + await fs.writeFile(filePath, response.data); + } catch (error) { + if (retries > 0 && (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT')) { + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); + return downloadWithRetry(url, filePath, retries - 1); + } + throw error; + } +} + +export const pluginRouter = router({ + getAllPlugins: authProcedure + .output(z.array(pluginInfoSchema)) + .query(async () => { + return cache.wrap('plugin-list', async () => { + try { + const response = await axios.get('https://raw.githubusercontent.com/blinko-space/blinko-plugin-marketplace/main/index.json'); + return response.data; + } catch (error) { + console.error('Failed to fetch plugin list:', error); + return []; + } + }, { + ttl: CACHE_DURATION + }); + }), + + saveDevPlugin: authProcedure + .input(z.object({ + code: z.string(), + fileName: z.string(), + metadata: z.any() + })) + .output(z.any()) + .mutation(async function ({ input }) { + const devPluginDir = path.join('.blinko', 'plugins', 'dev'); + try { + await fs.rm(devPluginDir, { recursive: true, force: true }); + } catch (error) { } + try { + await fs.mkdir(devPluginDir, { recursive: true }); + await fs.writeFile( + path.join(devPluginDir, input.fileName), + input.code + ); + return { success: true }; + } catch (error) { + console.error('Save dev plugin error:', error); + throw error; + } + }), + + installPlugin: authProcedure + .input(installPluginSchema) + .mutation(async ({ input }) => { + const pluginDir = path.join('.blinko', 'plugins', input.name); + const tempZipPath = path.join(pluginDir, 'release.zip'); + + try { + // Check if plugin already exists + const existingPlugin = await prisma.plugin.findFirst({ + where: { + metadata: { + path: ['name'], + equals: input.name + } + } + }); + + if (existingPlugin) { + const metadata = existingPlugin.metadata as { version: string }; + if (metadata.version !== input.version) { + await fs.rm(pluginDir, { recursive: true, force: true }); + } else { + throw new Error(`Plugin v${metadata.version} is already installed`); + } + } + + // Create plugin directory and download files + await fs.mkdir(pluginDir, { recursive: true }); + const releaseUrl = `${input.url}/releases/download/v${input.version}/release.zip`; + + // Use retry mechanism for download + await downloadWithRetry(releaseUrl, tempZipPath); + + // Extract zip file + const zipFile = await yauzl.open(tempZipPath); + for await (const entry of zipFile) { + if (entry.filename.endsWith('/')) { + await fs.mkdir(path.join(pluginDir, entry.filename), { recursive: true }); + continue; + } + + const targetPath = path.join(pluginDir, entry.filename); + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + + const readStream = await entry.openReadStream(); + const writeStream = createWriteStream(targetPath); + + await new Promise((resolve, reject) => { + readStream + .pipe(writeStream) + .on('finish', resolve) + .on('error', reject); + }); + } + + await zipFile.close(); + await fs.unlink(tempZipPath); + + // Save to database + return await prisma.$transaction(async (tx) => { + if (existingPlugin) { + return await tx.plugin.update({ + where: { id: existingPlugin.id }, + data: { + metadata: input, + path: `/plugins/${input.name}/index.js`, + } + }); + } else { + return await tx.plugin.create({ + data: { + metadata: input, + path: `/plugins/${input.name}/index.js`, + isUse: true, + isDev: false, + } + }); + } + }); + } catch (error) { + // Clean up on error + try { + await fs.rm(pluginDir, { recursive: true, force: true }); + } catch (cleanupError) { + console.error('Cleanup error:', cleanupError); + } + console.error('Install plugin error:', error); + throw error; + } + }), + + getInstalledPlugins: authProcedure + .output(z.array(pluginSchema)) + .query(async () => { + const plugins = await prisma.plugin.findMany(); + return plugins; + }), + + uninstallPlugin: authProcedure + .input(z.object({ + id: z.number() + })) + .mutation(async ({ input }) => { + try { + const plugin = await prisma.plugin.findUnique({ + where: { id: input.id } + }); + + if (!plugin) { + throw new Error('Plugin not found'); + } + + const metadata = plugin.metadata as { name: string }; + const pluginDir = path.join('.blinko', 'plugins', metadata.name); + + // Delete plugin files + await fs.rm(pluginDir, { recursive: true, force: true }); + + // Delete from database + await prisma.plugin.delete({ + where: { id: input.id } + }); + + return { success: true }; + } catch (error) { + console.error('Uninstall plugin error:', error); + throw error; + } + }), +}); + +ensurePluginDir().catch(console.error); diff --git a/src/server/types.ts b/src/server/types.ts index 84e0723b..6f91b322 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -8,6 +8,7 @@ export type Config = NonNullable export type LinkInfo = NonNullable export type ResourceType = NonNullable[0] export type Comment = NonNullable +export type InstalledPluginInfo = NonNullable[0] export enum NoteType { 'BLINKO', 'NOTE' @@ -75,7 +76,8 @@ export const ZConfigKey = z.union([ z.literal('oauth2Providers'), z.literal('embeddingApiEndpoint'), z.literal('embeddingApiKey'), - ZUserPerferConfigKey + ZUserPerferConfigKey, + z.any() ]); export type ConfigKey = z.infer; @@ -146,3 +148,25 @@ export const ZConfigSchema = z.object({ export type GlobalConfig = z.infer; +// Zod schema for plugin information +export const pluginInfoSchema = z.object({ + name: z.string(), + author: z.string(), + url: z.string(), + version: z.string(), + minAppVersion: z.string(), + displayName: z.any(), + description: z.any(), + readme: z.any(), + downloads: z.number() +}); + +// Schema for plugin installation input (subset of PluginInfo) +export const installPluginSchema = pluginInfoSchema.omit({ + readme: true, + downloads: true +}); + +// TypeScript types derived from the schemas +export type PluginInfo = z.infer; +export type InstallPluginInput = z.infer; \ No newline at end of file diff --git a/src/store/plugin/index.ts b/src/store/plugin/index.ts new file mode 100644 index 00000000..25bd6e25 --- /dev/null +++ b/src/store/plugin/index.ts @@ -0,0 +1,106 @@ +import { api } from "../../lib/trpc"; +import { eventBus } from "../../lib/event"; +import System from 'systemjs/dist/system.js'; +import i18n from "@/lib/i18n"; +import { ToastPlugin } from "../module/Toast/Toast"; +import { BaseStore } from "../baseStore"; +import { BlinkoStore } from "../blinkoStore"; +import { HubStore } from "../hubStore"; +import { ResourceStore } from "../resourceStore"; +import { StorageState } from "../standard/StorageState"; +import { PromiseState } from "../standard/PromiseState"; +import { PromisePageState } from "../standard/PromiseState"; +import { PluginApiStore } from "./pluginApiStore"; +import copy from "copy-to-clipboard" + +declare global { + interface Window { + Blinko: { + api: typeof api; + eventBus: typeof eventBus; + i18n: typeof i18n; + version: string; + copyToClipboard: typeof copy; + toast: InstanceType; + store: { + StorageState: typeof StorageState; + PromiseState: typeof PromiseState; + PromisePageState: typeof PromisePageState; + blinkoStore: InstanceType; + baseStore: InstanceType; + hubStore: InstanceType; + resourceStore: InstanceType; + }; + globalRefresh: () => void; + } & InstanceType; + System?: typeof System; + } +} + +export interface I18nString { + default: string; + zh?: string; + 'zh-tw'?: string; + en?: string; + vi?: string; + tr?: string; + ka?: string; + de?: string; + es?: string; + fr?: string; + pt?: string; + pl?: string; + ru?: string; + ko?: string; + ja?: string; + [key: string]: string | undefined; +} + +/** + * Abstract base class for all plugins in the application. + * Provides common properties and methods that all plugins should implement. + */ +export abstract class BasePlugin { + /** Plugin name (unique identifier) */ + name?: string; + /** Author of the plugin */ + author?: string; + /** URL for plugin documentation or repository */ + url?: string; + /** Current version of the plugin */ + version?: string; + /** Minimum required app version for compatibility */ + minAppVersion?: string; + /** Display name for the plugin (supports i18n) */ + displayName?: I18nString; + /** Short description of the plugin (supports i18n) */ + description?: I18nString; + /** Detailed readme content (supports i18n) */ + readme?: I18nString; + /** Icon URL or icon identifier for the plugin */ + icon?: string; + /** Flag indicating if the plugin has a settings panel */ + withSettingPanel?: boolean; + /** Function to render the settings panel UI */ + renderSettingPanel?: () => HTMLElement; + + /** + * Constructs a new BasePlugin instance + * @param args - Partial object containing plugin configuration + */ + constructor(args: Partial) { + Object.assign(this, args); + } + + /** + * Initialization method called when the plugin is loaded + * Must be implemented by derived classes + */ + abstract init(): void; + + /** + * Cleanup method called when the plugin is unloaded + * Must be implemented by derived classes + */ + abstract destroy(): void; +} \ No newline at end of file diff --git a/src/store/plugin/pluginApiStore.tsx b/src/store/plugin/pluginApiStore.tsx new file mode 100644 index 00000000..138cd880 --- /dev/null +++ b/src/store/plugin/pluginApiStore.tsx @@ -0,0 +1,89 @@ +import { RootStore } from "../root"; +import { DialogStandaloneStore } from "../module/DialogStandalone"; +import { Store } from "../standard/base"; +import { PluginRender } from "./pluginRender"; +import { Note } from "@/server/types"; +export type ToolbarIcon = { + name: string; + icon: string; + tooltip: string; + content: () => HTMLElement; + placement?: 'top' | 'bottom' | 'left' | 'right'; + maxWidth?: number; +} + +export type RightClickMenu = { + name: string; + label: string; + icon?: string; + onClick: (note: Note) => void; + disabled?: boolean; +} + +export type DialogOptions = { + title: string; + content: () => HTMLElement; +} + +export class PluginApiStore implements Store { + sid = 'pluginApiStore'; + autoObservable = true + + customToolbarIcons: ToolbarIcon[] = []; + customRightClickMenus: RightClickMenu[] = []; + customAiPrompts: { name: string; prompt: string; icon?: string }[] = []; + + addToolBarIcon(options: ToolbarIcon) { + const sameNameIndex = this.customToolbarIcons.findIndex(item => item.name === options.name); + if (sameNameIndex !== -1) { + this.customToolbarIcons[sameNameIndex] = { + ...options, + placement: options.placement || 'top', + maxWidth: options.maxWidth || 300, + }; + return; + } + this.customToolbarIcons.push({ + name: options.name, + icon: options.icon, + content: options.content, + placement: options.placement || 'top', + maxWidth: options.maxWidth || 300, + tooltip: options.tooltip, + }); + } + + addRightClickMenu(options: RightClickMenu) { + const sameNameIndex = this.customRightClickMenus.findIndex(item => item.name === options.name); + if (sameNameIndex !== -1) { + this.customRightClickMenus[sameNameIndex] = options; + console.log('this.customRightClickMenus', this.customRightClickMenus); + return; + } + this.customRightClickMenus.push(options); + console.log('this.customRightClickMenus', this.customRightClickMenus); + } + + showDialog(options: DialogOptions) { + RootStore.Get(DialogStandaloneStore).setData({ + isOpen: true, + title: options.title, + content:, + }); + } + + closeDialog() { + RootStore.Get(DialogStandaloneStore).setData({ + isOpen: false, + }); + } + + addAiWritePrompt(name: string, prompt: string, icon?: string) { + const existingIndex = this.customAiPrompts.findIndex(p => p.name === name); + if (existingIndex !== -1) { + this.customAiPrompts[existingIndex] = { name, prompt, icon }; + } else { + this.customAiPrompts.push({ name, prompt, icon }); + } + } +} \ No newline at end of file diff --git a/src/store/plugin/pluginManagerStore.ts b/src/store/plugin/pluginManagerStore.ts new file mode 100644 index 00000000..c84ba5e4 --- /dev/null +++ b/src/store/plugin/pluginManagerStore.ts @@ -0,0 +1,271 @@ +/// +import { api } from "@/lib/trpc"; +import { BasePlugin } from "."; +import { Store } from "../standard/base"; +import { eventBus } from "@/lib/event"; +import System from 'systemjs/dist/system.js'; +import i18n from "@/lib/i18n"; +import { PluginApiStore } from './pluginApiStore'; +import { RootStore } from "../root"; +import { ToastPlugin } from "../module/Toast/Toast"; +import { makeAutoObservable } from "mobx"; +import { PromisePageState, PromiseState } from "../standard/PromiseState"; +import { type PluginInfo, type InstallPluginInput, InstalledPluginInfo } from "@/server/types"; +import { StorageState } from "../standard/StorageState"; +import { BlinkoStore } from "../blinkoStore"; +import { BaseStore } from "../baseStore"; +import { ResourceStore } from "../resourceStore"; +import { HubStore } from "../hubStore"; +import copy from "copy-to-clipboard" + +export class PluginManagerStore implements Store { + sid = 'pluginManagerStore'; + private plugins: Map = new Map(); + ws: WebSocket | null = null; + devPluginMetadata: PluginInfo & { withSettingPanel?: boolean }; + latestDevFileName: string = ''; + public isLoading: boolean = false; + public wsConnectionStatus: 'connected' | 'disconnected' | 'error' = 'disconnected'; + devWebscoketUrl = new StorageState({ + key: 'blinko_dev_plugin_ws_url', + value: '' + }); + + constructor() { + makeAutoObservable(this); + this.initBlinkoContext(); + this.autoConnectDevPlugin(); + } + + private autoConnectDevPlugin() { + const savedUrl = this.devWebscoketUrl.value; + if (savedUrl) { + this.connectDevPlugin(savedUrl).catch(() => { + }); + } + } + + marketplacePlugins = new PromiseState({ + function: async () => { + const installedPlugins = await this.installedPlugins.getOrCall(); + const plugins = await api.plugin.getAllPlugins.query(); + return plugins.filter(plugin => !installedPlugins?.some(installedPlugin => installedPlugin.metadata.name === plugin.name)); + } + }) + + installedPlugins = new PromiseState({ + function: async () => { + const plugins = await api.plugin.getInstalledPlugins.query(); + return plugins; + } + }) + + isIntalledPluginWithSettingPanel(pluginName: string) { + return this.plugins.get(pluginName)?.withSettingPanel; + } + + loadAllPlugins() { + this.marketplacePlugins.call(); + this.installedPlugins.call(); + } + + async initInstalledPlugins() { + const plugins = await this.installedPlugins.getOrCall(); + if (plugins) { + plugins.forEach(plugin => { + this.loadPlugin(plugin.path); + }); + } + } + + async connectDevPlugin(url: string) { + try { + if (this.ws) { + this.ws.close(); + } + + this.devWebscoketUrl.save(url); + this.wsConnectionStatus = 'disconnected'; + + this.ws = new WebSocket(url.replace('http', 'ws')); + + this.ws.onmessage = async (event) => { + const data = JSON.parse(event.data); + try { + if (this.latestDevFileName) { + await this.destroyPlugin(this.latestDevFileName); + } + this.latestDevFileName = data.fileName; + this.devPluginMetadata = data.metadata; + this.wsConnectionStatus = 'connected'; + await this.saveDevPlugin(data.code, data.fileName, data.metadata); + await this.loadDevPlugin(data.fileName); + RootStore.Get(ToastPlugin).success(i18n.t('plugin-updated')); + } catch (error) { + console.error('Plugin update error:', error); + this.wsConnectionStatus = 'error'; + RootStore.Get(ToastPlugin).error(i18n.t('plugin-update-failed')); + } + }; + + this.ws.onopen = () => { + this.wsConnectionStatus = 'connected'; + }; + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error); + this.wsConnectionStatus = 'error'; + RootStore.Get(ToastPlugin).error(i18n.t('plugin-connection-failed')); + }; + + this.ws.onclose = () => { + this.ws = null; + this.wsConnectionStatus = 'disconnected'; + }; + + } catch (error) { + console.error('Connect dev plugin error:', error); + this.wsConnectionStatus = 'error'; + throw error; + } + } + + initBlinkoContext() { + if (typeof window !== 'undefined') { + const pluginApi = RootStore.Get(PluginApiStore); + const toast = RootStore.Get(ToastPlugin); + const blinkoStore = RootStore.Get(BlinkoStore); + const baseStore = RootStore.Get(BaseStore); + const hubStore = RootStore.Get(HubStore); + const resourceStore = RootStore.Get(ResourceStore); + //@ts-ignore + window.Blinko = { + api, + copyToClipboard: copy, + eventBus, + i18n, + //@ts-ignore + toast, + version: '1.0.0', + store: { + StorageState, + PromiseState, + PromisePageState, + blinkoStore, + baseStore, + hubStore, + resourceStore, + }, + globalRefresh: () => { + blinkoStore.updateTicker++; + }, + addToolBarIcon: pluginApi.addToolBarIcon.bind(pluginApi), + addRightClickMenu: pluginApi.addRightClickMenu.bind(pluginApi), + addAiWritePrompt: pluginApi.addAiWritePrompt.bind(pluginApi), + showDialog: pluginApi.showDialog.bind(pluginApi), + closeDialog: pluginApi.closeDialog.bind(pluginApi), + }; + } + + if (typeof window !== 'undefined' && !window.System) { + window.System = System; + } + } + + private async saveDevPlugin(code: string, fileName: string, metadata: any) { + try { + await api.plugin.saveDevPlugin.mutate({ code, fileName, metadata }); + } catch (error) { + console.error('Save dev plugin error:', error); + throw error; + } + } + + private async loadDevPlugin(fileName: string) { + console.log('loadDevPlugin'); + try { + const module = await window.System.import(`/plugins/dev/${fileName}`); + const PluginClass = module.default; + const plugin = new PluginClass(); + plugin.init(); + if (plugin.withSettingPanel) { + this.devPluginMetadata.withSettingPanel = true; + } + this.plugins.set("dev", plugin); + return plugin; + } catch (error) { + console.error('Load dev plugin error:', error); + } + } + + disconnectDevPlugin() { + if (this.ws) { + this.ws.close(); + this.ws = null; + this.wsConnectionStatus = 'disconnected'; + } + this.wsConnectionStatus = 'disconnected'; + this.devWebscoketUrl.save(''); + } + + async loadPlugin(pluginPath: string) { + try { + const module = await window.System.import(pluginPath); + const PluginClass = module.default; + const plugin = new PluginClass(); + plugin.init(); + this.plugins.set(plugin.name, plugin); + if (plugin.withSettingPanel) { + this.plugins[plugin.name].withSettingPanel = true; + } + return plugin; + } catch (error) { + console.error(`load plugin error: ${pluginPath}`, error); + } + } + + getPluginInstanceByName(pluginName: string) { + return this.plugins.get(pluginName); + } + + async destroyPlugin(pluginName: string) { + console.log('destroyPlugin', pluginName); + try { + const plugin = this.plugins.get(pluginName); + if (plugin) { + plugin.destroy(); + this.plugins.delete(pluginName); + } + } catch (error) { + console.error(`destroy plugin error: ${pluginName}`, error); + } + try { + + await window.System.delete(`/plugins/dev/${pluginName}`); + } catch (error) { + console.error(`destroy plugin error: ${pluginName}`, error); + } + } + + async installPlugin(plugin: PluginInfo) { + try { + await api.plugin.installPlugin.mutate(plugin); + await this.marketplacePlugins.call(); + await this.initInstalledPlugins(); + } catch (error) { + console.error('Install plugin error:', error); + throw error; + } + } + + async uninstallPlugin(id: number) { + try { + await api.plugin.uninstallPlugin.mutate({ id }); + await this.installedPlugins.call(); + window.location.reload(); + } catch (error) { + console.error('Uninstall plugin error:', error); + throw error; + } + } +} diff --git a/src/store/plugin/pluginRender.tsx b/src/store/plugin/pluginRender.tsx new file mode 100644 index 00000000..f00f6771 --- /dev/null +++ b/src/store/plugin/pluginRender.tsx @@ -0,0 +1,18 @@ +import React, { useRef } from 'react'; + +interface PluginRenderProps { + content: () => HTMLElement; +} + +export const PluginRender: React.FC = ({ content }) => { + const contentRef = useRef(null); + + return ( +
    { + if (el && !contentRef.current) { + contentRef.current = content(); + el.appendChild(contentRef.current); + } + }} /> + ); +}; \ No newline at end of file diff --git a/src/store/resourceStore.tsx b/src/store/resourceStore.tsx index b5957ab1..39ff43c2 100644 --- a/src/store/resourceStore.tsx +++ b/src/store/resourceStore.tsx @@ -7,7 +7,7 @@ import { ResourceType } from "@/server/types"; import { useEffect, useState } from "react"; import { api } from "@/lib/trpc"; import { PromiseCall } from "./standard/PromiseState"; -import { t } from "i18next"; +import { Resource, t } from "i18next"; import { ToastPlugin } from "./module/Toast/Toast"; import { DialogStore } from "./module/Dialog"; import { Button, Input } from "@nextui-org/react"; @@ -32,6 +32,15 @@ export class ResourceStore implements Store { this.currentFolder = folder; } + selectAllFiles = (resources: ResourceType[]) => { + this.selectedItems.clear(); + resources.forEach(resource => { + if (!resource.isFolder && resource.id) { + this.selectedItems.add(resource.id); + } + }); + }; + toggleSelect = (id: number) => { const newSet = new Set(this.selectedItems); if (newSet.has(id)) { diff --git a/src/store/user.ts b/src/store/user.ts index f853386f..2416d97b 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -200,6 +200,7 @@ export class UserStore implements User, Store { userStore.ready({ ...session.user }); this.initializeSettings(setTheme, i18n); userStore.userInfo.call(Number(this.id)) + userStore.canRegister.call() } }, [session]); diff --git a/tsconfig.types.json b/tsconfig.types.json new file mode 100644 index 00000000..be9a6f2c --- /dev/null +++ b/tsconfig.types.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "blinko-types/dist/types", + "noEmit": false, + "rootDir": ".", + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, + }, + "include": [ + "./src/store/plugin/index.ts", + "prisma/seed.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file