feat: basic ui layout
This commit is contained in:
parent
53682a1cdb
commit
38aa81b5bc
@ -4,3 +4,9 @@
|
|||||||
pnpm i
|
pnpm i
|
||||||
pnpm start
|
pnpm start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- 文件拖放 (利用 `react-dropzone`?)
|
||||||
|
- 各类算法
|
||||||
|
- 简易元数据编辑器
|
||||||
|
@ -2,12 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>音乐解锁 - Unlock Music</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<main id="root"></main>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -14,9 +14,11 @@
|
|||||||
"@chakra-ui/react": "^2.6.1",
|
"@chakra-ui/react": "^2.6.1",
|
||||||
"@emotion/react": "^11.11.0",
|
"@emotion/react": "^11.11.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"react-redux": "^8.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
|
105
pnpm-lock.yaml
105
pnpm-lock.yaml
@ -13,6 +13,9 @@ dependencies:
|
|||||||
'@emotion/styled':
|
'@emotion/styled':
|
||||||
specifier: ^11.11.0
|
specifier: ^11.11.0
|
||||||
version: 11.11.0(@emotion/react@11.11.0)(@types/react@18.0.28)(react@18.2.0)
|
version: 11.11.0(@emotion/react@11.11.0)(@types/react@18.0.28)(react@18.2.0)
|
||||||
|
'@reduxjs/toolkit':
|
||||||
|
specifier: ^1.9.5
|
||||||
|
version: 1.9.5(react-redux@8.0.5)(react@18.2.0)
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^10.12.8
|
specifier: ^10.12.8
|
||||||
version: 10.12.8(react-dom@18.2.0)(react@18.2.0)
|
version: 10.12.8(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -22,6 +25,9 @@ dependencies:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
|
react-redux:
|
||||||
|
specifier: ^8.0.5
|
||||||
|
version: 8.0.5(@types/react-dom@18.0.11)(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
@ -1818,6 +1824,32 @@ packages:
|
|||||||
resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==}
|
resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@reduxjs/toolkit@1.9.5(react-redux@8.0.5)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.9.0 || ^17.0.0 || ^18
|
||||||
|
react-redux: ^7.2.1 || ^8.0.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-redux:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
immer: 9.0.21
|
||||||
|
react: 18.2.0
|
||||||
|
react-redux: 8.0.5(@types/react-dom@18.0.11)(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
|
||||||
|
redux: 4.2.1
|
||||||
|
redux-thunk: 2.4.2(redux@4.2.1)
|
||||||
|
reselect: 4.1.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/hoist-non-react-statics@3.3.1:
|
||||||
|
resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.0.28
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/json-schema@7.0.11:
|
/@types/json-schema@7.0.11:
|
||||||
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -1843,7 +1875,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==}
|
resolution: {integrity: sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 18.0.28
|
'@types/react': 18.0.28
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/react@18.0.28:
|
/@types/react@18.0.28:
|
||||||
resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==}
|
resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==}
|
||||||
@ -1859,6 +1890,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
|
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/use-sync-external-store@0.0.3:
|
||||||
|
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@typescript-eslint/eslint-plugin@5.57.1(@typescript-eslint/parser@5.57.1)(eslint@8.38.0)(typescript@5.0.2):
|
/@typescript-eslint/eslint-plugin@5.57.1(@typescript-eslint/parser@5.57.1)(eslint@8.38.0)(typescript@5.0.2):
|
||||||
resolution: {integrity: sha512-1MeobQkQ9tztuleT3v72XmY0XuKXVXusAhryoLuU5YZ+mXoYKZP9SQ7Flulh1NX4DTjpGTc2b/eMu4u7M7dhnQ==}
|
resolution: {integrity: sha512-1MeobQkQ9tztuleT3v72XmY0XuKXVXusAhryoLuU5YZ+mXoYKZP9SQ7Flulh1NX4DTjpGTc2b/eMu4u7M7dhnQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
@ -2625,6 +2660,10 @@ packages:
|
|||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/immer@9.0.21:
|
||||||
|
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/import-fresh@3.3.0:
|
/import-fresh@3.3.0:
|
||||||
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -2978,6 +3017,44 @@ packages:
|
|||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-is@18.2.0:
|
||||||
|
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/react-redux@8.0.5(@types/react-dom@18.0.11)(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1):
|
||||||
|
resolution: {integrity: sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^16.8 || ^17.0 || ^18.0
|
||||||
|
'@types/react-dom': ^16.8 || ^17.0 || ^18.0
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-native: '>=0.59'
|
||||||
|
redux: ^4
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
react-native:
|
||||||
|
optional: true
|
||||||
|
redux:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.21.5
|
||||||
|
'@types/hoist-non-react-statics': 3.3.1
|
||||||
|
'@types/react': 18.0.28
|
||||||
|
'@types/react-dom': 18.0.11
|
||||||
|
'@types/use-sync-external-store': 0.0.3
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
react-is: 18.2.0
|
||||||
|
redux: 4.2.1
|
||||||
|
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-refresh@0.14.0:
|
/react-refresh@0.14.0:
|
||||||
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
|
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -3042,10 +3119,28 @@ packages:
|
|||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/redux-thunk@2.4.2(redux@4.2.1):
|
||||||
|
resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==}
|
||||||
|
peerDependencies:
|
||||||
|
redux: ^4
|
||||||
|
dependencies:
|
||||||
|
redux: 4.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/redux@4.2.1:
|
||||||
|
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.21.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
/regenerator-runtime@0.13.11:
|
/regenerator-runtime@0.13.11:
|
||||||
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/reselect@4.1.8:
|
||||||
|
resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/resolve-from@4.0.0:
|
/resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -3276,6 +3371,14 @@ packages:
|
|||||||
tslib: 2.5.0
|
tslib: 2.5.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/use-sync-external-store@1.2.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/vite@4.3.2:
|
/vite@4.3.2:
|
||||||
resolution: {integrity: sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==}
|
resolution: {integrity: sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
/* empty file here */
|
|
17
src/App.tsx
17
src/App.tsx
@ -1,11 +1,22 @@
|
|||||||
import './App.css';
|
import { Box, Center, Container } from '@chakra-ui/react';
|
||||||
import { SelectFile } from './SelectFile';
|
import { SelectFile } from './SelectFile';
|
||||||
|
|
||||||
|
import { FileListing } from './features/file-listing/FileListing';
|
||||||
|
import { Footer } from './Footer';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<Box height="full" width="full" pt="4">
|
||||||
|
<Container maxW="container.large">
|
||||||
|
<Center>
|
||||||
<SelectFile />
|
<SelectFile />
|
||||||
</main>
|
</Center>
|
||||||
|
<Box mt="8">
|
||||||
|
<FileListing />
|
||||||
|
</Box>
|
||||||
|
<Footer />
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
33
src/Footer.tsx
Normal file
33
src/Footer.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Center, Link, Text } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<Center height="footer.container">
|
||||||
|
<Center
|
||||||
|
height="footer.content"
|
||||||
|
fontSize="sm"
|
||||||
|
textAlign="center"
|
||||||
|
position="fixed"
|
||||||
|
bottom="0"
|
||||||
|
w="full"
|
||||||
|
bg="gray.100"
|
||||||
|
color="gray.800"
|
||||||
|
left="0"
|
||||||
|
flexDir="column"
|
||||||
|
>
|
||||||
|
<Text>音乐解锁 (x.x.x) - 移除已购音乐的加密保护。</Text>
|
||||||
|
<Text>
|
||||||
|
Copyright © 2019 - 2023{' '}
|
||||||
|
<Link href="https://git.unlock-music.dev/um" isExternal>
|
||||||
|
UnlockMusic 团队
|
||||||
|
</Link>{' '}
|
||||||
|
| 音乐解锁授权基于
|
||||||
|
<Link href="https://git.unlock-music.dev/um/um-react/src/branch/main/LICENSE" isExternal>
|
||||||
|
MIT许可协议
|
||||||
|
</Link>
|
||||||
|
。
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
export const PointerLabel = styled.label`
|
|
||||||
cursor: pointer;
|
|
||||||
`;
|
|
@ -1,25 +1,44 @@
|
|||||||
import { Box, Stack, Text } from '@chakra-ui/react';
|
|
||||||
import { UnlockIcon } from '@chakra-ui/icons';
|
|
||||||
import { useId } from 'react';
|
import { useId } from 'react';
|
||||||
import { PointerLabel } from './PointerLabel';
|
|
||||||
|
import { Box, Text } from '@chakra-ui/react';
|
||||||
|
import { UnlockIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
export function SelectFile() {
|
export function SelectFile() {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box borderWidth="1px" borderRadius="lg" p="6">
|
<Box
|
||||||
<Stack alignItems="center">
|
as="label"
|
||||||
<UnlockIcon />
|
htmlFor={id}
|
||||||
<Box>
|
w="100%"
|
||||||
将文件拖到此处,或
|
maxW={480}
|
||||||
<PointerLabel htmlFor={id}>
|
borderWidth="1px"
|
||||||
<Text as="span" color="teal.400">
|
borderRadius="lg"
|
||||||
点击选择
|
transitionDuration="0.5s"
|
||||||
</Text>
|
p="6"
|
||||||
</PointerLabel>
|
cursor="pointer"
|
||||||
<input id={id} type="file" hidden multiple />
|
display="flex"
|
||||||
|
flexDir="column"
|
||||||
|
alignItems="center"
|
||||||
|
_hover={{
|
||||||
|
borderColor: 'gray.400',
|
||||||
|
bg: 'gray.50',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box pb={3}>
|
||||||
|
<UnlockIcon boxSize={8} />
|
||||||
|
</Box>
|
||||||
|
<Box textAlign="center">
|
||||||
|
{/* 将文件拖到此处,或 */}
|
||||||
|
<Text as="span" color="teal.400">
|
||||||
|
点我选择
|
||||||
|
</Text>
|
||||||
|
需要解密的文件
|
||||||
|
<input id={id} type="file" hidden multiple />
|
||||||
|
<Text fontSize="sm" opacity="50%">
|
||||||
|
仅在浏览器内对文件进行解锁,无需消耗流量
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
56
src/features/file-listing/FileListing.tsx
Normal file
56
src/features/file-listing/FileListing.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Avatar, Box, Table, TableContainer, Tbody, Td, Text, Th, Thead, Tr, Wrap, WrapItem } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { addNewFile, selectFiles } from './fileListingSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../hooks';
|
||||||
|
|
||||||
|
export function FileListing() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const files = useAppSelector(selectFiles);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// FIXME: Remove test data
|
||||||
|
if (files.length === 0) {
|
||||||
|
dispatch(addNewFile({ id: String(Date.now()), fileName: '测试文件名.mgg', blobURI: '' }));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table variant="striped">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th w="1%">封面</Th>
|
||||||
|
<Th>元信息</Th>
|
||||||
|
<Th>操作</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{files.map((file) => (
|
||||||
|
<Tr key={file.id}>
|
||||||
|
<Td>
|
||||||
|
{file.metadata.cover && <Avatar size="sm" name="专辑封面" src={file.metadata.cover} />}
|
||||||
|
{!file.metadata.cover && <Text>暂无封面</Text>}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Box as="h4" fontWeight="semibold" mt="1">
|
||||||
|
{file.metadata.name || file.fileName}
|
||||||
|
</Box>
|
||||||
|
<Text>专辑: {file.metadata.album}</Text>
|
||||||
|
<Text>艺术家: {file.metadata.artist}</Text>
|
||||||
|
<Text>专辑艺术家: {file.metadata.albumArtist}</Text>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Wrap>
|
||||||
|
<WrapItem>播放</WrapItem>
|
||||||
|
<WrapItem>下载</WrapItem>
|
||||||
|
<WrapItem>删除</WrapItem>
|
||||||
|
</Wrap>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
79
src/features/file-listing/fileListingSlice.ts
Normal file
79
src/features/file-listing/fileListingSlice.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import type { RootState } from '../../store';
|
||||||
|
|
||||||
|
export enum ProcessState {
|
||||||
|
UNTOUCHED = 'UNTOUCHED',
|
||||||
|
COMPLETE = 'COMPLETE',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ListingMode {
|
||||||
|
LIST = 'LIST',
|
||||||
|
CARD = 'CARD',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioMetadata {
|
||||||
|
name: string;
|
||||||
|
artist: string;
|
||||||
|
album: string;
|
||||||
|
albumArtist: string;
|
||||||
|
cover: string; // blob uri
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecryptedAudioFile {
|
||||||
|
id: string;
|
||||||
|
fileName: string;
|
||||||
|
raw: string; // blob uri
|
||||||
|
decrypted: string; // blob uri
|
||||||
|
state: ProcessState;
|
||||||
|
errorMessage: null | string;
|
||||||
|
metadata: AudioMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileListingState {
|
||||||
|
files: DecryptedAudioFile[];
|
||||||
|
displayMode: ListingMode;
|
||||||
|
}
|
||||||
|
const initialState: FileListingState = {
|
||||||
|
files: [],
|
||||||
|
displayMode: ListingMode.LIST,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fileListingSlice = createSlice({
|
||||||
|
name: 'fileListing',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
addNewFile: (state, { payload }: PayloadAction<{ id: string; fileName: string; blobURI: string }>) => {
|
||||||
|
state.files.push({
|
||||||
|
id: payload.id,
|
||||||
|
fileName: payload.fileName,
|
||||||
|
raw: payload.blobURI,
|
||||||
|
decrypted: '',
|
||||||
|
state: ProcessState.UNTOUCHED,
|
||||||
|
errorMessage: null,
|
||||||
|
metadata: {
|
||||||
|
name: '',
|
||||||
|
artist: '',
|
||||||
|
album: '',
|
||||||
|
albumArtist: '',
|
||||||
|
cover: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setDecryptedContent: (state, { payload }: PayloadAction<{ id: string; decryptedBlobURI: string }>) => {
|
||||||
|
const file = state.files.find((file) => file.id === payload.id);
|
||||||
|
if (file) {
|
||||||
|
file.decrypted = payload.decryptedBlobURI;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { addNewFile, setDecryptedContent } = fileListingSlice.actions;
|
||||||
|
|
||||||
|
export const selectFileCount = (state: RootState) => state.fileListing.files.length;
|
||||||
|
export const selectFiles = (state: RootState) => state.fileListing.files;
|
||||||
|
export const selectFileListingMode = (state: RootState) => state.fileListing.displayMode;
|
||||||
|
|
||||||
|
export default fileListingSlice.reducer;
|
6
src/hooks.ts
Normal file
6
src/hooks.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import type { TypedUseSelectorHook } from 'react-redux';
|
||||||
|
import type { RootState, AppDispatch } from './store';
|
||||||
|
|
||||||
|
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
@ -1,69 +0,0 @@
|
|||||||
:root {
|
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
10
src/main.tsx
10
src/main.tsx
@ -1,14 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App.tsx';
|
import App from './App';
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
import { ChakraProvider } from '@chakra-ui/react';
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { store } from './store';
|
||||||
|
import { theme } from './theme';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ChakraProvider>
|
<ChakraProvider theme={theme}>
|
||||||
|
<Provider store={store}>
|
||||||
<App />
|
<App />
|
||||||
|
</Provider>
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
11
src/store.ts
Normal file
11
src/store.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import fileListing from './features/file-listing/fileListingSlice';
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
fileListing: fileListing,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
17
src/theme.ts
Normal file
17
src/theme.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { extendTheme } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export const theme = extendTheme({
|
||||||
|
styles: {
|
||||||
|
global: {
|
||||||
|
body: {
|
||||||
|
minHeight: '100vh',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sizes: {
|
||||||
|
footer: {
|
||||||
|
container: '7rem',
|
||||||
|
content: '5rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user