More stuff
This commit is contained in:
parent
13670d0770
commit
d7ae269d46
4
.env
Normal file
4
.env
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
VITE_DEFAULT_CLIENT_URL=
|
||||||
|
VITE_DEFAULT_CLIENT_USER=
|
||||||
|
VITE_DEFAULT_CLIENT_PASS=
|
||||||
|
VITE_DEFAULT_TORRENT_ADD=
|
@ -10,18 +10,20 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.0.4",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@tanstack/react-query": "^4.29.7",
|
"@tanstack/react-query": "^4.29.7",
|
||||||
"@tanstack/react-table": "^8.9.1",
|
"@tanstack/react-table": "^8.9.1",
|
||||||
"class-variance-authority": "^0.6.0",
|
"class-variance-authority": "^0.6.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"immer": "^10.0.2",
|
|
||||||
"lucide-react": "^0.221.0",
|
"lucide-react": "^0.221.0",
|
||||||
|
"pretty-bytes": "^6.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"tailwind-merge": "^1.12.0",
|
"tailwind-merge": "^1.12.0",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5"
|
||||||
"zustand": "^4.3.8"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.2.4",
|
"@types/node": "^20.2.4",
|
||||||
|
222
pnpm-lock.yaml
generated
222
pnpm-lock.yaml
generated
@ -1,6 +1,9 @@
|
|||||||
lockfileVersion: 5.4
|
lockfileVersion: 5.4
|
||||||
|
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@radix-ui/react-label': ^2.0.2
|
||||||
|
'@radix-ui/react-progress': ^1.0.3
|
||||||
|
'@radix-ui/react-scroll-area': ^1.0.4
|
||||||
'@radix-ui/react-slot': ^1.0.2
|
'@radix-ui/react-slot': ^1.0.2
|
||||||
'@tanstack/react-query': ^4.29.7
|
'@tanstack/react-query': ^4.29.7
|
||||||
'@tanstack/react-table': ^8.9.1
|
'@tanstack/react-table': ^8.9.1
|
||||||
@ -16,9 +19,9 @@ specifiers:
|
|||||||
eslint: ^8.38.0
|
eslint: ^8.38.0
|
||||||
eslint-plugin-react-hooks: ^4.6.0
|
eslint-plugin-react-hooks: ^4.6.0
|
||||||
eslint-plugin-react-refresh: ^0.3.4
|
eslint-plugin-react-refresh: ^0.3.4
|
||||||
immer: ^10.0.2
|
|
||||||
lucide-react: ^0.221.0
|
lucide-react: ^0.221.0
|
||||||
postcss: ^8.4.23
|
postcss: ^8.4.23
|
||||||
|
pretty-bytes: ^6.1.0
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
tailwind-merge: ^1.12.0
|
tailwind-merge: ^1.12.0
|
||||||
@ -26,21 +29,22 @@ specifiers:
|
|||||||
tailwindcss-animate: ^1.0.5
|
tailwindcss-animate: ^1.0.5
|
||||||
typescript: ^5.0.2
|
typescript: ^5.0.2
|
||||||
vite: ^4.3.2
|
vite: ^4.3.2
|
||||||
zustand: ^4.3.8
|
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@radix-ui/react-label': 2.0.2_h73yytdk5ntplvft5jzh7k37ce
|
||||||
|
'@radix-ui/react-progress': 1.0.3_h73yytdk5ntplvft5jzh7k37ce
|
||||||
|
'@radix-ui/react-scroll-area': 1.0.4_h73yytdk5ntplvft5jzh7k37ce
|
||||||
'@radix-ui/react-slot': 1.0.2_bfoz4c5kom3f237nig75ykjhhy
|
'@radix-ui/react-slot': 1.0.2_bfoz4c5kom3f237nig75ykjhhy
|
||||||
'@tanstack/react-query': 4.29.7_biqbaboplfbrettd7655fr4n2y
|
'@tanstack/react-query': 4.29.7_biqbaboplfbrettd7655fr4n2y
|
||||||
'@tanstack/react-table': 8.9.1_biqbaboplfbrettd7655fr4n2y
|
'@tanstack/react-table': 8.9.1_biqbaboplfbrettd7655fr4n2y
|
||||||
class-variance-authority: 0.6.0_typescript@5.0.4
|
class-variance-authority: 0.6.0_typescript@5.0.4
|
||||||
clsx: 1.2.1
|
clsx: 1.2.1
|
||||||
immer: 10.0.2
|
|
||||||
lucide-react: 0.221.0_react@18.2.0
|
lucide-react: 0.221.0_react@18.2.0
|
||||||
|
pretty-bytes: 6.1.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0_react@18.2.0
|
react-dom: 18.2.0_react@18.2.0
|
||||||
tailwind-merge: 1.12.0
|
tailwind-merge: 1.12.0
|
||||||
tailwindcss-animate: 1.0.5_tailwindcss@3.3.2
|
tailwindcss-animate: 1.0.5_tailwindcss@3.3.2
|
||||||
zustand: 4.3.8_immer@10.0.2+react@18.2.0
|
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node': 20.2.4
|
'@types/node': 20.2.4
|
||||||
@ -600,6 +604,18 @@ packages:
|
|||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.15.0
|
fastq: 1.15.0
|
||||||
|
|
||||||
|
/@radix-ui/number/1.0.1:
|
||||||
|
resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/primitive/1.0.1:
|
||||||
|
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-compose-refs/1.0.1_bfoz4c5kom3f237nig75ykjhhy:
|
/@radix-ui/react-compose-refs/1.0.1_bfoz4c5kom3f237nig75ykjhhy:
|
||||||
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -614,6 +630,149 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-context/1.0.1_bfoz4c5kom3f237nig75ykjhhy:
|
||||||
|
resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.0
|
||||||
|
'@types/react': 18.2.7
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-direction/1.0.1_bfoz4c5kom3f237nig75ykjhhy:
|
||||||
|
resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.0
|
||||||
|
'@types/react': 18.2.7
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-label/2.0.2_h73yytdk5ntplvft5jzh7k37ce:
|
||||||
|
resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.0
|
||||||
|
'@radix-ui/react-primitive': 1.0.3_h73yytdk5ntplvft5jzh7k37ce
|
||||||
|
'@types/react': 18.2.7
|
||||||
|
'@types/react-dom': 18.2.4
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-presence/1.0.1_h73yytdk5ntplvft5jzh7k37ce:
|
||||||
|
resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.0
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
|
||||||
|
'@types/react': 18.2.7
|
||||||
|
'@types/react-dom': 18.2.4
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-primitive/1.0.3_h73yytdk5ntplvft5jzh7k37ce:
|
||||||
|
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.0
|
||||||
|
'@radix-ui/react-slot': 1.0.2_bfoz4c5kom3f237nig75ykjhhy
|
||||||
|
'@types/react': 18.2.7
|
||||||
|
'@types/react-dom': 18.2.4
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-progress/1.0.3_h73yytdk5ntplvft5jzh7k37ce:
|
||||||
|
resolution: {integrity: sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.0
|
||||||
|
'@radix-ui/react-context': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
|
||||||
|
'@radix-ui/react-primitive': 1.0.3_h73yytdk5ntplvft5jzh7k37ce
|
||||||
|
'@types/react': 18.2.7
|
||||||
|
'@types/react-dom': 18.2.4
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-scroll-area/1.0.4_h73yytdk5ntplvft5jzh7k37ce:
|
||||||
|
resolution: {integrity: sha512-OIClwBkwPG+FKvC4OMTRaa/3cfD069nkKFFL/TQzRzaO42Ce5ivKU9VMKgT7UU6UIkjcQqKBrDOIzWtPGw6e6w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.0
|
||||||
|
'@radix-ui/number': 1.0.1
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
|
||||||
|
'@radix-ui/react-context': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
|
||||||
|
'@radix-ui/react-direction': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
|
||||||
|
'@radix-ui/react-presence': 1.0.1_h73yytdk5ntplvft5jzh7k37ce
|
||||||
|
'@radix-ui/react-primitive': 1.0.3_h73yytdk5ntplvft5jzh7k37ce
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.0.1_bfoz4c5kom3f237nig75ykjhhy
|
||||||
|
'@types/react': 18.2.7
|
||||||
|
'@types/react-dom': 18.2.4
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-slot/1.0.2_bfoz4c5kom3f237nig75ykjhhy:
|
/@radix-ui/react-slot/1.0.2_bfoz4c5kom3f237nig75ykjhhy:
|
||||||
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -629,6 +788,34 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-use-callback-ref/1.0.1_bfoz4c5kom3f237nig75ykjhhy:
|
||||||
|
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.0
|
||||||
|
'@types/react': 18.2.7
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-use-layout-effect/1.0.1_bfoz4c5kom3f237nig75ykjhhy:
|
||||||
|
resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.0
|
||||||
|
'@types/react': 18.2.7
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@tanstack/query-core/4.29.7:
|
/@tanstack/query-core/4.29.7:
|
||||||
resolution: {integrity: sha512-GXG4b5hV2Loir+h2G+RXhJdoZhJLnrBWsuLB2r0qBRyhWuXq9w/dWxzvpP89H0UARlH6Mr9DiVj4SMtpkF/aUA==}
|
resolution: {integrity: sha512-GXG4b5hV2Loir+h2G+RXhJdoZhJLnrBWsuLB2r0qBRyhWuXq9w/dWxzvpP89H0UARlH6Mr9DiVj4SMtpkF/aUA==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -683,7 +870,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==}
|
resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 18.2.7
|
'@types/react': 18.2.7
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/react/18.2.7:
|
/@types/react/18.2.7:
|
||||||
resolution: {integrity: sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==}
|
resolution: {integrity: sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==}
|
||||||
@ -1441,10 +1627,6 @@ packages:
|
|||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/immer/10.0.2:
|
|
||||||
resolution: {integrity: sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==}
|
|
||||||
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'}
|
||||||
@ -1793,6 +1975,11 @@ packages:
|
|||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pretty-bytes/6.1.0:
|
||||||
|
resolution: {integrity: sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ==}
|
||||||
|
engines: {node: ^14.13.1 || >=16.0.0}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/punycode/2.3.0:
|
/punycode/2.3.0:
|
||||||
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -2154,20 +2341,3 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/zustand/4.3.8_immer@10.0.2+react@18.2.0:
|
|
||||||
resolution: {integrity: sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==}
|
|
||||||
engines: {node: '>=12.7.0'}
|
|
||||||
peerDependencies:
|
|
||||||
immer: '>=9.0'
|
|
||||||
react: '>=16.8'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
immer:
|
|
||||||
optional: true
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
immer: 10.0.2
|
|
||||||
react: 18.2.0
|
|
||||||
use-sync-external-store: 1.2.0_react@18.2.0
|
|
||||||
dev: false
|
|
||||||
|
11
src/App.tsx
11
src/App.tsx
@ -1,6 +1,15 @@
|
|||||||
import Home from "./pages/home";
|
import { useTransmission } from "@/hooks/use-transmission";
|
||||||
|
import Home from "@/pages/home";
|
||||||
|
import Login from "@/pages/login";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { isLoggedIn } = useTransmission();
|
||||||
|
console.log(import.meta.env.HELLO);
|
||||||
|
|
||||||
|
if (isLoggedIn === false) {
|
||||||
|
return <Login />;
|
||||||
|
}
|
||||||
|
|
||||||
return <Home />;
|
return <Home />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
src/assets/login-bg.jpg
Normal file
BIN
src/assets/login-bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 MiB |
73
src/components/login/index.tsx
Normal file
73
src/components/login/index.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useTransmission } from "@/hooks/use-transmission";
|
||||||
|
import { env } from "@/lib/env";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const defaultUrl = new URL("/transmission/rpc", window.location.origin);
|
||||||
|
|
||||||
|
export function LoginCard() {
|
||||||
|
const client = useTransmission();
|
||||||
|
|
||||||
|
const [url, setUrl] = useState(env.VITE_DEFAULT_CLIENT_URL ?? defaultUrl);
|
||||||
|
const [user, setUser] = useState(env.VITE_DEFAULT_CLIENT_USER ?? "");
|
||||||
|
const [password, setPassword] = useState(env.VITE_DEFAULT_CLIENT_PASS ?? "");
|
||||||
|
|
||||||
|
const submitLogin = async () => await client.login({ url, user, password });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="space-y-1">
|
||||||
|
<CardTitle className="text-2xl">Login into Transmission</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your local or remote credentials.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="instance">Instance</Label>
|
||||||
|
<Input
|
||||||
|
id="instance"
|
||||||
|
type="url"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="username">Username</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
value={user}
|
||||||
|
onChange={(e) => setUser(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button className="w-full" onClick={submitLogin}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
67
src/components/sidebar/index.tsx
Normal file
67
src/components/sidebar/index.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
AlignJustify,
|
||||||
|
CheckCircle,
|
||||||
|
FileDown,
|
||||||
|
PauseCircle,
|
||||||
|
PlayCircle,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardHeader, CardDescription } from "../ui/card";
|
||||||
|
import { useTransmission } from "@/hooks/use-transmission";
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sidebar({ className }: SidebarProps) {
|
||||||
|
const { session } = useTransmission();
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col h-screen", className)}>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="px-4 py-2">
|
||||||
|
<h2 className="mb-2 px-2 text-lg font-semibold tracking-tight">
|
||||||
|
Status
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start"
|
||||||
|
>
|
||||||
|
<AlignJustify className="mr-2 h-4 w-4" />
|
||||||
|
All
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" className="w-full justify-start">
|
||||||
|
<FileDown className="mr-2 h-4 w-4" />
|
||||||
|
Downloading
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" className="w-full justify-start">
|
||||||
|
<CheckCircle className="mr-2 h-4 w-4" />
|
||||||
|
Completed
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" className="w-full justify-start">
|
||||||
|
<PlayCircle className="mr-2 h-4 w-4" />
|
||||||
|
Active
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" className="w-full justify-start">
|
||||||
|
<PauseCircle className="mr-2 h-4 w-4" />
|
||||||
|
Inactive
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 " />
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="px-4 py-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="py-3">
|
||||||
|
<CardDescription>Version: {session.version}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
23
src/components/torrents/cells.tsx
Normal file
23
src/components/torrents/cells.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
|
||||||
|
const messages = new Map<number, string>([
|
||||||
|
[0, "Stopped"],
|
||||||
|
[1, "Verifying"],
|
||||||
|
[2, "Verifying"],
|
||||||
|
[3, "Downloading"],
|
||||||
|
[4, "Downloading"],
|
||||||
|
[5, "Seeding"],
|
||||||
|
[6, "Seeding"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function TorrentStatus({ state }: { state: number }) {
|
||||||
|
return <span>{messages.get(state) ?? "Unknown state"}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TorrentSize({ sizeInBytes }: { sizeInBytes: number }) {
|
||||||
|
return <span>{prettyBytes(sizeInBytes)}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TorrentSpeed({ speedInBytes }: { speedInBytes: number }) {
|
||||||
|
return <span>{prettyBytes(speedInBytes ?? 0)}/s</span>;
|
||||||
|
}
|
@ -1,4 +1,25 @@
|
|||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
TorrentSize,
|
||||||
|
TorrentSpeed,
|
||||||
|
TorrentStatus,
|
||||||
|
} from "@/components/torrents/cells";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
|
||||||
|
export const fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"sizeWhenDone",
|
||||||
|
"status",
|
||||||
|
"rateDownload (B/s)",
|
||||||
|
"rateUpload (B/s)",
|
||||||
|
"eta",
|
||||||
|
"uploadRatio",
|
||||||
|
"percentDone",
|
||||||
|
"magnetLink",
|
||||||
|
"group",
|
||||||
|
"labels",
|
||||||
|
];
|
||||||
|
|
||||||
export interface Torrent {
|
export interface Torrent {
|
||||||
id: number;
|
id: number;
|
||||||
@ -9,6 +30,10 @@ export interface Torrent {
|
|||||||
"rateUpload (B/s)": number;
|
"rateUpload (B/s)": number;
|
||||||
eta: number;
|
eta: number;
|
||||||
uploadRatio: number;
|
uploadRatio: number;
|
||||||
|
percentDone: number;
|
||||||
|
magnetLink: string;
|
||||||
|
group: string;
|
||||||
|
labels: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const columns: ColumnDef<Torrent>[] = [
|
export const columns: ColumnDef<Torrent>[] = [
|
||||||
@ -19,18 +44,29 @@ export const columns: ColumnDef<Torrent>[] = [
|
|||||||
{
|
{
|
||||||
accessorKey: "sizeWhenDone",
|
accessorKey: "sizeWhenDone",
|
||||||
header: "Size",
|
header: "Size",
|
||||||
|
cell: (props) => <TorrentSize sizeInBytes={props.getValue<number>()} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "percentDone",
|
||||||
|
header: "Progress",
|
||||||
|
cell: (props) => <Progress value={props.getValue<number>() * 100} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "status",
|
accessorKey: "status",
|
||||||
header: "Status",
|
header: "Status",
|
||||||
|
cell: (props) => (
|
||||||
|
<TorrentStatus key={props.cell.id} state={props.getValue<number>()} />
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "rateDownload (B/s)",
|
accessorKey: "rateDownload (B/s)",
|
||||||
header: "Download",
|
header: "Download",
|
||||||
|
cell: (props) => <TorrentSpeed speedInBytes={props.getValue<number>()} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "rateUpload (B/s)",
|
accessorKey: "rateUpload (B/s)",
|
||||||
header: "Upload",
|
header: "Upload",
|
||||||
|
cell: (props) => <TorrentSpeed speedInBytes={props.getValue<number>()} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "eta",
|
accessorKey: "eta",
|
||||||
|
79
src/components/ui/card.tsx
Normal file
79
src/components/ui/card.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(" flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Label }
|
28
src/components/ui/progress.tsx
Normal file
28
src/components/ui/progress.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Progress = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||||
|
>(({ className, value, ...props }, ref) => (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
className="h-full w-full flex-1 bg-primary transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
))
|
||||||
|
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Progress }
|
48
src/components/ui/scroll-area.tsx
Normal file
48
src/components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative overflow-hidden", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
))
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
ref={ref}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none select-none transition-colors",
|
||||||
|
orientation === "vertical" &&
|
||||||
|
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
"h-2.5 border-t border-t-transparent p-[1px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
))
|
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
24
src/contexts/transmission.ts
Normal file
24
src/contexts/transmission.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
Credentials,
|
||||||
|
RpcCall,
|
||||||
|
RpcResponse,
|
||||||
|
SessionGetCmd,
|
||||||
|
} from "@/models/transmission";
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
interface TransmissionState {
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
session: SessionGetCmd["response"];
|
||||||
|
call: <T extends RpcCall>(
|
||||||
|
method: string,
|
||||||
|
args: T["arguments"]
|
||||||
|
) => Promise<RpcResponse<T["response"]>>;
|
||||||
|
login: (credentials: Credentials) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransmissionContext = createContext<TransmissionState>({
|
||||||
|
isLoggedIn: false,
|
||||||
|
session: { version: "" },
|
||||||
|
call: () => Promise.resolve({ arguments: {}, result: "" }),
|
||||||
|
login: () => Promise.resolve(),
|
||||||
|
});
|
@ -10,5 +10,5 @@ export function useInterval(callback: () => void, delay: number) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = setInterval(() => ref.current(), delay);
|
const id = setInterval(() => ref.current(), delay);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, []);
|
}, [delay]);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { TransmissionClient } from "@/lib/transmission";
|
import { TransmissionContext } from "@/contexts/transmission";
|
||||||
import { useRef } from "react";
|
import { useContext } from "react";
|
||||||
|
|
||||||
export function useTransmission() {
|
export function useTransmission() {
|
||||||
const ref = useRef(new TransmissionClient());
|
const context = useContext(TransmissionContext);
|
||||||
return ref.current;
|
return context;
|
||||||
}
|
}
|
||||||
|
1
src/lib/env.ts
Normal file
1
src/lib/env.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const env = import.meta.env;
|
@ -1,51 +0,0 @@
|
|||||||
interface RpcMessage<T> {
|
|
||||||
arguments: T;
|
|
||||||
method: string;
|
|
||||||
tag: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RpcResponse<T> {
|
|
||||||
arguments: T;
|
|
||||||
result: string;
|
|
||||||
tag: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RpcCall {
|
|
||||||
arguments: object;
|
|
||||||
response: object;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TransmissionClient {
|
|
||||||
private readonly url = "http://localhost:9091/transmission/rpc";
|
|
||||||
private counter = 0;
|
|
||||||
private credentials: [string, string] = ["user", "password"];
|
|
||||||
private csrfToken = "";
|
|
||||||
|
|
||||||
public async call<T extends RpcCall>(
|
|
||||||
method: string,
|
|
||||||
args: T["arguments"]
|
|
||||||
): Promise<RpcResponse<T["response"]>> {
|
|
||||||
const resp = await fetch(this.url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${btoa(this.credentials.join(":"))}`,
|
|
||||||
"X-Transmission-Session-Id": this.csrfToken,
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
arguments: args,
|
|
||||||
method: method,
|
|
||||||
tag: this.counter++,
|
|
||||||
} as RpcMessage<T["arguments"]>),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resp.status === 409) {
|
|
||||||
this.csrfToken = resp.headers.get("X-Transmission-Session-Id") ?? "";
|
|
||||||
return this.call(method, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: RpcResponse<T["response"]> = await resp.json();
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,9 +2,12 @@ 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.tsx";
|
||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
|
import { TransmissionProvider } from "@/providers/transmission";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<TransmissionProvider>
|
||||||
<App />
|
<App />
|
||||||
|
</TransmissionProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
29
src/models/transmission.ts
Normal file
29
src/models/transmission.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
export interface RpcMessage<T> {
|
||||||
|
arguments: T;
|
||||||
|
method: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RpcResponse<T> {
|
||||||
|
arguments: T;
|
||||||
|
result: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RpcCall {
|
||||||
|
arguments: object;
|
||||||
|
response: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Credentials {
|
||||||
|
url: string;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionGetCmd {
|
||||||
|
arguments: {
|
||||||
|
fields: string[];
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
import { Torrent, columns } from "@/components/torrents/columns";
|
import { Torrent, columns, fields } from "@/components/torrents/columns";
|
||||||
import { DataTable } from "@/components/torrents/data-table";
|
import { DataTable } from "@/components/torrents/data-table";
|
||||||
import { useTransmission } from "@/hooks/use-transmission";
|
import { useTransmission } from "@/hooks/use-transmission";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useInterval } from "@/hooks/use-interval";
|
import { useInterval } from "@/hooks/use-interval";
|
||||||
|
import { Sidebar } from "@/components/sidebar";
|
||||||
|
import { env } from "@/lib/env";
|
||||||
|
|
||||||
interface TorrentGetCmd {
|
interface TorrentGetCmd {
|
||||||
arguments: { fields: string[] };
|
arguments: { fields: string[] };
|
||||||
@ -18,23 +20,12 @@ interface TorrentAddCmd {
|
|||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const client = useTransmission();
|
const client = useTransmission();
|
||||||
const [urlInput, setUrlInput] = useState(
|
const [urlInput, setUrlInput] = useState(env.VITE_DEFAULT_TORRENT_ADD ?? "");
|
||||||
"https://cdimage.debian.org/debian-cd/current/amd64/bt-cd/debian-11.7.0-amd64-netinst.iso.torrent"
|
|
||||||
);
|
|
||||||
const [tableData, setTableData] = useState<Torrent[]>([]);
|
const [tableData, setTableData] = useState<Torrent[]>([]);
|
||||||
|
|
||||||
useInterval(async () => {
|
useInterval(async () => {
|
||||||
const resp = await client.call<TorrentGetCmd>("torrent-get", {
|
const resp = await client.call<TorrentGetCmd>("torrent-get", {
|
||||||
fields: [
|
fields: fields,
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"sizeWhenDone",
|
|
||||||
"status",
|
|
||||||
"rateDownload (B/s)",
|
|
||||||
"rateUpload (B/s)",
|
|
||||||
"eta",
|
|
||||||
"uploadRatio",
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
setTableData(resp.arguments.torrents);
|
setTableData(resp.arguments.torrents);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
@ -46,8 +37,9 @@ function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex">
|
||||||
<DataTable columns={columns} data={tableData} />
|
<Sidebar className="w-64" />
|
||||||
|
<div className="flex-1 container">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Input
|
<Input
|
||||||
type="url"
|
type="url"
|
||||||
@ -59,6 +51,8 @@ function Home() {
|
|||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<DataTable columns={columns} data={tableData} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
17
src/pages/login.tsx
Normal file
17
src/pages/login.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { LoginCard } from "@/components/login";
|
||||||
|
import image from "@/assets/login-bg.jpg";
|
||||||
|
|
||||||
|
function Login() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-screen h-screen flex justify-center items-center"
|
||||||
|
style={{ backgroundImage: `url(${image})` }}
|
||||||
|
>
|
||||||
|
<div className="w-1/5">
|
||||||
|
<LoginCard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
93
src/providers/transmission.tsx
Normal file
93
src/providers/transmission.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { PropsWithChildren, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Credentials,
|
||||||
|
RpcCall,
|
||||||
|
RpcMessage,
|
||||||
|
RpcResponse,
|
||||||
|
SessionGetCmd,
|
||||||
|
} from "@/models/transmission";
|
||||||
|
import { TransmissionContext } from "@/contexts/transmission";
|
||||||
|
|
||||||
|
export function TransmissionProvider(props: PropsWithChildren) {
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
const [session, setSession] = useState<SessionGetCmd["response"]>({
|
||||||
|
version: "",
|
||||||
|
});
|
||||||
|
const [credentials, setCredentials] = useState<Credentials>({
|
||||||
|
url: "",
|
||||||
|
user: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
const [csrfToken, setCsrfToken] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const inner = async () => {
|
||||||
|
if (isLoggedIn === true) return;
|
||||||
|
const resp = await _call<SessionGetCmd["arguments"]>("session-get", {
|
||||||
|
fields: ["version"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.status === 401) {
|
||||||
|
setSession({ version: "" });
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: RpcResponse<SessionGetCmd["response"]> = await resp.json();
|
||||||
|
setSession(data.arguments);
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
inner();
|
||||||
|
}, [credentials, isLoggedIn]);
|
||||||
|
|
||||||
|
const _call = async <T extends object>(
|
||||||
|
method: string,
|
||||||
|
args: T,
|
||||||
|
_csrfToken?: string
|
||||||
|
): Promise<Response> => {
|
||||||
|
_csrfToken ??= csrfToken;
|
||||||
|
const authToken = btoa(`${credentials.user}:${credentials.password}`);
|
||||||
|
const resp = await fetch(credentials.url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${authToken}`,
|
||||||
|
"X-Transmission-Session-Id": _csrfToken,
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
arguments: args,
|
||||||
|
method: method,
|
||||||
|
} as RpcMessage<T>),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.status === 409) {
|
||||||
|
_csrfToken = resp.headers.get("X-Transmission-Session-Id") ?? "";
|
||||||
|
setCsrfToken(_csrfToken);
|
||||||
|
return await _call(method, args, _csrfToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
};
|
||||||
|
|
||||||
|
const call = async <T extends RpcCall>(
|
||||||
|
method: string,
|
||||||
|
args: T["arguments"]
|
||||||
|
): Promise<RpcResponse<T["response"]>> => {
|
||||||
|
const resp = await _call<T["arguments"]>(method, args);
|
||||||
|
const data: RpcResponse<T["response"]> = await resp.json();
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const login = async (credentials: Credentials) => {
|
||||||
|
setCredentials(credentials);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TransmissionContext.Provider value={{ login, call, isLoggedIn, session }}>
|
||||||
|
{props.children}
|
||||||
|
</TransmissionContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
@ -34,6 +34,8 @@
|
|||||||
--ring: 215 20.2% 65.1%;
|
--ring: 215 20.2% 65.1%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
--font-sans: Roboto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user