mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-11-10 09:59:15 -06:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fcc4c3747 | ||
|
|
0d2c52adc3 | ||
|
|
af256e7f92 | ||
|
|
84830421ee | ||
|
|
9b278cdb0e | ||
|
|
9b40a5cf86 | ||
|
|
b8d980be33 | ||
|
|
195037af87 | ||
|
|
c98000069a | ||
|
|
2bd277b57d | ||
|
|
4852ed813b | ||
|
|
fb08df9e4e | ||
|
|
64f3b49e21 |
8
.github/workflows/issue-tracker.yml
vendored
8
.github/workflows/issue-tracker.yml
vendored
@@ -2,7 +2,7 @@ name: Issue Tracker
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
types: [opened, reopened, closed]
|
||||
|
||||
jobs:
|
||||
track-issue:
|
||||
@@ -28,8 +28,10 @@ jobs:
|
||||
--arg state "${{ github.event.issue.state }}" \
|
||||
--arg created_at "${{ github.event.issue.created_at }}" \
|
||||
--arg updated_at "${{ github.event.issue.updated_at }}" \
|
||||
--arg closed_at "${{ github.event.issue.closed_at }}" \
|
||||
--arg url "${{ github.event.issue.html_url }}" \
|
||||
--arg user_login "${{ github.event.issue.user.login }}" \
|
||||
--arg repository "${{ github.repository }}" \
|
||||
--argjson labels "$RAW_LABELS" \
|
||||
--argjson assignees "$RAW_ASSIGNEES" \
|
||||
'{
|
||||
@@ -42,11 +44,13 @@ jobs:
|
||||
state: $state,
|
||||
created_at: $created_at,
|
||||
updated_at: $updated_at,
|
||||
closed_at: $closed_at,
|
||||
url: $url,
|
||||
user: { login: $user_login },
|
||||
labels: $labels,
|
||||
assignees: $assignees
|
||||
}
|
||||
},
|
||||
repository: { name: $repository },
|
||||
}')
|
||||
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
|
||||
|
||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,4 +1,26 @@
|
||||
## 2.3.2 (2025-01-08)
|
||||
## 2.3.3 (2026-01-16)
|
||||
|
||||
### Features
|
||||
|
||||
* Added support for setting the display mode of the menu bar
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed a keyboard issue where IME composition events were not handled correctly
|
||||
* Resolved an issue where the `AltGr` key was not recognized on the Windows system
|
||||
* Fixed a bug where `Command` key combinations were not fully released on the macOS system
|
||||
* Fixed input issues with the `IntlBackslash` key on the German virtual keyboard
|
||||
|
||||
### Localization
|
||||
|
||||
* Updated Korean translations (thanks to [@kmw0410](https://github.com/kmw0410))
|
||||
|
||||
### Chores
|
||||
|
||||
* Bump react-router-dom from 6.27.0 to 6.30.3
|
||||
* Updated eslint
|
||||
|
||||
## 2.3.2 (2026-01-08)
|
||||
|
||||
### Features
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'prettier',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
32
web/eslint.config.js
Normal file
32
web/eslint.config.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import eslintConfigPrettier from 'eslint-config-prettier'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist', 'public/mockServiceWorker.js'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
eslintConfigPrettier
|
||||
)
|
||||
@@ -7,7 +7,7 @@
|
||||
"dev": "vite",
|
||||
"mocked": "vite --mode mocked",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint": "eslint . --report-unused-disable-directives",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -31,7 +31,7 @@
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-i18next": "^14.1.3",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"react-simple-keyboard": "^3.8.19",
|
||||
"semver": "^7.6.3",
|
||||
"vaul": "^0.9.9",
|
||||
@@ -39,6 +39,7 @@
|
||||
"yocto-queue": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
@@ -55,12 +56,14 @@
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^17.0.0",
|
||||
"msw": "^2.6.0",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"tailwindcss": "^3.4.18",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.53.0",
|
||||
"vite": "7.1.11",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
},
|
||||
@@ -69,4 +72,4 @@
|
||||
"public"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
251
web/pnpm-lock.yaml
generated
251
web/pnpm-lock.yaml
generated
@@ -69,8 +69,8 @@ importers:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(react@18.3.1)
|
||||
react-router-dom:
|
||||
specifier: ^6.27.0
|
||||
version: 6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
specifier: ^6.30.3
|
||||
version: 6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-simple-keyboard:
|
||||
specifier: ^3.8.19
|
||||
version: 3.8.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -87,6 +87,9 @@ importers:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^9.39.2
|
||||
version: 9.39.2
|
||||
'@ianvs/prettier-plugin-sort-imports':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(prettier@3.4.2)
|
||||
@@ -135,6 +138,9 @@ importers:
|
||||
eslint-plugin-react-refresh:
|
||||
specifier: ^0.4.16
|
||||
version: 0.4.16(eslint@9.39.1(jiti@1.21.7))
|
||||
globals:
|
||||
specifier: ^17.0.0
|
||||
version: 17.0.0
|
||||
msw:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0(@types/node@22.9.0)(typescript@5.6.3)
|
||||
@@ -153,6 +159,9 @@ importers:
|
||||
typescript:
|
||||
specifier: ^5.6.3
|
||||
version: 5.6.3
|
||||
typescript-eslint:
|
||||
specifier: ^8.53.0
|
||||
version: 8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)
|
||||
vite:
|
||||
specifier: 7.1.11
|
||||
version: 7.1.11(@types/node@22.9.0)(jiti@1.21.7)(yaml@2.6.0)
|
||||
@@ -498,10 +507,20 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.1':
|
||||
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||
|
||||
'@eslint-community/regexpp@4.12.1':
|
||||
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
|
||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||
|
||||
'@eslint-community/regexpp@4.12.2':
|
||||
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||
|
||||
'@eslint/config-array@0.21.1':
|
||||
resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -522,6 +541,10 @@ packages:
|
||||
resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/js@9.39.2':
|
||||
resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/object-schema@2.1.7':
|
||||
resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -959,8 +982,8 @@ packages:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@remix-run/router@1.20.0':
|
||||
resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==}
|
||||
'@remix-run/router@1.23.2':
|
||||
resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||
@@ -1138,6 +1161,14 @@ packages:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.53.0':
|
||||
resolution: {integrity: sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^8.53.0
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/parser@8.48.0':
|
||||
resolution: {integrity: sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1145,22 +1176,45 @@ packages:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/parser@8.53.0':
|
||||
resolution: {integrity: sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/project-service@8.48.0':
|
||||
resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/project-service@8.53.0':
|
||||
resolution: {integrity: sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/scope-manager@8.48.0':
|
||||
resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/scope-manager@8.53.0':
|
||||
resolution: {integrity: sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.48.0':
|
||||
resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.53.0':
|
||||
resolution: {integrity: sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/type-utils@8.48.0':
|
||||
resolution: {integrity: sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1168,16 +1222,33 @@ packages:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/type-utils@8.53.0':
|
||||
resolution: {integrity: sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/types@8.48.0':
|
||||
resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/types@8.53.0':
|
||||
resolution: {integrity: sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.48.0':
|
||||
resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.53.0':
|
||||
resolution: {integrity: sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/utils@8.48.0':
|
||||
resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1185,10 +1256,21 @@ packages:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/utils@8.53.0':
|
||||
resolution: {integrity: sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.48.0':
|
||||
resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.53.0':
|
||||
resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@vitejs/plugin-react@4.7.0':
|
||||
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -1787,6 +1869,10 @@ packages:
|
||||
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globals@17.0.0:
|
||||
resolution: {integrity: sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
globalthis@1.0.4:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2705,15 +2791,15 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
react-router-dom@6.27.0:
|
||||
resolution: {integrity: sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==}
|
||||
react-router-dom@6.30.3:
|
||||
resolution: {integrity: sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
react-dom: '>=16.8'
|
||||
|
||||
react-router@6.27.0:
|
||||
resolution: {integrity: sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==}
|
||||
react-router@6.30.3:
|
||||
resolution: {integrity: sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
@@ -2814,6 +2900,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
set-function-length@1.2.2:
|
||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2941,6 +3032,12 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4'
|
||||
|
||||
ts-api-utils@2.4.0:
|
||||
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
|
||||
engines: {node: '>=18.12'}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4'
|
||||
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
@@ -2991,6 +3088,13 @@ packages:
|
||||
typedarray-to-buffer@3.1.5:
|
||||
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
|
||||
|
||||
typescript-eslint@8.53.0:
|
||||
resolution: {integrity: sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
|
||||
typescript@5.6.3:
|
||||
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -3498,8 +3602,15 @@ snapshots:
|
||||
eslint: 9.39.1(jiti@1.21.7)
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.1(jiti@1.21.7))':
|
||||
dependencies:
|
||||
eslint: 9.39.1(jiti@1.21.7)
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.1': {}
|
||||
|
||||
'@eslint-community/regexpp@4.12.2': {}
|
||||
|
||||
'@eslint/config-array@0.21.1':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.7
|
||||
@@ -3532,6 +3643,8 @@ snapshots:
|
||||
|
||||
'@eslint/js@9.39.1': {}
|
||||
|
||||
'@eslint/js@9.39.2': {}
|
||||
|
||||
'@eslint/object-schema@2.1.7': {}
|
||||
|
||||
'@eslint/plugin-kit@0.4.1':
|
||||
@@ -3933,7 +4046,7 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@remix-run/router@1.20.0': {}
|
||||
'@remix-run/router@1.23.2': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||
|
||||
@@ -4078,6 +4191,22 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)
|
||||
'@typescript-eslint/scope-manager': 8.53.0
|
||||
'@typescript-eslint/type-utils': 8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)
|
||||
'@typescript-eslint/utils': 8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)
|
||||
'@typescript-eslint/visitor-keys': 8.53.0
|
||||
eslint: 9.39.1(jiti@1.21.7)
|
||||
ignore: 7.0.5
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.4.0(typescript@5.6.3)
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.48.0
|
||||
@@ -4090,6 +4219,18 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.53.0
|
||||
'@typescript-eslint/types': 8.53.0
|
||||
'@typescript-eslint/typescript-estree': 8.53.0(typescript@5.6.3)
|
||||
'@typescript-eslint/visitor-keys': 8.53.0
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.1(jiti@1.21.7)
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.48.0(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.6.3)
|
||||
@@ -4099,15 +4240,33 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.53.0(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.6.3)
|
||||
'@typescript-eslint/types': 8.53.0
|
||||
debug: 4.4.3
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/scope-manager@8.48.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.48.0
|
||||
'@typescript-eslint/visitor-keys': 8.48.0
|
||||
|
||||
'@typescript-eslint/scope-manager@8.53.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.53.0
|
||||
'@typescript-eslint/visitor-keys': 8.53.0
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.6.3)':
|
||||
dependencies:
|
||||
typescript: 5.6.3
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.6.3)':
|
||||
dependencies:
|
||||
typescript: 5.6.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.48.0
|
||||
@@ -4120,8 +4279,22 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/type-utils@8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.53.0
|
||||
'@typescript-eslint/typescript-estree': 8.53.0(typescript@5.6.3)
|
||||
'@typescript-eslint/utils': 8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.1(jiti@1.21.7)
|
||||
ts-api-utils: 2.4.0(typescript@5.6.3)
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@8.48.0': {}
|
||||
|
||||
'@typescript-eslint/types@8.53.0': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.48.0(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.48.0(typescript@5.6.3)
|
||||
@@ -4137,6 +4310,21 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.53.0(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.53.0(typescript@5.6.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.6.3)
|
||||
'@typescript-eslint/types': 8.53.0
|
||||
'@typescript-eslint/visitor-keys': 8.53.0
|
||||
debug: 4.4.3
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.3
|
||||
tinyglobby: 0.2.15
|
||||
ts-api-utils: 2.4.0(typescript@5.6.3)
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7))
|
||||
@@ -4148,11 +4336,27 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.1(jiti@1.21.7))
|
||||
'@typescript-eslint/scope-manager': 8.53.0
|
||||
'@typescript-eslint/types': 8.53.0
|
||||
'@typescript-eslint/typescript-estree': 8.53.0(typescript@5.6.3)
|
||||
eslint: 9.39.1(jiti@1.21.7)
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.48.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.48.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.53.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.53.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@vitejs/plugin-react@4.7.0(vite@7.1.11(@types/node@22.9.0)(jiti@1.21.7)(yaml@2.6.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
@@ -4952,6 +5156,8 @@ snapshots:
|
||||
|
||||
globals@14.0.0: {}
|
||||
|
||||
globals@17.0.0: {}
|
||||
|
||||
globalthis@1.0.4:
|
||||
dependencies:
|
||||
define-properties: 1.2.1
|
||||
@@ -5833,16 +6039,16 @@ snapshots:
|
||||
react: 18.3.1
|
||||
shallow-equal: 3.1.0
|
||||
|
||||
react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
react-router-dom@6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@remix-run/router': 1.20.0
|
||||
'@remix-run/router': 1.23.2
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-router: 6.27.0(react@18.3.1)
|
||||
react-router: 6.30.3(react@18.3.1)
|
||||
|
||||
react-router@6.27.0(react@18.3.1):
|
||||
react-router@6.30.3(react@18.3.1):
|
||||
dependencies:
|
||||
'@remix-run/router': 1.20.0
|
||||
'@remix-run/router': 1.23.2
|
||||
react: 18.3.1
|
||||
|
||||
react-simple-keyboard@3.8.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
@@ -5969,6 +6175,8 @@ snapshots:
|
||||
|
||||
semver@7.6.3: {}
|
||||
|
||||
semver@7.7.3: {}
|
||||
|
||||
set-function-length@1.2.2:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
@@ -6141,6 +6349,10 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.6.3
|
||||
|
||||
ts-api-utils@2.4.0(typescript@5.6.3):
|
||||
dependencies:
|
||||
typescript: 5.6.3
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
tsconfck@3.1.4(typescript@5.6.3):
|
||||
@@ -6195,6 +6407,17 @@ snapshots:
|
||||
dependencies:
|
||||
is-typedarray: 1.0.0
|
||||
|
||||
typescript-eslint@8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)
|
||||
'@typescript-eslint/parser': 8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)
|
||||
'@typescript-eslint/typescript-estree': 8.53.0(typescript@5.6.3)
|
||||
'@typescript-eslint/utils': 8.53.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.6.3)
|
||||
eslint: 9.39.1(jiti@1.21.7)
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
typescript@5.6.3: {}
|
||||
|
||||
unbox-primitive@1.0.2:
|
||||
|
||||
55
web/src/hooks/useMenuBounds.ts
Normal file
55
web/src/hooks/useMenuBounds.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { RefObject, useCallback, useEffect, useState } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { menuDisabledItemsAtom } from '@/jotai/settings.ts';
|
||||
|
||||
export interface MenuBounds {
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export function useMenuBounds(
|
||||
nodeRef: RefObject<HTMLDivElement | null>,
|
||||
isMenuExpanded: boolean
|
||||
): MenuBounds {
|
||||
const menuDisabledItems = useAtomValue(menuDisabledItemsAtom);
|
||||
const [menuBounds, setMenuBounds] = useState<MenuBounds>({
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0
|
||||
});
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
if (!nodeRef.current) return;
|
||||
|
||||
const elementRect = nodeRef.current.getBoundingClientRect();
|
||||
const width = (window.innerWidth - elementRect.width) / 2;
|
||||
|
||||
setMenuBounds({
|
||||
left: -width,
|
||||
top: -10,
|
||||
right: width,
|
||||
bottom: window.innerHeight - elementRect.height - 10
|
||||
});
|
||||
}, [nodeRef]);
|
||||
|
||||
// Update bounds on mount and resize
|
||||
useEffect(() => {
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [handleResize]);
|
||||
|
||||
// Update bounds when menu items or expansion state change
|
||||
useEffect(() => {
|
||||
handleResize();
|
||||
}, [menuDisabledItems, isMenuExpanded, handleResize]);
|
||||
|
||||
return menuBounds;
|
||||
}
|
||||
129
web/src/hooks/useMenuVisibility.ts
Normal file
129
web/src/hooks/useMenuVisibility.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
|
||||
import { getMenuDisabledItems, getMenuDisplayMode } from '@/lib/localstorage.ts';
|
||||
import {
|
||||
menuDisabledItemsAtom,
|
||||
menuDisplayModeAtom,
|
||||
submenuOpenCountAtom
|
||||
} from '@/jotai/settings.ts';
|
||||
|
||||
const HIDE_TIMEOUT = 5000;
|
||||
|
||||
export interface MenuVisibilityState {
|
||||
isInitialized: boolean;
|
||||
isMenuExpanded: boolean;
|
||||
isMenuHidden: boolean;
|
||||
isMenuMoved: boolean;
|
||||
isInvisible: boolean;
|
||||
handleHovered: (hovered: boolean) => void;
|
||||
handleMoved: () => void;
|
||||
setIsMenuExpanded: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
export function useMenuVisibility(): MenuVisibilityState {
|
||||
const [menuDisplayMode, setMenuDisplayMode] = useAtom(menuDisplayModeAtom);
|
||||
const setMenuDisabledItems = useSetAtom(menuDisabledItemsAtom);
|
||||
const submenuOpenCount = useAtomValue(submenuOpenCountAtom);
|
||||
|
||||
const [isMenuExpanded, setIsMenuExpanded] = useState(true);
|
||||
const [isMenuMoved, setIsMenuMoved] = useState(false);
|
||||
const [isMenuHovered, setIsMenuHovered] = useState(false);
|
||||
const [isMenuHidden, setIsMenuHidden] = useState(false);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const startCountdown = useCallback(() => {
|
||||
if (menuDisplayMode !== 'auto' || submenuOpenCount > 0 || !isMenuExpanded || isMenuMoved) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
|
||||
timerRef.current = setTimeout(() => {
|
||||
setIsMenuHidden(true);
|
||||
}, HIDE_TIMEOUT);
|
||||
}, [menuDisplayMode, submenuOpenCount, isMenuExpanded, isMenuMoved]);
|
||||
|
||||
const stopCountdown = useCallback(() => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initialize menu settings
|
||||
useEffect(() => {
|
||||
const displayMode = getMenuDisplayMode();
|
||||
setMenuDisplayMode(displayMode);
|
||||
|
||||
const items = getMenuDisabledItems();
|
||||
setMenuDisabledItems(items);
|
||||
|
||||
if (displayMode === 'off') {
|
||||
setIsMenuExpanded(false);
|
||||
}
|
||||
|
||||
setIsInitialized(true);
|
||||
|
||||
return () => {
|
||||
stopCountdown();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle display mode changes
|
||||
useEffect(() => {
|
||||
if (menuDisplayMode === 'off') {
|
||||
setIsMenuExpanded(false);
|
||||
setIsMenuHidden(false);
|
||||
stopCountdown();
|
||||
} else if (menuDisplayMode === 'always') {
|
||||
setIsMenuExpanded(true);
|
||||
setIsMenuHidden(false);
|
||||
stopCountdown();
|
||||
} else if (menuDisplayMode === 'auto') {
|
||||
setIsMenuExpanded(true);
|
||||
setIsMenuHidden(false);
|
||||
startCountdown();
|
||||
}
|
||||
}, [menuDisplayMode, startCountdown, stopCountdown]);
|
||||
|
||||
// Handle hover and submenu state
|
||||
useEffect(() => {
|
||||
if (submenuOpenCount === 0 && !isMenuHovered) {
|
||||
startCountdown();
|
||||
} else {
|
||||
stopCountdown();
|
||||
}
|
||||
}, [submenuOpenCount, isMenuHovered, startCountdown, stopCountdown]);
|
||||
|
||||
// Handle hover state
|
||||
const handleHovered = useCallback((hovered: boolean) => {
|
||||
setIsMenuHovered(hovered);
|
||||
if (hovered) {
|
||||
setIsMenuHidden(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMoved = useCallback(() => {
|
||||
if (!isMenuMoved) {
|
||||
setIsMenuMoved(true);
|
||||
}
|
||||
}, [isMenuMoved]);
|
||||
|
||||
const isInvisible = menuDisplayMode === 'off' && !isMenuExpanded;
|
||||
|
||||
return {
|
||||
isMenuExpanded,
|
||||
isMenuHidden,
|
||||
isMenuMoved,
|
||||
isInvisible,
|
||||
isInitialized,
|
||||
handleHovered,
|
||||
handleMoved,
|
||||
setIsMenuExpanded
|
||||
};
|
||||
}
|
||||
@@ -243,10 +243,18 @@ const en = {
|
||||
display: 'Display',
|
||||
language: 'Language',
|
||||
languageDesc: 'Select the language for the interface',
|
||||
menuBar: 'Menu Bar',
|
||||
menuBarDesc: 'Display icons in the menu bar',
|
||||
webTitle: 'Web Title',
|
||||
webTitleDesc: 'Customize the web page title'
|
||||
webTitleDesc: 'Customize the web page title',
|
||||
menuBar: {
|
||||
title: 'Menu Bar',
|
||||
mode: 'Display Mode',
|
||||
modeDesc: 'Display menu bar on the screen',
|
||||
modeOff: 'Off',
|
||||
modeAuto: 'Auto hide',
|
||||
modeAlways: 'Always visible',
|
||||
icons: 'Submenu Icons',
|
||||
iconsDesc: 'Display submenu icons in the menu bar'
|
||||
}
|
||||
},
|
||||
device: {
|
||||
title: 'Device',
|
||||
|
||||
@@ -217,6 +217,7 @@ const ko = {
|
||||
title: '디자인',
|
||||
display: '표시',
|
||||
language: '언어',
|
||||
languageDesc: '인터페이스 언어를 선택하세요',
|
||||
menuBar: '메뉴 바',
|
||||
menuBarDesc: '메뉴 바에 아이콘을 표시',
|
||||
webTitle: '웹 제목',
|
||||
@@ -336,7 +337,14 @@ const ko = {
|
||||
cancel: '취소',
|
||||
preview: '미리보기 업데이트',
|
||||
previewDesc: '새로운 기능과 개선 사항에 미리 접근하세요',
|
||||
previewTip: '미리보기 버전에는 버그나 완성되지 않은 기능이 포함될 수 있으니 주의하세요!'
|
||||
previewTip: '미리보기 버전에는 버그나 완성되지 않은 기능이 포함될 수 있으니 주의하세요!',
|
||||
offline: {
|
||||
title: '오프라인 업데이트',
|
||||
desc: '로컬 설치 패키지를 통한 업데이트',
|
||||
upload: '업로드',
|
||||
invalidName: '유효하지 않은 파일 이름 형식입니다. GitHub 릴리즈에서 다운로드하세요.',
|
||||
updateFailed: '업데이트에 실패했습니다. 재시도하세요.'
|
||||
}
|
||||
},
|
||||
account: {
|
||||
title: '계정',
|
||||
|
||||
@@ -94,7 +94,7 @@ const tr = {
|
||||
resetHid: 'İnsan Arayüz Cihazı\'nı sıfırlayın',
|
||||
hidOnly: {
|
||||
title: 'Sadece İnsan Arayüz Cihazı modu',
|
||||
desc: "Eğer fare ve klavye çalışmayı bırakırsa ve İnsan Arayüz Cihazı\'nı sıfırlamak sorunu çözmezse bunun sebebi cihazınız ve NanoKVM arasındaki bir uyumluluk sorunu olabilir. Uyumluluğu arttırmak için Sadece İnsan Arayüz Cihazı modunu kullanın.",
|
||||
desc: "Eğer fare ve klavye çalışmayı bırakırsa ve İnsan Arayüz Cihazı'nı sıfırlamak sorunu çözmezse bunun sebebi cihazınız ve NanoKVM arasındaki bir uyumluluk sorunu olabilir. Uyumluluğu arttırmak için Sadece İnsan Arayüz Cihazı modunu kullanın.",
|
||||
tip1: 'Sadece İnsan Arayüz Cihazı modunu aktifleştirmek, sanal U-disk’i ve sanal ağı devre dışı bırakacaktır.',
|
||||
tip2: 'Sadece İnsan Arayüz Cihazı modunda disk imajı bağlama özelliği devre dışı bırakılır',
|
||||
tip3: 'NanoKVM mod değişiminden sonra kendiliğinden yeniden başlatılacaktır',
|
||||
|
||||
@@ -212,10 +212,18 @@ const zh = {
|
||||
display: '显示',
|
||||
language: '语言',
|
||||
languageDesc: '选择界面语言',
|
||||
menuBar: '菜单栏',
|
||||
menuBarDesc: '是否在菜单栏中显示图标',
|
||||
webTitle: '网页标题',
|
||||
webTitleDesc: '自定义网站页面标题'
|
||||
webTitleDesc: '自定义网站页面标题',
|
||||
menuBar: {
|
||||
title: '菜单栏',
|
||||
mode: '显示方式',
|
||||
modeDesc: '菜单栏在屏幕上的显示方式',
|
||||
modeOff: '关闭',
|
||||
modeAuto: '自动隐藏',
|
||||
modeAlways: '始终显示',
|
||||
icons: '菜单图标',
|
||||
iconsDesc: '是否在菜单栏中显示子菜单图标'
|
||||
}
|
||||
},
|
||||
device: {
|
||||
title: '设备',
|
||||
|
||||
@@ -8,3 +8,6 @@ export const submenuOpenCountAtom = atom(0);
|
||||
|
||||
// web title
|
||||
export const webTitleAtom = atom('');
|
||||
|
||||
// menu display mode: 'off' | 'auto' | 'always'
|
||||
export const menuDisplayModeAtom = atom<string>('auto');
|
||||
|
||||
56
web/src/lib/browser.ts
Normal file
56
web/src/lib/browser.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export interface BrowserInfo {
|
||||
os: string;
|
||||
isChrome: boolean;
|
||||
}
|
||||
|
||||
export function getOperatingSystem(): string {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
const platformSource =
|
||||
// @ts-expect-error check userAgentData.platform
|
||||
'userAgentData' in navigator ? navigator.userAgentData.platform : navigator.platform;
|
||||
|
||||
if (!platformSource) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
const platform = platformSource.toLowerCase();
|
||||
|
||||
if (platform.startsWith('win')) return 'Windows';
|
||||
if (platform.startsWith('mac')) return 'macOS';
|
||||
if (platform.startsWith('linux')) return 'Linux';
|
||||
|
||||
return platformSource;
|
||||
}
|
||||
|
||||
export function isChromeBrowser(): boolean {
|
||||
if (typeof window === 'undefined' || !window.navigator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('userAgentData' in navigator) {
|
||||
// @ts-expect-error userAgentData.brands
|
||||
return navigator.userAgentData?.brands?.some(
|
||||
(brand: any) => brand.brand === 'Google Chrome' || brand.brand === 'Chromium'
|
||||
);
|
||||
}
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
const vendor = navigator.vendor;
|
||||
|
||||
return (
|
||||
/Chrome|Chromium/.test(userAgent) &&
|
||||
/Google Inc/.test(vendor) &&
|
||||
!/Edg/.test(userAgent) &&
|
||||
!/OPR/.test(userAgent)
|
||||
);
|
||||
}
|
||||
|
||||
export function getBrowserInfo(): BrowserInfo {
|
||||
return {
|
||||
os: getOperatingSystem(),
|
||||
isChrome: isChromeBrowser()
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,7 @@ const KEYBOARD_SYSTEM_KEY = 'nano-kvm-keyboard-system';
|
||||
const KEYBOARD_LANGUAGE_KEY = 'nano-kvm-keyboard-language';
|
||||
const SKIP_MODIFY_PASSWORD_KEY = 'nano-kvm-skip-modify-password';
|
||||
const MENU_DISABLED_ITEMS_KEY = 'nano-kvm-menu-disabled-items';
|
||||
const MENU_AUTO_HIDE_KEY = 'nano-kvm-menu-auto-hide';
|
||||
const POWER_CONFIRM_KEY = 'nano-kvm-power-confirm';
|
||||
|
||||
type ItemWithExpiry = {
|
||||
@@ -212,6 +213,15 @@ export function getMenuDisabledItems(): string[] {
|
||||
return value ? JSON.parse(value) : [];
|
||||
}
|
||||
|
||||
export function getMenuDisplayMode(): string {
|
||||
const value = localStorage.getItem(MENU_AUTO_HIDE_KEY);
|
||||
return value || 'auto';
|
||||
}
|
||||
|
||||
export function setMenuDisplayMode(mode: string) {
|
||||
localStorage.setItem(MENU_AUTO_HIDE_KEY, mode);
|
||||
}
|
||||
|
||||
export function getPowerConfirm() {
|
||||
const enabled = localStorage.getItem(POWER_CONFIRM_KEY);
|
||||
return enabled === 'true';
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { getOperatingSystem } from '@/lib/browser.ts';
|
||||
import { KeyboardReport } from '@/lib/keyboard.ts';
|
||||
import { isModifier } from '@/lib/keymap';
|
||||
import { client, MessageEvent } from '@/lib/websocket.ts';
|
||||
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
|
||||
|
||||
interface AltGrState {
|
||||
active: boolean;
|
||||
ctrlLeftTimestamp: number;
|
||||
}
|
||||
|
||||
const ALTGR_THRESHOLD_MS = 10;
|
||||
|
||||
export const Keyboard = () => {
|
||||
const isKeyboardEnabled = useAtomValue(isKeyboardEnableAtom);
|
||||
|
||||
const keyboardRef = useRef(new KeyboardReport());
|
||||
const pressedKeys = useRef(new Set<string>());
|
||||
const altGrState = useRef<AltGrState | null>(null);
|
||||
const isComposing = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (getOperatingSystem() === 'Windows' && !altGrState.current) {
|
||||
altGrState.current = { active: false, ctrlLeftTimestamp: 0 };
|
||||
}
|
||||
|
||||
if (!isKeyboardEnabled) {
|
||||
releaseKeys();
|
||||
return;
|
||||
@@ -19,6 +34,8 @@ export const Keyboard = () => {
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener('keyup', handleKeyUp);
|
||||
document.addEventListener('compositionstart', handleCompositionStart);
|
||||
document.addEventListener('compositionend', handleCompositionEnd);
|
||||
window.addEventListener('blur', handleBlur);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
@@ -26,12 +43,27 @@ export const Keyboard = () => {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (!isKeyboardEnabled) return;
|
||||
|
||||
// Skip during IME composition
|
||||
if (isComposing.current || event.isComposing) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const code = event.code;
|
||||
if (pressedKeys.current.has(code)) {
|
||||
return;
|
||||
if (pressedKeys.current.has(code)) return;
|
||||
|
||||
// When AltGr is pressed, browsers send ControlLeft followed immediately by AltRight
|
||||
if (altGrState.current) {
|
||||
if (code === 'ControlLeft') {
|
||||
altGrState.current.ctrlLeftTimestamp = event.timeStamp;
|
||||
} else if (code === 'AltRight') {
|
||||
const timeDiff = event.timeStamp - altGrState.current.ctrlLeftTimestamp;
|
||||
if (timeDiff < ALTGR_THRESHOLD_MS && pressedKeys.current.has('ControlLeft')) {
|
||||
pressedKeys.current.delete('ControlLeft');
|
||||
handleKeyEvent({ type: 'keyup', code: 'ControlLeft' });
|
||||
altGrState.current.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pressedKeys.current.add(code);
|
||||
@@ -42,14 +74,51 @@ export const Keyboard = () => {
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
if (!isKeyboardEnabled) return;
|
||||
|
||||
if (isComposing.current || event.isComposing) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const code = event.code;
|
||||
|
||||
// Handle AltGr state for Windows
|
||||
if (altGrState.current?.active) {
|
||||
if (code === 'ControlLeft') return;
|
||||
|
||||
if (code === 'AltRight') {
|
||||
altGrState.current.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compatible with macOS's command key combinations
|
||||
if (code === 'MetaLeft' || code === 'MetaRight') {
|
||||
const keysToRelease: string[] = [];
|
||||
pressedKeys.current.forEach((pressedCode) => {
|
||||
if (!isModifier(pressedCode)) {
|
||||
keysToRelease.push(pressedCode);
|
||||
}
|
||||
});
|
||||
|
||||
for (const key of keysToRelease) {
|
||||
handleKeyEvent({ type: 'keyup', code: key });
|
||||
pressedKeys.current.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
pressedKeys.current.delete(code);
|
||||
handleKeyEvent({ type: 'keyup', code });
|
||||
}
|
||||
|
||||
// Composition start event
|
||||
function handleCompositionStart() {
|
||||
isComposing.current = true;
|
||||
}
|
||||
|
||||
// Composition end event
|
||||
function handleCompositionEnd() {
|
||||
isComposing.current = false;
|
||||
}
|
||||
|
||||
// Release all keys when window loses focus
|
||||
function handleBlur() {
|
||||
releaseKeys();
|
||||
@@ -70,6 +139,12 @@ export const Keyboard = () => {
|
||||
|
||||
pressedKeys.current.clear();
|
||||
|
||||
// Reset AltGr state
|
||||
if (altGrState.current) {
|
||||
altGrState.current.active = false;
|
||||
altGrState.current.ctrlLeftTimestamp = 0;
|
||||
}
|
||||
|
||||
const report = keyboardRef.current.reset();
|
||||
sendReport(report);
|
||||
}
|
||||
@@ -77,8 +152,12 @@ export const Keyboard = () => {
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener('keyup', handleKeyUp);
|
||||
document.removeEventListener('compositionstart', handleCompositionStart);
|
||||
document.removeEventListener('compositionend', handleCompositionEnd);
|
||||
window.removeEventListener('blur', handleBlur);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
releaseKeys();
|
||||
};
|
||||
}, [isKeyboardEnabled]);
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { Divider } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { GripVerticalIcon } from 'lucide-react';
|
||||
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
|
||||
|
||||
import { getMenuDisabledItems } from '@/lib/localstorage.ts';
|
||||
import { menuDisabledItemsAtom, submenuOpenCountAtom } from '@/jotai/settings.ts';
|
||||
import { menuDisabledItemsAtom, menuDisplayModeAtom } from '@/jotai/settings.ts';
|
||||
import { useMenuBounds } from '@/hooks/useMenuBounds.ts';
|
||||
import { useMenuVisibility } from '@/hooks/useMenuVisibility.ts';
|
||||
|
||||
import { DownloadImage } from './download.tsx';
|
||||
import { Fullscreen } from './fullscreen';
|
||||
@@ -21,99 +22,27 @@ import { Settings } from './settings';
|
||||
import { Terminal } from './terminal';
|
||||
import { Wol } from './wol';
|
||||
|
||||
const HIDE_TIMEOUT = 8000;
|
||||
|
||||
export const Menu = () => {
|
||||
const [menuDisabledItems, setMenuDisabledItems] = useAtom(menuDisabledItemsAtom);
|
||||
const submenuOpenCount = useAtomValue(submenuOpenCountAtom);
|
||||
|
||||
const [isMenuExpanded, setIsMenuExpanded] = useState(true);
|
||||
const [isMenuMoved, setIsMenuMoved] = useState(false);
|
||||
const [isMenuHovered, setIsMenuHovered] = useState(false);
|
||||
const [isMenuHidden, setIsMenuHidden] = useState(false);
|
||||
const [menuBounds, setMenuBounds] = useState({ left: 0, right: 0, top: 0, bottom: 0 });
|
||||
|
||||
const nodeRef = useRef<HTMLDivElement | null>(null);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// init menu
|
||||
useEffect(() => {
|
||||
const items = getMenuDisabledItems();
|
||||
setMenuDisabledItems(items);
|
||||
const menuDisplayMode = useAtomValue(menuDisplayModeAtom);
|
||||
const menuDisabledItems = useAtomValue(menuDisabledItemsAtom);
|
||||
|
||||
startCountdown();
|
||||
handleResize();
|
||||
const {
|
||||
isInitialized,
|
||||
isMenuExpanded,
|
||||
isMenuHidden,
|
||||
isInvisible,
|
||||
handleHovered,
|
||||
handleMoved,
|
||||
setIsMenuExpanded
|
||||
} = useMenuVisibility();
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
const menuBounds = useMenuBounds(nodeRef, isMenuExpanded);
|
||||
|
||||
return () => {
|
||||
stopCountdown();
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// handle menu bounds
|
||||
useEffect(() => {
|
||||
handleResize();
|
||||
}, [menuDisabledItems, isMenuExpanded]);
|
||||
|
||||
// handle hide timer
|
||||
useEffect(() => {
|
||||
if (submenuOpenCount === 0 && !isMenuHovered) {
|
||||
startCountdown();
|
||||
return;
|
||||
}
|
||||
|
||||
stopCountdown();
|
||||
}, [submenuOpenCount, isMenuHovered]);
|
||||
|
||||
function handleResize() {
|
||||
if (!nodeRef.current) return;
|
||||
|
||||
const elementRect = nodeRef.current.getBoundingClientRect();
|
||||
const width = (window.innerWidth - elementRect.width) / 2;
|
||||
|
||||
setMenuBounds({
|
||||
left: -width,
|
||||
top: -10,
|
||||
right: width,
|
||||
bottom: window.innerHeight - elementRect.height - 10
|
||||
});
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
if (submenuOpenCount > 0 || !isMenuExpanded || isMenuMoved) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
|
||||
timerRef.current = setTimeout(() => {
|
||||
setIsMenuHidden(true);
|
||||
}, HIDE_TIMEOUT);
|
||||
}
|
||||
|
||||
function stopCountdown() {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMoved(_e: DraggableEvent, data: DraggableData) {
|
||||
function onDragStop(_e: DraggableEvent, data: DraggableData) {
|
||||
if (data.x === 0 && data.y === 0) return;
|
||||
if (isMenuMoved) return;
|
||||
|
||||
setIsMenuMoved(true);
|
||||
}
|
||||
|
||||
function handleHovered(hovered: boolean) {
|
||||
setIsMenuHovered(hovered);
|
||||
if (hovered) {
|
||||
setIsMenuHidden(false);
|
||||
}
|
||||
handleMoved();
|
||||
}
|
||||
|
||||
function isEnabled(item: string) {
|
||||
@@ -126,11 +55,15 @@ export const Menu = () => {
|
||||
bounds={menuBounds}
|
||||
handle="strong"
|
||||
positionOffset={{ x: '-50%', y: '0%' }}
|
||||
onStop={handleMoved}
|
||||
onStop={onDragStop}
|
||||
>
|
||||
<div
|
||||
ref={nodeRef}
|
||||
className="fixed left-1/2 top-[10px] z-[1000] -translate-x-1/2"
|
||||
className={clsx(
|
||||
'fixed left-1/2 top-[10px] z-[1000] -translate-x-1/2 transition-opacity duration-300',
|
||||
isInitialized ? 'opacity-100' : 'opacity-0',
|
||||
isInvisible && 'invisible'
|
||||
)}
|
||||
onMouseEnter={() => handleHovered(true)}
|
||||
onMouseLeave={() => handleHovered(false)}
|
||||
onBlur={() => handleHovered(false)}
|
||||
@@ -144,7 +77,6 @@ export const Menu = () => {
|
||||
<div className="sticky top-[10px] flex w-full justify-center">
|
||||
<div
|
||||
className={clsx(
|
||||
'h-[36px] items-center rounded bg-neutral-800/80 transition-all duration-300',
|
||||
'h-[36px] items-center rounded bg-neutral-800/80 pl-1 pr-2 transition-all duration-300',
|
||||
isMenuExpanded ? 'flex' : 'hidden',
|
||||
isMenuHidden ? '-translate-y-[110%] opacity-80' : 'translate-y-0 opacity-100'
|
||||
@@ -186,7 +118,7 @@ export const Menu = () => {
|
||||
</div>
|
||||
|
||||
{/* Menubar expand button */}
|
||||
{!isMenuExpanded && <Expand toggleMenu={setIsMenuExpanded} />}
|
||||
{!isMenuExpanded && menuDisplayMode !== 'off' && <Expand toggleMenu={setIsMenuExpanded} />}
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Divider } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Language } from './language.tsx';
|
||||
import { MenuBar } from './menu-bar.tsx';
|
||||
import { MenuIcons } from './menu-icons.tsx';
|
||||
import { MenuMode } from './menu-mode.tsx';
|
||||
import { WebTitle } from './web-title.tsx';
|
||||
|
||||
export const Appearance = () => {
|
||||
@@ -19,7 +20,9 @@ export const Appearance = () => {
|
||||
|
||||
<Divider className="opacity-50" style={{ margin: '32px 0' }} />
|
||||
|
||||
<MenuBar />
|
||||
<div className="text-neutral-400">{t('settings.appearance.menuBar.title')}</div>
|
||||
<MenuMode />
|
||||
<MenuIcons />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import * as ls from '@/lib/localstorage.ts';
|
||||
import { menuDisabledItemsAtom } from '@/jotai/settings.ts';
|
||||
|
||||
export const MenuBar = () => {
|
||||
export const MenuIcons = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [menuDisabledItems, setMenuDisabledItems] = useAtom(menuDisabledItemsAtom);
|
||||
@@ -43,13 +43,15 @@ export const MenuBar = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-8 flex flex-col space-y-5">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-neutral-400">{t('settings.appearance.menuBar')}</span>
|
||||
<span className="text-xs text-neutral-500">{t('settings.appearance.menuBarDesc')}</span>
|
||||
<span className="text-neutral-400">{t('settings.appearance.menuBar.icons')}</span>
|
||||
<span className="text-xs text-neutral-500">
|
||||
{t('settings.appearance.menuBar.iconsDesc')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-col space-y-5">
|
||||
<div className="mt-5 flex flex-col space-y-5">
|
||||
{items.map((item) => (
|
||||
<div key={item.key} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2 text-neutral-400">
|
||||
@@ -66,6 +68,6 @@ export const MenuBar = () => {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
38
web/src/pages/desktop/menu/settings/appearance/menu-mode.tsx
Normal file
38
web/src/pages/desktop/menu/settings/appearance/menu-mode.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Segmented } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as storage from '@/lib/localstorage.ts';
|
||||
import { menuDisplayModeAtom } from '@/jotai/settings.ts';
|
||||
|
||||
export const MenuMode = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [menuDisplayMode, setMenuDisplayMode] = useAtom(menuDisplayModeAtom);
|
||||
|
||||
const options = [
|
||||
// { value: 'off', label: t('settings.appearance.menuBar.modeOff') },
|
||||
{ value: 'auto', label: t('settings.appearance.menuBar.modeAuto') },
|
||||
{ value: 'always', label: t('settings.appearance.menuBar.modeAlways') }
|
||||
];
|
||||
|
||||
function handleChange(mode: string) {
|
||||
if (mode === menuDisplayMode) return;
|
||||
|
||||
setMenuDisplayMode(mode);
|
||||
storage.setMenuDisplayMode(mode);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-5 flex w-full items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-neutral-400">{t('settings.appearance.menuBar.mode')}</span>
|
||||
<span className="text-xs text-neutral-500">
|
||||
{t('settings.appearance.menuBar.modeDesc')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Segmented value={menuDisplayMode} options={options} onChange={handleChange} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -137,7 +137,6 @@ export const VirtualKeyboard = () => {
|
||||
// Tausch
|
||||
if (base === 'KeyZ') return getKeycode('KeyY');
|
||||
if (base === 'KeyY') return getKeycode('KeyZ');
|
||||
if (base === 'IntlBackslash') return getKeycode('IntlBackslash_qwertz');
|
||||
// all other labels use their own code
|
||||
return getKeycode(base);
|
||||
}
|
||||
|
||||
@@ -79,11 +79,11 @@ export const keyboardOptions = {
|
||||
|
||||
KeyY_qwertz: 'Y',
|
||||
KeyZ_qwertz: 'Z',
|
||||
|
||||
|
||||
KeyQ_qwertz: 'Q<br/>@',
|
||||
KeyE_qwertz: 'E<br/>€',
|
||||
BracketRight_qwertz: '+~*',
|
||||
Backslash_qwertz: '#<br/>\'',
|
||||
Backslash_qwertz: "#<br/>'",
|
||||
IntlBackslash_qwertz: '<|>',
|
||||
Comma_qwertz: ',<br/>;',
|
||||
Period_qwertz: '.<br/>:',
|
||||
@@ -91,11 +91,11 @@ export const keyboardOptions = {
|
||||
'{controlleft_qwertz}': 'STRG',
|
||||
'{altright_qwertz}': 'ALT GR',
|
||||
'{controlright_qwertz}': 'STRG',
|
||||
|
||||
|
||||
BracketLeft_qwertz: 'Ü',
|
||||
Semicolon_qwertz: 'Ö',
|
||||
Quote_qwertz: 'Ä',
|
||||
|
||||
|
||||
'{tab}': 'Tab',
|
||||
KeyQ: 'Q',
|
||||
KeyW: 'W',
|
||||
@@ -336,7 +336,7 @@ export const modifierKeys = [
|
||||
'{altright}',
|
||||
'{metaright}',
|
||||
'{winright}',
|
||||
|
||||
|
||||
// DE-spezifische Modifier-Tokens (für qwertz-Layout)
|
||||
'{controlleft_qwertz}',
|
||||
'{controlright_qwertz}',
|
||||
|
||||
Reference in New Issue
Block a user