mirror of
https://github.com/snowykami/sfkm.me-next.git
synced 2025-08-31 17:36:39 +00:00
feat: Implement desktop environment with responsive design and window management
All checks were successful
Build and Push Container Image, Deploy to Host / build-and-push-and-deploy (push) Successful in 2m4s
All checks were successful
Build and Push Container Image, Deploy to Host / build-and-push-and-deploy (push) Successful in 2m4s
- Refactored layout to include device context and internationalization support. - Created a Desktop component that conditionally renders PC and mobile versions. - Developed Background, PCDesktop, MobileDesktop, Content, Dock, and TopBar components for the desktop UI. - Added window management context to handle multiple windows with operations like open, close, minimize, and update. - Implemented BaseWindow component for draggable and resizable windows. - Integrated datetime and icon widgets into the TopBar. - Configured application metadata and environment variables for dynamic title, description, and icon. - Established device context for theme and background management with local storage persistence. - Added i18n support for localization based on user preferences.
This commit is contained in:
@ -1,8 +1,10 @@
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
export default withNextIntl(nextConfig);
|
||||
|
16
package.json
16
package.json
@ -9,19 +9,25 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.3.1",
|
||||
"framer-motion": "^12.23.12",
|
||||
"next": "15.4.6",
|
||||
"next-intl": "^4.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"next": "15.4.6"
|
||||
"react-rnd": "^10.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.6",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
225
pnpm-lock.yaml
generated
225
pnpm-lock.yaml
generated
@ -8,15 +8,30 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
deepmerge:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
framer-motion:
|
||||
specifier: ^12.23.12
|
||||
version: 12.23.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
next:
|
||||
specifier: 15.4.6
|
||||
version: 15.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
next-intl:
|
||||
specifier: ^4.3.5
|
||||
version: 4.3.5(next@15.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.9.2)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react:
|
||||
specifier: 19.1.0
|
||||
version: 19.1.0
|
||||
react-dom:
|
||||
specifier: 19.1.0
|
||||
version: 19.1.0(react@19.1.0)
|
||||
react-rnd:
|
||||
specifier: ^10.5.2
|
||||
version: 10.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
devDependencies:
|
||||
'@eslint/eslintrc':
|
||||
specifier: ^3
|
||||
@ -39,6 +54,9 @@ importers:
|
||||
eslint-config-next:
|
||||
specifier: 15.4.6
|
||||
version: 15.4.6(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
|
||||
eslint-plugin-react-hooks:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0(eslint@9.33.0(jiti@2.5.1))
|
||||
tailwindcss:
|
||||
specifier: ^4
|
||||
version: 4.1.11
|
||||
@ -103,6 +121,24 @@ packages:
|
||||
resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.4':
|
||||
resolution: {integrity: sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==}
|
||||
|
||||
'@formatjs/fast-memoize@2.2.7':
|
||||
resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==}
|
||||
|
||||
'@formatjs/icu-messageformat-parser@2.11.2':
|
||||
resolution: {integrity: sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==}
|
||||
|
||||
'@formatjs/icu-skeleton-parser@1.8.14':
|
||||
resolution: {integrity: sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==}
|
||||
|
||||
'@formatjs/intl-localematcher@0.5.10':
|
||||
resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.1':
|
||||
resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==}
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@ -341,6 +377,9 @@ packages:
|
||||
'@rushstack/eslint-patch@1.12.0':
|
||||
resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==}
|
||||
|
||||
'@schummar/icu-type-parser@1.21.5':
|
||||
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
@ -727,6 +766,10 @@ packages:
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
clsx@1.2.1:
|
||||
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@ -783,9 +826,16 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decimal.js@10.6.0:
|
||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||
|
||||
deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
define-data-property@1.1.4:
|
||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1016,6 +1066,20 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
framer-motion@12.23.12:
|
||||
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
@ -1114,6 +1178,9 @@ packages:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
intl-messageformat@10.7.16:
|
||||
resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==}
|
||||
|
||||
is-array-buffer@3.0.5:
|
||||
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1385,6 +1452,12 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
motion-dom@12.23.12:
|
||||
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
|
||||
|
||||
motion-utils@12.23.6:
|
||||
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@ -1401,6 +1474,26 @@ packages:
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
negotiator@1.0.0:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
next-intl@4.3.5:
|
||||
resolution: {integrity: sha512-tT3SltfpPOCAQ9kVNr+8t6FUtVf8G0WFlJcVc8zj4WCMfuF8XFk4gZCN/MtjgDgkUISw5aKamOClJB4EsV95WQ==}
|
||||
peerDependencies:
|
||||
next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||
typescript: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
next-themes@0.4.6:
|
||||
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next@15.4.6:
|
||||
resolution: {integrity: sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
@ -1522,14 +1615,32 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
re-resizable@6.11.2:
|
||||
resolution: {integrity: sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==}
|
||||
peerDependencies:
|
||||
react: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
react-dom@19.1.0:
|
||||
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
|
||||
peerDependencies:
|
||||
react: ^19.1.0
|
||||
|
||||
react-draggable@4.4.6:
|
||||
resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==}
|
||||
peerDependencies:
|
||||
react: '>= 16.3.0'
|
||||
react-dom: '>= 16.3.0'
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
react-rnd@10.5.2:
|
||||
resolution: {integrity: sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==}
|
||||
peerDependencies:
|
||||
react: '>=16.3.0'
|
||||
react-dom: '>=16.3.0'
|
||||
|
||||
react@19.1.0:
|
||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -1723,6 +1834,9 @@ packages:
|
||||
tsconfig-paths@3.15.0:
|
||||
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
|
||||
|
||||
tslib@2.6.2:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
@ -1764,6 +1878,11 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
use-intl@4.3.5:
|
||||
resolution: {integrity: sha512-qyL1TZNesVbzj/75ZbYsi+xzNSiFqp5rIVsiAN0JT8rPMSjX0/3KQz76aJIrngI1/wIQdVYFVdImWh5yAv+dWA==}
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1866,6 +1985,36 @@ snapshots:
|
||||
'@eslint/core': 0.15.2
|
||||
levn: 0.4.1
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.4':
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/intl-localematcher': 0.6.1
|
||||
decimal.js: 10.6.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/fast-memoize@2.2.7':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/icu-messageformat-parser@2.11.2':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.4
|
||||
'@formatjs/icu-skeleton-parser': 1.8.14
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/icu-skeleton-parser@1.8.14':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/intl-localematcher@0.5.10':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.6':
|
||||
@ -2038,6 +2187,8 @@ snapshots:
|
||||
|
||||
'@rushstack/eslint-patch@1.12.0': {}
|
||||
|
||||
'@schummar/icu-type-parser@1.21.5': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@ -2434,6 +2585,8 @@ snapshots:
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
clsx@1.2.1: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
@ -2490,8 +2643,12 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decimal.js@10.6.0: {}
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
define-data-property@1.1.4:
|
||||
dependencies:
|
||||
es-define-property: 1.0.1
|
||||
@ -2877,6 +3034,15 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
framer-motion@12.23.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
motion-dom: 12.23.12
|
||||
motion-utils: 12.23.6
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
function.prototype.name@1.1.8:
|
||||
@ -2978,6 +3144,13 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
intl-messageformat@10.7.16:
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.4
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/icu-messageformat-parser': 2.11.2
|
||||
tslib: 2.8.1
|
||||
|
||||
is-array-buffer@3.0.5:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@ -3231,6 +3404,12 @@ snapshots:
|
||||
|
||||
mkdirp@3.0.1: {}
|
||||
|
||||
motion-dom@12.23.12:
|
||||
dependencies:
|
||||
motion-utils: 12.23.6
|
||||
|
||||
motion-utils@12.23.6: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
@ -3239,6 +3418,23 @@ snapshots:
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
next-intl@4.3.5(next@15.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.5.10
|
||||
negotiator: 1.0.0
|
||||
next: 15.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
use-intl: 4.3.5(react@19.1.0)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.2
|
||||
|
||||
next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
next@15.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@next/env': 15.4.6
|
||||
@ -3369,13 +3565,33 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
re-resizable@6.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
react-dom@19.1.0(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
scheduler: 0.26.0
|
||||
|
||||
react-draggable@4.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
clsx: 1.2.1
|
||||
prop-types: 15.8.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-rnd@10.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
re-resizable: 6.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-draggable: 4.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
tslib: 2.6.2
|
||||
|
||||
react@19.1.0: {}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
@ -3643,6 +3859,8 @@ snapshots:
|
||||
minimist: 1.2.8
|
||||
strip-bom: 3.0.0
|
||||
|
||||
tslib@2.6.2: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
@ -3721,6 +3939,13 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
use-intl@4.3.5(react@19.1.0):
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@schummar/icu-type-parser': 1.21.5
|
||||
intl-messageformat: 10.7.16
|
||||
react: 19.1.0
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
dependencies:
|
||||
is-bigint: 1.1.0
|
||||
|
@ -1,7 +1,13 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import config from "@/config";
|
||||
import "./globals.css";
|
||||
|
||||
import { DeviceProvider } from "@/contexts/device";
|
||||
import {NextIntlClientProvider} from 'next-intl';
|
||||
import {getLocale} from 'next-intl/server';
|
||||
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
@ -13,21 +19,29 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
icons: {
|
||||
icon: config.icon,
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const locale = await getLocale();
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang={locale}>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<NextIntlClientProvider locale={locale}>
|
||||
<DeviceProvider>
|
||||
{children}
|
||||
</DeviceProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
102
src/app/page.tsx
102
src/app/page.tsx
@ -1,103 +1,9 @@
|
||||
import Image from "next/image";
|
||||
import Desktop from "@/components/desktop";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
<>
|
||||
<Desktop />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
34
src/components/desktop/Background.tsx
Normal file
34
src/components/desktop/Background.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useDevice } from "@/contexts/device";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Background() {
|
||||
const { background } = useDevice();
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
// 只在客户端渲染,避免水合错误
|
||||
useEffect(() => {
|
||||
setIsLoaded(true);
|
||||
}, []);
|
||||
|
||||
if (!isLoaded) {
|
||||
return null; // 服务端渲染时不显示任何内容
|
||||
}
|
||||
|
||||
console.log("Background component rendered with background:", background);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[-1] transition-opacity duration-500"
|
||||
style={{
|
||||
backgroundImage: background ? `url(${background})` : "",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
opacity: background ? 1 : 0,
|
||||
pointerEvents: "none" // 不阻挡鼠标事件
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
7
src/components/desktop/MobileDesktop.tsx
Normal file
7
src/components/desktop/MobileDesktop.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export default function MobileDesktop() {
|
||||
return (
|
||||
<div className="block md:hidden">
|
||||
<h1>Mobile Desktop Component</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
22
src/components/desktop/PCDesktop.tsx
Normal file
22
src/components/desktop/PCDesktop.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import Content from "./content";
|
||||
import Dock from "./dock";
|
||||
import TopBar from "./topbar";
|
||||
|
||||
export default function PCDesktop() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hidden md:flex flex-col min-h-screen"
|
||||
>
|
||||
<div className="h-8 w-full">
|
||||
<TopBar />
|
||||
</div>
|
||||
<div className="flex-1 w-full">
|
||||
<Content />
|
||||
</div>
|
||||
<div className="h-16 w-full flex justify-center">
|
||||
<Dock />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
8
src/components/desktop/content/index.tsx
Normal file
8
src/components/desktop/content/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export default function Content() {
|
||||
return (
|
||||
<div className="content">
|
||||
<h1>Desktop Content Component</h1>
|
||||
{/* Other content can be added here */}
|
||||
</div>
|
||||
);
|
||||
}
|
9
src/components/desktop/dock/index.tsx
Normal file
9
src/components/desktop/dock/index.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
export default function Dock() {
|
||||
return (
|
||||
<div
|
||||
className={`bg-slate-100/90 dark:bg-slate-900/95 backdrop-blur-md rounded-2xl`}
|
||||
>
|
||||
Dock Component
|
||||
</div>
|
||||
);
|
||||
}
|
14
src/components/desktop/index.tsx
Normal file
14
src/components/desktop/index.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import PCDesktop from "./PCDesktop";
|
||||
import MobileDesktop from "./MobileDesktop";
|
||||
import { WindowsManagerProvider } from "@/contexts/windows";
|
||||
import Background from "./Background";
|
||||
|
||||
export default function Desktop() {
|
||||
return (
|
||||
<WindowsManagerProvider>
|
||||
<Background />
|
||||
<PCDesktop />
|
||||
<MobileDesktop />
|
||||
</WindowsManagerProvider>
|
||||
);
|
||||
}
|
31
src/components/desktop/topbar/index.tsx
Normal file
31
src/components/desktop/topbar/index.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import Datetime from "./widgets/Datetime";
|
||||
import Icon from "./widgets/Icon";
|
||||
import WindowTitle from "./widgets/WindowTitle";
|
||||
|
||||
function TopBarLeftMenu() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 h-full">
|
||||
<Icon />
|
||||
<WindowTitle />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TopBarRightMenu() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 h-full">
|
||||
<Datetime />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TopBar() {
|
||||
return (
|
||||
<div className="flex justify-between items-center h-full px-2 py-1.5 text-slate-700 dark:text-slate-300 text-sm bg-white/50 dark:bg-gray-800/50 shadow-md">
|
||||
<TopBarLeftMenu />
|
||||
<TopBarRightMenu />
|
||||
</div>
|
||||
);
|
||||
}
|
47
src/components/desktop/topbar/widgets/Datetime.tsx
Normal file
47
src/components/desktop/topbar/widgets/Datetime.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
|
||||
export default function Datetime() {
|
||||
const [time, setTime] = useState<Date | null>(null);
|
||||
const locale = useLocale();
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
setTime(new Date());
|
||||
const timer = setInterval(() => {
|
||||
setTime(new Date());
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleTimeString("zh-CN", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString(locale, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
weekday: "short",
|
||||
});
|
||||
};
|
||||
|
||||
if (!time) return null;
|
||||
|
||||
return (
|
||||
<div className="font-bold">
|
||||
<span className="mt-0.5">
|
||||
{formatDate(time)}
|
||||
</span>
|
||||
<span className="mx-1"></span>
|
||||
<span className="leading-none">
|
||||
{formatTime(time)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
9
src/components/desktop/topbar/widgets/Icon.tsx
Normal file
9
src/components/desktop/topbar/widgets/Icon.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import config from "@/config";
|
||||
|
||||
export default function Icon() {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center overflow-hidden">
|
||||
<img src={config.icon} alt="Icon" className="max-w-full max-h-full object-contain" />
|
||||
</div>
|
||||
);
|
||||
}
|
16
src/components/desktop/topbar/widgets/WindowTitle.tsx
Normal file
16
src/components/desktop/topbar/widgets/WindowTitle.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { useWindowsManager } from "@/contexts/windows";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function WindowTitle() {
|
||||
const { getTopWindow } = useWindowsManager();
|
||||
const [title, setTitle] = useState("");
|
||||
useEffect(() => {
|
||||
setTitle(getTopWindow()?.title ?? "");
|
||||
}, [getTopWindow]);
|
||||
|
||||
return (
|
||||
<div className="flex-1 h-full flex items-center justify-center overflow-hidden">
|
||||
<span className="text-sm font-bold text-slate-700 dark:text-slate-300">{title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
288
src/components/windows/BaseWindow.tsx
Normal file
288
src/components/windows/BaseWindow.tsx
Normal file
@ -0,0 +1,288 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Rnd, RndDragCallback, RndResizeCallback } from "react-rnd";
|
||||
import type { Rnd as RndType } from "react-rnd";
|
||||
import { useWindowsManager } from "@/contexts/windows";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export interface BaseWindowProps {
|
||||
id?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
zIndex?: number;
|
||||
draggable?: boolean;
|
||||
resizable?: boolean;
|
||||
maximized?: boolean;
|
||||
visible?: boolean;
|
||||
onDragStart?: RndDragCallback;
|
||||
onDragStop?: RndDragCallback;
|
||||
onResizeStop?: RndResizeCallback;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
dockHeight?: number; // 可选的停靠栏高度
|
||||
windowMargin?: number; // 窗口边距
|
||||
dragHandleClassName?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface PreMaximizeState {
|
||||
size: { width: number; height: number };
|
||||
position: { x: number; y: number };
|
||||
mouseOffset: { x: number; y: number };
|
||||
}
|
||||
|
||||
export const BaseWindow: React.FC<BaseWindowProps> = ({
|
||||
id,
|
||||
x = 100,
|
||||
y = 100,
|
||||
width = 400,
|
||||
height = 300,
|
||||
minWidth = 220,
|
||||
minHeight = 120,
|
||||
zIndex = 100,
|
||||
draggable = true,
|
||||
resizable = true,
|
||||
maximized = false,
|
||||
visible = true,
|
||||
onDragStart,
|
||||
onDragStop,
|
||||
onResizeStop,
|
||||
onClick,
|
||||
dragHandleClassName,
|
||||
dockHeight = 0, // 默认停靠栏顶部绝对高度
|
||||
windowMargin = 0, // 窗口边距
|
||||
children,
|
||||
}) => {
|
||||
const windowsManager = useWindowsManager?.();
|
||||
const win =
|
||||
id && windowsManager
|
||||
? windowsManager.windows.find((w) => w.id === id)
|
||||
: undefined;
|
||||
const [position, setPosition] = useState({ x, y });
|
||||
const [size, setSize] = useState({ width, height });
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const t = useTranslations('HomePage');
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize();
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!win) setPosition({ x, y });
|
||||
}, [x, y, win]);
|
||||
useEffect(() => {
|
||||
if (!win) setSize({ width, height });
|
||||
}, [width, height, win]);
|
||||
|
||||
const [shouldRender, setShouldRender] = useState(visible);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setShouldRender(true);
|
||||
} else {
|
||||
const timer = setTimeout(() => setShouldRender(false), 250);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const maximizedStyle =
|
||||
(win?.maximized ?? maximized)
|
||||
? {
|
||||
x: windowMargin,
|
||||
y: windowMargin,
|
||||
width: window.innerWidth - windowMargin * 2,
|
||||
height: window.innerHeight - windowMargin * 2 - dockHeight,
|
||||
}
|
||||
: {
|
||||
x: win?.position?.x ?? position.x,
|
||||
y: win?.position?.y ?? position.y,
|
||||
width: win?.size?.width ?? size.width,
|
||||
height: win?.size?.height ?? size.height,
|
||||
};
|
||||
|
||||
const [showMaximizeHint, setShowMaximizeHint] = useState(false);
|
||||
const [preMaximize, setPreMaximize] = useState<PreMaximizeState | null>(null);
|
||||
const rndRef = useRef<RndType | null>(null);
|
||||
const handleDrag: RndDragCallback = (_e, d) => {
|
||||
if (win?.maximized ?? maximized) return;
|
||||
if (d.y <= windowMargin) {
|
||||
setShowMaximizeHint(true);
|
||||
} else {
|
||||
setShowMaximizeHint(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragStop: RndDragCallback = (e, d) => {
|
||||
if (showMaximizeHint) {
|
||||
setShowMaximizeHint(false);
|
||||
if (id && windowsManager) {
|
||||
windowsManager.updateWindow(id, { maximized: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (id && windowsManager) {
|
||||
windowsManager.updateWindow(id, { position: { x: d.x, y: d.y } });
|
||||
} else {
|
||||
setPosition({ x: d.x, y: d.y });
|
||||
}
|
||||
onDragStop?.(e, d);
|
||||
};
|
||||
const handleDragStart: RndDragCallback = (e, d) => {
|
||||
if (win?.maximized ?? maximized) {
|
||||
const mouseX = (e as MouseEvent).clientX;
|
||||
const mouseY = (e as MouseEvent).clientY;
|
||||
const widthVal = win?.size?.width ?? size.width;
|
||||
const heightVal = win?.size?.height ?? size.height;
|
||||
setPreMaximize({
|
||||
size: { width: widthVal, height: heightVal },
|
||||
position: { x: d.x, y: d.y },
|
||||
mouseOffset: {
|
||||
x: mouseX - (maximizedStyle.x ?? 0),
|
||||
y: mouseY - (maximizedStyle.y ?? 0),
|
||||
},
|
||||
});
|
||||
if (id && windowsManager) {
|
||||
windowsManager.updateWindow(id, { maximized: false });
|
||||
}
|
||||
}
|
||||
onDragStart?.(e, d);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!win?.maximized && preMaximize && rndRef.current) {
|
||||
const mouse = preMaximize.mouseOffset;
|
||||
const newX = Math.max(
|
||||
0,
|
||||
window.innerWidth / 2 - preMaximize.size.width / 2,
|
||||
);
|
||||
const newY = Math.max(
|
||||
windowMargin,
|
||||
mouse.y - preMaximize.size.height / 2,
|
||||
);
|
||||
rndRef.current.updatePosition({ x: newX, y: newY });
|
||||
rndRef.current.updateSize({
|
||||
width: preMaximize.size.width,
|
||||
height: preMaximize.size.height,
|
||||
});
|
||||
setPreMaximize(null);
|
||||
if (id && windowsManager) {
|
||||
windowsManager.updateWindow(id, {
|
||||
position: { x: newX, y: newY },
|
||||
size: {
|
||||
width: preMaximize.size.width,
|
||||
height: preMaximize.size.height,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setPosition({ x: newX, y: newY });
|
||||
setSize({
|
||||
width: preMaximize.size.width,
|
||||
height: preMaximize.size.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
win?.maximized,
|
||||
preMaximize,
|
||||
id,
|
||||
windowsManager,
|
||||
size.width,
|
||||
size.height,
|
||||
windowMargin,
|
||||
]);
|
||||
if (!shouldRender) return null;
|
||||
// 移动端下直接全屏
|
||||
const rndSize = isMobile
|
||||
? { width: "100vw", height: "100vh" }
|
||||
: { width: maximizedStyle.width, height: maximizedStyle.height };
|
||||
const rndPosition = isMobile
|
||||
? { x: 0, y: 0 }
|
||||
: { x: maximizedStyle.x, y: maximizedStyle.y };
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visible && (
|
||||
<Rnd
|
||||
ref={rndRef}
|
||||
size={rndSize}
|
||||
position={rndPosition}
|
||||
minWidth={minWidth}
|
||||
minHeight={minHeight}
|
||||
bounds="parent"
|
||||
disableDragging={
|
||||
isMobile || !draggable || (win?.maximized ?? maximized)
|
||||
}
|
||||
enableResizing={
|
||||
!isMobile && resizable && !(win?.maximized ?? maximized)
|
||||
}
|
||||
style={{ zIndex: win?.zIndex ?? zIndex }}
|
||||
className="base-window"
|
||||
dragHandleClassName={dragHandleClassName}
|
||||
onDrag={handleDrag}
|
||||
onDragStart={handleDragStart}
|
||||
onDragStop={handleDragStop}
|
||||
onMouseDown={() => {
|
||||
if (id && windowsManager) {
|
||||
windowsManager.placeOnTop(id);
|
||||
}
|
||||
}}
|
||||
onResizeStop={(e, dir, ref, delta, pos) => {
|
||||
if (id && windowsManager) {
|
||||
windowsManager.updateWindow(id, {
|
||||
size: {
|
||||
width: parseInt(ref.style.width, 10),
|
||||
height: parseInt(ref.style.height, 10),
|
||||
},
|
||||
position: { x: pos.x, y: pos.y },
|
||||
});
|
||||
} else {
|
||||
setSize({
|
||||
width: parseInt(ref.style.width, 10),
|
||||
height: parseInt(ref.style.height, 10),
|
||||
});
|
||||
setPosition({ x: pos.x, y: pos.y });
|
||||
}
|
||||
onResizeStop?.(e, dir, ref, delta, pos);
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<motion.div
|
||||
layout
|
||||
className="w-full h-full relative"
|
||||
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.7, y: -30 }}
|
||||
transition={{ duration: 0.4, type: "spring" }}
|
||||
>
|
||||
{showMaximizeHint && (
|
||||
<div
|
||||
className="
|
||||
base-window-container
|
||||
absolute top-0 left-1/2 -translate-x-1/2
|
||||
bg-blue-500/50 text-white px-4 py-1 rounded-lg z-[9999] text
|
||||
text-sm pointer-events-none shadow-md select-none w-full h-full flex justify-center items-start
|
||||
"
|
||||
>
|
||||
{t("ui.releaseToMaximize")}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</motion.div>
|
||||
</Rnd>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseWindow;
|
0
src/components/windows/MacOSWindow.tsx
Normal file
0
src/components/windows/MacOSWindow.tsx
Normal file
5
src/config.ts
Normal file
5
src/config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
title: process.env.NEXT_PUBLIC_APP_TITLE || "Snowykami OS",
|
||||
description: process.env.NEXT_PUBLIC_APP_DESCRIPTION || "A modern OS built with Next.js",
|
||||
icon: process.env.NEXT_PUBLIC_APP_ICON || "https://cdn.liteyuki.org/snowykami/avatar_alpha.png"
|
||||
}
|
93
src/contexts/device.tsx
Normal file
93
src/contexts/device.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
const THEME_STORAGE_KEY = "theme";
|
||||
|
||||
const BACKGROUND_STORAGE_KEY = "background";
|
||||
|
||||
type Theme = "light" | "dark" | "system";
|
||||
|
||||
interface DeviceContextProps {
|
||||
isMobile: boolean;
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
background: string;
|
||||
setBackground: (background: string) => void;
|
||||
}
|
||||
|
||||
const DeviceContext = createContext<DeviceContextProps>({
|
||||
background: 'https://cdn.liteyuki.org/blog/background.png',
|
||||
isMobile: false,
|
||||
theme: "system",
|
||||
setTheme: () => { },
|
||||
setBackground: () => { },
|
||||
});
|
||||
|
||||
export const DeviceProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [theme, setTheme] = useState<Theme>("system");
|
||||
const [background, setBackground] = useState<string>('https://cdn.liteyuki.org/blog/background.png');
|
||||
|
||||
// 挂载后读取 localStorage
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const savedTheme = window.localStorage.getItem(THEME_STORAGE_KEY) as Theme;
|
||||
if (savedTheme) setTheme(savedTheme);
|
||||
const savedBg = window.localStorage.getItem(BACKGROUND_STORAGE_KEY);
|
||||
if (savedBg) setBackground(savedBg);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 监听系统主题变化
|
||||
useEffect(() => {
|
||||
if (theme === "system") {
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handler = () => {
|
||||
document.documentElement.classList.toggle("dark", media.matches);
|
||||
};
|
||||
handler();
|
||||
media.addEventListener("change", handler);
|
||||
return () => media.removeEventListener("change", handler);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
// 监听配置主题变化
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem("theme", theme);
|
||||
if (theme !== "system") {
|
||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
||||
}
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
// 监听背景变化并持久化
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem(BACKGROUND_STORAGE_KEY, background);
|
||||
}
|
||||
}, [background]);
|
||||
|
||||
// 响应式判断 isMobile
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
return () => window.removeEventListener("resize", checkMobile);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DeviceContext.Provider value={{ isMobile, theme, setTheme, background, setBackground }}>
|
||||
{children}
|
||||
</DeviceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useDevice = () => {
|
||||
const context = useContext(DeviceContext);
|
||||
if (!context) {
|
||||
throw new Error("useDevice must be used within a DeviceProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
183
src/contexts/windows.tsx
Normal file
183
src/contexts/windows.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
"use client"
|
||||
|
||||
import { WindowProps } from "@/types/window";
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
|
||||
const WINDOWS_STORAGE_KEY = "windows";
|
||||
|
||||
interface WindowsManagerContextProps {
|
||||
windows: WindowProps[];
|
||||
// window operations
|
||||
openWindow: (id: string, props: Partial<WindowProps>) => void;
|
||||
closeWindow: (id: string) => void;
|
||||
minimizeWindow: (id: string) => void;
|
||||
placeOnTop: (id: string) => void;
|
||||
updateWindow: (id: string, props: Partial<WindowProps>) => void;
|
||||
// get and global
|
||||
getWindowById: (id: string) => WindowProps | undefined;
|
||||
getTopWindow: () => WindowProps | undefined;
|
||||
resetWindowsOfLocalStorage?: () => void;
|
||||
}
|
||||
|
||||
const WindowManagerContext = createContext<WindowsManagerContextProps | null>(
|
||||
null
|
||||
);
|
||||
|
||||
export const WindowsManagerProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
// 彻底关闭窗口(移除)
|
||||
const closeWindow = useCallback((id: string) => {
|
||||
setWindows(ws => ws.filter(w => w.id !== id));
|
||||
}, []);
|
||||
|
||||
// 最小化窗口(只设置 visible=false,不移除)
|
||||
const minimizeWindow = useCallback((id: string) => {
|
||||
setWindows(ws => ws.map(w => w.id === id ? { ...w, visible: false } : w));
|
||||
}, []);
|
||||
const [windows, setWindows] = useState<WindowProps[]>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(WINDOWS_STORAGE_KEY);
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(WINDOWS_STORAGE_KEY, JSON.stringify(windows));
|
||||
}, [windows]);
|
||||
|
||||
|
||||
const openWindow = useCallback(
|
||||
(id: string, initial: Partial<WindowProps> = {}) => {
|
||||
setWindows((ws) => {
|
||||
const exist = ws.find((w) => w.id === id);
|
||||
if (exist) {
|
||||
return ws.map((w) =>
|
||||
w.id === id
|
||||
? {
|
||||
...w,
|
||||
visible: true,
|
||||
minimized: false,
|
||||
...initial,
|
||||
}
|
||||
: w,
|
||||
);
|
||||
}
|
||||
if (ws.find((w) => w.id === id)) return ws;
|
||||
const maxZ = ws.reduce((max, w) => Math.max(max, w.zIndex), 100);
|
||||
const size = initial.size ?? { width: 400, height: 300 };
|
||||
const position =
|
||||
initial.position ?? calculateNewWindowPosition(ws.length, size);
|
||||
return [
|
||||
...ws,
|
||||
{
|
||||
id,
|
||||
title: initial.title ?? "",
|
||||
visible: true,
|
||||
zIndex: maxZ + 1,
|
||||
maximized: false,
|
||||
minimized: false,
|
||||
position,
|
||||
size,
|
||||
...initial,
|
||||
},
|
||||
];
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const placeOnTop = useCallback(
|
||||
(id: string) => {
|
||||
setWindows((ws) => {
|
||||
const win = ws.find((w) => w.id === id);
|
||||
if (!win) return ws;
|
||||
|
||||
const maxZ = ws.reduce((max, w) => Math.max(max, w.zIndex), 100);
|
||||
return ws.map((w) =>
|
||||
w.id === id
|
||||
? { ...w, zIndex: maxZ + 1 }
|
||||
: w,
|
||||
);
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const updateWindow = useCallback(
|
||||
(id: string, props: Partial<WindowProps>) => {
|
||||
setWindows((ws) => {
|
||||
const win = ws.find((w) => w.id === id);
|
||||
if (!win) return ws;
|
||||
return ws.map((w) =>
|
||||
w.id === id ? { ...w, ...props } : w,
|
||||
);
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const getWindowById = useCallback(
|
||||
(id: string) => {
|
||||
return windows.find((w) => w.id === id);
|
||||
},
|
||||
[windows],
|
||||
);
|
||||
|
||||
const getTopWindow = useCallback(() => {
|
||||
if (windows.length === 0) return undefined;
|
||||
return windows.reduce((top, w) => (w.zIndex > top.zIndex ? w : top), windows[0]);
|
||||
}, [windows]);
|
||||
|
||||
const resetWindowsOfLocalStorage = useCallback(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.removeItem("windows");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WindowManagerContext.Provider
|
||||
value={{
|
||||
windows,
|
||||
openWindow,
|
||||
closeWindow,
|
||||
minimizeWindow,
|
||||
placeOnTop,
|
||||
updateWindow,
|
||||
getTopWindow,
|
||||
getWindowById,
|
||||
resetWindowsOfLocalStorage,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WindowManagerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useWindowsManager = () => {
|
||||
const ctx = useContext(WindowManagerContext);
|
||||
if (!ctx)
|
||||
throw new Error(
|
||||
"useWindowsManager must be used within WindowsManagerProvider",
|
||||
);
|
||||
return ctx;
|
||||
};
|
||||
|
||||
const calculateNewWindowPosition = (
|
||||
count: number,
|
||||
size = { width: 400, height: 300 },
|
||||
) => {
|
||||
// 屏幕中心
|
||||
const centerX = Math.max(window.innerWidth / 2 - size.width / 2, 0);
|
||||
const centerY = Math.max(window.innerHeight / 2 - size.height / 2, 0);
|
||||
const offset = 20;
|
||||
let x = centerX + offset * (count % 6);
|
||||
let y = centerY + offset * (count % 6);
|
||||
// 限制窗口不超出右下边界
|
||||
x = Math.min(x, window.innerWidth - size.width);
|
||||
y = Math.min(y, window.innerHeight - size.height);
|
||||
// 限制窗口不超出左上边界
|
||||
x = Math.max(x, 0);
|
||||
y = Math.max(y, 0);
|
||||
return { x, y };
|
||||
};
|
37
src/i18n/request.ts
Normal file
37
src/i18n/request.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { getRequestConfig } from 'next-intl/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
export default getRequestConfig(async () => {
|
||||
const locales = await getUserLocales();
|
||||
const messages = await Promise.all(
|
||||
locales.map(async (locale) => {
|
||||
try {
|
||||
return (await import(`@/locales/${locale}.json`)).default;
|
||||
} catch (err) {
|
||||
console.debug(`Failed to load locale ${locale}: ${err}`);
|
||||
return {};
|
||||
}
|
||||
})
|
||||
).then((msgs) => msgs.reduce((acc, msg) => deepmerge(acc, msg), {}));
|
||||
return {
|
||||
locale: locales[0],
|
||||
messages
|
||||
};
|
||||
});
|
||||
|
||||
export async function getUserLocales(): Promise<string[]> {
|
||||
const defaultLocales = ["zh-CN", "zh", "en-US", "en"];
|
||||
const cookieStore = await cookies();
|
||||
const languageInCookie = cookieStore.get('language')?.value;
|
||||
let locales: string[] = [];
|
||||
if (languageInCookie) {
|
||||
locales.push(languageInCookie);
|
||||
locales.push(languageInCookie.split('-')[0]);
|
||||
}
|
||||
locales.push(...defaultLocales);
|
||||
|
||||
// 去重,保留顺序
|
||||
locales = Array.from(new Set(locales));
|
||||
return locales;
|
||||
}
|
18
src/types/window.ts
Normal file
18
src/types/window.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export interface WindowProps {
|
||||
id: string;
|
||||
title: string;
|
||||
appId?: string;
|
||||
visible: boolean;
|
||||
zIndex: number;
|
||||
maximized: boolean;
|
||||
minimized: boolean;
|
||||
position: { x: number; y: number };
|
||||
size: { width: number; height: number };
|
||||
showClose?: boolean;
|
||||
showMinimize?: boolean;
|
||||
showMaximize?: boolean;
|
||||
className?: string;
|
||||
onClose?: () => void;
|
||||
onMinimize?: () => void;
|
||||
onMaximize?: () => void;
|
||||
}
|
Reference in New Issue
Block a user