├── .eslintrc.json ├── deluge ├── index.ts ├── got.ts ├── types.ts └── deluge.ts ├── styles └── globals.css ├── _docs ├── add.jpg ├── home.jpg ├── menu.jpg ├── sort.jpg ├── detail.jpg ├── files.jpg ├── filter.jpg ├── options.jpg ├── globaldl.jpg ├── globalup.jpg ├── add_torrent.jpg └── pagination.jpg ├── public ├── logo.png ├── favicon.ico ├── icon-192x192.png ├── icon-256x256.png ├── icon-384x384.png ├── icon-512x512.png ├── manifest.json └── vercel.svg ├── components ├── Loading.tsx ├── DetailGridCol.tsx ├── ListSkeleton.tsx ├── RouterTransition.tsx ├── AddTorrentModal.tsx ├── BadgeToolTip.tsx ├── MoveStorage.tsx ├── TorrentPageNav.tsx ├── Directory.tsx ├── ContentDropDown.tsx ├── Statusbar.tsx ├── Sort.tsx ├── Label.tsx ├── TorrentDetail.tsx ├── DetailLoader.tsx ├── File.tsx ├── NavBar.tsx ├── TorrentButtons.tsx ├── TorrentFiles.tsx ├── GlobalDown.tsx ├── GlobalUp.tsx ├── Options.tsx ├── GlobalConnection.tsx ├── TorrentOptionForm.tsx ├── Filter.tsx ├── Torrent.tsx ├── TorrentMenu.tsx ├── TorrentOption.tsx ├── AddTorrentButton.tsx └── TorrentList.tsx ├── pages ├── api │ ├── auth │ │ └── [...nextauth].ts │ ├── hello.ts │ └── trpc │ │ └── [trpc].ts ├── _document.tsx ├── _app.tsx ├── home │ ├── index.tsx │ └── [id].tsx └── index.tsx ├── server ├── deluge.ts ├── routers │ ├── _app.ts │ ├── auth.ts │ └── deluge.ts ├── trpc.ts └── context.ts ├── types └── next-auth.d.ts ├── next.config.js ├── tsconfig.json ├── utils ├── get-server-auth-session.ts ├── requiredAuth.ts ├── nextAuthOption.ts ├── trpc.ts └── helper.ts ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── docker-image.yml ├── .gitignore ├── LICENCE ├── stores ├── useTorrentStore.ts └── useTableStore.ts ├── package.json ├── Dockerfile ├── README.md └── methods.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /deluge/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./deluge"; 2 | export * from "./types"; 3 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | height: 100% 5 | } -------------------------------------------------------------------------------- /_docs/add.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/add.jpg -------------------------------------------------------------------------------- /_docs/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/home.jpg -------------------------------------------------------------------------------- /_docs/menu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/menu.jpg -------------------------------------------------------------------------------- /_docs/sort.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/sort.jpg -------------------------------------------------------------------------------- /_docs/detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/detail.jpg -------------------------------------------------------------------------------- /_docs/files.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/files.jpg -------------------------------------------------------------------------------- /_docs/filter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/filter.jpg -------------------------------------------------------------------------------- /_docs/options.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/options.jpg -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/public/logo.png -------------------------------------------------------------------------------- /_docs/globaldl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/globaldl.jpg -------------------------------------------------------------------------------- /_docs/globalup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/globalup.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /_docs/add_torrent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/add_torrent.jpg -------------------------------------------------------------------------------- /_docs/pagination.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/_docs/pagination.jpg -------------------------------------------------------------------------------- /public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/public/icon-192x192.png -------------------------------------------------------------------------------- /public/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/public/icon-256x256.png -------------------------------------------------------------------------------- /public/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/public/icon-384x384.png -------------------------------------------------------------------------------- /public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maulik9898/barrage/HEAD/public/icon-512x512.png -------------------------------------------------------------------------------- /components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Loader } from '@mantine/core' 2 | 3 | const Loading = () => { 4 | return ( 5 | 6 | ) 7 | } 8 | 9 | export default Loading -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | 2 | import NextAuth from "next-auth"; 3 | import { nextAuthOptions } from "../../../utils/nextAuthOption"; 4 | 5 | 6 | export default NextAuth(nextAuthOptions) 7 | -------------------------------------------------------------------------------- /server/deluge.ts: -------------------------------------------------------------------------------- 1 | import { Deluge } from "../deluge/index"; 2 | 3 | const delugeClient = new Deluge({ 4 | baseUrl: process.env.DELUGE_URL, 5 | password: process.env.DELUGE_PASSWORD, 6 | }); 7 | 8 | export default delugeClient; 9 | -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { type DefaultSession } from "next-auth"; 2 | 3 | declare module "next-auth" { 4 | /** 5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context 6 | */ 7 | interface Session { 8 | user?: { 9 | id: string; 10 | } & DefaultSession["user"]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const nextConfig = { 2 | reactStrictMode: true, 3 | swcMinify: true, 4 | output: "standalone", 5 | compiler: { 6 | removeConsole: process.env.NODE_ENV !== "development", 7 | }, 8 | }; 9 | 10 | const withPWA = require("next-pwa")({ 11 | dest: "public", 12 | disable: process.env.NODE_ENV === "development", 13 | register: true, 14 | }); 15 | 16 | module.exports = withPWA(nextConfig); -------------------------------------------------------------------------------- /pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import * as trpcNext from "@trpc/server/adapters/next"; 2 | import { env } from "process"; 3 | import { createContext } from "../../../server/context"; 4 | import { appRouter } from "../../../server/routers/_app"; 5 | 6 | // export API handler 7 | export default trpcNext.createNextApiHandler({ 8 | router: appRouter, 9 | createContext, 10 | onError: 11 | env.NODE_ENV === "development" 12 | ? ({ path, error }) => { 13 | console.error(`❌ tRPC failed on ${path}: ${error}`); 14 | } 15 | : undefined, 16 | }); 17 | -------------------------------------------------------------------------------- /deluge/got.ts: -------------------------------------------------------------------------------- 1 | import got from "got"; 2 | 3 | const logger = got.extend({ 4 | handlers: [ 5 | (options, next) => { 6 | console.log(`Sending ${options.method} to ${options.url} options: ${(options.json as any)?.method}`); 7 | return next(options); 8 | } 9 | ], 10 | hooks: { 11 | afterResponse: [ 12 | (response, retryWithMergedOptions) => { 13 | console.log(`Total time ${response.timings.phases.total}`) 14 | return response 15 | } 16 | ] 17 | }, 18 | dnsCache: true 19 | }); 20 | export default logger 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /utils/get-server-auth-session.ts: -------------------------------------------------------------------------------- 1 | import { type GetServerSidePropsContext } from "next"; 2 | import { unstable_getServerSession } from "next-auth"; 3 | import { nextAuthOptions } from "./nextAuthOption"; 4 | 5 | 6 | /** 7 | * Wrapper for unstable_getServerSession https://next-auth.js.org/configuration/nextjs 8 | * See example usage in trpc createContext or the restricted API route 9 | */ 10 | export const getServerAuthSession = async (ctx: { 11 | req: GetServerSidePropsContext["req"]; 12 | res: GetServerSidePropsContext["res"]; 13 | }) => { 14 | return await unstable_getServerSession(ctx.req, ctx.res, nextAuthOptions); 15 | }; 16 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { createGetInitialProps } from "@mantine/next"; 3 | import Document, { Head, Html, Main, NextScript } from "next/document"; 4 | 5 | const getInitialProps = createGetInitialProps(); 6 | 7 | export default class _Document extends Document { 8 | static getInitialProps = getInitialProps; 9 | 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/routers/_app.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import client from "../deluge"; 3 | import { publicProcedure, router } from "../trpc"; 4 | import { auth } from "./auth"; 5 | import { deluge } from "./deluge"; 6 | 7 | export const appRouter = router({ 8 | hello: publicProcedure 9 | .input( 10 | z.object({ 11 | text: z.string().nullish(), 12 | }) 13 | ) 14 | .query(async ({ input }) => { 15 | const a = await client.getAllData(); 16 | return { 17 | a, 18 | }; 19 | }), 20 | auth: auth, 21 | deluge: deluge 22 | }); 23 | 24 | // export type definition of API 25 | export type AppRouter = typeof appRouter; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /server/routers/auth.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError } from "@trpc/server"; 2 | import { z } from "zod"; 3 | import { publicProcedure, router } from "../trpc"; 4 | 5 | export const auth = router({ 6 | login: publicProcedure 7 | .input( 8 | z.object({ 9 | password: z.string(), 10 | }) 11 | ) 12 | .mutation(({ input }) => { 13 | if (input.password == process.env.BARRAGE_PASSWORD) { 14 | return { 15 | status: "ok", 16 | token: Buffer.from(input.password).toString("base64"), 17 | }; 18 | } else { 19 | throw new TRPCError({ 20 | code: "UNAUTHORIZED", 21 | message: "Invalid Password" 22 | }) 23 | } 24 | }), 25 | }); 26 | -------------------------------------------------------------------------------- /utils/requiredAuth.ts: -------------------------------------------------------------------------------- 1 | // @/src/common/requireAuth.ts 2 | import type { GetServerSideProps, GetServerSidePropsContext } from "next"; 3 | import { unstable_getServerSession } from "next-auth"; 4 | import { nextAuthOptions } from "./nextAuthOption"; 5 | 6 | export const requireAuth = 7 | (func: GetServerSideProps) => async (ctx: GetServerSidePropsContext) => { 8 | const session = await unstable_getServerSession( 9 | ctx.req, 10 | ctx.res, 11 | nextAuthOptions 12 | ); 13 | 14 | if (!session) { 15 | return { 16 | redirect: { 17 | destination: "/", // login path 18 | permanent: false, 19 | }, 20 | }; 21 | } 22 | 23 | return await func(ctx); 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | docker-compose.yaml 39 | 40 | #pwa 41 | **/public/sw.js 42 | **/public/workbox-*.js 43 | **/public/worker-*.js 44 | **/public/sw.js.map 45 | **/public/workbox-*.js.map 46 | **/public/worker-*.js.map -------------------------------------------------------------------------------- /components/DetailGridCol.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Text } from "@mantine/core"; 2 | import React from "react"; 3 | 4 | const DetailGridCol = ({ label, value }: { label: string; value: string }) => { 5 | return ( 6 | <> 7 | 8 | 9 | {label} 10 | 11 | 12 | 13 | 21 | {value} 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default DetailGridCol; 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | 27 | **Device (please complete the following information):** 28 | - Browser [e.g. stock browser, safari] 29 | - Deluge version Version [e.g. 1.0.3] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /server/trpc.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError, initTRPC } from "@trpc/server"; 2 | import { Context } from "./context"; 3 | 4 | // Avoid exporting the entire t-object since it's not very 5 | // descriptive and can be confusing to newcomers used to t 6 | // meaning translation in i18n libraries. 7 | const t = initTRPC.context().create(); 8 | 9 | // Base router and procedure helpers 10 | export const router = t.router; 11 | export const publicProcedure = t.procedure; 12 | 13 | const isAuthed = t.middleware(({ ctx, next }) => { 14 | if (!ctx.session || !ctx.session.user?.id) { 15 | throw new TRPCError({ code: "UNAUTHORIZED" }); 16 | } 17 | return next({ 18 | ctx: { 19 | // infers the `session` as non-nullable 20 | session: { ...ctx.session, user: ctx.session.user }, 21 | }, 22 | }); 23 | }); 24 | export const protectedProcedure = t.procedure.use(isAuthed); 25 | -------------------------------------------------------------------------------- /components/ListSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | Flex, 4 | Center, 5 | Title, 6 | MediaQuery, 7 | Box, 8 | Progress, 9 | useMantineTheme, 10 | Skeleton, 11 | Loader, 12 | Grid, 13 | } from "@mantine/core"; 14 | import { useMediaQuery } from "@mantine/hooks"; 15 | import { IconTriangleInverted, IconTriangle } from "@tabler/icons"; 16 | import Link from "next/link"; 17 | import React from "react"; 18 | import { humanFileSize, forHumansSeconds } from "../utils/helper"; 19 | import BadgeToolTip from "./BadgeToolTip"; 20 | import TorrentButtons from "./TorrentButtons"; 21 | import MemoizedTorrentMenu from "./TorrentMenu"; 22 | 23 | const ListSkeleton = () => { 24 | return ( 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default ListSkeleton; 32 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme_color": "#25262b", 3 | "background_color": "#278beb", 4 | "display": "standalone", 5 | "scope": "/", 6 | "start_url": "/", 7 | "name": "Barrage", 8 | "short_name": "Barrage", 9 | "description": "Minimal deluge WebUI", 10 | "icons": [ 11 | { 12 | "src": "/icon-192x192.png", 13 | "sizes": "192x192", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "/icon-256x256.png", 18 | "sizes": "256x256", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "/icon-384x384.png", 23 | "sizes": "384x384", 24 | "type": "image/png" 25 | }, 26 | { 27 | "src": "/icon-512x512.png", 28 | "sizes": "512x512", 29 | "type": "image/png" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /components/RouterTransition.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | // components/RouterTransition.tsx 3 | import { 4 | completeNavigationProgress, 5 | NavigationProgress, startNavigationProgress 6 | } from '@mantine/nprogress'; 7 | import { useRouter } from 'next/router'; 8 | import { useEffect } from 'react'; 9 | 10 | export function RouterTransition() { 11 | const router = useRouter(); 12 | 13 | useEffect(() => { 14 | const handleStart = (url: string) => url !== router.asPath && startNavigationProgress(); 15 | const handleComplete = () => completeNavigationProgress(); 16 | 17 | router.events.on('routeChangeStart', handleStart); 18 | router.events.on('routeChangeComplete', handleComplete); 19 | router.events.on('routeChangeError', handleComplete); 20 | 21 | return () => { 22 | router.events.off('routeChangeStart', handleStart); 23 | router.events.off('routeChangeComplete', handleComplete); 24 | router.events.off('routeChangeError', handleComplete); 25 | }; 26 | }, [router.asPath]); 27 | 28 | return ; 29 | } -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 maulik9898 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /components/AddTorrentModal.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import { 3 | Accordion, 4 | Box, 5 | Button, 6 | Checkbox, 7 | Grid, 8 | Group, 9 | Loader, 10 | NumberInput, 11 | Radio, 12 | Space, 13 | Text, 14 | TextInput, 15 | } from "@mantine/core"; 16 | import { UseFormReturnType } from "@mantine/form"; 17 | import { IconSettings } from "@tabler/icons"; 18 | import { ConfigValues } from "../deluge"; 19 | import TorrentOptionForm from "./TorrentOptionForm"; 20 | 21 | const AddTorrentModal = ({ 22 | 23 | form, 24 | }: { 25 | form: UseFormReturnType ConfigValues>; 26 | }) => { 27 | return ( 28 | 29 | 30 | }> 31 | Advance options 32 | 33 | 34 |
35 | 36 | 37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default AddTorrentModal; 44 | -------------------------------------------------------------------------------- /components/BadgeToolTip.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActionIcon, Badge, Tooltip, useMantineTheme 3 | } from "@mantine/core"; 4 | import { useMediaQuery } from "@mantine/hooks"; 5 | import { IconTag } from "@tabler/icons"; 6 | 7 | const BadgeToolTip = ({ 8 | tag, 9 | toolTipLabel, 10 | }: { 11 | tag?: string; 12 | toolTipLabel: string; 13 | }) => { 14 | const theme = useMantineTheme(); 15 | const largeScreen = useMediaQuery("(min-width: 800px)"); 16 | return ( 17 | 18 | 32 | 33 | 34 | } 35 | > 36 | {tag || "No Label"} 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default BadgeToolTip; 43 | -------------------------------------------------------------------------------- /components/MoveStorage.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Flex, TextInput } from "@mantine/core"; 2 | import { useForm } from "@mantine/form"; 3 | import React from "react"; 4 | import { trpc } from "../utils/trpc"; 5 | 6 | const MoveStorage = ({ id, close }: { id: string; close: () => void }) => { 7 | const form = useForm({ 8 | initialValues: { 9 | location: "", 10 | }, 11 | validate: { 12 | location: (value) => (value ? null : "Location is required"), 13 | }, 14 | }); 15 | 16 | const moveStorage = trpc.deluge.moveStorage.useMutation({}); 17 | return ( 18 |
19 | 20 | 25 | 38 | 39 |
40 | ); 41 | }; 42 | 43 | export default MoveStorage; 44 | -------------------------------------------------------------------------------- /server/context.ts: -------------------------------------------------------------------------------- 1 | import { type inferAsyncReturnType } from "@trpc/server"; 2 | import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; 3 | import { type Session } from "next-auth"; 4 | 5 | import { getServerAuthSession } from "../utils/get-server-auth-session"; 6 | 7 | type CreateContextOptions = { 8 | session: Session | null; 9 | }; 10 | 11 | /** Use this helper for: 12 | * - testing, so we dont have to mock Next.js' req/res 13 | * - trpc's `createSSGHelpers` where we don't have req/res 14 | * @see https://beta.create.t3.gg/en/usage/trpc#-servertrpccontextts 15 | **/ 16 | export const createContextInner = async (opts: CreateContextOptions) => { 17 | return { 18 | session: opts.session, 19 | }; 20 | }; 21 | 22 | /** 23 | * This is the actual context you'll use in your router 24 | * @link https://trpc.io/docs/context 25 | **/ 26 | export const createContext = async (opts?: CreateNextContextOptions) => { 27 | const { req, res } = opts!; 28 | 29 | // Get the session from the server using the unstable_getServerSession wrapper function 30 | const session = await getServerAuthSession({ req, res }); 31 | 32 | return await createContextInner({ 33 | session, 34 | }); 35 | }; 36 | 37 | export type Context = inferAsyncReturnType; 38 | -------------------------------------------------------------------------------- /components/TorrentPageNav.tsx: -------------------------------------------------------------------------------- 1 | import { ActionIcon, Card, Center, Text, Tooltip } from "@mantine/core"; 2 | import { IconArrowLeft } from "@tabler/icons"; 3 | import Link from "next/link"; 4 | 5 | const TorrentPageNav = ({ name }: { name: string }) => { 6 | return ( 7 | 8 | 14 |
15 | 22 | ({ 24 | borderWidth: 1, 25 | borderColor: theme.colors.cyan, 26 | })} 27 | size={"xl"} 28 | component={Link} 29 | href="/home" 30 | color={"cyan"} 31 | variant="light" 32 | > 33 | 34 | 35 | 36 | 37 | {name} 38 | 39 |
40 |
41 |
42 | ); 43 | }; 44 | 45 | export default TorrentPageNav; 46 | -------------------------------------------------------------------------------- /utils/nextAuthOption.ts: -------------------------------------------------------------------------------- 1 | import { Awaitable, NextAuthOptions, RequestInternal, User } from "next-auth"; 2 | import Credentials from "next-auth/providers/credentials"; 3 | export const nextAuthOptions: NextAuthOptions = { 4 | providers: [ 5 | Credentials({ 6 | name: "credentials", 7 | credentials: { 8 | password: { 9 | label: "Password", 10 | type: "password", 11 | }, 12 | }, 13 | async authorize(credentials, req) { 14 | if (!credentials?.password) return null; 15 | if (credentials.password == process.env.BARRAGE_PASSWORD) { 16 | const user = { 17 | id: Buffer.from(credentials.password).toString("base64"), 18 | }; 19 | console.log("login"); 20 | return user; 21 | } 22 | return null; 23 | }, 24 | }), 25 | ], 26 | callbacks: { 27 | jwt: async ({ token, user }) => { 28 | if (user) { 29 | token.id = user.id; 30 | token.email = user.email; 31 | } 32 | 33 | return token; 34 | ``; 35 | }, 36 | session: async ({ session, token }) => { 37 | if (token && session.user) { 38 | session.user.id = token.id as string; 39 | } 40 | 41 | return session; 42 | }, 43 | }, 44 | 45 | pages: { 46 | signIn: "/", 47 | }, 48 | session: { 49 | maxAge: 24 * 60 * 60, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /utils/trpc.ts: -------------------------------------------------------------------------------- 1 | import { httpBatchLink } from "@trpc/client"; 2 | import { createTRPCNext } from "@trpc/next"; 3 | import superJSON from "superjson"; 4 | import type { AppRouter } from "../server/routers/_app"; 5 | 6 | function getBaseUrl() { 7 | if (typeof window !== "undefined") 8 | // browser should use relative path 9 | return ""; 10 | 11 | if (process.env.VERCEL_URL) 12 | // reference for vercel.com 13 | return `https://${process.env.VERCEL_URL}`; 14 | 15 | if (process.env.RENDER_INTERNAL_HOSTNAME) 16 | // reference for render.com 17 | return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`; 18 | 19 | // assume localhost 20 | return `http://localhost:${process.env.PORT ?? 3000}`; 21 | } 22 | 23 | export const trpc = createTRPCNext({ 24 | config({ ctx }) { 25 | return { 26 | links: [ 27 | httpBatchLink({ 28 | /** 29 | * If you want to use SSR, you need to use the server's full URL 30 | * @link https://trpc.io/docs/ssr 31 | **/ 32 | url: `${getBaseUrl()}/api/trpc`, 33 | }), 34 | ], 35 | /** 36 | * @link https://tanstack.com/query/v4/docs/reference/QueryClient 37 | **/ 38 | queryClientConfig: { defaultOptions: { queries: { retry: false } } }, 39 | }; 40 | }, 41 | /** 42 | * @link https://trpc.io/docs/ssr 43 | **/ 44 | ssr: false, 45 | }); 46 | // => { useQuery: ..., useMutation: ...} 47 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { MantineProvider } from "@mantine/core"; 4 | import Head from "next/head"; 5 | import { trpc } from "../utils/trpc"; 6 | import { RouterTransition } from "../components/RouterTransition"; 7 | import { SessionProvider } from "next-auth/react"; 8 | import { ModalsProvider } from "@mantine/modals"; 9 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 10 | import Label from "../components/Label"; 11 | 12 | export function App({ Component, pageProps }: AppProps) { 13 | return ( 14 | <> 15 | 16 | Barrage 17 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | 42 | export default trpc.withTRPC(App); 43 | -------------------------------------------------------------------------------- /stores/useTorrentStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ColumnFiltersState, 3 | functionalUpdate, 4 | OnChangeFn, 5 | SortingState, 6 | Updater, 7 | } from "@tanstack/react-table"; 8 | import create from "zustand"; 9 | import { Label, Stats, TorrentState } from "../deluge"; 10 | 11 | interface TorrentStoreState { 12 | lables: Label[]; 13 | states: Array<[TorrentState, number]>; 14 | isv2: boolean; 15 | stats: Stats; 16 | setStats: (stats: Stats) => void; 17 | setIsv2: (v2: boolean) => void; 18 | setState: (states: Array<[TorrentState, number]>) => void; 19 | setLables: (labels: Label[]) => void; 20 | } 21 | 22 | const useTorrentStore = create((set) => ({ 23 | lables: [], 24 | stats: { 25 | dht_nodes: 0, 26 | download_protocol_rate: 0, 27 | download_rate: 0, 28 | free_space: 0, 29 | has_incoming_connections: false, 30 | max_download: 0, 31 | max_num_connections: 0, 32 | max_upload: 0, 33 | num_connections: 0, 34 | upload_protocol_rate: 0, 35 | upload_rate: 0, 36 | }, 37 | isv2: true, 38 | states: [], 39 | setIsv2: (v2) => 40 | set(() => ({ 41 | isv2: v2, 42 | })), 43 | setLables: (labels: Label[]) => 44 | set(() => ({ 45 | lables: labels, 46 | })), 47 | setState: (states) => 48 | set(() => ({ 49 | states: states, 50 | })), 51 | setStats: (stats) => 52 | set(() => ({ 53 | stats: stats, 54 | })), 55 | })); 56 | 57 | export default useTorrentStore; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barrage", 3 | "version": "0.2.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ctrl/magnet-link": "^3.1.1", 13 | "@ctrl/url-join": "^2.0.2", 14 | "@emotion/react": "^11.10.5", 15 | "@emotion/server": "^11.10.0", 16 | "@mantine/core": "^5.7.0", 17 | "@mantine/form": "^5.7.0", 18 | "@mantine/hooks": "^5.7.0", 19 | "@mantine/modals": "^5.7.0", 20 | "@mantine/next": "^5.7.0", 21 | "@mantine/nprogress": "^5.7.0", 22 | "@tabler/icons": "^1.109.0", 23 | "@tanstack/react-query": "^4.14.1", 24 | "@tanstack/react-query-devtools": "^4.14.3", 25 | "@tanstack/react-table": "^8.5.22", 26 | "@trpc/client": "^10.0.0-proxy-beta.26", 27 | "@trpc/next": "^10.0.0-proxy-beta.26", 28 | "@trpc/react-query": "^10.0.0-proxy-beta.26", 29 | "@trpc/server": "^10.0.0-proxy-beta.26", 30 | "@types/node": "18.11.9", 31 | "@types/react": "18.0.24", 32 | "@types/react-dom": "18.0.8", 33 | "@types/tough-cookie": "4.0.2", 34 | "date-fns": "^2.29.3", 35 | "eslint": "8.26.0", 36 | "eslint-config-next": "13.0.1", 37 | "formdata-node": "^5.0.0", 38 | "got": "^12.5.0", 39 | "next": "13.0.1", 40 | "next-auth": "^4.16.2", 41 | "next-pwa": "^5.6.0", 42 | "preact": "^10.11.2", 43 | "react": "18.2.0", 44 | "react-dom": "18.2.0", 45 | "superjson": "^1.11.0", 46 | "tough-cookie": "^4.1.2", 47 | "typedoc": "0.23.15", 48 | "typescript": "4.8.4", 49 | "zod": "^3.19.1", 50 | "zustand": "^4.1.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /stores/useTableStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ColumnFiltersState, 3 | SortingState, 4 | OnChangeFn, 5 | functionalUpdate, 6 | Updater, 7 | Column, 8 | PaginationState, 9 | } from "@tanstack/react-table"; 10 | import create from "zustand"; 11 | import { NormalizedTorrent } from "../deluge"; 12 | import useTorrentStore from "./useTorrentStore"; 13 | 14 | interface TableStoreState { 15 | filter: ColumnFiltersState; 16 | sorting: SortingState; 17 | setSorting: OnChangeFn; 18 | pagination: PaginationState; 19 | setFilter: OnChangeFn; 20 | columns: Column[]; 21 | setColumns: (columns: Column[]) => void; 22 | setPagination: OnChangeFn; 23 | } 24 | 25 | const useTableStore = create((set) => ({ 26 | filter: [], 27 | columns: [], 28 | pagination: { 29 | pageIndex: 0, 30 | pageSize: 10, 31 | }, 32 | sorting: [ 33 | { 34 | id: "queuePosition", 35 | desc: false, 36 | }, 37 | ], 38 | setSorting: (sortingState: Updater) => 39 | set((s) => ({ 40 | sorting: functionalUpdate(sortingState, s.sorting), 41 | })), 42 | setFilter: (filterState: Updater) => 43 | set((s) => ({ 44 | pagination: { ...s.pagination, ...{ pageIndex: 0 } }, 45 | filter: functionalUpdate(filterState, s.filter), 46 | })), 47 | setColumns: (columns) => 48 | set(() => ({ 49 | columns: columns, 50 | })), 51 | setPagination: (pagination) => 52 | set((s) => ({ 53 | pagination: functionalUpdate(pagination, s.pagination), 54 | })), 55 | })); 56 | 57 | export default useTableStore; 58 | -------------------------------------------------------------------------------- /components/Directory.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, Box, Center, Select, Text } from "@mantine/core"; 2 | import { Content } from "../deluge"; 3 | import { getFileMap } from "../utils/helper"; 4 | 5 | const Directory = ({ 6 | name, 7 | content, 8 | onChange, 9 | }: { 10 | name: string; 11 | content: Content; 12 | onChange: (data: Record) => void; 13 | }) => { 14 | const setDirPriority = (priority: number) => { 15 | const fileMap = getFileMap(content.contents!, priority); 16 | onChange(fileMap); 17 | }; 18 | return ( 19 | ({ 21 | display: "flex", 22 | alignItems: "center", 23 | "&:hover": { 24 | backgroundColor: theme.colors.dark[6], 25 | }, 26 | })} 27 | > 28 | { 59 | if (e !== null) { 60 | if (!labels.some((l) => l.name === e) && e !== "") { 61 | createLabel.mutate({ 62 | label: e, 63 | }); 64 | } else { 65 | setLabel.mutate({ 66 | id: innerProps.id, 67 | label: e, 68 | }); 69 | } 70 | } 71 | }} 72 | getCreateLabel={(query) => `+ Set ${query}`} 73 | onCreate={(query) => { 74 | const item = { value: query, label: query }; 75 | return item; 76 | }} 77 | /> 78 | 81 | 82 | ); 83 | }; 84 | 85 | export default Label; 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Barrage


2 |

3 | 4 | 5 | > Introducing Barrage 6 | > 7 | > Minimal Deluge WebUI with full mobile support 8 | 9 | 10 |   11 | 12 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/maulik9898/barrage/DOCKER-BUILD?style=for-the-badge) 13 | 14 | ## Features 15 | 16 | * Responsive mobile first design 17 | * Add torrent by URL or magnet 18 | * Sort and Filter Torrents 19 | * Global upload and Download speed limits 20 | * Change File Priority 21 | * Change Torrent options 22 | 23 | ## Screenshots 24 | 25 |
26 | Click me 27 |   28 |

29 | 30 |     31 | 32 |     33 | 34 |     35 | 36 |     37 | 38 |     39 | 40 |     41 | 42 |     43 | 44 |     45 | 46 |     47 | 48 |     49 | 50 |

51 | 52 |
53 | 54 | 55 | ## Deploy 56 | 57 | You can deploy barrage with docker. 58 | 59 | ``` 60 | docker run --name barrage \ 61 | -p 3000:3000 \ 62 | -e NEXTAUTH_SECRET=secret \ 63 | -e DELUGE_URL=http://localhost:8112 \ 64 | -e DELUGE_PASSWORD=password \ 65 | -e BARRAGE_PASSWORD=password \ 66 | maulik9898/barrage 67 | 68 | ``` 69 | 70 | Then you can use the following environment variables to configure Barrage 71 | 72 | | Environment | Description | 73 | | ----------- | ----------- | 74 | | `NEXTAUTH_SECRET` | Used to encrypt the NextAuth.js JWT | 75 | | `DELUGE_URL` | The Deluge WebUI URL | 76 | | `DELUGE_PASSWORD` | The password from deluge WebUI | 77 | | `BARRAGE_PASSWORD` | The password for accessing Barrage | 78 | 79 | 80 | You can quickly create a good value of NEXTAUTH_SECRET on the command line via this openssl command. 81 | 82 | ``` 83 | openssl rand -base64 32 84 | ``` 85 | 86 | ### Deploy on vercel 87 | 88 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmaulik9898%2Fbarrage&env=BARRAGE_PASSWORD,NEXTAUTH_SECRET,DELUGE_URL,DELUGE_PASSWORD&project-name=barrage) 89 | 90 | ## Acknowledgments 91 | 92 | Thanks to [@scttcper](https://github.com/scttcper) for [Deluge api wrapper](https://github.com/scttcper/deluge). 93 | -------------------------------------------------------------------------------- /components/TorrentDetail.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Grid, Text } from "@mantine/core"; 2 | import React from "react"; 3 | import { humanFileSize } from "../utils/helper"; 4 | import { trpc } from "../utils/trpc"; 5 | import DetailGridCol from "./DetailGridCol"; 6 | import DetailLoader from "./DetailLoader"; 7 | 8 | const TorrentDetail = ({ id }: { id: string }) => { 9 | const torrent = trpc.deluge.getInfo.useQuery( 10 | { id: id }, 11 | { 12 | refetchInterval: 3000, 13 | } 14 | ); 15 | if (torrent.isLoading) { 16 | ``; 17 | return ; 18 | } 19 | return ( 20 | 21 | {torrent.data && ( 22 | 23 | 24 | 25 | 26 | 27 | { torrent.data.label && } 28 | 29 | 33 | 37 | 41 | 45 | 49 | 53 | 57 | 61 | 65 | 71 | 72 | )} 73 | 74 | ); 75 | }; 76 | 77 | export default TorrentDetail; 78 | -------------------------------------------------------------------------------- /components/DetailLoader.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Grid, Loader, Skeleton } from "@mantine/core"; 2 | import React from "react"; 3 | 4 | const DetailLoader = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ); 83 | }; 84 | 85 | export default DetailLoader; 86 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, Flex, Group, PasswordInput } from "@mantine/core"; 2 | import { unstable_getServerSession } from "next-auth"; 3 | import { signIn, useSession } from "next-auth/react"; 4 | import { useRouter } from "next/router"; 5 | import { GetServerSideProps } from "next/types"; 6 | import React, { useState } from "react"; 7 | import { nextAuthOptions } from "../utils/nextAuthOption"; 8 | 9 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 10 | const session = await unstable_getServerSession( 11 | ctx.req, 12 | ctx.res, 13 | nextAuthOptions 14 | ); 15 | 16 | if (session) { 17 | return { 18 | redirect: { 19 | destination: "/home", 20 | permanent: false, 21 | }, 22 | }; 23 | } 24 | 25 | return { 26 | props: {}, 27 | }; 28 | // ... 29 | }; 30 | 31 | const Login = () => { 32 | const router = useRouter(); 33 | const [password, setPassword] = useState(""); 34 | const [error, setError] = useState(""); 35 | const { data: session, status } = useSession(); 36 | const [loading, setLoading] = useState(false); 37 | 38 | if (status === "authenticated") { 39 | router.push("/home"); 40 | } 41 | 42 | const handleSignIn = async () => { 43 | setLoading(true); 44 | const d = await signIn("credentials", { 45 | password: password, 46 | redirect: false, 47 | }); 48 | if (d?.ok) { 49 | router.push("/home"); 50 | } 51 | if (d?.error) { 52 | setError("Invalid Password"); 53 | } 54 | setLoading(false); 55 | }; 56 | 57 | return ( 58 | 59 | 63 |
{ 64 | e.preventDefault() 65 | handleSignIn() 66 | }}> 67 | ({ 74 | label: { 75 | marginBottom: theme.spacing.sm, 76 | }, 77 | })} 78 | onChange={(e) => { 79 | setError(""); 80 | setPassword(e.currentTarget.value); 81 | }} 82 | /> 83 | 84 | 93 | 94 | 95 |
96 |
97 | ); 98 | }; 99 | 100 | export default Login; 101 | -------------------------------------------------------------------------------- /components/File.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | Center, 4 | Flex, 5 | Group, Select, 6 | Text 7 | } from "@mantine/core"; 8 | import { Content } from "../deluge"; 9 | import { humanFileSize } from "../utils/helper"; 10 | 11 | const File = ({ 12 | name, 13 | content, 14 | onChange, 15 | }: { 16 | name: string; 17 | content: Content; 18 | onChange: (data: Record) => void; 19 | }) => { 20 | return ( 21 | ({ 27 | backgroundColor: theme.colors.dark, 28 | //borderColor: content.priority === 0 ? theme.colors.red : theme.colors.blue[content.priority] 29 | })} 30 | > 31 | 32 |
33 | { 108 | if (e?.toLowerCase() === "all") { 109 | setFilter((old) => [ 110 | ...old.filter((s) => s.id != "state"), 111 | { 112 | id: "state", 113 | value: "", 114 | }, 115 | ]); 116 | } else { 117 | setFilter((old) => [ 118 | ...old.filter((s) => s.id != "state"), 119 | { 120 | id: "state", 121 | value: e, 122 | }, 123 | ]); 124 | } 125 | }} 126 | nothingFound="Nobody here" 127 | /> 128 | i + 1 244 | ).map((s) => s.toString())} 245 | size={largeScreen ? "sm" : "xs"} 246 | ml={"xs"} 247 | value={( 248 | torrentTable.getState().pagination.pageIndex + 1 249 | ).toString()} 250 | styles={(theme) => ({ 251 | input: { 252 | width: largeScreen ? "50px" : "45px", 253 | padding: "4px", 254 | textAlign: "center", 255 | }, 256 | rightSection: { 257 | display: "none", 258 | }, 259 | })} 260 | onChange={(e) => { 261 | if (!e) return; 262 | if (parseInt(e)) { 263 | torrentTable.setPageIndex(parseInt(e) - 1); 264 | } 265 | }} 266 | /> 267 |
268 |
269 | Page Size: 270 |