├── .devcontainer ├── Dockerfile ├── devcontainer.json └── docker-compose.yml ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .github └── workflows │ └── publish.yaml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── apps ├── aria │ ├── aria2.conf │ ├── darwin │ │ ├── arm64 │ │ │ └── aria2c │ │ └── x64 │ │ │ └── aria2c │ ├── linux │ │ ├── arm64 │ │ │ └── aria2c │ │ └── x64 │ │ │ └── aria2c │ ├── package.json │ ├── start.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── client │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── dload.svg │ ├── src │ │ ├── App.tsx │ │ ├── hooks │ │ │ └── useColorScheme.ts │ │ ├── layouts │ │ │ └── MainLayout.tsx │ │ ├── libs │ │ │ ├── request.ts │ │ │ └── socket.ts │ │ ├── main.tsx │ │ ├── modules │ │ │ ├── notification │ │ │ │ ├── NotificationProvider.tsx │ │ │ │ └── notification-service.ts │ │ │ ├── settings │ │ │ │ ├── SettingProvider.tsx │ │ │ │ ├── setting-service.ts │ │ │ │ └── setting-store.ts │ │ │ └── tasks │ │ │ │ ├── TaskProvider.tsx │ │ │ │ ├── task-service.ts │ │ │ │ └── task-store.ts │ │ ├── pages │ │ │ └── Main │ │ │ │ ├── Main.tsx │ │ │ │ ├── components │ │ │ │ ├── AddTask.tsx │ │ │ │ ├── Settings.tsx │ │ │ │ ├── Task.tsx │ │ │ │ └── TaskActions.tsx │ │ │ │ └── index.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── server │ ├── .env │ ├── .eslintrc.js │ ├── package.json │ ├── src │ ├── libs │ │ ├── aria2.ts │ │ ├── logger.ts │ │ ├── socket.ts │ │ ├── store.ts │ │ ├── utils.ts │ │ └── waitToInitialize.ts │ ├── main.ts │ ├── modules │ │ ├── notifications │ │ │ └── notification-service.ts │ │ ├── settings │ │ │ ├── setting-controller.ts │ │ │ └── setting-store.ts │ │ └── tasks │ │ │ ├── task-controller.ts │ │ │ ├── task-service.ts │ │ │ ├── task-store.ts │ │ │ └── task-utils.ts │ ├── server.ts │ └── services │ │ ├── database.ts │ │ └── error-handler.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── docker ├── Dockerfile ├── nginx-dev.conf ├── nginx.conf ├── root-fs │ └── etc │ │ └── s6-overlay │ │ └── s6-rc.d │ │ ├── aria-svc │ │ ├── dependencies.d │ │ │ └── setup │ │ ├── run │ │ └── type │ │ ├── nginx-svc │ │ ├── dependencies.d │ │ │ └── setup │ │ ├── run │ │ └── type │ │ ├── server-svc │ │ ├── dependencies.d │ │ │ └── aria-svc │ │ ├── run │ │ └── type │ │ ├── setup │ │ ├── dependencies.d │ │ │ └── base │ │ ├── run │ │ ├── type │ │ └── up │ │ └── user │ │ └── contents.d │ │ ├── aria-svc │ │ ├── nginx-svc │ │ ├── server-svc │ │ └── setup └── scripts │ └── install-s6overlay.sh ├── package.json ├── packages ├── shared │ ├── package.json │ ├── src │ │ ├── api_routes.ts │ │ ├── index.ts │ │ ├── socket_events.ts │ │ └── types.ts │ ├── tsconfig.json │ └── tsup.config.ts └── tsconfig │ ├── base.json │ ├── node.json │ ├── package.json │ └── react.json ├── screenshots ├── Dload.png ├── download-modal.jpg ├── main-page.jpg └── settings.jpg ├── turbo.json └── yarn.lock /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fuzzknob/octodevbase:latest 2 | 3 | RUN apt update && apt install nginx -y 4 | 5 | RUN export NVM_DIR="$HOME/.nvm" && \ 6 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ 7 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" && \ 8 | nvm use default && \ 9 | npm i -g yarn 10 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dload", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "development", 5 | "customizations": { 6 | "vscode": { 7 | "settings": { 8 | "terminal.integrated.defaultProfile.linux": "zsh", 9 | "terminal.integrated.profiles.linux": { 10 | "zsh": { 11 | "path": "/bin/zsh" 12 | } 13 | } 14 | } 15 | } 16 | }, 17 | "forwardPorts": [8080], 18 | "postAttachCommand": "nginx", 19 | "otherPortsAttributes": { 20 | "onAutoForward": "ignore" 21 | }, 22 | "workspaceFolder": "/workspaces/dload" 23 | } 24 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | development: 5 | build: 6 | context: ../ 7 | dockerfile: .devcontainer/Dockerfile 8 | environment: 9 | - TZ=UTC 10 | volumes: 11 | - ..:/workspaces/dload:cached 12 | - /var/run/docker.sock:/var/run/docker.sock 13 | - ../docker/nginx-dev.conf:/etc/nginx/nginx.conf 14 | command: sleep infinity 15 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | dist 3 | *.config.js 4 | *.config.ts 5 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Image to Docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | tags: 8 | - "v*" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build_publish: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Docker meta 19 | id: meta 20 | uses: docker/metadata-action@v5 21 | with: 22 | images: fuzzknob/dload 23 | tags: | 24 | type=edge,branch=main 25 | type=semver,pattern={{version}} 26 | type=semver,pattern={{major}}.{{minor}} 27 | 28 | - name: Set up QEMU 29 | uses: docker/setup-qemu-action@v3 30 | 31 | - name: Set up Docker Buildx 32 | uses: docker/setup-buildx-action@v3 33 | 34 | - name: Dockerhub login 35 | uses: docker/login-action@v3 36 | with: 37 | username: ${{ secrets.DOCKER_USERNAME }} 38 | password: ${{ secrets.DOCKER_ACCESSTOKEN }} 39 | 40 | - name: Build and Publish 41 | uses: docker/build-push-action@v5 42 | with: 43 | context: . 44 | file: ./docker/Dockerfile 45 | platforms: linux/amd64,linux/arm64 46 | push: ${{ github.event_name != 'pull_request' }} 47 | tags: ${{ steps.meta.outputs.tags }} 48 | labels: ${{ steps.meta.outputs.labels }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build 34 | 35 | dist/ 36 | 37 | temp/ 38 | 39 | # Dependency directories 40 | node_modules/ 41 | tests_output/ 42 | jspm_packages/ 43 | playground/ 44 | pgdata/ 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env.* 65 | env 66 | 67 | # next.js build output 68 | .next 69 | 70 | .parcel-cache 71 | .idea 72 | .DS_Store 73 | *.log 74 | 75 | /playground 76 | out 77 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | "semi": false, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "endOfLine": "lf", 8 | "printWidth": 80 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "EditorConfig.EditorConfig", 6 | "ms-azuretools.vscode-docker", 7 | "heybourn.headwind", 8 | "mikestead.dotenv", 9 | "albert.tabout", 10 | "bradlc.vscode-tailwindcss", 11 | "visualstudioexptteam.vscodeintellicode", 12 | "visualstudioexptteam.intellicode-api-usage-examples", 13 | "ms-vscode-remote.remote-containers", 14 | "aaron-bond.better-comments", 15 | "streetsidesoftware.code-spell-checker", 16 | "github.vscode-github-actions" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "cSpell.words": [ 6 | "dload" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2023, Gagan Rai(@fuzzknob) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Dload (Containerized Download Manager)

4 |
5 | 6 | Dload, A download manager that runs inside a docker container 7 | 8 | # Quick Setup 9 | 10 | 1. Install [Docker](https://docs.docker.com/engine/install/) 11 | 2. Run the following command to quickly create dload container: 12 | ```bash 13 | docker run -d \ 14 | --name=dload \ 15 | -e PUID=1000 \ 16 | -e PGID=1000 \ 17 | -p 8080:80 \ 18 | -v ./config:/config \ 19 | -v ./downloads:/downloads \ 20 | --restart unless-stopped \ 21 | fuzzknob/dload 22 | ``` 23 | ### With Docker Compose 24 | 25 | 1. Create `docker-compose.yml` file with this contents: 26 | ```yml 27 | version: "3" 28 | 29 | services: 30 | dload: 31 | container_name: dload 32 | image: fuzzknob/dload 33 | environment: 34 | - PUID=1000 35 | - PGID=1000 36 | volumes: 37 | - ./config:/config 38 | - ./downloads:/downloads 39 | ports: 40 | - 8080:80 41 | restart: unless-stopped 42 | ``` 43 | 44 | 2. Run the following command: 45 | ```bash 46 | docker compose up -d 47 | ``` 48 | 49 | You can access the dload UI at http://localhost:8080 50 | 51 | # Screenshots 52 | 53 | ### Main Page 54 | 55 | 56 | ### Add Download 57 | 58 | 59 | ### Settings 60 | 61 | 62 | # Development 63 | 64 | For development you can either use [vscode devcontainer](https://code.visualstudio.com/docs/devcontainers/containers). Or run it locally with Node.js. 65 | 66 | ### Devcontainer (Recommended) 67 | Just open this project in devcontainer and run: 68 | ```bash 69 | yarn dev 70 | ``` 71 | You can access the client in http://localhost:8080 72 | 73 | ### Local (Not Recommended) 74 | To run it locally you will need to have the following tools: 75 | * [Node.js (18 or above)](https://nodejs.org/) 76 | * [yarn (1.22.19)](https://classic.yarnpkg.com/en/docs/install) 77 | 78 | But before we run the app we need to change somethings: 79 | 80 | 1. Change the url at [client/request.ts:L4](https://github.com/fuzzknob/dload/blob/main/apps/client/src/libs/request.ts#L4) to http://localhost:8000 and add ws://localhost:8000 at [client/socket.ts:L3](https://github.com/fuzzknob/dload/blob/main/apps/client/src/libs/socket.ts#L3) 81 | 2. Replace line at [server/socket.ts:L3](https://github.com/fuzzknob/dload/blob/main/apps/server/src/libs/socket.ts#L3) with 82 | 83 | ```ts 84 | export const io = new Server({ 85 | cors: { 86 | origin: 'http://localhost:3000', 87 | methods: ['GET', 'POST'], 88 | }, 89 | }) 90 | ``` 91 | 92 | After run the following command: 93 | ```bash 94 | yarn dev 95 | ``` 96 | 97 | You can access the application at: 98 | * Server: http://localhost:8000 99 | * Client: http://localhost:3000 100 | -------------------------------------------------------------------------------- /apps/aria/aria2.conf: -------------------------------------------------------------------------------- 1 | enable-rpc=true 2 | rpc-listen-port=6800 3 | rpc-allow-origin-all=true 4 | rpc-listen-all=true 5 | allow-overwrite=false 6 | auto-file-renaming=true 7 | disk-cache=64M 8 | continue=true 9 | max-connection-per-server=64 10 | max-download-limit=0 11 | max-overall-download-limit=0 12 | max-overall-upload-limit=0 13 | split=64 14 | pause=true 15 | file-allocation=none 16 | no-file-allocation-limit=64M 17 | follow-metalink=true 18 | pause-metadata=false 19 | check-certificate=false 20 | max-file-not-found=10 21 | max-tries=0 22 | retry-wait=10 23 | connect-timeout=10 24 | timeout=10 25 | min-split-size=1M 26 | http-accept-gzip=true 27 | summary-interval=0 28 | content-disposition-default-utf8=true 29 | user-agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36' 30 | -------------------------------------------------------------------------------- /apps/aria/darwin/arm64/aria2c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzzknob/dload/536759535d741cfd28cc7112b75dd81b9851a30e/apps/aria/darwin/arm64/aria2c -------------------------------------------------------------------------------- /apps/aria/darwin/x64/aria2c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzzknob/dload/536759535d741cfd28cc7112b75dd81b9851a30e/apps/aria/darwin/x64/aria2c -------------------------------------------------------------------------------- /apps/aria/linux/arm64/aria2c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzzknob/dload/536759535d741cfd28cc7112b75dd81b9851a30e/apps/aria/linux/arm64/aria2c -------------------------------------------------------------------------------- /apps/aria/linux/x64/aria2c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuzzknob/dload/536759535d741cfd28cc7112b75dd81b9851a30e/apps/aria/linux/x64/aria2c -------------------------------------------------------------------------------- /apps/aria/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dload/aria", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "nodemon -r tsconfig-paths/register --watch './**/*.ts' --exec 'node --experimental-specifier-resolution=node --loader ts-node/esm' start.ts", 6 | "build": "tsup", 7 | "start": "node dist/start.js", 8 | "clean": "rm -rf ./dist" 9 | }, 10 | "devDependencies": { 11 | "@dload/tsconfig": "*", 12 | "nodemon": "^3.0.1", 13 | "ts-node": "^10.9.1", 14 | "tsup": "^7.2.0", 15 | "typescript": "^5.2.2" 16 | }, 17 | "dependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /apps/aria/start.ts: -------------------------------------------------------------------------------- 1 | import { platform, arch } from 'node:os' 2 | import { resolve } from 'node:path' 3 | import { spawn } from 'node:child_process' 4 | import readline from 'node:readline' 5 | 6 | const cwd = process.cwd() 7 | 8 | const aria2cPath = resolve(cwd, `./${platform()}/${arch()}/aria2c`) 9 | const aira2configPath = resolve(cwd, `./aria2.conf`) 10 | const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) 11 | 12 | 13 | const ariaInstance = spawn( 14 | aria2cPath, 15 | [`--conf-path=${aira2configPath}`, '--disable-ipv6'], 16 | { 17 | windowsHide: false, 18 | stdio: 'pipe', 19 | }, 20 | ) 21 | 22 | ariaInstance.on('spawn', () => console.log('*** Started Aria2 ***')) 23 | 24 | ariaInstance.stdout.on('data', (data: string) => { 25 | console.log(data.toString()) 26 | }) 27 | 28 | ariaInstance.stderr.on('data', (data: string) => { 29 | console.log(data.toString()) 30 | }) 31 | 32 | ariaInstance.on('exit', () => { 33 | console.log('\n*** Stopped Aria2 ***') 34 | process.exit(0) 35 | }) 36 | 37 | rl.on('SIGINT', () => { 38 | console.log('\n*** Stopping Aria2 ***') 39 | ariaInstance.kill('SIGINT') 40 | }) 41 | 42 | -------------------------------------------------------------------------------- /apps/aria/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@dload/tsconfig/node.json", 3 | "include": ["./**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/aria/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig((options) => ({ 4 | entry: ['start.ts'], 5 | treeshake: true, 6 | minify: true, 7 | ...options, 8 | })) 9 | 10 | -------------------------------------------------------------------------------- /apps/client/.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | tailwind.config.js 3 | dist 4 | -------------------------------------------------------------------------------- /apps/client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'prettier', 4 | 'plugin:prettier/recommended', 5 | 'plugin:react/recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 8 | ], 9 | plugins: [ 10 | 'jest-dom', 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | project: ['./apps/*/tsconfig.json'], 15 | ecmaFeatures: { 16 | jsx: true 17 | }, 18 | ecmaVersion: 12, 19 | sourceType: 'module' 20 | }, 21 | rules: { 22 | 'react/prop-types': 0, 23 | 'no-console': 'warn', 24 | 'react/react-in-jsx-scope': 'off', 25 | '@typescript-eslint/array-type': 'off', 26 | '@typescript-eslint/ban-ts-ignore': 'off', 27 | '@typescript-eslint/camelcase': 'off', 28 | '@typescript-eslint/no-unsafe-member-access': 'off', 29 | '@typescript-eslint/explicit-function-return-type': 'off', 30 | '@typescript-eslint/explicit-module-boundary-types': 'off', 31 | "@typescript-eslint/no-misused-promises": "off", 32 | '@typescript-eslint/member-delimiter-style': 'off', 33 | '@typescript-eslint/no-explicit-any': 'off', 34 | '@typescript-eslint/no-non-null-assertion': 'off', 35 | '@typescript-eslint/no-object-literal-type-assertion': 'off', 36 | '@typescript-eslint/no-this-alias': 'off', 37 | '@typescript-eslint/no-floating-promises': 'off', 38 | '@typescript-eslint/no-for-in-array': 'off', 39 | '@typescript-eslint/semi': ['error', 'never'], 40 | 'react/display-name': 'off', 41 | semi: ['error', 'never'], 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /apps/client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /apps/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Dload 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dload/client", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview", 9 | "clean": "rm -rf ./dist" 10 | }, 11 | "dependencies": { 12 | "@mantine/core": "^7.0.2", 13 | "@mantine/form": "^7.0.2", 14 | "@mantine/hooks": "^7.0.2", 15 | "@mantine/modals": "^7.0.2", 16 | "@mantine/notifications": "^7.0.2", 17 | "axios": "^1.3.6", 18 | "copy-to-clipboard": "^3.3.3", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-icons": "^4.4.0", 22 | "socket.io-client": "^4.5.4", 23 | "zod": "^3.21.4", 24 | "zustand": "^4.1.1" 25 | }, 26 | "devDependencies": { 27 | "@dload/shared": "*", 28 | "@dload/tsconfig": "*", 29 | "@types/react": "^18.0.17", 30 | "@types/react-dom": "^18.0.6", 31 | "@typescript-eslint/eslint-plugin": "^5.56.0", 32 | "@typescript-eslint/parser": "^5.56.0", 33 | "@vitejs/plugin-react": "^2.0.1", 34 | "eslint": "^8.23.0", 35 | "eslint-config-prettier": "^8.5.0", 36 | "eslint-plugin-jest-dom": "^4.0.2", 37 | "eslint-plugin-prettier": "^4.2.1", 38 | "eslint-plugin-react": "^7.31.1", 39 | "eslint-plugin-react-hooks": "^4.6.0", 40 | "postcss": "^8.4.30", 41 | "postcss-preset-mantine": "^1.7.0", 42 | "postcss-simple-vars": "^7.0.1", 43 | "prettier": "^2.7.1", 44 | "typescript": "^4.6.4", 45 | "vite": "^3.0.7" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-preset-mantine': {}, 4 | 'postcss-simple-vars': { 5 | variables: { 6 | 'mantine-breakpoint-xs': '36em', 7 | 'mantine-breakpoint-sm': '48em', 8 | 'mantine-breakpoint-md': '62em', 9 | 'mantine-breakpoint-lg': '75em', 10 | 'mantine-breakpoint-xl': '88em', 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /apps/client/public/dload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /apps/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import '@mantine/core/styles.css' 3 | import '@mantine/notifications/styles.css' 4 | import { MantineProvider } from '@mantine/core' 5 | import { ModalsProvider } from '@mantine/modals' 6 | 7 | import TaskProvider from '@/modules/tasks/TaskProvider' 8 | import SettingProvider from '@/modules/settings/SettingProvider' 9 | import NotificationProvider from '@/modules/notification/NotificationProvider' 10 | 11 | import Main from './pages/Main' 12 | 13 | const App: React.FC = () => { 14 | return ( 15 | 16 | 17 | 18 | 19 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export default App 33 | -------------------------------------------------------------------------------- /apps/client/src/hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | import { useComputedColorScheme } from '@mantine/core' 2 | 3 | export const useColorScheme = () => { 4 | const colorScheme = useComputedColorScheme('dark') 5 | return colorScheme 6 | } 7 | -------------------------------------------------------------------------------- /apps/client/src/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Container } from '@mantine/core' 3 | 4 | interface MainLayoutProps { 5 | children: React.ReactNode 6 | } 7 | 8 | const MainLayout: React.FC = ({ children }) => { 9 | return ( 10 | 17 | {children} 18 | 19 | ) 20 | } 21 | 22 | export default MainLayout 23 | -------------------------------------------------------------------------------- /apps/client/src/libs/request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const request = axios.create({ 4 | baseURL: '/api', 5 | }) 6 | 7 | export * from 'axios' 8 | export default request 9 | -------------------------------------------------------------------------------- /apps/client/src/libs/socket.ts: -------------------------------------------------------------------------------- 1 | import { io } from 'socket.io-client' 2 | 3 | export const socket = io('') 4 | -------------------------------------------------------------------------------- /apps/client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 6 | , 7 | ) 8 | -------------------------------------------------------------------------------- /apps/client/src/modules/notification/NotificationProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { Notifications, notifications } from '@mantine/notifications' 3 | import { onNotification } from './notification-service' 4 | 5 | type NotificationProviderProps = { 6 | children: React.ReactNode 7 | } 8 | 9 | const NotificationProvider: React.FC = ({ 10 | children, 11 | }) => { 12 | useEffect(() => { 13 | onNotification((notification) => { 14 | const isError = notification.type === 'ERROR' 15 | notifications.show({ 16 | title: isError ? 'Failure' : 'Success', 17 | message: notification.message, 18 | color: isError ? 'red' : 'green', 19 | }) 20 | }) 21 | }, []) 22 | 23 | return ( 24 | <> 25 | 26 | {children} 27 | 28 | ) 29 | } 30 | 31 | export default NotificationProvider 32 | -------------------------------------------------------------------------------- /apps/client/src/modules/notification/notification-service.ts: -------------------------------------------------------------------------------- 1 | import { socket } from '@/libs/socket' 2 | import { SOCKET_EVENTS } from '@dload/shared' 3 | 4 | type Notification = { 5 | code: string 6 | message: string 7 | type?: 'SUCCESS' | 'ERROR' 8 | } 9 | 10 | export const onNotification = (cb: (notification: Notification) => void) => 11 | socket.on(SOCKET_EVENTS.NOTIFY, cb) 12 | -------------------------------------------------------------------------------- /apps/client/src/modules/settings/SettingProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | 3 | import { useSettingStore } from './setting-store' 4 | import { fetchSettings } from './setting-service' 5 | 6 | interface SettingProviderProps { 7 | children: React.ReactNode 8 | } 9 | 10 | const SettingProvider: React.FC = ({ children }) => { 11 | const setSettings = useSettingStore(({ setSettings }) => setSettings) 12 | const [hasLoaded, setLoaded] = useState(false) 13 | 14 | useEffect(() => { 15 | // eslint-disable-next-line prettier/prettier 16 | (async () => { 17 | const settigs = await fetchSettings() 18 | setSettings(settigs) 19 | setLoaded(true) 20 | })() 21 | }, []) 22 | 23 | if (!hasLoaded) return
24 | 25 | return <>{children} 26 | } 27 | 28 | export default SettingProvider 29 | -------------------------------------------------------------------------------- /apps/client/src/modules/settings/setting-service.ts: -------------------------------------------------------------------------------- 1 | import { API_ROUTES, Settings } from '@dload/shared' 2 | 3 | import request from '@/libs/request' 4 | 5 | export const fetchSettings = async () => { 6 | const { data: settings } = await request.get( 7 | API_ROUTES.GET_SETTINGS, 8 | ) 9 | return settings 10 | } 11 | 12 | export const updateSettings = (settings: Settings) => 13 | request.post(API_ROUTES.UPDATE_SETTINGS, settings) 14 | -------------------------------------------------------------------------------- /apps/client/src/modules/settings/setting-store.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand' 2 | import { Settings } from '@dload/shared' 3 | 4 | export const useSettingStore = create<{ 5 | settings: Settings 6 | setSettings: (settings: Settings) => void 7 | }>((set) => ({ 8 | settings: { 9 | downloadPaths: [], 10 | maximumActiveDownloads: 2, 11 | }, 12 | setSettings: (newSettings) => set(() => ({ settings: newSettings })), 13 | })) 14 | -------------------------------------------------------------------------------- /apps/client/src/modules/tasks/TaskProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | 3 | import { useTaskStore } from './task-store' 4 | import { onTasksUpdate } from './task-service' 5 | 6 | interface TaskProviderProps { 7 | children: React.ReactNode 8 | } 9 | 10 | const TaskProvider: React.FC = ({ children }) => { 11 | const upsertTasks = useTaskStore(({ upsertTasks }) => upsertTasks) 12 | useEffect(() => { 13 | onTasksUpdate(upsertTasks) 14 | }, []) 15 | 16 | return <>{children} 17 | } 18 | 19 | export default TaskProvider 20 | -------------------------------------------------------------------------------- /apps/client/src/modules/tasks/task-service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/libs/request' 2 | import { socket } from '@/libs/socket' 3 | import { SOCKET_EVENTS, API_ROUTES, Task } from '@dload/shared' 4 | 5 | export type TaskPayload = { 6 | url: string 7 | name: string 8 | type: 'DOWNLOAD' | 'QUEUE' 9 | downloadPath: string 10 | directDownload: boolean 11 | } 12 | 13 | export const onTasksUpdate = (cb: (tasks: Task[]) => void) => 14 | socket.on(SOCKET_EVENTS.UPDATE_TASK, cb) 15 | 16 | export const addTask = (task: TaskPayload) => 17 | request.post(API_ROUTES.ADD_TASK, task) 18 | 19 | export const removeTask = (taskId: string) => 20 | request.post(API_ROUTES.REMOVE_TASK, { taskId }) 21 | 22 | export const togglePause = (taskId: string) => 23 | request.post(API_ROUTES.TASK_TOGGLE_PAUSE, { taskId }) 24 | -------------------------------------------------------------------------------- /apps/client/src/modules/tasks/task-store.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand' 2 | import { Task } from '@dload/shared' 3 | 4 | export const useTaskStore = create<{ 5 | tasks: Task[] 6 | upsertTasks: (tasks: Task[]) => void 7 | }>((set) => ({ 8 | tasks: [], 9 | upsertTasks: (newTasks) => set(() => ({ tasks: newTasks || [] })), 10 | })) 11 | -------------------------------------------------------------------------------- /apps/client/src/pages/Main/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Space, Group, Box, Title, Text, ActionIcon } from '@mantine/core' 3 | import { IoAddOutline, IoSettingsOutline } from 'react-icons/io5' 4 | import { useHotkeys } from '@mantine/hooks' 5 | 6 | import MainLayout from '@/layouts/MainLayout' 7 | import { useTaskStore } from '@/modules/tasks/task-store' 8 | 9 | import Task from './components/Task' 10 | import AddTask from './components/AddTask' 11 | import Settings from './components/Settings' 12 | import { useColorScheme } from '@/hooks/useColorScheme' 13 | 14 | const Main: React.FC = () => { 15 | const [isTaskVisible, setTaskVisible] = useState(false) 16 | const [isSettingsVisible, setSettingVisible] = useState(false) 17 | const colorScheme = useColorScheme() 18 | const tasks = useTaskStore((state) => state.tasks) 19 | const activeTasks = tasks.filter((task) => 20 | ['DOWNLOAD', 'COPY'].includes(task.type), 21 | ) 22 | const queuedTasks = tasks.filter((task) => task.type === 'QUEUE') 23 | 24 | useHotkeys([['n', () => setTaskVisible(true)]]) 25 | 26 | return ( 27 | 28 | setTaskVisible(false)} /> 29 | setSettingVisible(false)} 32 | /> 33 | 34 | setTaskVisible(true)} 38 | > 39 | 40 | 41 | setSettingVisible(true)} 45 | > 46 | 47 | 48 | 49 | 50 | {tasks.length ? ( 51 | <> 52 | 53 | In Progress 54 | 55 | {activeTasks.map((task) => ( 56 | 57 | ))} 58 | 59 | 60 | {!!queuedTasks.length && ( 61 | 62 | In Queue 63 | 64 | {queuedTasks.map((task) => ( 65 | 66 | ))} 67 | 68 | )} 69 | 70 | ) : ( 71 | 78 | No tasks in hand 79 | 80 | )} 81 | 82 | ) 83 | } 84 | 85 | export default Main 86 | -------------------------------------------------------------------------------- /apps/client/src/pages/Main/components/AddTask.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { 3 | Box, 4 | TextInput, 5 | Modal, 6 | Textarea, 7 | Space, 8 | Select, 9 | Group, 10 | Button, 11 | } from '@mantine/core' 12 | import { z } from 'zod' 13 | import { useForm, zodResolver } from '@mantine/form' 14 | import { notifications } from '@mantine/notifications' 15 | 16 | import * as taskService from '@/modules/tasks/task-service' 17 | import { useSettingStore } from '@/modules/settings/setting-store' 18 | 19 | const addTaskSchema = z.object({ 20 | url: z.string().url(), 21 | name: z.string().optional(), 22 | downloadPath: z.string(), 23 | }) 24 | 25 | type AddTaskProps = { 26 | visible: boolean 27 | onClose: () => void 28 | } 29 | 30 | const AddTask: React.FC = ({ visible, onClose }) => { 31 | const [isSubmitting, setSubmitting] = useState(false) 32 | const [directDownload, setDirectDownload] = useState(true) 33 | const { downloadPaths } = useSettingStore(({ settings }) => settings) 34 | const form = useForm({ 35 | validate: zodResolver(addTaskSchema), 36 | initialValues: { 37 | url: '', 38 | name: '', 39 | downloadPath: downloadPaths[0], 40 | }, 41 | transformValues: (values) => ({ 42 | ...values, 43 | url: values.url.trim(), 44 | name: values.name.trim(), 45 | downloadPath: values.downloadPath.trim(), 46 | }), 47 | }) 48 | 49 | function close() { 50 | onClose() 51 | form.reset() 52 | setDirectDownload(false) 53 | } 54 | 55 | async function handleSubmit(type: 'DOWNLOAD' | 'QUEUE') { 56 | const result = form.validate() 57 | if (result.hasErrors) return 58 | setSubmitting(true) 59 | try { 60 | await taskService.addTask({ 61 | ...form.getTransformedValues(), 62 | type, 63 | directDownload, 64 | }) 65 | close() 66 | } catch (e) { 67 | notifications.show({ 68 | title: 'Failure', 69 | message: 'Error while adding task', 70 | color: 'red', 71 | }) 72 | } finally { 73 | setSubmitting(false) 74 | } 75 | } 76 | 77 | return ( 78 | 86 | 91 |
handleSubmit('DOWNLOAD'))}> 92 |