More stuff
This commit is contained in:
parent
13670d0770
commit
d7ae269d46
.envpackage.jsonpnpm-lock.yaml
src
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"
|
||||
},
|
||||
"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",
|
||||
"@tanstack/react-query": "^4.29.7",
|
||||
"@tanstack/react-table": "^8.9.1",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"immer": "^10.0.2",
|
||||
"lucide-react": "^0.221.0",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwind-merge": "^1.12.0",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"zustand": "^4.3.8"
|
||||
"tailwindcss-animate": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.2.4",
|
||||
|
222
pnpm-lock.yaml
generated
222
pnpm-lock.yaml
generated
@ -1,6 +1,9 @@
|
||||
lockfileVersion: 5.4
|
||||
|
||||
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
|
||||
'@tanstack/react-query': ^4.29.7
|
||||
'@tanstack/react-table': ^8.9.1
|
||||
@ -16,9 +19,9 @@ specifiers:
|
||||
eslint: ^8.38.0
|
||||
eslint-plugin-react-hooks: ^4.6.0
|
||||
eslint-plugin-react-refresh: ^0.3.4
|
||||
immer: ^10.0.2
|
||||
lucide-react: ^0.221.0
|
||||
postcss: ^8.4.23
|
||||
pretty-bytes: ^6.1.0
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
tailwind-merge: ^1.12.0
|
||||
@ -26,21 +29,22 @@ specifiers:
|
||||
tailwindcss-animate: ^1.0.5
|
||||
typescript: ^5.0.2
|
||||
vite: ^4.3.2
|
||||
zustand: ^4.3.8
|
||||
|
||||
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
|
||||
'@tanstack/react-query': 4.29.7_biqbaboplfbrettd7655fr4n2y
|
||||
'@tanstack/react-table': 8.9.1_biqbaboplfbrettd7655fr4n2y
|
||||
class-variance-authority: 0.6.0_typescript@5.0.4
|
||||
clsx: 1.2.1
|
||||
immer: 10.0.2
|
||||
lucide-react: 0.221.0_react@18.2.0
|
||||
pretty-bytes: 6.1.0
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
tailwind-merge: 1.12.0
|
||||
tailwindcss-animate: 1.0.5_tailwindcss@3.3.2
|
||||
zustand: 4.3.8_immer@10.0.2+react@18.2.0
|
||||
|
||||
devDependencies:
|
||||
'@types/node': 20.2.4
|
||||
@ -600,6 +604,18 @@ packages:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
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:
|
||||
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
||||
peerDependencies:
|
||||
@ -614,6 +630,149 @@ packages:
|
||||
react: 18.2.0
|
||||
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:
|
||||
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
||||
peerDependencies:
|
||||
@ -629,6 +788,34 @@ packages:
|
||||
react: 18.2.0
|
||||
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:
|
||||
resolution: {integrity: sha512-GXG4b5hV2Loir+h2G+RXhJdoZhJLnrBWsuLB2r0qBRyhWuXq9w/dWxzvpP89H0UARlH6Mr9DiVj4SMtpkF/aUA==}
|
||||
dev: false
|
||||
@ -683,7 +870,6 @@ packages:
|
||||
resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==}
|
||||
dependencies:
|
||||
'@types/react': 18.2.7
|
||||
dev: true
|
||||
|
||||
/@types/react/18.2.7:
|
||||
resolution: {integrity: sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==}
|
||||
@ -1441,10 +1627,6 @@ packages:
|
||||
engines: {node: '>= 4'}
|
||||
dev: true
|
||||
|
||||
/immer/10.0.2:
|
||||
resolution: {integrity: sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==}
|
||||
dev: false
|
||||
|
||||
/import-fresh/3.3.0:
|
||||
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
||||
engines: {node: '>=6'}
|
||||
@ -1793,6 +1975,11 @@ packages:
|
||||
engines: {node: '>= 0.8.0'}
|
||||
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:
|
||||
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
||||
engines: {node: '>=6'}
|
||||
@ -2154,20 +2341,3 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
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() {
|
||||
const { isLoggedIn } = useTransmission();
|
||||
console.log(import.meta.env.HELLO);
|
||||
|
||||
if (isLoggedIn === false) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
return <Home />;
|
||||
}
|
||||
|
||||
|
BIN
src/assets/login-bg.jpg
Normal file
BIN
src/assets/login-bg.jpg
Normal file
Binary file not shown.
After ![]() (image error) 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 {
|
||||
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 {
|
||||
id: number;
|
||||
@ -9,6 +30,10 @@ export interface Torrent {
|
||||
"rateUpload (B/s)": number;
|
||||
eta: number;
|
||||
uploadRatio: number;
|
||||
percentDone: number;
|
||||
magnetLink: string;
|
||||
group: string;
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<Torrent>[] = [
|
||||
@ -19,18 +44,29 @@ export const columns: ColumnDef<Torrent>[] = [
|
||||
{
|
||||
accessorKey: "sizeWhenDone",
|
||||
header: "Size",
|
||||
cell: (props) => <TorrentSize sizeInBytes={props.getValue<number>()} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "percentDone",
|
||||
header: "Progress",
|
||||
cell: (props) => <Progress value={props.getValue<number>() * 100} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: (props) => (
|
||||
<TorrentStatus key={props.cell.id} state={props.getValue<number>()} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "rateDownload (B/s)",
|
||||
header: "Download",
|
||||
cell: (props) => <TorrentSpeed speedInBytes={props.getValue<number>()} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "rateUpload (B/s)",
|
||||
header: "Upload",
|
||||
cell: (props) => <TorrentSpeed speedInBytes={props.getValue<number>()} />,
|
||||
},
|
||||
{
|
||||
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(() => {
|
||||
const id = setInterval(() => ref.current(), delay);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
}, [delay]);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { TransmissionClient } from "@/lib/transmission";
|
||||
import { useRef } from "react";
|
||||
import { TransmissionContext } from "@/contexts/transmission";
|
||||
import { useContext } from "react";
|
||||
|
||||
export function useTransmission() {
|
||||
const ref = useRef(new TransmissionClient());
|
||||
return ref.current;
|
||||
const context = useContext(TransmissionContext);
|
||||
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 App from "./App.tsx";
|
||||
import "@/styles/globals.css";
|
||||
import { TransmissionProvider } from "@/providers/transmission";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<TransmissionProvider>
|
||||
<App />
|
||||
</TransmissionProvider>
|
||||
</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 { useTransmission } from "@/hooks/use-transmission";
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useInterval } from "@/hooks/use-interval";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
interface TorrentGetCmd {
|
||||
arguments: { fields: string[] };
|
||||
@ -18,23 +20,12 @@ interface TorrentAddCmd {
|
||||
|
||||
function Home() {
|
||||
const client = useTransmission();
|
||||
const [urlInput, setUrlInput] = useState(
|
||||
"https://cdimage.debian.org/debian-cd/current/amd64/bt-cd/debian-11.7.0-amd64-netinst.iso.torrent"
|
||||
);
|
||||
const [urlInput, setUrlInput] = useState(env.VITE_DEFAULT_TORRENT_ADD ?? "");
|
||||
const [tableData, setTableData] = useState<Torrent[]>([]);
|
||||
|
||||
useInterval(async () => {
|
||||
const resp = await client.call<TorrentGetCmd>("torrent-get", {
|
||||
fields: [
|
||||
"id",
|
||||
"name",
|
||||
"sizeWhenDone",
|
||||
"status",
|
||||
"rateDownload (B/s)",
|
||||
"rateUpload (B/s)",
|
||||
"eta",
|
||||
"uploadRatio",
|
||||
],
|
||||
fields: fields,
|
||||
});
|
||||
setTableData(resp.arguments.torrents);
|
||||
}, 10000);
|
||||
@ -46,18 +37,21 @@ function Home() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataTable columns={columns} data={tableData} />
|
||||
<div className="flex">
|
||||
<Input
|
||||
type="url"
|
||||
placeholder=".torrent URL"
|
||||
value={urlInput}
|
||||
onChange={(e) => setUrlInput(e.target.value)}
|
||||
/>
|
||||
<Button variant="outline" onClick={downloadTorrent}>
|
||||
Download
|
||||
</Button>
|
||||
<div className="flex">
|
||||
<Sidebar className="w-64" />
|
||||
<div className="flex-1 container">
|
||||
<div className="flex">
|
||||
<Input
|
||||
type="url"
|
||||
placeholder=".torrent URL"
|
||||
value={urlInput}
|
||||
onChange={(e) => setUrlInput(e.target.value)}
|
||||
/>
|
||||
<Button variant="outline" onClick={downloadTorrent}>
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable columns={columns} data={tableData} />
|
||||
</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%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
--font-sans: Roboto;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
Loading…
x
Reference in New Issue
Block a user