├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── lint.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── ci └── generate_pkgbuild.py ├── package-lock.json ├── package.json ├── src-svg ├── active.svg ├── fontsize.svg ├── paused.svg └── priority.svg ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Info.plist ├── app.desktop ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── README.md │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── app.png │ ├── app.svg │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── installer.nsi ├── sound │ └── ping.mp3 ├── src │ ├── commands.rs │ ├── createtorrent.rs │ ├── geoip.rs │ ├── integrations.rs │ ├── ipc.rs │ ├── macos.rs │ ├── main.rs │ ├── poller.rs │ ├── sound.rs │ ├── torrentcache.rs │ └── tray.rs └── tauri.conf.json ├── src ├── cachedfiletree.ts ├── clientmanager.ts ├── components │ ├── app.tsx │ ├── colorchooser.tsx │ ├── contextmenu.tsx │ ├── createtorrentform.tsx │ ├── details.tsx │ ├── fileicon.tsx │ ├── filters.tsx │ ├── mantinetheme.tsx │ ├── miscbuttons.tsx │ ├── modals │ │ ├── add.tsx │ │ ├── common.tsx │ │ ├── daemon.tsx │ │ ├── editlabels.tsx │ │ ├── edittorrent.tsx │ │ ├── edittrackers.tsx │ │ ├── interfacepanel.tsx │ │ ├── move.tsx │ │ ├── remove.tsx │ │ ├── servermodals.tsx │ │ ├── settings.tsx │ │ └── version.tsx │ ├── piecescanvas.tsx │ ├── progressbar.tsx │ ├── sectionscontextmenu.tsx │ ├── server.tsx │ ├── servertabs.tsx │ ├── splitlayout.tsx │ ├── statusbar.tsx │ ├── statusicons.tsx │ ├── strictmodedroppable.tsx │ ├── tables │ │ ├── common.tsx │ │ ├── filetreetable.tsx │ │ ├── peerstable.tsx │ │ ├── torrenttable.tsx │ │ └── trackertable.tsx │ ├── toolbar.tsx │ └── webapp.tsx ├── config.ts ├── createtorrent.tsx ├── css │ ├── custom.css │ ├── loader.css │ ├── progressbar.css │ └── torrenttable.css ├── flagsshim.ts ├── hotkeys.ts ├── index.html ├── index.tsx ├── queries.ts ├── rpc │ ├── client.ts │ ├── torrent.ts │ └── transmission.ts ├── status.ts ├── svg │ ├── app.svg │ ├── icons │ │ ├── active.svg │ │ ├── fontsize.svg │ │ ├── paused.svg │ │ └── priority.svg │ ├── reactjs.svg │ └── tauri.svg ├── taurishim.ts ├── themehooks.tsx ├── trutil.ts └── types │ ├── json.d.ts │ ├── mantine.d.ts │ └── svg.d.ts ├── tsconfig.json ├── update_version.sh ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "settings": { 7 | "react": { 8 | "version": "detect" 9 | } 10 | }, 11 | "extends": [ 12 | "plugin:react/recommended", 13 | "standard-with-typescript" 14 | ], 15 | "overrides": [], 16 | "parserOptions": { 17 | "ecmaVersion": "latest", 18 | "sourceType": "module", 19 | "project": [ 20 | "./tsconfig.json" 21 | ] 22 | }, 23 | "plugins": [ 24 | "react", 25 | "react-hooks" 26 | ], 27 | "rules": { 28 | "import/no-webpack-loader-syntax": "off", 29 | "react-hooks/rules-of-hooks": "error", 30 | "react-hooks/exhaustive-deps": "error", 31 | "indent": [ 32 | "error", 33 | 4 34 | ], 35 | "@typescript-eslint/indent": [ 36 | "error", 37 | 4 38 | ], 39 | "semi": [ 40 | "error", 41 | "always" 42 | ], 43 | "@typescript-eslint/semi": [ 44 | "error", 45 | "always" 46 | ], 47 | "@typescript-eslint/no-extra-semi": "error", 48 | "quotes": [ 49 | "error", 50 | "double" 51 | ], 52 | "@typescript-eslint/quotes": [ 53 | "error", 54 | "double" 55 | ], 56 | "@typescript-eslint/member-delimiter-style": [ 57 | "error", 58 | { 59 | "multiline": { 60 | "delimiter": "comma", 61 | "requireLast": true 62 | }, 63 | "singleline": { 64 | "delimiter": "comma", 65 | "requireLast": false 66 | }, 67 | "multilineDetection": "brackets" 68 | } 69 | ], 70 | "@typescript-eslint/explicit-function-return-type": "off", 71 | "@typescript-eslint/space-before-function-paren": "off", 72 | "comma-dangle": [ 73 | "error", 74 | "always-multiline" 75 | ], 76 | "@typescript-eslint/comma-dangle": [ 77 | "error", 78 | "always-multiline" 79 | ], 80 | "@typescript-eslint/consistent-type-imports": [ 81 | "error", 82 | { 83 | "prefer": "type-imports", 84 | "disallowTypeAnnotations": true, 85 | "fixStyle": "separate-type-imports" 86 | } 87 | ] 88 | } 89 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Cargo insists on rewritig this file with unix line endings 2 | # which results in dirty checkouts after build on windows 3 | # with default git settings. This will force git to keep 4 | # the line endings as is. 5 | Cargo.toml text eol=lf 6 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'build' 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - v** 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | download-dbip: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Download dbip database 18 | run: wget -nv -O- "https://download.db-ip.com/free/dbip-country-lite-2024-04.mmdb.gz" | zcat > dbip.mmdb 19 | - uses: actions/upload-artifact@v3 20 | with: 21 | name: dbip 22 | path: "dbip.mmdb" 23 | 24 | build-binaries: 25 | needs: download-dbip 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | platform: 30 | - os: ubuntu-latest 31 | rust_target: x86_64-unknown-linux-gnu 32 | - os: macos-latest 33 | rust_target: x86_64-apple-darwin 34 | - os: macos-latest 35 | rust_target: aarch64-apple-darwin 36 | - os: windows-latest 37 | rust_target: x86_64-pc-windows-msvc 38 | 39 | runs-on: ${{ matrix.platform.os }} 40 | steps: 41 | - name: Disable crlf 42 | if: matrix.platform.os == 'windows-latest' 43 | run: | 44 | git config --global core.autocrlf input 45 | 46 | - uses: actions/checkout@v3 47 | 48 | # Download the previously uploaded artifacts 49 | - uses: actions/download-artifact@v3 50 | id: download 51 | with: 52 | name: dbip 53 | path: src-tauri/ 54 | 55 | - name: setup node 56 | uses: actions/setup-node@v3 57 | with: 58 | node-version: 18 59 | cache: 'npm' 60 | 61 | - name: 'Setup Rust' 62 | if: matrix.platform.rust_target == 'aarch64-apple-darwin' 63 | run: rustup target add aarch64-apple-darwin 64 | 65 | - uses: Swatinem/rust-cache@v2 66 | with: 67 | shared-key: ${{ matrix.platform.rust_target }} 68 | workspaces: | 69 | src-tauri 70 | 71 | - name: install dependencies (ubuntu only) 72 | if: matrix.platform.os == 'ubuntu-latest' 73 | run: | 74 | sudo apt-get update 75 | sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev libfontconfig-dev 76 | - name: install npm packages 77 | run: npm ci 78 | 79 | - uses: qu1ck/action-tauri-build@5c69c9fdbb4231a738b4a668a2caddf6af45eab8 80 | id: tauri_build 81 | with: 82 | target: ${{ matrix.platform.rust_target }} 83 | 84 | - name: Git status and version 85 | run: | 86 | git status 87 | git describe --tags --dirty --always 88 | git diff 89 | 90 | # The artifacts output can now be used to upload the artifacts 91 | - uses: actions/upload-artifact@v3 92 | with: 93 | name: ${{ matrix.platform.rust_target }} 94 | path: "${{ join(fromJSON(steps.tauri_build.outputs.artifacts), '\n') }}" 95 | 96 | - name: pack webapp 97 | if: matrix.platform.os == 'ubuntu-latest' 98 | working-directory: dist 99 | id: pack-webapp 100 | run: | 101 | GIT_VERSION=`git describe --tags --always` 102 | zip -9 -r "trguing-web-$GIT_VERSION.zip" ./* -x create\* -x \*.map -x \*flag-icons\* 103 | echo "ZIPFILE=trguing-web-$GIT_VERSION.zip" >> $GITHUB_OUTPUT 104 | 105 | - uses: actions/upload-artifact@v3 106 | if: matrix.platform.os == 'ubuntu-latest' 107 | with: 108 | name: build web 109 | path: "dist/${{ steps.pack-webapp.outputs.ZIPFILE }}" 110 | 111 | publish: 112 | needs: build-binaries 113 | if: startsWith(github.ref, 'refs/tags/') 114 | runs-on: ubuntu-latest 115 | permissions: 116 | contents: write 117 | steps: 118 | - uses: actions/checkout@v3 119 | 120 | # Download the previously uploaded artifacts 121 | - uses: actions/download-artifact@v3 122 | id: download 123 | with: 124 | path: artifacts 125 | - name: Rename mac app archives 126 | run: | 127 | mv artifacts/x86_64-apple-darwin/TrguiNG.app.tar.gz artifacts/x86_64-apple-darwin/TrguiNG_x86_64.app.tar.gz 128 | mv artifacts/aarch64-apple-darwin/TrguiNG.app.tar.gz artifacts/aarch64-apple-darwin/TrguiNG_aarch64.app.tar.gz 129 | - name: Downloaded artifacts 130 | run: ls -lhR artifacts/ 131 | 132 | # Generate chagnelog 133 | - id: prevtag 134 | run: | 135 | PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo '') 136 | echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT 137 | 138 | - name: Generate changelog 139 | id: changelog 140 | uses: jaywcjlove/changelog-generator@main 141 | if: steps.prevtag.outputs.previous_tag 142 | with: 143 | token: ${{ secrets.GITHUB_TOKEN }} 144 | show-emoji: false 145 | 146 | # And create a release with the artifacts attached 147 | - name: 'create release' 148 | uses: softprops/action-gh-release@v0.1.15 149 | env: 150 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 151 | with: 152 | draft: true 153 | files: ./artifacts/**/* 154 | body: | 155 | ${{ steps.changelog.outputs.compareurl }} 156 | 157 | ${{ steps.changelog.outputs.changelog }} 158 | 159 | update_aur: 160 | name: Publish AUR package 161 | needs: build-binaries 162 | if: startsWith(github.ref, 'refs/tags/') 163 | runs-on: ubuntu-latest 164 | steps: 165 | - uses: actions/checkout@v3 166 | - name: Determine version 167 | id: determine-version 168 | run: | 169 | GIT_VERSION=`git describe --tags --always` 170 | echo "GIT_VERSION=$GIT_VERSION" >> $GITHUB_OUTPUT 171 | # Download the previously uploaded artifacts 172 | - uses: actions/download-artifact@v3 173 | id: download 174 | with: 175 | name: dbip 176 | - name: Download desktop file 177 | run: wget -nv -O TrguiNG.desktop "https://raw.githubusercontent.com/flathub/org.openscopeproject.TrguiNG/master/org.openscopeproject.TrguiNG.desktop" 178 | - name: Generate PKGBUILD 179 | run: | 180 | python3 ci/generate_pkgbuild.py 181 | cat PKGBUILD 182 | - name: Publish to the AUR 183 | uses: KSXGitHub/github-actions-deploy-aur@v2.7.0 184 | with: 185 | pkgname: trgui-ng 186 | pkgbuild: ./PKGBUILD 187 | commit_username: ${{ secrets.AUR_USERNAME }} 188 | commit_email: ${{ secrets.AUR_EMAIL }} 189 | ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} 190 | commit_message: Update to ${{ steps.determine-version.outputs.GIT_VERSION }} 191 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | changes: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | pull-requests: read 10 | outputs: 11 | src: ${{ steps.changes.outputs.src }} 12 | srctauri: ${{ steps.changes.outputs.srctauri }} 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: dorny/paths-filter@v2 16 | id: changes 17 | with: 18 | filters: | 19 | src: 20 | - 'src/**' 21 | srctauri: 22 | - 'src-tauri/**' 23 | 24 | eslint: 25 | name: eslint 26 | needs: changes 27 | if: needs.changes.outputs.src == 'true' 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: read 31 | pull-requests: write 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: actions/setup-node@v3 35 | with: 36 | node-version: 18 37 | cache: 'npm' 38 | - run: npm ci 39 | - uses: reviewdog/action-eslint@v1 40 | if: github.event_name == 'push' 41 | with: 42 | reporter: github-check 43 | eslint_flags: 'src/' 44 | - uses: reviewdog/action-eslint@v1 45 | if: github.event_name == 'pull_request' 46 | with: 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | reporter: github-pr-review 49 | eslint_flags: 'src/' 50 | 51 | clippy_check: 52 | name: clippy check 53 | needs: changes 54 | if: needs.changes.outputs.srctauri == 'true' 55 | runs-on: ubuntu-latest 56 | permissions: 57 | contents: read 58 | pull-requests: write 59 | steps: 60 | - uses: actions/checkout@v3 61 | - uses: Swatinem/rust-cache@v2 62 | with: 63 | key: clippy 64 | workspaces: | 65 | src-tauri 66 | 67 | - name: install dependencies 68 | run: | 69 | mkdir -p dist 70 | sudo apt-get update 71 | sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev 72 | touch src-tauri/dbip.mmdb 73 | 74 | - uses: sksat/action-clippy@main 75 | if: github.event_name == 'push' 76 | with: 77 | working_directory: ./src-tauri 78 | reporter: github-check 79 | 80 | - uses: sksat/action-clippy@main 81 | if: github.event_name == 'pull_request' 82 | with: 83 | github_token: ${{ secrets.GITHUB_TOKEN }} 84 | working_directory: ./src-tauri 85 | reporter: github-pr-review 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | .idea 5 | webpack-report.html 6 | tests/manual.torrent 7 | *bkp 8 | src/build 9 | .DS_Store 10 | *.mmdb 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 基于 [openscopeproject/TrguiNG](https://github.com/openscopeproject/TrguiNG) 汉化并增加部分功能 3 | 4 | ### 更新 (240607a) 5 | 1. merge: openscopeproject/TrguiNG 6 | 2. add: 暂停增加子状态(已完成/未完成) 7 | 3. add: 支持tr3批量修改tracker 8 | 4. add: 列表不显示子目录种子(可配置,默认显示) 9 | 5. add: web增加字体大小调整 10 | 6. impr: 拖拽时批量处理 11 | 7. impr: 分享率固定2位小数 12 | 8. impr: web增加一些字体 13 | 9. fix: 取消当前状态显示时异常 14 | 15 | ### 更新 (240422a) 16 | 1. add: 目录分组增加复制路径功能 17 | 2. Issue #3 | fix: 一键URL访问跨域问题(简单转跳处理) 18 | 3. Issue #4 #10 | 合并 openscopeproject/TrguiNG:1.3.0 19 | 4. Issue #9 | impr: 隐藏/展示运行状态(右上角) 20 | 5. Issue #6 #7 #12 | 翻译调整 21 | 6. windows应用程序部分页面汉化补充 22 | 23 | ### 更新 (240417a) 24 | 1. fix: 体积支持 PB EB 展示 25 | 2. fix: 工具栏的一些同步问题 26 | 3. impr: version 页汉化 27 | 28 | ## 新增功能 (240416a) 29 | 1. 分组体积展示(可在分组区右键关闭该功能) 30 | 2. 双击全选分组,方便快捷操作(可在分组区右键关闭该功能) 31 | 3. 增加错误分布分组(可在分组区右键关闭该功能) 32 | 4. 增加分组后的Tracker二级过滤(位于顶部搜索框右侧) 33 | 5. 多链接下载,可设置下载间隔 34 | 6. 调整布局,左下角增加状态指示(主要用于多链接下载展示进度,平常展示列表选中项) 35 | 7. 种子列表右键菜单增加复制名称和路径(去重) 36 | 37 | ## PS. 主要是自用,有想加功能的可以提 issues,不保证实现 38 | 39 | ## 安装介绍(docker 环境) 40 | 1. 从 [releases](https://github.com/jayzcoder/TrguiNG/releases) 下载 `trguing-web-xxxx-zh.zip` 41 | 2. 解压到 transmission 设置的 webui 目录即可 42 | 3. transmission 需要正确映射并设置环境变量(确保 index.html 位于 TRANSMISSION_WEB_HOME 所在的目录第一层): 43 | ``` 44 | TRANSMISSION_WEB_HOME=/config/webui/trguing-zh 45 | ``` 46 | -------------------------------------------------------------------------------- /ci/generate_pkgbuild.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | if "GITHUB_REF" not in os.environ: 5 | print("GITHUB_REF variable not set") 6 | exit(1) 7 | 8 | REF = os.environ.get("GITHUB_REF") 9 | if REF is None or not REF.startswith("refs/tags/v"): 10 | print("Invalid version ref:", REF) 11 | exit(1) 12 | 13 | VERSION = REF[len("refs/tags/v"):] 14 | 15 | with subprocess.Popen(["sha256sum", "dbip.mmdb"], stdout=subprocess.PIPE) as proc: 16 | DBIP_SHA = proc.stdout.read().decode()[:64] 17 | with subprocess.Popen(["sha256sum", "TrguiNG.desktop"], stdout=subprocess.PIPE) as proc: 18 | DESKTOP_SHA = proc.stdout.read().decode()[:64] 19 | 20 | 21 | TEMPLATE = ''' 22 | # MAINTAINER username227 gfrank227[at]gmail[dot]com 23 | # MAINTAINER qu1ck anlutsenko[at]gmail[dot]com 24 | # This file is generated automatically by CI job at https://github.com/openscopeproject/TrguiNG 25 | pkgname=trgui-ng 26 | pkgver='%VERSION%' 27 | pkgrel=1 28 | pkgdesc='Remote GUI for Transmission torrent daemon' 29 | url="https://github.com/openscopeproject/TrguiNG" 30 | arch=('x86_64') 31 | license=('AGPL-3.0') 32 | depends=('alsa-lib' 'cairo' 'desktop-file-utils' 'fontconfig' 'gdk-pixbuf2' 'glib2' 'gtk3' 'hicolor-icon-theme' 'libayatana-appindicator' 'libsoup' 'openssl' 'webkit2gtk') 33 | makedepends=('rust>=1.69.0' 'nodejs>=16.0.0' 'npm' 'git') 34 | conflicts=('trgui-ng-git') 35 | source=("git+https://github.com/openscopeproject/TrguiNG#tag=v$pkgver" 36 | "https://github.com/openscopeproject/TrguiNG/releases/download/v$pkgver/dbip.mmdb" 37 | "TrguiNG.desktop"::"https://raw.githubusercontent.com/flathub/org.openscopeproject.TrguiNG/master/org.openscopeproject.TrguiNG.desktop") 38 | noextract=('dbip.mmdb') 39 | sha256sums=('SKIP' 40 | '%DBIP_SHA256%' 41 | '%DESKTOP_SHA256%') 42 | options=('!lto') 43 | 44 | prepare() { 45 | cd "$srcdir/TrguiNG" 46 | 47 | cp "../dbip.mmdb" "src-tauri/dbip.mmdb" 48 | } 49 | 50 | build() { 51 | cd "$srcdir/TrguiNG" 52 | 53 | npm ci 54 | npm run build -- -b 55 | } 56 | 57 | package() { 58 | install -dm755 "$pkgdir/usr/bin" 59 | install -dm755 "$pkgdir/usr/lib/trgui-ng" 60 | install -dm755 "$pkgdir/usr/share/icons/hicolor/32x32/apps" 61 | install -dm755 "$pkgdir/usr/share/icons/hicolor/128x128/apps" 62 | install -Dm755 "$srcdir/TrguiNG/src-tauri/target/release/trgui-ng" "$pkgdir/usr/bin/trgui-ng" 63 | install -Dm644 "$srcdir/TrguiNG/src-tauri/dbip.mmdb" "$pkgdir/usr/lib/trgui-ng/dbip.mmdb" 64 | install -Dm755 "$srcdir/TrguiNG.desktop" "$pkgdir/usr/share/applications/TrguiNG.desktop" 65 | install -Dm644 "$srcdir/TrguiNG/src-tauri/icons/32x32.png" "$pkgdir/usr/share/icons/hicolor/32x32/apps/trgui-ng.png" 66 | install -Dm644 "$srcdir/TrguiNG/src-tauri/icons/128x128.png" "$pkgdir/usr/share/icons/hicolor/128x128/apps/trgui-ng.png" 67 | install -Dm644 "$srcdir/TrguiNG/src-tauri/icons/app.svg" "$pkgdir/usr/share/icons/hicolor/scalable/apps/trgui-ng.svg" 68 | } 69 | ''' 70 | 71 | TEMPLATE_VALUES = { 72 | "%VERSION%": VERSION, 73 | "%DBIP_SHA256%": DBIP_SHA, 74 | "%DESKTOP_SHA256%": DESKTOP_SHA, 75 | } 76 | 77 | pkgbuild = TEMPLATE[1:] 78 | for k, v in TEMPLATE_VALUES.items(): 79 | pkgbuild = pkgbuild.replace(k, v) 80 | 81 | with open("PKGBUILD", "w") as f: 82 | f.write(pkgbuild) 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trguing", 3 | "version": "1.3.0", 4 | "sideEffects": [ 5 | "*.css" 6 | ], 7 | "type": "module", 8 | "scripts": { 9 | "tauri-dev": "tauri dev", 10 | "build": "tauri build", 11 | "build-bin": "tauri build -b none", 12 | "webpack-serve": "webpack serve --config webpack.dev.js", 13 | "webpack-dev": "webpack --config webpack.dev.js", 14 | "webpack-prod": "webpack --config webpack.prod.js", 15 | "info": "tauri info", 16 | "react-devtools": "npx react-devtools" 17 | }, 18 | "devDependencies": { 19 | "@svgr/webpack": "^8.1.0", 20 | "@tauri-apps/cli": "^1.5.10", 21 | "@types/lodash-es": "^4.17.12", 22 | "@types/luxon": "^3.3.7", 23 | "@types/react": "18.2.42", 24 | "@types/react-beautiful-dnd": "^13.1.7", 25 | "@types/react-dom": "^18.2.18", 26 | "@types/ua-parser-js": "^0.7.39", 27 | "@typescript-eslint/eslint-plugin": "^5.62.0", 28 | "copy-webpack-plugin": "^11.0.0", 29 | "css-loader": "^6.8.1", 30 | "eslint": "^8.56.0", 31 | "eslint-config-standard-with-typescript": "^34.0.1", 32 | "eslint-plugin-import": "^2.29.1", 33 | "eslint-plugin-n": "^15.7.0", 34 | "eslint-plugin-promise": "^6.1.1", 35 | "eslint-plugin-react": "^7.33.2", 36 | "eslint-plugin-react-hooks": "^4.6.0", 37 | "execa": "^7.2.0", 38 | "html-webpack-plugin": "^5.6.0", 39 | "mini-css-extract-plugin": "^2.7.6", 40 | "react-select": "^5.8.0", 41 | "style-loader": "^3.3.3", 42 | "ts-loader": "^9.5.1", 43 | "typescript": "^5.3.3", 44 | "webpack": "^5.89.0", 45 | "webpack-bundle-analyzer": "^4.10.1", 46 | "webpack-cli": "^5.1.4", 47 | "webpack-dev-server": "^4.15.1", 48 | "webpack-merge": "^5.10.0" 49 | }, 50 | "dependencies": { 51 | "@emotion/react": "^11.11.1", 52 | "@mantine/core": "^6.0.2", 53 | "@mantine/dropzone": "^6.0.21", 54 | "@mantine/form": "^6.0.21", 55 | "@mantine/hooks": "^6.0.2", 56 | "@mantine/modals": "^6.0.21", 57 | "@mantine/notifications": "^6.0.21", 58 | "@popperjs/core": "^2.11.8", 59 | "@tanstack/react-query": "^4.36.1", 60 | "@tanstack/react-query-devtools": "^4.36.1", 61 | "@tanstack/react-table": "^8.11.2", 62 | "@tanstack/react-virtual": "^3.0.1", 63 | "@tauri-apps/api": "^1.5.3", 64 | "@yornaath/batshit": "^0.7.1", 65 | "buffer": "^6.0.3", 66 | "flag-icons": "^6.15.0", 67 | "lodash-es": "^4.17.21", 68 | "react": "^18.2.0", 69 | "react-beautiful-dnd": "^13.1.1", 70 | "react-bootstrap-icons": "^1.10.3", 71 | "react-dom": "^18.2.0", 72 | "react-resize-detector": "^8.1.0", 73 | "react-split": "^2.0.14", 74 | "split.js": "^1.6.5", 75 | "ua-parser-js": "^1.0.37" 76 | }, 77 | "madge": { 78 | "webpackConfig": "./webpack.prod.js", 79 | "tsConfig": "./tsconfig.json", 80 | "fileExtensions": [ 81 | "ts", 82 | "tsx" 83 | ], 84 | "detectiveOptions": { 85 | "ts": { 86 | "skipTypeImports": true 87 | }, 88 | "tsx": { 89 | "skipTypeImports": true 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src-svg/active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src-svg/fontsize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 40 | 45 | 46 | 48 | 52 | A 63 | A 74 | 75 | 76 | -------------------------------------------------------------------------------- /src-svg/paused.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 40 | 45 | 46 | 48 | 52 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src-svg/priority.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 40 | 45 | 46 | 48 | 52 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | WixTools 5 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "trguing" 3 | version = "1.3.0" 4 | description = "Remote control GUI for Transmission torrent daemon" 5 | authors = ["qu1ck"] 6 | license = "GNU-AGPL-3.0" 7 | repository = "https://github.com/openscopeproject/trguing/" 8 | default-run = "trguing" 9 | edition = "2021" 10 | rust-version = "1.60" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "^1.4", features = [] } 16 | 17 | 18 | [dependencies] 19 | serde_json = "^1.0" 20 | base64 = "^0.21" 21 | lava_torrent = { git = "https://github.com/openscopeproject/lava_torrent", branch = "patches" } 22 | urlencoding = "^2.1" 23 | hyper = { version = "^0.14", features = ["full"] } 24 | hyper-timeout = "^0.4" 25 | tokio = { version = "^1.28", features = ["net"] } 26 | serde = { version = "^1.0", features = ["derive"] } 27 | tauri = { version = "^1.6", features = [ "clipboard-write-text", "cli", "devtools", "dialog-all", "fs-read-file", "fs-write-file", "notification", "path-all", "shell-open", "system-tray", "window-center", "window-close", "window-create", "window-hide", "window-set-focus", "window-set-position", "window-set-size", "window-set-title", "window-show", "window-unminimize"] } 28 | tauri-utils = "^1.5.3" 29 | opener = { version = "0.7", features = ["reveal"], default-features = false } 30 | rodio = { version = "0.17.1", features = ["mp3"], default-features = false } 31 | hyper-tls = "0.5.0" 32 | flate2 = "1.0.26" 33 | maxminddb = "^0.24" 34 | font-loader = "0.11.0" 35 | 36 | [target.'cfg(windows)'.dependencies] 37 | winreg = "^0.52.0" 38 | 39 | [target.'cfg(target_os = "macos")'.dependencies] 40 | objc2 = "0.4.0" 41 | once_cell = "1" 42 | 43 | [features] 44 | # by default Tauri runs in production mode 45 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 46 | default = ["custom-protocol"] 47 | # this feature is used used for production builds where `devPath` points to the filesystem 48 | # DO NOT remove this 49 | custom-protocol = ["tauri/custom-protocol"] 50 | 51 | [profile.release] 52 | panic = "abort" # Strip expensive panic clean-up logic 53 | codegen-units = 1 # Compile crates one after another so the compiler can optimize better 54 | lto = true # Enables link to optimizations 55 | opt-level = "s" # Optimize for binary size 56 | -------------------------------------------------------------------------------- /src-tauri/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDocumentTypes 6 | 7 | 8 | CFBundleTypeExtensions 9 | 10 | torrent 11 | 12 | CFBundleTypeIconFile 13 | icon 14 | CFBundleTypeName 15 | BitTorrent Document 16 | CFBundleTypeRole 17 | Viewer 18 | LSHandlerRank 19 | Owner 20 | LSItemContentTypes 21 | 22 | org.bittorrent.torrent 23 | 24 | 25 | 26 | CFBundleURLTypes 27 | 28 | 29 | CFBundleTypeRole 30 | Viewer 31 | CFBundleURLName 32 | BitTorrent Magnet URL 33 | CFBundleURLSchemes 34 | 35 | magnet 36 | file 37 | 38 | 39 | 40 | NSHumanReadableCopyright 41 | Copyright © 2023 qu1ck (github.com/openscopeproject) 42 | UTExportedTypeDeclarations 43 | 44 | 45 | UTTypeConformsTo 46 | 47 | public.data 48 | public.item 49 | com.bittorrent.torrent 50 | 51 | UTTypeDescription 52 | BitTorrent Document 53 | UTTypeIconFile 54 | icon 55 | UTTypeIdentifier 56 | org.bittorrent.torrent 57 | UTTypeReferenceURL 58 | http://www.bittorrent.org/beps/bep_0000.html 59 | UTTypeTagSpecification 60 | 61 | com.apple.ostype 62 | TORR 63 | public.filename-extension 64 | 65 | torrent 66 | 67 | public.mime-type 68 | application/x-bittorrent 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src-tauri/app.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Categories={{{categories}}} 3 | Exec={{{exec}}} %u 4 | Icon={{{icon}}} 5 | Name={{{name}}} 6 | Comment={{{comment}}} 7 | Terminal=false 8 | Type=Application 9 | MimeType=application/x-bittorrent;x-scheme-handler/magnet; 10 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | // TrguiNG - next gen remote GUI for transmission torrent daemon 2 | // Copyright (C) 2023 qu1ck (mail at qu1ck.org) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published 6 | // by the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | fn main() { 18 | tauri_build::build() 19 | } 20 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/README.md: -------------------------------------------------------------------------------- 1 | # How to update 2 | 3 | 1. Update svg 4 | 2. Export png (1024x1024px, 32bit, with transparency) 5 | 3. From `src-tauri` run `cargo tauri icon ./icons/app.png` 6 | 4. Do a clean build 7 | -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/app.png -------------------------------------------------------------------------------- /src-tauri/icons/app.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 40 | 42 | 45 | 49 | 53 | 57 | 61 | 62 | 73 | 74 | 78 | 87 | 95 | 103 | 111 | 119 | 127 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/sound/ping.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayzcoder/TrguiNG/90c9407625ef6fd0934cad4122137bceebeae67c/src-tauri/sound/ping.mp3 -------------------------------------------------------------------------------- /src-tauri/src/createtorrent.rs: -------------------------------------------------------------------------------- 1 | // TrguiNG - next gen remote GUI for transmission torrent daemon 2 | // Copyright (C) 2023 qu1ck (mail at qu1ck.org) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published 6 | // by the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | use std::{ 18 | collections::HashMap, 19 | sync::Arc, 20 | time::{SystemTime, UNIX_EPOCH}, 21 | }; 22 | 23 | use lava_torrent::{ 24 | bencode::BencodeElem, 25 | torrent::v1::{Torrent, TorrentBuild, TorrentBuilder}, 26 | LavaTorrentError, 27 | }; 28 | use tokio::sync::Mutex; 29 | 30 | #[derive(serde::Deserialize)] 31 | #[serde(rename_all = "camelCase")] 32 | pub struct TorrentCreateInfo { 33 | name: String, 34 | path: String, 35 | piece_length: i64, 36 | comment: String, 37 | source: String, 38 | private: bool, 39 | announce_list: Vec, 40 | url_list: Vec, 41 | version: String, 42 | } 43 | 44 | #[derive(serde::Serialize)] 45 | pub struct ProgressData { 46 | hashed: u64, 47 | total: u64, 48 | } 49 | #[derive(serde::Serialize)] 50 | #[serde(rename_all = "camelCase")] 51 | pub enum CreateCheckResult { 52 | NotFound, 53 | Error(String), 54 | Complete(String), 55 | InProgress(ProgressData), 56 | } 57 | 58 | enum BuildOrTorrent { 59 | Build(TorrentBuild), 60 | Result(Result), 61 | } 62 | 63 | #[derive(Default)] 64 | pub struct CreationRequests { 65 | requests: HashMap, 66 | } 67 | 68 | #[derive(Default)] 69 | pub struct CreationRequestsHandle(pub Arc>); 70 | 71 | impl CreationRequests { 72 | pub fn add(&mut self, id: i32, info: TorrentCreateInfo) -> Result<(), LavaTorrentError> { 73 | let url_list = info 74 | .url_list 75 | .iter() 76 | .filter(|s| !s.is_empty()) 77 | .map(|url| BencodeElem::String(url.clone())) 78 | .collect::>(); 79 | 80 | let announce_list = info 81 | .announce_list 82 | .split(|s| s.is_empty()) 83 | .filter(|tier| !(*tier).is_empty()) 84 | .map(|tier| tier.to_vec()) 85 | .collect::>>(); 86 | 87 | let mut builder = TorrentBuilder::new(info.path, info.piece_length) 88 | .set_name(info.name) 89 | .set_announce(info.announce_list.first().cloned()) 90 | .add_extra_field( 91 | "created by".into(), 92 | BencodeElem::String(format!("TrguiNG {}", info.version)), 93 | ) 94 | .add_extra_field( 95 | "creation date".into(), 96 | BencodeElem::Integer( 97 | SystemTime::now() 98 | .duration_since(UNIX_EPOCH) 99 | .expect("System time is before Unix epoch!").as_secs().try_into().unwrap(), 100 | ), 101 | ); 102 | 103 | if !url_list.is_empty() { 104 | builder = builder.add_extra_field("url-list".into(), BencodeElem::List(url_list)); 105 | } 106 | if !info.announce_list.is_empty() { 107 | builder = builder.set_announce_list(announce_list); 108 | } 109 | if !info.comment.is_empty() { 110 | builder = builder.add_extra_field("comment".into(), BencodeElem::String(info.comment.clone())); 111 | } 112 | if !info.source.is_empty() { 113 | builder = builder.add_extra_info_field("source".into(), BencodeElem::String(info.source.clone())); 114 | } 115 | if info.private { 116 | builder = builder.set_privacy(true); 117 | } 118 | 119 | let build = builder.build_non_blocking()?; 120 | if let Some(BuildOrTorrent::Build(old_build)) = 121 | self.requests.insert(id, BuildOrTorrent::Build(build)) 122 | { 123 | old_build.cancel(); 124 | } 125 | 126 | Ok(()) 127 | } 128 | 129 | pub fn check(&mut self, id: i32) -> CreateCheckResult { 130 | if let Some(BuildOrTorrent::Build(build)) = self.requests.get(&id) { 131 | if build.is_finished() { 132 | let BuildOrTorrent::Build(build) = self.requests.remove(&id).unwrap() 133 | else { 134 | panic!("The build entry was just here") 135 | }; 136 | self.requests 137 | .insert(id, BuildOrTorrent::Result(build.get_output())); 138 | } 139 | } 140 | 141 | match self.requests.get(&id) { 142 | Some(BuildOrTorrent::Build(build)) => CreateCheckResult::InProgress(ProgressData { 143 | hashed: build.get_n_piece_processed(), 144 | total: build.get_n_piece_total(), 145 | }), 146 | Some(BuildOrTorrent::Result(Ok(torrent))) => { 147 | CreateCheckResult::Complete(torrent.info_hash()) 148 | } 149 | Some(BuildOrTorrent::Result(Err(e))) => CreateCheckResult::Error(e.to_string()), 150 | None => CreateCheckResult::NotFound, 151 | } 152 | } 153 | 154 | pub fn cancel(&mut self, id: i32) -> Result<(), String> { 155 | if let Some(build_or_torrent) = self.requests.remove(&id) { 156 | if let BuildOrTorrent::Build(build) = build_or_torrent { 157 | build.cancel(); 158 | } 159 | Ok(()) 160 | } else { 161 | Err("Torrent build request not found".into()) 162 | } 163 | } 164 | 165 | pub fn save(&mut self, id: i32, path: &String) -> Result<(), String> { 166 | if let Some(BuildOrTorrent::Result(Ok(torrent))) = self.requests.remove(&id) { 167 | torrent.write_into_file(path).map_err(|e| e.to_string()) 168 | } else { 169 | Err("Torrent build request not found".into()) 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src-tauri/src/geoip.rs: -------------------------------------------------------------------------------- 1 | // TrguiNG - next gen remote GUI for transmission torrent daemon 2 | // Copyright (C) 2023 qu1ck (mail at qu1ck.org) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published 6 | // by the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | use std::{net::IpAddr, sync::Arc}; 18 | 19 | use tauri::{AppHandle, Manager, State}; 20 | use tokio::sync::Mutex; 21 | 22 | #[derive(Default)] 23 | pub struct MmdbReaderHandle(pub Arc>>>>); 24 | 25 | #[derive(serde::Serialize, Clone)] 26 | #[serde(rename_all = "camelCase")] 27 | pub struct LookupResult { 28 | ip: IpAddr, 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | iso_code: Option, 31 | #[serde(skip_serializing_if = "Option::is_none")] 32 | name: Option, 33 | } 34 | 35 | pub async fn lookup(app: &AppHandle, ips: Vec) -> Vec { 36 | let reader_handle: State = app.state(); 37 | let mut reader = reader_handle.0.lock().await; 38 | if reader.is_none() { 39 | let dbip_path = app 40 | .path_resolver() 41 | .resolve_resource("dbip.mmdb") 42 | .expect("failed to resolve resource"); 43 | 44 | if dbip_path.is_file() { 45 | match maxminddb::Reader::open_readfile(&dbip_path) { 46 | Ok(db) => *reader = Some(db), 47 | Err(_) => { 48 | println!("{} is invalid", dbip_path.as_path().display()); 49 | return vec![]; 50 | } 51 | } 52 | } else { 53 | println!("{} does not exist", dbip_path.as_path().display()); 54 | return vec![]; 55 | } 56 | } 57 | 58 | let reader = reader.as_ref().unwrap(); 59 | 60 | let response: Vec<_> = ips 61 | .into_iter() 62 | .map(|ip| { 63 | match reader 64 | .lookup::(ip) 65 | .ok() 66 | .and_then(|c| c.country) 67 | { 68 | Some(country) => LookupResult { 69 | ip, 70 | iso_code: country.iso_code.map(|c| c.to_string()), 71 | name: country 72 | .names 73 | .and_then(|names| names.get("en").map(|s| s.to_string())), 74 | }, 75 | None => LookupResult { 76 | ip, 77 | iso_code: None, 78 | name: None, 79 | }, 80 | } 81 | }) 82 | .collect(); 83 | 84 | response 85 | } 86 | -------------------------------------------------------------------------------- /src-tauri/src/integrations.rs: -------------------------------------------------------------------------------- 1 | // TrguiNG - next gen remote GUI for transmission torrent daemon 2 | // Copyright (C) 2023 qu1ck (mail at qu1ck.org) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published 6 | // by the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | #[cfg(target_os = "windows")] 18 | use winreg::RegKey; 19 | 20 | #[cfg(target_os = "windows")] 21 | const APP_NAME: &str = "TrguiNG"; 22 | 23 | #[cfg(target_os = "windows")] 24 | fn register_app_class() -> std::io::Result<()> { 25 | match std::env::current_exe() { 26 | Ok(exe) => { 27 | let exe = exe.to_str().unwrap_or_default(); 28 | let hkcu = RegKey::predef(winreg::enums::HKEY_CURRENT_USER); 29 | let (key, _) = 30 | hkcu.create_subkey(format!("SOFTWARE\\Classes\\{}\\DefaultIcon", APP_NAME))?; 31 | let icon = format!("\"{}\",0", exe); 32 | key.set_value("", &icon)?; 33 | 34 | let (key, _) = hkcu.create_subkey(format!( 35 | "SOFTWARE\\Classes\\{}\\shell\\open\\command", 36 | APP_NAME 37 | ))?; 38 | let icon = format!("\"{}\" \"%1\"", exe); 39 | key.set_value("", &icon)?; 40 | } 41 | Err(e) => println!("Error getting exe path: {}", e), 42 | } 43 | Ok(()) 44 | } 45 | 46 | #[cfg(target_os = "windows")] 47 | fn register_torrent_class() -> std::io::Result<()> { 48 | let hkcu = RegKey::predef(winreg::enums::HKEY_CURRENT_USER); 49 | let (key, _) = hkcu.create_subkey("SOFTWARE\\Classes\\.torrent")?; 50 | key.set_value("", &APP_NAME)?; 51 | 52 | Ok(()) 53 | } 54 | 55 | #[cfg(target_os = "windows")] 56 | fn register_magnet_class() -> std::io::Result<()> { 57 | match std::env::current_exe() { 58 | Ok(exe) => { 59 | let exe = exe.to_str().unwrap_or_default(); 60 | let hkcu = RegKey::predef(winreg::enums::HKEY_CURRENT_USER); 61 | let (key, _) = hkcu.create_subkey("SOFTWARE\\Classes\\Magnet")?; 62 | key.set_value("", &"Magnet URI")?; 63 | key.set_value("Content Type", &"application/x-magnet")?; 64 | key.set_value("URL Protocol", &"")?; 65 | 66 | let (key, _) = hkcu.create_subkey("SOFTWARE\\Classes\\Magnet\\DefaultIcon")?; 67 | let icon = format!("\"{}\",0", exe); 68 | key.set_value("", &icon)?; 69 | 70 | let (key, _) = hkcu.create_subkey("SOFTWARE\\Classes\\Magnet\\shell")?; 71 | key.set_value("", &"open")?; 72 | 73 | let (key, _) = hkcu.create_subkey("SOFTWARE\\Classes\\Magnet\\shell\\open\\command")?; 74 | let icon = format!("\"{}\" \"%1\"", exe); 75 | key.set_value("", &icon)?; 76 | } 77 | Err(e) => println!("Error getting exe path: {}", e), 78 | } 79 | Ok(()) 80 | } 81 | 82 | #[cfg(target_os = "windows")] 83 | fn register_autorun(run: bool) -> std::io::Result<()> { 84 | match std::env::current_exe() { 85 | Ok(exe) => { 86 | let exe = format!("\"{}\"", exe.to_str().unwrap_or_default()); 87 | let hkcu = RegKey::predef(winreg::enums::HKEY_CURRENT_USER); 88 | let (key, _) = 89 | hkcu.create_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run")?; 90 | if run { 91 | key.set_value(APP_NAME, &exe)?; 92 | } else { 93 | key.delete_value(APP_NAME)?; 94 | } 95 | } 96 | Err(e) => println!("Error getting exe path: {}", e), 97 | } 98 | Ok(()) 99 | } 100 | 101 | #[cfg(target_os = "windows")] 102 | fn check_autorun() -> bool { 103 | match std::env::current_exe() { 104 | Ok(exe) => { 105 | let exe = format!("\"{}\"", exe.to_str().unwrap_or_default()); 106 | let hkcu = RegKey::predef(winreg::enums::HKEY_CURRENT_USER); 107 | if let Ok((key, _)) = 108 | hkcu.create_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run") 109 | { 110 | if let Ok(val) = key.get_value::(APP_NAME) { 111 | return val == exe; 112 | } 113 | } 114 | } 115 | Err(e) => println!("Error getting exe path: {}", e), 116 | } 117 | false 118 | } 119 | 120 | #[cfg(target_os = "windows")] 121 | pub fn app_integration_impl(mode: String) -> bool { 122 | match mode.as_str() { 123 | "torrent" => { 124 | println!("Associating .torrent files with the app"); 125 | if let Err(e) = register_app_class() { 126 | println!("Error writing to registry: {}", e); 127 | } 128 | if let Err(e) = register_torrent_class() { 129 | println!("Error writing to registry: {}", e); 130 | } 131 | } 132 | "magnet" => { 133 | println!("Associating magnet links with the app"); 134 | if let Err(e) = register_app_class() { 135 | println!("Error writing to registry: {}", e); 136 | } 137 | if let Err(e) = register_magnet_class() { 138 | println!("Error writing to registry: {}", e); 139 | } 140 | } 141 | "autostart" => { 142 | println!("Adding app to auto start"); 143 | if let Err(e) = register_autorun(true) { 144 | println!("Error writing to registry: {}", e); 145 | } 146 | } 147 | "noautostart" => { 148 | println!("Removing app from auto start"); 149 | if let Err(e) = register_autorun(false) { 150 | println!("Error writing to registry: {}", e); 151 | } 152 | } 153 | "getautostart" => { 154 | println!("Checking auto start"); 155 | return check_autorun(); 156 | } 157 | _ => { 158 | println!("Bad app_integration call"); 159 | } 160 | } 161 | false 162 | } 163 | 164 | #[cfg(not(target_os = "windows"))] 165 | pub fn app_integration_impl(_mode: String) -> bool { 166 | false 167 | } 168 | -------------------------------------------------------------------------------- /src-tauri/src/macos.rs: -------------------------------------------------------------------------------- 1 | // TrguiNG - next gen remote GUI for transmission torrent daemon 2 | // Copyright (C) 2023 qu1ck (mail at qu1ck.org) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published 6 | // by the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | // Based on https://github.com/FabianLars/tauri-plugin-deep-link 18 | 19 | use std::{ 20 | io::{ErrorKind, Result}, 21 | sync::Mutex, 22 | }; 23 | 24 | use objc2::{ 25 | class, declare_class, 26 | ffi::NSInteger, 27 | msg_send, msg_send_id, 28 | mutability::Immutable, 29 | rc::Id, 30 | runtime::{NSObject, Object}, 31 | sel, ClassType, 32 | }; 33 | use once_cell::sync::OnceCell; 34 | use tauri::{Menu, MenuItem, Submenu, CustomMenuItem}; 35 | 36 | type THandler = OnceCell) + Send + 'static>>>; 37 | 38 | // If the Mutex turns out to be a problem, or FnMut turns out to be useless, we can remove the Mutex and turn FnMut into Fn 39 | static HANDLER: THandler = OnceCell::new(); 40 | 41 | // keyDirectObject 42 | const KEY_DIRECT_OBJECT: u32 = 0x2d2d2d2d; 43 | 44 | // kInternetEventClass 45 | const GURL_EVENT_CLASS: u32 = 0x4755524c; 46 | // kAEGetURL 47 | const EVENT_GET_URL: u32 = 0x4755524c; 48 | 49 | // kCoreEventClass 50 | const CORE_EVENT_CLASS: u32 = 0x61657674; 51 | // kAEOpenDocuments 52 | const EVENT_OPEN_DOCUMENTS: u32 = 0x6F646F63; 53 | // kAEReopenApplication 54 | const EVENT_REOPEN_APP: u32 = 0x72617070; 55 | 56 | // Adapted from https://github.com/mrmekon/fruitbasket/blob/aad14e400d710d1d46317c0d8c55ff742bfeaadd/src/osx.rs#L848 57 | fn parse_event(event: *mut Object) -> Vec { 58 | if event as u64 == 0u64 { 59 | return vec![]; 60 | } 61 | unsafe { 62 | let class: u32 = msg_send![event, eventClass]; 63 | let id: u32 = msg_send![event, eventID]; 64 | 65 | match (class, id) { 66 | (GURL_EVENT_CLASS, EVENT_GET_URL) => { 67 | let url: *mut Object = 68 | msg_send![event, paramDescriptorForKeyword: KEY_DIRECT_OBJECT]; 69 | let nsstring: *mut Object = msg_send![url, stringValue]; 70 | let cstr: *const i8 = msg_send![nsstring, UTF8String]; 71 | 72 | if !cstr.is_null() { 73 | vec![std::ffi::CStr::from_ptr(cstr).to_string_lossy().to_string()] 74 | } else { 75 | vec![] 76 | } 77 | } 78 | (CORE_EVENT_CLASS, EVENT_OPEN_DOCUMENTS) => { 79 | let documents: *mut Object = 80 | msg_send![event, paramDescriptorForKeyword: KEY_DIRECT_OBJECT]; 81 | let count: NSInteger = msg_send![documents, numberOfItems]; 82 | 83 | let mut paths = Vec::::new(); 84 | 85 | for i in 1..count + 1 { 86 | let path: *mut Object = msg_send![documents, descriptorAtIndex: i]; 87 | let nsstring: *mut Object = msg_send![path, stringValue]; 88 | let cstr: *const i8 = msg_send![nsstring, UTF8String]; 89 | 90 | if !cstr.is_null() { 91 | let path_str = std::ffi::CStr::from_ptr(cstr).to_string_lossy().to_string(); 92 | paths.push(path_str); 93 | } 94 | } 95 | 96 | paths 97 | } 98 | // reopen app event has no useful payload 99 | (_, _) => { 100 | vec![] 101 | } 102 | } 103 | } 104 | } 105 | 106 | declare_class!( 107 | struct Handler; 108 | 109 | unsafe impl ClassType for Handler { 110 | type Super = NSObject; 111 | type Mutability = Immutable; 112 | const NAME: &'static str = "TauriPluginDeepLinkHandler"; 113 | } 114 | 115 | unsafe impl Handler { 116 | #[method(handleEvent:withReplyEvent:)] 117 | fn handle_event(&self, event: *mut Object, _replace: *const Object) { 118 | let s = parse_event(event); 119 | let mut cb = HANDLER.get().unwrap().lock().unwrap(); 120 | cb(s); 121 | } 122 | } 123 | ); 124 | 125 | impl Handler { 126 | pub fn new() -> Id { 127 | let cls = Self::class(); 128 | unsafe { msg_send_id![msg_send_id![cls, alloc], init] } 129 | } 130 | } 131 | 132 | // Call this once early in app main() or setup hook 133 | pub fn set_handler) + Send + 'static>(handler: F) -> Result<()> { 134 | if HANDLER.set(Mutex::new(Box::new(handler))).is_err() { 135 | return Err(std::io::Error::new( 136 | ErrorKind::AlreadyExists, 137 | "Handler was already set", 138 | )); 139 | } 140 | 141 | Ok(()) 142 | } 143 | 144 | fn listen_apple_event(event_class: u32, event_id: u32) { 145 | unsafe { 146 | let event_manager: Id = 147 | msg_send_id![class!(NSAppleEventManager), sharedAppleEventManager]; 148 | 149 | let handler = Handler::new(); 150 | let handler_boxed = Box::into_raw(Box::new(handler)); 151 | 152 | let _: () = msg_send![&event_manager, 153 | setEventHandler: &**handler_boxed 154 | andSelector: sel!(handleEvent:withReplyEvent:) 155 | forEventClass:event_class 156 | andEventID:event_id]; 157 | } 158 | } 159 | 160 | // Call this in app setup hook 161 | pub fn listen_url() { 162 | listen_apple_event(GURL_EVENT_CLASS, EVENT_GET_URL); 163 | } 164 | 165 | // Call this after app is initialised 166 | pub fn listen_open_documents() { 167 | listen_apple_event(CORE_EVENT_CLASS, EVENT_OPEN_DOCUMENTS); 168 | } 169 | 170 | // Call this after app is initialised 171 | pub fn listen_reopen_app() { 172 | listen_apple_event(CORE_EVENT_CLASS, EVENT_REOPEN_APP); 173 | } 174 | 175 | pub fn make_menu(app_name: &str) -> Menu { 176 | let mut menu = Menu::new(); 177 | 178 | menu = menu.add_submenu(Submenu::new( 179 | app_name, 180 | Menu::new() 181 | .add_native_item(MenuItem::CloseWindow) 182 | .add_item(CustomMenuItem::new("quit", "Quit").accelerator("Cmd+q")), 183 | )); 184 | 185 | let mut edit_menu = Menu::new(); 186 | edit_menu = edit_menu.add_native_item(MenuItem::Cut); 187 | edit_menu = edit_menu.add_native_item(MenuItem::Copy); 188 | edit_menu = edit_menu.add_native_item(MenuItem::Paste); 189 | menu = menu.add_submenu(Submenu::new("Edit", edit_menu)); 190 | 191 | menu = menu.add_submenu(Submenu::new( 192 | "View", 193 | Menu::new().add_native_item(MenuItem::EnterFullScreen), 194 | )); 195 | 196 | let mut window_menu = Menu::new(); 197 | window_menu = window_menu.add_native_item(MenuItem::Minimize); 198 | window_menu = window_menu.add_native_item(MenuItem::Zoom); 199 | window_menu = window_menu.add_native_item(MenuItem::Separator); 200 | window_menu = window_menu.add_native_item(MenuItem::CloseWindow); 201 | menu = menu.add_submenu(Submenu::new("Window", window_menu)); 202 | 203 | menu 204 | } 205 | -------------------------------------------------------------------------------- /src-tauri/src/sound.rs: -------------------------------------------------------------------------------- 1 | // TrguiNG - next gen remote GUI for transmission torrent daemon 2 | // Copyright (C) 2023 qu1ck (mail at qu1ck.org) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published 6 | // by the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | use std::io::Cursor; 18 | 19 | use rodio::{OutputStream, Decoder, Sink}; 20 | 21 | static PING: &[u8] = include_bytes!("../sound/ping.mp3"); 22 | 23 | pub fn play_ping() { 24 | if let Ok((_stream, stream_handle)) = OutputStream::try_default() { 25 | let source = Decoder::new(Cursor::new(PING)).unwrap(); 26 | let sink = Sink::try_new(&stream_handle).unwrap(); 27 | sink.append(source); 28 | sink.sleep_until_end(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src-tauri/src/torrentcache.rs: -------------------------------------------------------------------------------- 1 | // TrguiNG - next gen remote GUI for transmission torrent daemon 2 | // Copyright (C) 2023 qu1ck (mail at qu1ck.org) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published 6 | // by the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | use std::{collections::HashMap, io::Read, sync::Arc}; 18 | 19 | use hyper::{body::to_bytes, Body, Response}; 20 | use serde::Deserialize; 21 | use tauri::{ 22 | api::notification::Notification, 23 | async_runtime::{self, Mutex}, 24 | AppHandle, Manager, State, 25 | }; 26 | 27 | use crate::sound::play_ping; 28 | 29 | #[derive(Deserialize, Debug)] 30 | struct Torrent { 31 | id: i64, 32 | name: String, 33 | status: i64, 34 | } 35 | 36 | #[derive(Deserialize, Debug, Default)] 37 | #[serde(default)] 38 | struct Arguments { 39 | torrents: Vec, 40 | } 41 | 42 | #[derive(Deserialize, Debug, Default)] 43 | #[serde(default)] 44 | struct ServerResponse { 45 | result: String, 46 | arguments: Option, 47 | } 48 | 49 | #[derive(Default)] 50 | pub struct TorrentCache { 51 | server_data: HashMap>, 52 | } 53 | 54 | #[derive(Default)] 55 | pub struct TorrentCacheHandle(Arc>); 56 | 57 | pub async fn process_response( 58 | app: &AppHandle, 59 | response: Response, 60 | toast: bool, 61 | sound: bool, 62 | ) -> hyper::Result> { 63 | let status = response.status(); 64 | let headers = response.headers().clone(); 65 | let original_url = headers.get("X-Original-URL").unwrap().to_str().unwrap(); 66 | let version = response.version(); 67 | 68 | let response_bytes = to_bytes(response.into_body()).await?; 69 | let data_bytes; 70 | let mut buf = Vec::new(); 71 | 72 | match headers.get(hyper::header::CONTENT_ENCODING) { 73 | Some(value) => match value.to_str().unwrap().to_lowercase().as_str() { 74 | "deflate" => { 75 | let mut deflater = flate2::bufread::DeflateDecoder::new(response_bytes.as_ref()); 76 | buf.reserve(128 * 1024); 77 | deflater.read_to_end(&mut buf).ok(); 78 | data_bytes = buf.as_slice(); 79 | } 80 | "gzip" => { 81 | let mut unzipper = flate2::bufread::GzDecoder::new(response_bytes.as_ref()); 82 | buf.reserve(128 * 1024); 83 | unzipper.read_to_end(&mut buf).ok(); 84 | data_bytes = buf.as_slice(); 85 | } 86 | encoding => { 87 | println!("Unexpected response encoding: {}", encoding); 88 | data_bytes = response_bytes.as_ref(); 89 | } 90 | }, 91 | None => { 92 | data_bytes = response_bytes.as_ref(); 93 | } 94 | } 95 | 96 | match serde_json::from_slice::(data_bytes) { 97 | Ok(server_response) => { 98 | if server_response.result != "success" { 99 | println!("Server returned error {:?}", server_response.result); 100 | } 101 | match server_response.arguments { 102 | Some(Arguments { torrents }) => { 103 | process_torrents(app, torrents, original_url, toast, sound).await; 104 | } 105 | None => println!("Server returned success but no arguments!"), 106 | } 107 | } 108 | Err(e) => println!("Failed to parse {:?}", e), 109 | }; 110 | 111 | let mut response_builder = Response::builder().status(status).version(version); 112 | headers.iter().for_each(|(name, value)| { 113 | response_builder 114 | .headers_mut() 115 | .unwrap() 116 | .insert(name, value.clone()); 117 | }); 118 | let body = Body::from(response_bytes); 119 | Ok(response_builder.body(body).unwrap()) 120 | } 121 | 122 | async fn process_torrents( 123 | app: &AppHandle, 124 | mut torrents: Vec, 125 | original_url: &str, 126 | toast: bool, 127 | sound: bool, 128 | ) { 129 | // This is a hacky way to determine if details of a single torrent were 130 | // requested or a full update. Proper way would be to inspect the request. 131 | let partial_update = torrents.len() <= 1; 132 | 133 | let mut map = HashMap::::new(); 134 | 135 | torrents.drain(..).for_each(|t| { 136 | map.insert(t.id, t); 137 | }); 138 | 139 | let cache_handle: State = app.state(); 140 | let mut cache = cache_handle.0.lock().await; 141 | 142 | if let Some(old_map) = cache.server_data.get::(original_url) { 143 | let mut play_sound = false; 144 | old_map.iter().for_each(|(id, old_torrent)| { 145 | if let Some(new_torrent) = map.get(id) { 146 | // If status switches from downloading (4) to seeding (6) or queued to seed (5) 147 | // then show a "download complete" notification. 148 | // Also check that torrent name is still the same just in case there was a restart 149 | // since the last pull and the torrent ids are reassigned. 150 | if new_torrent.name == old_torrent.name 151 | && new_torrent.status > 4 152 | && old_torrent.status == 4 153 | { 154 | play_sound = sound; 155 | if toast { 156 | show_notification(app, new_torrent.name.as_str()); 157 | } 158 | } 159 | } else if partial_update { 160 | map.insert( 161 | *id, 162 | Torrent { 163 | id: *id, 164 | name: old_torrent.name.clone(), 165 | status: old_torrent.status, 166 | }, 167 | ); 168 | } 169 | }); 170 | if play_sound { 171 | async_runtime::spawn_blocking(play_ping); 172 | } 173 | } 174 | 175 | cache.server_data.insert(original_url.into(), map); 176 | } 177 | 178 | fn show_notification(app: &AppHandle, name: &str) { 179 | if let Err(e) = Notification::new(app.config().tauri.bundle.identifier.as_str()) 180 | .title("Download complete") 181 | .body(name) 182 | .show() 183 | { 184 | println!("Cannot show notification: {:?}", e); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src-tauri/src/tray.rs: -------------------------------------------------------------------------------- 1 | // TrguiNG - next gen remote GUI for transmission torrent daemon 2 | // Copyright (C) 2023 qu1ck (mail at qu1ck.org) 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published 6 | // by the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | use std::sync::{Arc, Mutex}; 18 | 19 | use tauri::{ 20 | async_runtime, AppHandle, CustomMenuItem, Manager, State, SystemTray, SystemTrayEvent, 21 | SystemTrayMenu, SystemTrayMenuItem, Window, WindowBuilder, 22 | }; 23 | use tokio::sync::oneshot; 24 | 25 | use crate::ListenerHandle; 26 | 27 | pub const TRAY_ID: &str = "tray"; 28 | 29 | pub fn create_tray(app: AppHandle) -> SystemTray { 30 | let hide = CustomMenuItem::new("showhide", "Hide"); 31 | let quit = CustomMenuItem::new("quit", "Quit"); 32 | let tray_menu = SystemTrayMenu::new() 33 | .add_item(hide) 34 | .add_native_item(SystemTrayMenuItem::Separator) 35 | .add_item(quit); 36 | 37 | SystemTray::new() 38 | .with_id(TRAY_ID) 39 | .with_menu(tray_menu) 40 | .on_event(move |event| on_tray_event(&app, event)) 41 | } 42 | 43 | pub fn set_tray_showhide_text(app: &AppHandle, text: &str) { 44 | if let Some(tray) = app.tray_handle_by_id(TRAY_ID) { 45 | tray.get_item("showhide").set_title(text).ok(); 46 | } 47 | } 48 | 49 | fn on_tray_event(app: &AppHandle, event: SystemTrayEvent) { 50 | let main_window = app.get_window("main"); 51 | match event { 52 | SystemTrayEvent::LeftClick { .. } => { 53 | #[cfg(not(target_os = "macos"))] 54 | toggle_main_window(app.clone(), main_window); 55 | } 56 | SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { 57 | "quit" => { 58 | exit(app.clone()); 59 | } 60 | "showhide" => { 61 | toggle_main_window(app.clone(), main_window); 62 | } 63 | _ => {} 64 | }, 65 | _ => {} 66 | } 67 | } 68 | 69 | pub fn toggle_main_window(app: AppHandle, window: Option) { 70 | match window { 71 | Some(window) => { 72 | if !window.is_visible().unwrap() { 73 | window.show().ok(); 74 | window.unminimize().ok(); 75 | window.set_focus().ok(); 76 | window.emit("window-shown", "").ok(); 77 | set_tray_showhide_text(&app, "Hide"); 78 | return; 79 | } 80 | set_tray_showhide_text(&app, "Show"); 81 | async_runtime::spawn(async move { 82 | close_main(window).await; 83 | }); 84 | } 85 | None => { 86 | let window = 87 | WindowBuilder::new(&app, "main", tauri::WindowUrl::App("index.html".into())) 88 | .build() 89 | .unwrap(); 90 | set_tray_showhide_text(&app, "Hide"); 91 | window.set_title("Transmission GUI").ok(); 92 | window.set_focus().ok(); 93 | } 94 | } 95 | } 96 | 97 | pub fn exit(app: AppHandle) { 98 | async_runtime::spawn(async move { 99 | if let Some(window) = app.get_window("main") { 100 | close_main(window).await; 101 | } 102 | 103 | let listener_state: State = app.state(); 104 | let mut listener = listener_state.0.write().await; 105 | println!("Stopping"); 106 | listener.stop(); 107 | app.exit(0); 108 | }); 109 | } 110 | 111 | async fn close_main(window: Window) { 112 | let (tx, rx) = oneshot::channel::<()>(); 113 | let tx = Arc::new(Mutex::new(Some(tx))); 114 | window.listen("frontend-done", move |_| { 115 | if let Some(tx) = tx.lock().unwrap().take() { 116 | tx.send(()).ok(); 117 | } 118 | }); 119 | window.emit("exit-requested", ()).ok(); 120 | rx.await.ok(); 121 | window.close().ok(); 122 | } 123 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "productName": "TrguiNG", 4 | "version": "1.3.0" 5 | }, 6 | "build": { 7 | "distDir": "../dist", 8 | "devPath": "http://localhost:8080/", 9 | "beforeDevCommand": "", 10 | "beforeBuildCommand": "npm run webpack-prod" 11 | }, 12 | "tauri": { 13 | "bundle": { 14 | "active": true, 15 | "targets": [ 16 | "app", 17 | "appimage", 18 | "deb", 19 | "nsis", 20 | "msi" 21 | ], 22 | "identifier": "org.openscopeproject.trguing", 23 | "publisher": "OpenScopeProject", 24 | "icon": [ 25 | "icons/32x32.png", 26 | "icons/128x128.png", 27 | "icons/128x128@2x.png", 28 | "icons/icon.icns", 29 | "icons/icon.ico" 30 | ], 31 | "resources": [ 32 | "dbip.mmdb" 33 | ], 34 | "externalBin": [], 35 | "copyright": "", 36 | "category": "Utility", 37 | "shortDescription": "Transmission torrent daemon remote GUI", 38 | "longDescription": "Remote control GUI for Transmission torrent daemon", 39 | "deb": { 40 | "depends": [ 41 | "libasound2" 42 | ], 43 | "desktopTemplate": "app.desktop" 44 | }, 45 | "macOS": { 46 | "frameworks": [], 47 | "minimumSystemVersion": "", 48 | "exceptionDomain": "", 49 | "signingIdentity": null, 50 | "providerShortName": null, 51 | "entitlements": null 52 | }, 53 | "windows": { 54 | "certificateThumbprint": null, 55 | "digestAlgorithm": "sha256", 56 | "timestampUrl": "", 57 | "nsis": { 58 | "template": "installer.nsi", 59 | "license": "../LICENSE.txt" 60 | } 61 | } 62 | }, 63 | "updater": { 64 | "active": false 65 | }, 66 | "allowlist": { 67 | "clipboard": { 68 | "writeText": true 69 | }, 70 | "fs": { 71 | "scope": [ 72 | "$CONFIG/trguing.json" 73 | ], 74 | "readFile": true, 75 | "writeFile": true 76 | }, 77 | "path": { 78 | "all": true 79 | }, 80 | "shell": { 81 | "open": "^(/)|([a-zA-Z]:[\\\\/])|(\\\\\\\\)|((file:|https?:)//)" 82 | }, 83 | "window": { 84 | "setTitle": true, 85 | "close": true, 86 | "create": true, 87 | "setSize": true, 88 | "setPosition": true, 89 | "setFocus": true, 90 | "center": true, 91 | "hide": true, 92 | "show": true, 93 | "unminimize": true 94 | }, 95 | "dialog": { 96 | "all": true 97 | } 98 | }, 99 | "windows": [ 100 | { 101 | "title": "Transmission GUI", 102 | "width": 1024, 103 | "height": 800, 104 | "resizable": true, 105 | "fullscreen": false, 106 | "visible": false 107 | } 108 | ], 109 | "security": { 110 | "csp": null 111 | }, 112 | "cli": { 113 | "description": "TrguiNG - Remote control GUI for the Transmission torrent daemon", 114 | "args": [ 115 | { 116 | "name": "torrent", 117 | "index": 1, 118 | "multiple": true, 119 | "takesValue": true, 120 | "description": "torrent file or magnet link url" 121 | } 122 | ] 123 | }, 124 | "systemTray": { 125 | "iconPath": "icons/icon.png", 126 | "iconAsTemplate": false 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/clientmanager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TrguiNG - next gen remote GUI for transmission torrent daemon 3 | * Copyright (C) 2023 qu1ck (mail at qu1ck.org) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import type { ServerConfig, Config } from "./config"; 20 | import { TransmissionClient } from "./rpc/client"; 21 | 22 | export class ClientManager { 23 | clients = new Map(); 24 | config: Config; 25 | 26 | constructor(config: Config) { 27 | this.config = config; 28 | } 29 | 30 | open(server: string, toastNotifications: boolean, toastNotificationSound: boolean) { 31 | if (this.clients.has(server)) return; 32 | 33 | const serverConfig = this.config.getServer(server) as ServerConfig; 34 | const client = new TransmissionClient(serverConfig.connection, toastNotifications, toastNotificationSound); 35 | 36 | this.clients.set(server, client); 37 | } 38 | 39 | close(server: string) { 40 | this.clients.delete(server); 41 | } 42 | 43 | getClient(server: string) { 44 | return this.clients.get(server) as TransmissionClient; 45 | } 46 | 47 | getHostname(server: string) { 48 | return this.getClient(server).hostname; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/app.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * TrguiNG - next gen remote GUI for transmission torrent daemon 3 | * Copyright (C) 2023 qu1ck (mail at qu1ck.org) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { ConfigContext, ServerConfigContext } from "../config"; 20 | import type { ServerConfig } from "../config"; 21 | import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; 22 | import { Server } from "../components/server"; 23 | import { ClientManager } from "../clientmanager"; 24 | import { ActionIcon, Box, Button, Flex, Stack, useMantineColorScheme } from "@mantine/core"; 25 | import { QueryClientProvider } from "@tanstack/react-query"; 26 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 27 | import { queryClient } from "queries"; 28 | import { Notifications } from "@mantine/notifications"; 29 | import { ClientContext } from "rpc/client"; 30 | import type { ServerTabsRef } from "./servertabs"; 31 | import { ServerTabs } from "./servertabs"; 32 | import { useDisclosure, useHotkeys } from "@mantine/hooks"; 33 | import * as Icon from "react-bootstrap-icons"; 34 | import { modKeyString } from "trutil"; 35 | import { ColorSchemeToggle, FontSizeToggle, ShowVersion } from "./miscbuttons"; 36 | import { AppSettingsModal } from "./modals/settings"; 37 | 38 | const { appWindow, invoke, makeCreateTorrentView } = await import(/* webpackChunkName: "taurishim" */"taurishim"); 39 | 40 | interface PassEventData { 41 | from: string, 42 | payload: string, 43 | } 44 | 45 | function CreateTorrentButton() { 46 | const config = useContext(ConfigContext); 47 | const { colorScheme } = useMantineColorScheme(); 48 | 49 | useEffect(() => { 50 | const unlisten = appWindow.listen("pass-from-window", ({ payload: data }) => { 51 | if (data.payload === "ready") { 52 | void invoke("pass_to_window", { 53 | to: data.from, 54 | payload: JSON.stringify({ colorScheme, defaultTrackers: config.values.interface.defaultTrackers }), 55 | }); 56 | } 57 | }); 58 | return () => { void unlisten.then((u) => { u(); }); }; 59 | }, [colorScheme, config]); 60 | 61 | const onClick = useCallback(() => { 62 | void makeCreateTorrentView(); 63 | }, []); 64 | 65 | useHotkeys([ 66 | ["mod + T", onClick], 67 | ]); 68 | 69 | return ( 70 | 77 | 78 | 79 | ); 80 | } 81 | 82 | export function App(props: React.PropsWithChildren) { 83 | return ( 84 | 85 | 86 | {props.children} 87 | 88 | 89 | ); 90 | } 91 | 92 | export default function TauriApp() { 93 | const config = useContext(ConfigContext); 94 | const clientManager = useMemo(() => { 95 | const cm = new ClientManager(config); 96 | config.getOpenTabs().forEach((tab) => { 97 | cm.open(tab, config.values.app.toastNotifications, config.values.app.toastNotificationSound); 98 | }); 99 | return cm; 100 | }, [config]); 101 | 102 | const tabsRef = useRef(null); 103 | 104 | const [currentServer, setCurrentServer] = useState( 105 | config.getServer(config.getLastOpenTab())); 106 | const [servers, setServers] = useState(config.getServers()); 107 | 108 | const [showServerConfig, serverConfigHandlers] = useDisclosure(false); 109 | 110 | const onServerSave = useCallback((servers: ServerConfig[]) => { 111 | setServers(servers); 112 | config.setServers(servers); 113 | }, [config]); 114 | 115 | return ( 116 | 117 | 120 | 121 | 125 | 126 | 127 | 128 | 129 | 133 | 134 | 135 | 136 | {currentServer !== undefined 137 | ? 138 | 139 | 140 | 141 | 142 | : 143 | 144 | {servers.map((s, i) => { 145 | return ; 148 | })} 149 | 150 | 153 | 154 | 155 | } 156 | 157 | 158 | ); 159 | } 160 | -------------------------------------------------------------------------------- /src/components/colorchooser.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * TrguiNG - next gen remote GUI for transmission torrent daemon 3 | * Copyright (C) 2023 qu1ck (mail at qu1ck.org) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { ActionIcon, ColorSwatch, Grid, Popover, useMantineTheme } from "@mantine/core"; 20 | import type { ColorSetting } from "config"; 21 | import React, { useState } from "react"; 22 | 23 | interface ColorChooserProps { 24 | value: ColorSetting, 25 | onChange: (value: ColorSetting | undefined) => void, 26 | } 27 | 28 | const shades = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 29 | 30 | export default function ColorChooser(props: ColorChooserProps) { 31 | const theme = useMantineTheme(); 32 | const [opened, setOpened] = useState(false); 33 | const swatchOutline = theme.colorScheme === "dark" ? theme.colors.gray[7] : theme.colors.dark[6]; 34 | 35 | return ( 36 | 37 | 38 | { setOpened((o) => !o); }}> 39 | 42 | 43 | 44 | 45 | { 46 | props.onChange(undefined); 47 | setOpened(false); 48 | }}> 49 | 还原 50 | 51 | 52 | {Object.keys(theme.colors).map((color) => shades.map((shade) => ( 53 | 54 | { 55 | props.onChange({ color, shade, computed: theme.colors[color][shade] }); 56 | setOpened(false); 57 | }}> 58 | 59 | 60 | 61 | )))} 62 | 63 | 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/components/contextmenu.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * TrguiNG - next gen remote GUI for transmission torrent daemon 3 | * Copyright (C) 2023 qu1ck (mail at qu1ck.org) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import type { MenuProps, PortalProps } from "@mantine/core"; 20 | import { Button, Menu, Portal, ScrollArea } from "@mantine/core"; 21 | import React, { useCallback, useEffect, useState } from "react"; 22 | 23 | export interface ContextMenuInfo { 24 | x: number, 25 | y: number, 26 | opened: boolean, 27 | } 28 | 29 | export function useContextMenu(): [ContextMenuInfo, React.Dispatch, React.MouseEventHandler] { 30 | const [info, setInfo] = useState({ x: 0, y: 0, opened: false }); 31 | 32 | const contextMenuHandler = useCallback>((e) => { 33 | e.preventDefault(); 34 | e.stopPropagation(); 35 | setInfo({ x: e.clientX, y: e.clientY, opened: true }); 36 | }, [setInfo]); 37 | 38 | return [info, setInfo, contextMenuHandler]; 39 | } 40 | 41 | export interface ContextMenuProps extends MenuProps { 42 | contextMenuInfo: ContextMenuInfo, 43 | containerRef?: PortalProps["innerRef"], 44 | closeOnClickOutside?: boolean, 45 | setContextMenuInfo: (i: ContextMenuInfo) => void, 46 | } 47 | 48 | export function ContextMenu({ 49 | contextMenuInfo, 50 | containerRef, 51 | closeOnClickOutside = true, 52 | setContextMenuInfo, 53 | children, 54 | ...other 55 | }: ContextMenuProps) { 56 | const onClose = useCallback( 57 | () => { setContextMenuInfo({ ...contextMenuInfo, opened: false }); }, 58 | [contextMenuInfo, setContextMenuInfo]); 59 | 60 | const [opened, setOpened] = useState(false); 61 | 62 | useEffect(() => { setOpened(contextMenuInfo.opened); }, [contextMenuInfo.opened]); 63 | 64 | return ( 65 | 73 | 74 | 75 | 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/components/fileicon.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * TrguiNG - next gen remote GUI for transmission torrent daemon 3 | * Copyright (C) 2023 qu1ck (mail at qu1ck.org) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import * as Icon from "react-bootstrap-icons"; 20 | import React from "react"; 21 | import { useMantineTheme, type DefaultMantineColor } from "@mantine/core"; 22 | import { useRowSelected } from "./tables/common"; 23 | 24 | interface FileType { 25 | icon: React.FunctionComponent, 26 | color: DefaultMantineColor, 27 | extensions: Readonly, 28 | } 29 | 30 | const fileTypes: Readonly = [ 31 | // Video 32 | { 33 | icon: Icon.Film, 34 | extensions: [ 35 | "3gp", "avi", "flv", "m4v", "mkv", "mov", "mp4", "mpeg", 36 | "mpg", "rm", "swf", "vob", "webm", "wmv"], 37 | color: "grape", 38 | }, 39 | // Audio 40 | { 41 | icon: Icon.MusicNoteBeamed, 42 | extensions: ["aac", "aif", "cda", "flac", "m4a", "mid", "midi", "mp3", "mpa", "ogg", "wav", "wma", "wpl"], 43 | color: "cyan", 44 | }, 45 | // Image 46 | { 47 | icon: Icon.FileEarmarkImage, 48 | extensions: ["ai", "bmp", "gif", "ico", "jpg", "jpeg", "png", "ps", "psd", "svg", "tif", "tiff", "webp"], 49 | color: "yellow", 50 | }, 51 | // Archive 52 | { 53 | icon: Icon.FileZip, 54 | extensions: [ 55 | "7z", "arc", "arj", "bz", "bz2", "cbz", "cbr", "deb", "gz", 56 | "pkg", "rar", "rpm", "tar", "z", "zip"], 57 | color: "lime", 58 | }, 59 | // Binary 60 | { 61 | icon: Icon.FileEarmarkBinary, 62 | extensions: ["exe", "bin", "dmg", "dll", "apk", "jar", "msi", "sys", "cab"], 63 | color: "red", 64 | }, 65 | // Text/doc 66 | { 67 | icon: Icon.FileEarmarkText, 68 | extensions: ["doc", "docx", "rtf", "txt", "md", "adoc", "ass", "epub", "mobi", "fb2"], 69 | color: "gray", 70 | }, 71 | // Disc image 72 | { 73 | icon: Icon.Disc, 74 | extensions: ["iso", "vcd", "toast", "mdf", "nrg", "img"], 75 | color: "cyan", 76 | }, 77 | ] as const; 78 | 79 | const extensions = fileTypes.reduce>((v, c) => { 80 | for (const ext of c.extensions) v[ext] = c; 81 | return v; 82 | }, {}); 83 | 84 | export function FileIcon({ name }: { name: string }) { 85 | const theme = useMantineTheme(); 86 | const selected = useRowSelected(); 87 | const ext = name.substring(name.lastIndexOf(".") + 1).toLowerCase(); 88 | const FileIcon = Object.prototype.hasOwnProperty.call(extensions, ext) ? extensions[ext].icon : Icon.FileEarmark; 89 | const color = Object.prototype.hasOwnProperty.call(extensions, ext) ? extensions[ext].color : "gray"; 90 | const shade = (theme.colorScheme === "dark" || selected) ? 3 : 9; 91 | 92 | return ; 93 | } 94 | -------------------------------------------------------------------------------- /src/components/mantinetheme.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * TrguiNG - next gen remote GUI for transmission torrent daemon 3 | * Copyright (C) 2023 qu1ck (mail at qu1ck.org) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { ColorSchemeProvider, Global, MantineProvider, useMantineTheme } from "@mantine/core"; 20 | import type { ColorScheme, MantineThemeOverride } from "@mantine/core"; 21 | import { useColorScheme } from "@mantine/hooks"; 22 | import { ConfigContext } from "config"; 23 | import { FontsizeContextProvider, GlobalStyleOverridesContext, useFontSize, useGlobalStyleOverrides } from "themehooks"; 24 | import React, { useCallback, useContext, useEffect, useMemo, useState } from "react"; 25 | 26 | const Theme: (colorScheme: ColorScheme, font?: string) => MantineThemeOverride = (colorScheme, font) => ({ 27 | colorScheme, 28 | fontFamily: font ?? "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif", 29 | headings: { 30 | fontFamily: font ?? "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif", 31 | }, 32 | components: { 33 | Table: { 34 | styles: { 35 | root: { 36 | "& tbody tr td": { 37 | padding: "0 0.5rem", 38 | }, 39 | }, 40 | }, 41 | }, 42 | Checkbox: { 43 | styles: { 44 | input: { 45 | ".selected &": { 46 | boxShadow: "inset 0 0 3px 0 white", 47 | }, 48 | }, 49 | }, 50 | }, 51 | Modal: { 52 | styles: { 53 | header: { 54 | paddingRight: "0.5rem", 55 | }, 56 | }, 57 | }, 58 | }, 59 | colors: { 60 | turquoise: ["#dcfdff", "#b2f4fd", "#85ebf9", "#58e3f6", "#36d9f3", "#25c0d9", "#1696aa", "#066b7a", "#00404a", "#00171b"], 61 | }, 62 | spacing: { 63 | xs: "0.3rem", 64 | sm: "0.4rem", 65 | md: "0.5rem", 66 | lg: "0.7rem", 67 | xl: "1rem", 68 | }, 69 | }); 70 | 71 | function GlobalStyles() { 72 | const theme = useMantineTheme(); 73 | const fontSize = useFontSize(); 74 | const { style } = useGlobalStyleOverrides(); 75 | const overrides = style[theme.colorScheme]; 76 | 77 | return ( 78 | ({ 79 | html: { 80 | fontSize: `${fontSize.value}em`, 81 | }, 82 | body: { 83 | color: overrides.color === undefined 84 | ? undefined 85 | : theme.colors[overrides.color.color][overrides.color.shade], 86 | backgroundColor: overrides.backgroundColor === undefined 87 | ? undefined 88 | : theme.colors[overrides.backgroundColor.color][overrides.backgroundColor.shade], 89 | }, 90 | "::-webkit-scrollbar": { 91 | width: "0.75em", 92 | height: "0.75em", 93 | }, 94 | "::-webkit-scrollbar-track": { 95 | backgroundColor: theme.colorScheme === "dark" 96 | ? theme.fn.rgba(theme.colors.gray[7], 0.4) 97 | : theme.fn.rgba(theme.colors.gray[5], 0.4), 98 | }, 99 | "::-webkit-scrollbar-thumb": { 100 | backgroundColor: theme.colorScheme === "dark" 101 | ? theme.colors.gray[7] 102 | : theme.colors.gray[5], 103 | borderRadius: "0.375em", 104 | minHeight: "3rem", 105 | minWidth: "3rem", 106 | }, 107 | "::-webkit-scrollbar-thumb:hover": { 108 | backgroundColor: theme.colorScheme === "dark" 109 | ? theme.colors.gray[6] 110 | : theme.colors.gray[6], 111 | border: "1px solid #77777744", 112 | }, 113 | "::-webkit-scrollbar-corner": { 114 | backgroundColor: theme.colorScheme === "dark" 115 | ? theme.fn.rgba(theme.colors.gray[7], 0.5) 116 | : theme.fn.rgba(theme.colors.gray[5], 0.5), 117 | }, 118 | "svg:not(:root)": { 119 | overflow: "visible", 120 | }, 121 | })} /> 122 | ); 123 | } 124 | 125 | export default function CustomMantineProvider({ children }: { children: React.ReactNode }) { 126 | const config = useContext(ConfigContext); 127 | 128 | const preferredColorScheme = useColorScheme(); 129 | const [colorScheme, setColorScheme] = useState( 130 | config.getTheme() ?? preferredColorScheme); 131 | 132 | const toggleColorScheme = useCallback((value?: ColorScheme) => { 133 | value = value ?? (colorScheme === "dark" ? "light" : "dark"); 134 | config.setTheme(value); 135 | setColorScheme(value); 136 | }, [config, colorScheme]); 137 | 138 | const [style, setStyle] = useState(config.values.interface.styleOverrides); 139 | 140 | useEffect(() => { 141 | config.values.interface.styleOverrides = style; 142 | }, [config, style]); 143 | 144 | const theme = useMemo(() => { 145 | return Theme(colorScheme, style.font); 146 | }, [colorScheme, style.font]); 147 | 148 | return ( 149 | 150 | 151 | 152 | 153 | 154 | {children} 155 | 156 | 157 | 158 | 159 | ); 160 | } 161 | -------------------------------------------------------------------------------- /src/components/miscbuttons.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * TrguiNG - next gen remote GUI for transmission torrent daemon 3 | * Copyright (C) 2023 qu1ck (mail at qu1ck.org) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import type { MantineNumberSize } from "@mantine/core"; 20 | import { ActionIcon, useMantineColorScheme } from "@mantine/core"; 21 | import * as Icon from "react-bootstrap-icons"; 22 | import FontSizeIcon from "svg/icons/fontsize.svg"; 23 | import React from "react"; 24 | import { VersionModal } from "components/modals/version"; 25 | import { useDisclosure, useHotkeys } from "@mantine/hooks"; 26 | import { modKeyString } from "trutil"; 27 | import { useFontSize } from "themehooks"; 28 | 29 | export function ColorSchemeToggle(props: { sz?: string, btn?: MantineNumberSize }) { 30 | const { colorScheme, toggleColorScheme } = useMantineColorScheme(); 31 | const dark = colorScheme === "dark"; 32 | 33 | useHotkeys([ 34 | ["mod + U", () => { toggleColorScheme(); }], 35 | ]); 36 | 37 | return ( 38 | { toggleColorScheme(); }} 42 | title={`切换主题 (${modKeyString()} + U)`} 43 | my="auto" 44 | > 45 | {dark 46 | ? 47 | : } 48 | 49 | ); 50 | } 51 | 52 | export function ShowVersion(props: { sz?: string, btn?: MantineNumberSize }) { 53 | const [showVersionModal, { open: openVersionModal, close: closeVersionModal }] = useDisclosure(false); 54 | 55 | return ( 56 | <> 57 | 58 | 64 | 65 | 66 | 67 | ); 68 | } 69 | 70 | export function FontSizeToggle(props: { sz?: string, btn?: MantineNumberSize }) { 71 | const { toggle } = useFontSize(); 72 | 73 | useHotkeys([ 74 | ["mod + =", () => { toggle(); }], 75 | ]); 76 | 77 | return ( 78 | { toggle(); }} 82 | title={`调整字体大小 (${modKeyString()} + =)`} 83 | my="auto" 84 | > 85 | 86 | 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/components/modals/editlabels.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * TrguiNG - next gen remote GUI for transmission torrent daemon 3 | * Copyright (C) 2023 qu1ck (mail at qu1ck.org) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { Box, Text } from "@mantine/core"; 20 | import type { ModalState } from "./common"; 21 | import { SaveCancelModal, TorrentLabels, TorrentsNames } from "./common"; 22 | import React, { useCallback, useEffect, useState } from "react"; 23 | import { useMutateTorrent } from "queries"; 24 | import { notifications } from "@mantine/notifications"; 25 | import { useServerRpcVersion, useServerSelectedTorrents, useServerTorrentData } from "rpc/torrent"; 26 | 27 | export function EditLabelsModal(props: ModalState) { 28 | const { opened, close } = props; 29 | const serverData = useServerTorrentData(); 30 | const serverSelected = useServerSelectedTorrents(); 31 | const rpcVersion = useServerRpcVersion(); 32 | const [labels, setLabels] = useState([]); 33 | 34 | const calculateInitialLabels = useCallback(() => { 35 | const selected = serverData.torrents.filter( 36 | (t) => serverSelected.has(t.id)) ?? []; 37 | const labels: string[] = []; 38 | selected.forEach((t) => t.labels?.forEach((l: string) => { 39 | if (!labels.includes(l)) labels.push(l); 40 | })); 41 | return labels; 42 | }, [serverData.torrents, serverSelected]); 43 | 44 | useEffect(() => { 45 | if (opened) setLabels(calculateInitialLabels()); 46 | }, [calculateInitialLabels, opened]); 47 | 48 | const { mutate } = useMutateTorrent(); 49 | 50 | const onSave = useCallback(() => { 51 | if (rpcVersion < 16) { 52 | notifications.show({ 53 | title: "用户标签设置失败", 54 | message: "用户标签设置仅支持 >= transmission 3.0", 55 | color: "red", 56 | }); 57 | close(); 58 | return; 59 | } 60 | mutate( 61 | { 62 | torrentIds: Array.from(serverSelected), 63 | fields: { labels }, 64 | }, 65 | { 66 | onSuccess: () => { 67 | notifications.show({ 68 | message: "用户标签已更新", 69 | color: "green", 70 | }); 71 | }, 72 | onError: (error) => { 73 | notifications.show({ 74 | title: "用户标签设置失败", 75 | message: String(error), 76 | color: "red", 77 | }); 78 | }, 79 | }, 80 | ); 81 | close(); 82 | }, [rpcVersion, mutate, serverSelected, labels, close]); 83 | 84 | return <> 85 | {props.opened && 86 | 94 | {rpcVersion < 16 95 | ? 标签功能仅支持 transmission 3.0 或以上版本 96 | : <> 97 | 输入新的用户标签 98 | 99 | } 100 | 101 | 102 | 103 | } 104 | ; 105 | } 106 | -------------------------------------------------------------------------------- /src/components/modals/edittrackers.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * TrguiNG - next gen remote GUI for transmission torrent daemon 3 | * Copyright (C) 2023 qu1ck (mail at qu1ck.org) 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import React, { useCallback, useContext, useEffect, useMemo } from "react"; 20 | import type { ModalState } from "./common"; 21 | import { SaveCancelModal, TorrentsNames } from "./common"; 22 | import { useForm } from "@mantine/form"; 23 | import { useMutateTorrent, useTorrentDetails } from "queries"; 24 | import { notifications } from "@mantine/notifications"; 25 | import { Button, Grid, LoadingOverlay, Text, Textarea } from "@mantine/core"; 26 | import { ConfigContext } from "config"; 27 | import type { TrackerStats } from "rpc/torrent"; 28 | import { useServerRpcVersion, useServerSelectedTorrents, useServerTorrentData } from "rpc/torrent"; 29 | 30 | interface FormValues { 31 | trackerList: string, 32 | } 33 | 34 | export function EditTrackers(props: ModalState) { 35 | const rpcVersion = useServerRpcVersion(); 36 | const config = useContext(ConfigContext); 37 | const serverData = useServerTorrentData(); 38 | const selected = useServerSelectedTorrents(); 39 | 40 | const torrentId = useMemo(() => { 41 | if (serverData.current === undefined || !selected.has(serverData.current)) { 42 | return [...selected][0]; 43 | } 44 | return serverData.current; 45 | }, [selected, serverData]); 46 | 47 | const { data: torrent, isLoading } = useTorrentDetails( 48 | torrentId ?? -1, torrentId !== undefined && props.opened, false, true); 49 | 50 | const form = useForm({}); 51 | 52 | const { setValues } = form; 53 | useEffect(() => { 54 | if (torrent === undefined) return; 55 | setValues({ 56 | trackerList: rpcVersion >= 17 57 | ? torrent.trackerList 58 | : torrent.trackerStats.map((s: TrackerStats) => s.announce).join("\n"), 59 | }); 60 | }, [rpcVersion, setValues, torrent]); 61 | 62 | const { mutate } = useMutateTorrent(); 63 | 64 | const onSave = useCallback(() => { 65 | if (torrentId === undefined || torrent === undefined) return; 66 | const dedupedTrackers: string[] = []; 67 | for (const tracker of form.values.trackerList.split("\n")) { 68 | if (tracker !== "" && dedupedTrackers.includes(tracker)) continue; 69 | if (tracker === "" && dedupedTrackers.length === 0) continue; 70 | if (tracker === "" && dedupedTrackers[dedupedTrackers.length - 1] === "") continue; 71 | dedupedTrackers.push(tracker); 72 | } 73 | let toAdd; 74 | let toRemove; 75 | if (rpcVersion < 17) { 76 | const trackers = dedupedTrackers.filter((s) => s !== ""); 77 | const currentTrackers = Object.fromEntries( 78 | torrent.trackerStats.map((s: TrackerStats) => [s.announce, s.id])); 79 | 80 | toAdd = trackers.filter((t) => !Object.prototype.hasOwnProperty.call(currentTrackers, t)); 81 | toRemove = (torrent.trackerStats as TrackerStats[]) 82 | .filter((s: TrackerStats) => !trackers.includes(s.announce)) 83 | .map((s: TrackerStats) => s.id as number); 84 | if (toAdd.length === 0) toAdd = undefined; 85 | if (toRemove.length === 0) toRemove = undefined; 86 | } 87 | mutate( 88 | { 89 | torrentIds: [...selected], 90 | fields: { 91 | trackerList: dedupedTrackers.join("\n"), 92 | trackerAdd: toAdd, 93 | trackerRemove: toRemove, 94 | }, 95 | }, 96 | { 97 | onError: (e) => { 98 | console.error("Failed to update torrent properties", e); 99 | notifications.show({ 100 | message: "Error updating torrent", 101 | color: "red", 102 | }); 103 | }, 104 | }, 105 | ); 106 | props.close(); 107 | }, [torrentId, torrent, rpcVersion, mutate, selected, form.values, props]); 108 | 109 | const addDefaultTrackers = useCallback(() => { 110 | let list = form.values.trackerList; 111 | if (!list.endsWith("\n")) list += "\n"; 112 | list += config.values.interface.defaultTrackers.join("\n"); 113 | form.setFieldValue("trackerList", list); 114 | }, [config, form]); 115 | 116 | return <>{props.opened && 117 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | Tracker列表,一行一个 133 | 134 | 135 | 136 | 137 | 138 |