├── .env.development
├── .github
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── config.yml
│ ├── feature_request.yml
│ └── wiki.yml
├── dependabot.yaml
├── pull_request_template.md
└── workflows
│ ├── build-release.yml
│ ├── codeql-analysis.yml
│ ├── notify-release.yml
│ ├── test.yml
│ └── translations.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .release-please-manifest.json
├── .vscode
└── extensions.json
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── VueTorrent-logo.png
├── docker-compose.yml
├── eslint.config.mjs
├── index.html
├── package-lock.json
├── package.json
├── public
├── apple-touch-icon.png
├── favicon.ico
├── icon-192.png
├── icon-512.png
├── icon.svg
├── manifest.webmanifest
├── registerSW.js
├── robots.txt
├── screenshots
│ ├── screenshot-desktop-dark-mode.jpeg
│ ├── screenshot-desktop.jpeg
│ ├── screenshot-mobile-dark-mode.jpeg
│ ├── screenshot-mobile-navbar-dark-mode.jpeg
│ ├── screenshot-mobile-navbar.jpeg
│ └── screenshot-mobile.jpeg
└── sw.js
├── release-please-config.json
├── src
├── App.vue
├── components
│ ├── AddPanel.vue
│ ├── Core
│ │ ├── ColoredChip.spec.ts
│ │ ├── ColoredChip.vue
│ │ ├── DataCard.spec.ts
│ │ ├── DataCard.vue
│ │ ├── HistoryField.vue
│ │ ├── MixedButton.spec.ts
│ │ ├── MixedButton.vue
│ │ ├── PasswordField.vue
│ │ ├── RightClickMenu
│ │ │ ├── RightClickMenu.vue
│ │ │ ├── RightClickMenuEntry.vue
│ │ │ └── index.ts
│ │ ├── ServerPathField.vue
│ │ ├── SpeedCard.vue
│ │ └── StringCard.vue
│ ├── Dashboard
│ │ ├── DashboardItems
│ │ │ ├── ItemAmount.vue
│ │ │ ├── ItemBoolean.vue
│ │ │ ├── ItemChip.vue
│ │ │ ├── ItemData.vue
│ │ │ ├── ItemDateTime.vue
│ │ │ ├── ItemDuration.vue
│ │ │ ├── ItemPercent.vue
│ │ │ ├── ItemRelativeTime.vue
│ │ │ ├── ItemSpeed.vue
│ │ │ └── ItemText.vue
│ │ ├── RightClick.vue
│ │ ├── Toolbar.vue
│ │ └── Views
│ │ │ ├── Grid
│ │ │ ├── GridTorrent.vue
│ │ │ └── GridView.vue
│ │ │ ├── List
│ │ │ ├── ListTorrent.vue
│ │ │ └── ListView.vue
│ │ │ └── Table
│ │ │ ├── DashboardItems
│ │ │ ├── ItemAmount.vue
│ │ │ ├── ItemBoolean.vue
│ │ │ ├── ItemChip.vue
│ │ │ ├── ItemData.vue
│ │ │ ├── ItemDateTime.vue
│ │ │ ├── ItemDuration.vue
│ │ │ ├── ItemPercent.vue
│ │ │ ├── ItemRelativeTime.vue
│ │ │ ├── ItemSpeed.vue
│ │ │ └── ItemText.vue
│ │ │ ├── Header.vue
│ │ │ ├── TableTorrent.vue
│ │ │ └── TableView.vue
│ ├── Dialogs
│ │ ├── AddTorrentDialog.vue
│ │ ├── AddTorrentParamsDialog.vue
│ │ ├── AddTorrentParamsForm.vue
│ │ ├── BulkRenameFilesDialog.vue
│ │ ├── BulkUpdateTrackers
│ │ │ ├── BulkUpdateTrackersDialog.vue
│ │ │ ├── TrackerEditRow.vue
│ │ │ └── TrackersEditField.vue
│ │ ├── CategoryFormDialog.vue
│ │ ├── ConfirmDeleteDialog.vue
│ │ ├── ConfirmShutdownDialog.vue
│ │ ├── ConnectionStatusDialog.vue
│ │ ├── ContentFilterDialog.vue
│ │ ├── ImportSettingsDialog.vue
│ │ ├── MoveTorrentDialog.vue
│ │ ├── MoveTorrentFileDialog.vue
│ │ ├── PluginManagerDialog.vue
│ │ ├── RenameTorrentDialog.vue
│ │ ├── RssFeedDialog.vue
│ │ ├── RssRuleDialog.vue
│ │ ├── SampleDialog.vue.template
│ │ ├── ShareLimitDialog.vue
│ │ ├── SpeedLimitDialog.vue
│ │ ├── TagFormDialog.vue
│ │ └── TorrentCreatorFormDialog.vue
│ ├── DnDZone.vue
│ ├── Navbar
│ │ ├── Navbar.vue
│ │ ├── SideWidgets
│ │ │ ├── BottomActions.vue
│ │ │ ├── CurrentSpeed.vue
│ │ │ ├── FilterSelect.vue
│ │ │ ├── FreeSpace.vue
│ │ │ ├── SpeedGraph.vue
│ │ │ └── TransferStats.vue
│ │ └── TopWidgets
│ │ │ ├── ActiveFilters.vue
│ │ │ ├── TopActions.vue
│ │ │ ├── TopContainer.vue
│ │ │ └── TopOverflow.vue
│ ├── RSS
│ │ ├── Feeds
│ │ │ ├── Article.vue
│ │ │ ├── ArticleList.vue
│ │ │ ├── Feed.vue
│ │ │ ├── FeedIcon.vue
│ │ │ ├── FeedList.vue
│ │ │ └── Feeds.vue
│ │ └── Rules
│ │ │ ├── Rule.vue
│ │ │ └── Rules.vue
│ ├── Settings
│ │ ├── Advanced.vue
│ │ ├── Behavior.vue
│ │ ├── BitTorrent.vue
│ │ ├── Connection.vue
│ │ ├── Downloads.vue
│ │ ├── RSS.vue
│ │ ├── Speed.vue
│ │ ├── TagsAndCategories.vue
│ │ ├── VueTorrent
│ │ │ ├── DashboardItem.vue
│ │ │ ├── General.vue
│ │ │ └── TorrentCard
│ │ │ │ ├── Grid.vue
│ │ │ │ ├── List.vue
│ │ │ │ └── Table.vue
│ │ ├── WebUI.vue
│ │ └── addons
│ │ │ └── EnhancedEdition.vue
│ ├── TorrentDetail
│ │ ├── Content
│ │ │ ├── Content.vue
│ │ │ ├── ContentNode.vue
│ │ │ └── index.ts
│ │ ├── Info
│ │ │ ├── Info.vue
│ │ │ ├── InfoBase.vue
│ │ │ ├── PanelBoolean.vue
│ │ │ ├── PanelData.vue
│ │ │ ├── PanelDatetime.vue
│ │ │ ├── PanelDuration.vue
│ │ │ ├── PanelLongText.vue
│ │ │ ├── PanelSpeed.vue
│ │ │ └── PanelText.vue
│ │ ├── Overview.vue
│ │ ├── Peers.vue
│ │ ├── PieceCanvas.vue
│ │ ├── TagsAndCategories.vue
│ │ └── Trackers.vue
│ └── TorrentSearchbar.vue
├── composables
│ ├── ArrayPagination.ts
│ ├── BackendSync.ts
│ ├── Dialog.ts
│ ├── SearchQuery.spec.ts
│ ├── SearchQuery.ts
│ ├── TorrentBuilder.ts
│ ├── TreeBuilder.ts
│ ├── i18n.ts
│ └── index.ts
├── constants
│ ├── qbit
│ │ ├── AppPreferences.ts
│ │ ├── ConnectionStatus.ts
│ │ ├── DirectoryContentMode.ts
│ │ ├── FilePriority.ts
│ │ ├── FilterState.ts
│ │ ├── LogType.ts
│ │ ├── PieceState.ts
│ │ ├── TorrentCreatorTaskStatus.ts
│ │ ├── TorrentFormat.ts
│ │ ├── TorrentOperatingMode.ts
│ │ ├── TorrentState.ts
│ │ ├── TrackerStatus.ts
│ │ └── index.ts
│ └── vuetorrent
│ │ ├── Comparators.ts
│ │ ├── DashboardDefaults.ts
│ │ ├── DashboardDisplayMode.ts
│ │ ├── DashboardProperty.ts
│ │ ├── DashboardPropertyType.ts
│ │ ├── FeedState.ts
│ │ ├── FileIcon.ts
│ │ ├── FilterType.ts
│ │ ├── HistoryKey.ts
│ │ ├── ThemeMode.ts
│ │ ├── TitleOptions.ts
│ │ ├── TorrentState.ts
│ │ ├── TrackerSpecialFilter.ts
│ │ └── index.ts
├── globalErrorHandler.ts
├── helpers
│ ├── colors.spec.ts
│ ├── colors.ts
│ ├── comparators.spec.ts
│ ├── comparators.ts
│ ├── data.spec.ts
│ ├── data.ts
│ ├── datetime.spec.ts
│ ├── datetime.ts
│ ├── index.ts
│ ├── number.spec.ts
│ ├── number.ts
│ ├── path.spec.ts
│ ├── path.ts
│ ├── speed.spec.ts
│ ├── speed.ts
│ ├── system.ts
│ ├── text.spec.ts
│ └── text.ts
├── locales
│ ├── cs.json
│ ├── en.json
│ ├── es.json
│ ├── fr.json
│ ├── hu.json
│ ├── index.ts
│ ├── it.json
│ ├── ja.json
│ ├── ko.json
│ ├── nl.json
│ ├── pl.json
│ ├── pt-BR.json
│ ├── ru.json
│ ├── tr.json
│ ├── uk.json
│ ├── zh-Hans.json
│ └── zh-Hant.json
├── main.ts
├── pages
│ ├── Dashboard.vue
│ ├── Login.vue
│ ├── Logs.vue
│ ├── MagnetHandler.vue
│ ├── RssArticles.vue
│ ├── SearchEngine.vue
│ ├── Settings.vue
│ ├── TorrentCreator.vue
│ ├── TorrentDetail.vue
│ └── index.ts
├── plugins
│ ├── dayjs.ts
│ ├── i18n.ts
│ ├── pinia.ts
│ ├── router.ts
│ ├── toastify.ts
│ └── vuetify.ts
├── polyfills.d.ts
├── services
│ ├── Github.ts
│ ├── backend.ts
│ └── qbit
│ │ ├── IProvider.ts
│ │ ├── MockProvider.ts
│ │ ├── QbitProvider.ts
│ │ └── index.ts
├── stores
│ ├── addTorrents.ts
│ ├── app.ts
│ ├── categories.ts
│ ├── content.ts
│ ├── dashboard.ts
│ ├── dialog.ts
│ ├── global.ts
│ ├── history.ts
│ ├── index.ts
│ ├── logs.ts
│ ├── maindata.ts
│ ├── navbar.ts
│ ├── preferences.ts
│ ├── rss.ts
│ ├── searchEngine.ts
│ ├── tags.ts
│ ├── torrentCreator.ts
│ ├── torrentDetail.ts
│ ├── torrents.ts
│ ├── trackers.ts
│ └── vuetorrent.ts
├── styles
│ └── styles.scss
├── themes
│ ├── dark
│ │ ├── legacy.ts
│ │ ├── oled.ts
│ │ └── redesigned.ts
│ ├── global.ts
│ ├── index.ts
│ └── light
│ │ ├── legacy.ts
│ │ └── redesigned.ts
├── types
│ ├── qbit
│ │ ├── models
│ │ │ ├── AddTorrentParams.ts
│ │ │ ├── AppPreferences.ts
│ │ │ ├── BuildInfo.ts
│ │ │ ├── Category.ts
│ │ │ ├── Feed.ts
│ │ │ ├── FeedArticle.ts
│ │ │ ├── FeedRule.ts
│ │ │ ├── Log.ts
│ │ │ ├── Peer.ts
│ │ │ ├── SSLParameters.ts
│ │ │ ├── SearchJob.ts
│ │ │ ├── SearchPlugin.ts
│ │ │ ├── SearchResult.ts
│ │ │ ├── SearchStatus.ts
│ │ │ ├── ServerState.ts
│ │ │ ├── Torrent.ts
│ │ │ ├── TorrentCreatorParams.ts
│ │ │ ├── TorrentCreatorTask.ts
│ │ │ ├── TorrentFile.ts
│ │ │ ├── TorrentProperties.ts
│ │ │ ├── Tracker.ts
│ │ │ └── index.ts
│ │ ├── payloads
│ │ │ ├── AddTorrentPayload.ts
│ │ │ ├── AppPreferencesPayload.ts
│ │ │ ├── CreateFeedPayload.ts
│ │ │ ├── GetTorrentPayload.ts
│ │ │ ├── LoginPayload.ts
│ │ │ ├── PeerLogPayload.ts
│ │ │ └── index.ts
│ │ └── responses
│ │ │ ├── MaindataResponse.ts
│ │ │ ├── PeerLogResponse.ts
│ │ │ ├── SearchResultsResponse.ts
│ │ │ ├── TorrentPeersResponse.ts
│ │ │ └── index.ts
│ └── vuetorrent
│ │ ├── RightClickMenuEntryType.ts
│ │ ├── RightClickProperties.ts
│ │ ├── RssArticle.ts
│ │ ├── SearchData.ts
│ │ ├── SearchResult.ts
│ │ ├── Torrent.ts
│ │ ├── TreeObjects.spec.ts
│ │ ├── TreeObjects.ts
│ │ └── index.ts
└── vite-env.d.ts
├── tests
└── setup.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── write-version.cjs
/.env.development:
--------------------------------------------------------------------------------
1 | VITE_QBITTORRENT_TARGET=http://localhost:8080
2 |
3 | VITE_USE_MOCK_PROVIDER=false
4 | VITE_FAKE_TORRENTS_COUNT=5
5 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @WDaan @Larsluph
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [Larsluph, WDaan]
2 | custom: ["https://www.buymeacoffee.com/wdaan"]
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: Discord
4 | url: https://discord.gg/KDQP7fR467
5 | about: Join the discord server to contact us directly
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Suggest a new feature for this project
3 | title: '[Feature Request]: '
4 | labels:
5 | - triage
6 | - Feature
7 | body:
8 | - type: textarea
9 | id: description
10 | attributes:
11 | label: Description
12 | description: A clear and concise description of the problem or missing capability
13 | validations:
14 | required: true
15 | - type: textarea
16 | id: proposition
17 | attributes:
18 | label: Describe the solution you'd like
19 | description: If you have a solution in mind, please describe it.
20 | - type: textarea
21 | id: alternatives
22 | attributes:
23 | label: Describe alternatives you've considered
24 | description: Have you considered any alternative solutions or workarounds?
25 | - type: checkboxes
26 | attributes:
27 | label: Complementary informations
28 | options:
29 | - label: Is this feature already implemented in the default WebUI?
30 | required: false
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/wiki.yml:
--------------------------------------------------------------------------------
1 | name: Wiki edit proposal
2 | description: Propose changes to the wiki
3 | title: '[Wiki]: '
4 | labels:
5 | - wiki
6 | - triage
7 |
8 | body:
9 | - type: textarea
10 | id: description
11 | attributes:
12 | label: Description
13 | description: Provide which changes this will refer to
14 | validations:
15 | required: true
16 | - type: textarea
17 | id: changes
18 | attributes:
19 | label: Changes
20 | description: |
21 | Attach all changes here.
22 | - For single page modification, include it as text or attach a file, either in patch format or the whole page in markdown.
23 | - For multiple page modifications, always attach a file. It must be a zip of the changes or a single patch file.
24 | validations:
25 | required: true
26 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | # Maintain dependencies for GitHub Actions
4 | - package-ecosystem: 'github-actions'
5 | directory: '/'
6 | schedule:
7 | interval: 'weekly'
8 |
9 | # Maintain dependencies for npm
10 | - package-ecosystem: 'npm'
11 | directory: '/'
12 | schedule:
13 | interval: 'weekly'
14 | groups:
15 | all:
16 | patterns:
17 | - '*'
18 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # My Title [feat/fix/perf/chore]
2 |
3 | Please explain what kind of feature your PR's brings or what kind of bug it fixes. Feel free to include screenshots for UI related changes. Keep all the commits inside your PR
4 | related to topic of the PR, otherwise create a seperate PR. Don't forget to link the related issue! (if applicable)
5 |
6 | ## PR Checklist
7 |
8 | - [ ] I've started from master
9 | - [ ] I've only committed changes related to this PR
10 | - [ ] All tests pass
11 | - [ ] I've removed all commented code
12 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: 'CodeQL'
2 |
3 | on:
4 | schedule:
5 | # At 00:00 on Sunday
6 | - cron: '0 0 * * 0'
7 | push:
8 | branches:
9 | - master
10 | pull_request:
11 | branches:
12 | - master
13 | paths:
14 | - src
15 |
16 | jobs:
17 | analyze:
18 | name: Analyze
19 | runs-on: ubuntu-latest
20 | timeout-minutes: 360
21 |
22 | permissions:
23 | actions: read
24 | contents: read
25 | security-events: write
26 |
27 | strategy:
28 | fail-fast: false
29 | matrix:
30 | language: ['javascript-typescript']
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@v4
34 |
35 | - name: Initialize CodeQL
36 | uses: github/codeql-action/init@v3
37 | with:
38 | languages: ${{ matrix.language }}
39 |
40 | - name: Autobuild
41 | uses: github/codeql-action/autobuild@v3
42 |
43 | - name: Perform CodeQL Analysis
44 | uses: github/codeql-action/analyze@v3
45 | with:
46 | category: '/language:${{matrix.language}}'
47 |
--------------------------------------------------------------------------------
/.github/workflows/notify-release.yml:
--------------------------------------------------------------------------------
1 | name: Notify Discord webhook of stable releases
2 | on:
3 | release:
4 | types:
5 | - released
6 |
7 | jobs:
8 | release-to-discord:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v4
13 | with:
14 | token: ${{ secrets.GITHUB_TOKEN }}
15 |
16 | - name: Send release to Discord Webhook
17 | uses: VueTorrent/release-to-discord@v1
18 | with:
19 | webhook_url: ${{ secrets.WEBHOOK_URL }}
20 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test Core Components
2 | on:
3 | pull_request:
4 | types:
5 | - opened
6 | - ready_for_review
7 | - reopened
8 | - synchronize
9 | branches:
10 | - master
11 |
12 | jobs:
13 | test:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v4
18 |
19 | - name: Setup Node.js
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: '20'
23 |
24 | - name: Build Node.js cache
25 | uses: actions/cache@v4
26 | with:
27 | path: ~/.npm
28 | key: "${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}"
29 | restore-keys: "${{ runner.os }}-node-"
30 |
31 | - name: Install dependencies
32 | run: npm ci
33 |
34 | - name: Run tests
35 | run: npm run test
36 |
37 | - name: Test Build
38 | run: npm run build
39 |
40 | - name: Test lint
41 | run: npm run lint
42 |
--------------------------------------------------------------------------------
/.github/workflows/translations.yml:
--------------------------------------------------------------------------------
1 | name: Update Translations
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | # At 00:00 on Sunday.
7 | - cron: '0 0 * * 0'
8 |
9 | permissions:
10 | contents: write
11 | pull-requests: write
12 |
13 | jobs:
14 | update-translations:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout repository
18 | uses: actions/checkout@v4
19 |
20 | - name: Setup Node.js
21 | uses: actions/setup-node@v4
22 | with:
23 | node-version: '20'
24 |
25 | - name: Build Node.js cache
26 | uses: actions/cache@v4
27 | with:
28 | path: ~/.npm
29 | key: "${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}"
30 | restore-keys: "${{ runner.os }}-node"
31 |
32 | - name: Install dependencies
33 | run: npm ci
34 |
35 | - name: Update locales data
36 | run: |
37 | curl -H 'X-API-KEY: ${{ secrets.TOLGEE_TOKEN }}' 'https://app.tolgee.io/v2/projects/export' -o locales.zip
38 | rm ./src/locales/*
39 | unzip -o -d ./src/locales locales.zip
40 | rm locales.zip
41 |
42 | - name: Update locales metadata
43 | uses: VueTorrent/tolgee-action@v1
44 | with:
45 | tolgee_secret: ${{ secrets.TOLGEE_TOKEN }}
46 |
47 | - name: Lint
48 | run: npm run lint
49 |
50 | - name: Create Pull Request
51 | uses: peter-evans/create-pull-request@v7
52 | with:
53 | base: master
54 | commit-message: 'chore: update translations'
55 | branch: 'update-tolgee-translations'
56 | title: 'chore: update translations'
57 | delete-branch: true
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | # non-essential
11 | node_modules
12 | .DS_Store
13 | dist
14 | dist-ssr
15 | coverage
16 | *.local
17 |
18 | # Editor directories and files
19 | .vscode/*
20 | !.vscode/extensions.json
21 | .idea
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 |
28 | # Custom
29 | vuetorrent/**
30 | vuetorrent-demo/**
31 | docker/**
32 | coverage/
33 | config/
34 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | .release-please-manifest.json
3 | public/
4 | .github/
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSameLine": true,
4 | "bracketSpacing": true,
5 | "endOfLine": "auto",
6 | "htmlWhitespaceSensitivity": "css",
7 | "printWidth": 180,
8 | "proseWrap": "always",
9 | "quoteProps": "as-needed",
10 | "semi": false,
11 | "singleQuote": true,
12 | "tabWidth": 2,
13 | "trailingComma": "none",
14 | "useTabs": false,
15 | "vueIndentScriptAndStyle": false
16 | }
17 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {".":"2.23.0"}
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar"]
3 | }
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM scratch
2 | COPY /vuetorrent /vuetorrent
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # VueRenting
2 |
3 | The sleekest looking WebUI for qBittorrent made with Vue.js!
4 |
5 | ## Development
6 |
7 | - Clone the repo
8 | - `npm install`
9 | - `npm start`
10 | - `npm run lint` (to format the code)
11 | - `docker-compose up -d` (starts a qbittorrent docker, optional)
12 | - Open the WebUI on localhost with the default credentials
13 | - See #1720 for more details
14 | - Make sure WebUI > "Host header validation" is disabled in the qBittorrent preferences
15 | - Edit `env.development` to tweak your dev environment (e.g. mocked data)
16 |
17 | ## Features
18 |
19 | - Renting
20 | - add / remove / pause / resume / rename torrents
21 | - selectively download files
22 | - view info / trackers / peers / content / tags & categories
23 | - search for new torrents straight from the WebUI!
24 | - Keyboard shortcuts!
25 | - Mac keymap is supported (use Cmd instead of Ctrl)
26 | - Press Escape to dismiss any dialogs or to return to Dashboard view
27 | - Dashboard
28 | - Select all torrents with Ctrl-A
29 | - Focus search input with Ctrl-F
30 | - Press again to enable native browser search
31 | - When no dialogs are opened, press Escape to unfocus search input
32 | - Press again to unselect all torrents
33 | - Delete selected torrents with Delete (Fn-Backspace on Mac)
34 | - Ctrl-click on a torrent card to enable multi-select mode
35 | - Hold Shift and click on a torrent card to select all torrents between the last selected torrent and the clicked torrent
36 | - System
37 | - see session stats (down / upload speed, session uploaded / downloaded, free space)
38 | - beautiful transfer graphs
39 | - change the most common settings
40 | - Extra features the default WebUI doesn't have
41 | - mobile friendly! (can be installed as a PWA)
42 | - Configureable Dashboard: choose which torrent properties are shown for both busy and completed torrents
43 | - Optimized for the latest version of qBittorrent
44 | - This is a work in progress, and is not required to use VueTorrent
45 | - Stores server-side settings
46 |
47 | ## Important Information
48 |
49 | VueRenting is a **WebUI** (think of it as a "visual skin") that uses qBittorrent's API, enabling compatibility with automation solutions like the Servarr stack.
50 |
51 | Everything that is compatible with the classic qBittorrent WebUI will work regardless of the WebUI you chose to use, whether its VueTorrent or another one.
52 |
--------------------------------------------------------------------------------
/VueTorrent-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaestroDev05/VueRenting/91dc915054e7881a81c47ff8ce7ad540ea62d0ed/VueTorrent-logo.png
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | qbit:
3 | image: linuxserver/qbittorrent:latest
4 | container_name: qbit
5 | restart: unless-stopped
6 | environment:
7 | - PUID=1000
8 | - PGID=1000
9 | - UMASK_SET=022
10 | - TZ=Europe/London
11 | - WEBUI_PORT=8080
12 | - DOCKER_MODS=ghcr.io/vuetorrent/vuetorrent-lsio-mod:latest
13 | volumes:
14 | - ./docker/config:/config
15 | - ./docker/downloads:/downloads
16 | ports:
17 | - '8080:8080'
18 | - '6881:6881'
19 | - '6881:6881/udp'
20 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from 'globals'
2 | import importPlugin from 'eslint-plugin-import'
3 | import pluginVue from 'eslint-plugin-vue'
4 | import prettier from 'eslint-plugin-prettier'
5 | import vueTsEslintConfig from '@vue/eslint-config-typescript'
6 |
7 | export default [
8 | importPlugin.flatConfigs.recommended,
9 | {
10 | ignores: ['**/.gitignore', '**/.release-please-manifest.json', '**/public/', '**/.github/']
11 | },
12 | {
13 | plugins: {
14 | vue: pluginVue.configs.recommended,
15 | prettier
16 | },
17 | languageOptions: {
18 | globals: {
19 | ...globals.node,
20 | ...globals.browser,
21 | ...globals.es2016
22 | },
23 | ecmaVersion: 2020,
24 | sourceType: 'module'
25 | },
26 | rules: {
27 | ...vueTsEslintConfig.rules,
28 | 'vue/require-default-prop': 'off',
29 | 'vue/multi-word-component-names': 'off',
30 | 'vue/first-attribute-linebreak': 'off',
31 | '@typescript-eslint/explicit-module-boundary-types': 'off',
32 | 'prefer-const': 'off',
33 | 'sort-imports': 'warn'
34 | }
35 | }
36 | ]
37 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | VueTorrent
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vuetorrent",
3 | "private": true,
4 | "version": "2.23.0",
5 | "type": "module",
6 | "scripts": {
7 | "start": "vite",
8 | "dev": "vite",
9 | "build": "vue-tsc && vite build",
10 | "postbuild": "node write-version.cjs",
11 | "build-demo": "vue-tsc && vite build -m demo",
12 | "check-build": "vue-tsc",
13 | "preview": "vite preview",
14 | "preview-demo": "vite preview -m demo",
15 | "lint": "eslint --fix && prettier . -w -u",
16 | "test": "vitest run",
17 | "coverage": "vitest run --coverage"
18 | },
19 | "dependencies": {
20 | "@ctrl/tinycolor": "^4.1.0",
21 | "@faker-js/faker": "^9.5.1",
22 | "@flatten-js/interval-tree": "^1.1.3",
23 | "@fontsource/roboto": "^5.2.5",
24 | "@fontsource/roboto-mono": "^5.2.5",
25 | "@mdi/font": "^7.4.47",
26 | "@mdi/js": "^7.4.47",
27 | "@vueuse/core": "^12.7.0",
28 | "@zip.js/zip.js": "^2.7.57",
29 | "apexcharts": "^4.5.0",
30 | "array.prototype.tosorted": "^1.1.4",
31 | "axios": "^1.8.1",
32 | "dayjs": "^1.11.13",
33 | "lodash.debounce": "^4.0.8",
34 | "pinia": "^2.3.1",
35 | "pinia-persistence-plugin": "^0.0.5",
36 | "pixi.js": "^8.8.1",
37 | "uuid": "^11.1.0",
38 | "vite-plugin-vuetify": "^2.1.0",
39 | "vue": "^3.5.13",
40 | "vue-concurrency": "^5.0.3",
41 | "vue-i18n": "^11.1.1",
42 | "vue-router": "^4.5.0",
43 | "vue3-apexcharts": "^1.8.0",
44 | "vue3-toastify": "^0.2.8",
45 | "vuedraggable": "^4.1.0",
46 | "vuetify": "^3.7.14"
47 | },
48 | "devDependencies": {
49 | "@pinia/testing": "^0.1.7",
50 | "@types/lodash.debounce": "^4.0.9",
51 | "@types/node": "^22.13.9",
52 | "@types/uuid": "^10.0.0",
53 | "@typescript-eslint/eslint-plugin": "^8.26.0",
54 | "@typescript-eslint/parser": "^8.26.0",
55 | "@vitejs/plugin-vue": "^5.2.1",
56 | "@vitest/coverage-v8": "^3.0.7",
57 | "@vue/eslint-config-typescript": "^14.2.0",
58 | "@vue/test-utils": "^2.4.6",
59 | "eslint": "^9.17.0",
60 | "eslint-config-prettier": "^10.0.2",
61 | "eslint-plugin-import": "^2.31.0",
62 | "eslint-plugin-prettier": "^5.2.3",
63 | "eslint-plugin-vue": "^9.32.0",
64 | "globals": "^16.0.0",
65 | "jsdom": "^26.0.0",
66 | "prettier": "^3.5.3",
67 | "sass": "^1.85.1",
68 | "timezone-mock": "^1.3.6",
69 | "typescript": "^5.8.2",
70 | "vite": "^6.2.0",
71 | "vite-plugin-top-level-await": "^1.5.0",
72 | "vitest": "^3.0.5",
73 | "vue-tsc": "^2.2.8"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaestroDev05/VueRenting/91dc915054e7881a81c47ff8ce7ad540ea62d0ed/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaestroDev05/VueRenting/91dc915054e7881a81c47ff8ce7ad540ea62d0ed/public/favicon.ico
--------------------------------------------------------------------------------
/public/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaestroDev05/VueRenting/91dc915054e7881a81c47ff8ce7ad540ea62d0ed/public/icon-192.png
--------------------------------------------------------------------------------
/public/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaestroDev05/VueRenting/91dc915054e7881a81c47ff8ce7ad540ea62d0ed/public/icon-512.png
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/registerSW.js:
--------------------------------------------------------------------------------
1 | "serviceWorker"in navigator&&window.addEventListener("load",(async()=>{await navigator.serviceWorker.register("sw.js")}))
2 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 |
--------------------------------------------------------------------------------
/public/screenshots/screenshot-desktop-dark-mode.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaestroDev05/VueRenting/91dc915054e7881a81c47ff8ce7ad540ea62d0ed/public/screenshots/screenshot-desktop-dark-mode.jpeg
--------------------------------------------------------------------------------
/public/screenshots/screenshot-desktop.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaestroDev05/VueRenting/91dc915054e7881a81c47ff8ce7ad540ea62d0ed/public/screenshots/screenshot-desktop.jpeg
--------------------------------------------------------------------------------
/public/screenshots/screenshot-mobile-dark-mode.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaestroDev05/VueRenting/91dc915054e7881a81c47ff8ce7ad540ea62d0ed/public/screenshots/screenshot-mobile-dark-mode.jpeg
--------------------------------------------------------------------------------
/public/screenshots/screenshot-mobile-navbar-dark-mode.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaestroDev05/VueRenting/91dc915054e7881a81c47ff8ce7ad540ea62d0ed/public/screenshots/screenshot-mobile-navbar-dark-mode.jpeg
--------------------------------------------------------------------------------
/public/screenshots/screenshot-mobile-navbar.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaestroDev05/VueRenting/91dc915054e7881a81c47ff8ce7ad540ea62d0ed/public/screenshots/screenshot-mobile-navbar.jpeg
--------------------------------------------------------------------------------
/public/screenshots/screenshot-mobile.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MaestroDev05/VueRenting/91dc915054e7881a81c47ff8ce7ad540ea62d0ed/public/screenshots/screenshot-mobile.jpeg
--------------------------------------------------------------------------------
/public/sw.js:
--------------------------------------------------------------------------------
1 | self.addEventListener("install",(()=>{})),self.addEventListener("fetch",(()=>{}))
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": {
3 | ".": {
4 | "release-type": "node",
5 | "include-component-in-tag": false,
6 | "changelog-path": "CHANGELOG.md",
7 | "changelog-sections": [
8 | {
9 | "type": "feat",
10 | "section": "Features",
11 | "hidden": false
12 | },
13 | {
14 | "type": "fix",
15 | "section": "Bug Fixes",
16 | "hidden": false
17 | },
18 | {
19 | "type": "perf",
20 | "section": "Improvements",
21 | "hidden": false
22 | }
23 | ]
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/AddPanel.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/components/Core/ColoredChip.vue:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
45 | {{ chipValue }}
46 |
47 |
48 | {{ chipValue }}
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/components/Core/DataCard.spec.ts:
--------------------------------------------------------------------------------
1 | import i18n from '@/plugins/i18n'
2 | import vuetify from '@/plugins/vuetify'
3 | import { createTestingPinia } from '@pinia/testing'
4 | import { mount, VueWrapper } from '@vue/test-utils'
5 | import { beforeEach, describe, expect, it } from 'vitest'
6 |
7 | import DataCard from './DataCard.vue'
8 |
9 | const title = 'Downloaded'
10 | const value = 10000
11 | const color = 'download'
12 |
13 | let wrapper: VueWrapper
14 | describe('DataCard.vue', () => {
15 | beforeEach(() => {
16 | wrapper = mount(DataCard, {
17 | propsData: { title, value, color },
18 | global: {
19 | plugins: [createTestingPinia(), i18n, vuetify]
20 | }
21 | })
22 | })
23 |
24 | it('should render the title', () => {
25 | expect(wrapper.find('[data-testid="card-title"]').text()).toEqual(title)
26 | })
27 |
28 | it('should render value and unit & be formatted', () => {
29 | expect(wrapper.find('[data-testid="card-value"]').exists()).toBe(true)
30 | expect(wrapper.find('[data-testid="card-value"]').text()).toBe('10.0')
31 |
32 | expect(wrapper.find('[data-testid="card-unit"]').exists()).toBe(true)
33 | expect(wrapper.find('[data-testid="card-unit"]').text()).toBe('kB')
34 | })
35 |
36 | it('text should have the passed-in color', () => {
37 | expect(wrapper.find('[data-testid="card-wrapper"]').classes()).toContain('text-' + color)
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/src/components/Core/DataCard.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 | {{ title }}
14 |
15 | {{ formatDataValue(value, vueTorrentStore.useBinarySize) }}
16 | {{ formatDataUnit(value, vueTorrentStore.useBinarySize) }}
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/components/Core/HistoryField.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/Core/MixedButton.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 | {{ icon }}
29 | {{ text }}
30 | {{ icon }}
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/components/Core/PasswordField.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/components/Core/RightClickMenu/RightClickMenu.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/components/Core/RightClickMenu/RightClickMenuEntry.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 | {{ entryData.disabledIcon }}
19 | {{ entryData.icon }}
20 | {{ entryData.disabledText }}
21 | {{ entryData.text }}
22 |
23 | mdi-chevron-right
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/components/Core/RightClickMenu/index.ts:
--------------------------------------------------------------------------------
1 | import RightClickMenu from './RightClickMenu.vue'
2 |
3 | export default RightClickMenu
4 |
--------------------------------------------------------------------------------
/src/components/Core/ServerPathField.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/Core/SpeedCard.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {{ formatSpeedValue(value, vueTorrentStore.useBitSpeed) }}
39 |
40 |
41 |
42 |
43 | {{ formatSpeedUnit(value, vueTorrentStore.useBitSpeed) }}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/components/Core/StringCard.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | {{ title }}
9 |
10 | {{ value }}
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardItems/ItemAmount.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 | {{ $t(titleKey) }}
11 |
12 |
13 | {{ value(torrent) }}
14 | / {{ total(torrent) }}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardItems/ItemBoolean.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 | {{ $t(titleKey) }}
11 |
12 |
13 | mdi-check
14 | mdi-close
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardItems/ItemChip.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 | {{ $t(titleKey) }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardItems/ItemData.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 | {{ $t(titleKey) }}
18 |
19 |
20 | {{ formatDataValue(val, useBinarySize) }}
21 |
22 | {{ formatDataUnit(val, useBinarySize) }}
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardItems/ItemDateTime.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 | {{ $t(titleKey) }}
18 |
19 |
20 |
21 | {{ formatTimeSec(val, dateFormat) }}
22 |
23 | {{ $t('dashboard.not_complete') }}
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardItems/ItemDuration.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 | {{ $t(titleKey) }}
25 |
26 |
27 |
28 | {{ formatDuration(val, props.unit, durationFormat) }}
29 |
30 | {{ $t('common.NA') }}
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardItems/ItemPercent.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | {{ $t(titleKey) }}
15 |
16 |
17 | {{ formatPercent(val) }}
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardItems/ItemRelativeTime.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 | {{ $t(titleKey) }}
12 |
13 |
14 | {{ dayjs(value(torrent) * 1000).fromNow() }}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardItems/ItemSpeed.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 | {{ $t(titleKey) }}
19 |
20 |
21 | {{ formatSpeedValue(val, useBitSpeed) }}
22 |
23 | {{ formatSpeedUnit(val, useBitSpeed) }}
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/components/Dashboard/DashboardItems/ItemText.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 | {{ $t(titleKey) }}
14 |
15 |
{{ val }}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Grid/GridView.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
40 |
41 |
42 |
49 |
50 | $emit('onTorrentClick', e, t)" />
51 |
52 |
53 |
54 |
55 |
56 |
61 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/List/ListView.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
39 |
40 |
41 |
48 |
49 | $emit('onTorrentClick', e, t)" />
50 |
51 |
52 |
53 |
54 |
55 |
60 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/DashboardItems/ItemAmount.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {{ value(torrent) }}
10 | / {{ total(torrent) }}
11 | |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/DashboardItems/ItemBoolean.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | mdi-check
10 | mdi-close
11 | |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/DashboardItems/ItemChip.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/DashboardItems/ItemData.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | {{ formatData(value(torrent), useBinarySize) }} |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/DashboardItems/ItemDateTime.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 | {{ formatTimeSec(val, dateFormat) }}
17 | |
18 | {{ $t('dashboard.not_complete') }} |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/DashboardItems/ItemDuration.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 | {{ formatDuration(val, props.unit, durationFormat) }}
19 | |
20 | {{ $t('common.NA') }} |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/DashboardItems/ItemPercent.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | {{ formatPercent(val) }}
15 |
16 | |
17 |
18 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/DashboardItems/ItemRelativeTime.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 | {{ dayjs(value(torrent) * 1000).fromNow() }} |
10 |
11 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/DashboardItems/ItemSpeed.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | {{ formatSpeed(value(torrent), useBitSpeed) }} |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/DashboardItems/ItemText.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | {{ val }} |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/Header.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 | {{ title }}
24 |
25 |
26 | |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/Dashboard/Views/Table/TableTorrent.vue:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/src/components/Dialogs/AddTorrentParamsDialog.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {{ t('dialogs.add.params.title') }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/Dialogs/BulkUpdateTrackers/TrackerEditRow.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/components/Dialogs/BulkUpdateTrackers/TrackersEditField.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 | updateRow(i, v)" @delete="deleteRow(i)" />
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/Dialogs/ConfirmShutdownDialog.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {{ $t('common.yes') }}
39 |
40 |
41 | {{ $t('common.no') }}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/components/Dialogs/ImportSettingsDialog.vue:
--------------------------------------------------------------------------------
1 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | {{ $t('common.cancel') }}
55 | {{ $t('common.save') }}
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/components/Dialogs/RenameTorrentDialog.vue:
--------------------------------------------------------------------------------
1 |
49 |
50 |
51 |
52 |
53 | {{ $t('dialogs.renameTorrent.title') }}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | {{ $t('common.cancel') }}
63 | {{ $t('common.save') }}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/components/Dialogs/SampleDialog.vue.template:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{ $t('CHANGEME') }}
25 |
26 |
27 |
28 |
29 | CHANGEME
30 |
31 |
32 | {{ $t('common.close') }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/components/Dialogs/SpeedLimitDialog.vue:
--------------------------------------------------------------------------------
1 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
67 |
68 |
69 |
70 |
71 | {{ $t('common.cancel') }}
72 | {{ $t('common.save') }}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/src/components/Navbar/SideWidgets/FreeSpace.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/Navbar/SideWidgets/TransferStats.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 | {{ title }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/Navbar/TopWidgets/TopActions.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/components/Navbar/TopWidgets/TopOverflow.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | mdi-play
19 |
20 |
21 |
22 |
23 |
24 | mdi-pause
25 |
26 |
27 |
28 |
29 |
30 | mdi-delete
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | mdi-search-web
39 |
40 |
41 |
42 |
43 |
44 | mdi-rss
45 |
46 |
47 |
48 |
49 |
50 | mdi-file-plus
51 |
52 |
53 |
54 |
55 |
56 | mdi-file-document-multiple
57 |
58 |
59 |
60 |
61 |
62 | mdi-cog
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/components/RSS/Feeds/Article.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {{ $t('rssArticles.feeds.item.new') }}
26 |
27 | {{ value.title }}
28 |
29 |
30 |
31 | {{ value.parsedDate.toLocaleString() }}
32 | {{ $t('rssArticles.feeds.item.feedName', { name: rssStore.getFeedNames(value.id).join(' | ') }) }}
33 | {{ $t('rssArticles.feeds.item.author', { author: value.author }) }}
34 | {{ $t('rssArticles.feeds.item.category', { category: value.category }) }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/components/RSS/Feeds/ArticleList.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | {{ $t('common.emptyList') }}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/components/RSS/Feeds/Feed.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 | {{ title }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/components/RSS/Feeds/FeedIcon.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/components/RSS/Rules/Rule.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 | {{ value.name }}
29 |
30 |
31 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/components/RSS/Rules/Rules.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
52 |
53 |
--------------------------------------------------------------------------------
/src/components/Settings/VueTorrent/DashboardItem.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 | |
16 |
17 |
23 | |
24 | {{ $t(`torrent.properties.${property.name}`) }} |
25 |
26 |
27 |
28 |
33 |
--------------------------------------------------------------------------------
/src/components/Settings/VueTorrent/TorrentCard/Table.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
31 | {{ $t('settings.vuetorrent.torrentCard.table.tip') }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/components/Settings/addons/EnhancedEdition.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
34 |
35 |
36 |
37 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/components/TorrentDetail/Content/index.ts:
--------------------------------------------------------------------------------
1 | import Content from './Content.vue'
2 |
3 | export default Content
4 |
--------------------------------------------------------------------------------
/src/components/TorrentDetail/Info/Info.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/components/TorrentDetail/Info/InfoBase.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/TorrentDetail/Info/PanelData.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {{ $t(`torrent.properties.${ppt.title}`) }}
32 | {{ formatData(ppt.getter(), useBinarySize) }}
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/TorrentDetail/Info/PanelDatetime.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{ $t(`torrent.properties.${ppt.title}`) }}
28 |
29 | {{ formatTimeSec(ppt.getter(), dateFormat) }}
30 |
31 | {{ $t('common.NA') }}
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/components/TorrentDetail/Info/PanelDuration.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {{ $t(`torrent.properties.${ppt.title}`) }}
29 |
30 | {{ formatDuration(ppt.getter(), ppt.unit, durationFormat) }}
31 |
32 | {{ $t('common.NA') }}
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/TorrentDetail/Info/PanelLongText.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {{ ppt.getter() || $t('common.none') }}
32 |
33 |
34 |
35 |
36 |
37 |
38 | {{ $t('torrent.properties.empty_tags') }}
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/TorrentDetail/Info/PanelSpeed.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {{ $t(`torrent.properties.${ppt.title}`) }}
30 | {{ formatSpeed(ppt.getter(), useBitSpeed) }}
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/TorrentDetail/Info/PanelText.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {{ $t(`torrent.properties.${ppt.title}`) }}
37 | {{ ppt.getter() }}
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/components/TorrentDetail/TagsAndCategories.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {{ $t('torrentDetail.tagsAndCategories.tags') }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {{ $t('torrentDetail.tagsAndCategories.categories') }}
43 |
44 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/src/components/TorrentSearchbar.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/composables/ArrayPagination.ts:
--------------------------------------------------------------------------------
1 | import { useOffsetPagination } from '@vueuse/core'
2 | import { computed, MaybeRef, MaybeRefOrGetter, toValue } from 'vue'
3 |
4 | export function useArrayPagination(items: MaybeRefOrGetter, pageSize: MaybeRefOrGetter, page: MaybeRef = 1) {
5 | const { currentPage, currentPageSize, pageCount, isFirstPage, isLastPage, next, prev } = useOffsetPagination({
6 | total: () => toValue(items).length,
7 | page,
8 | pageSize: () => (toValue(pageSize) === -1 ? toValue(items).length : toValue(pageSize))
9 | })
10 |
11 | const paginatedResults = computed(() => {
12 | const start = (currentPage.value - 1) * toValue(pageSize)
13 | const end = start + currentPageSize.value
14 | return toValue(items).slice(start, end)
15 | })
16 |
17 | return {
18 | currentPage,
19 | currentPageSize,
20 | pageCount,
21 | isFirstPage,
22 | isLastPage,
23 | next,
24 | prev,
25 | paginatedResults
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/composables/BackendSync.ts:
--------------------------------------------------------------------------------
1 | import { isObjectEqual } from '@/helpers'
2 | import { backend } from '@/services/backend'
3 | import { Store } from 'pinia'
4 | import { shallowRef } from 'vue'
5 |
6 | export function useBackendSync(store: Store, key: string, config: { blacklist?: string[]; whitelist?: string[] } = {}) {
7 | const cancelWatcherCallback = shallowRef(() => {})
8 | const _lastState: Record = shallowRef({})
9 |
10 | function keyMatchesFilter(k: string) {
11 | if (config.whitelist) {
12 | return config.whitelist.includes(k)
13 | }
14 | if (config.blacklist) {
15 | return !config.blacklist.includes(k)
16 | }
17 | return true
18 | }
19 |
20 | async function loadState() {
21 | const data = await backend.get(key)
22 | if (!data) return
23 |
24 | const newState = JSON.parse(data) as Record
25 | const temp = {} as Record
26 | Object.entries(newState).forEach(([k, v]) => {
27 | if (keyMatchesFilter(k)) {
28 | temp[k] = v
29 | }
30 | })
31 | store.$patch(temp)
32 | _lastState.value = JSON.parse(JSON.stringify(temp))
33 | }
34 |
35 | async function saveState() {
36 | const state = {} as Record
37 | Object.entries(store.$state).forEach(([k, v]) => {
38 | if (keyMatchesFilter(k)) {
39 | state[k] = v
40 | }
41 | })
42 |
43 | if (!isObjectEqual(state, _lastState.value)) {
44 | const success = await backend.set(key, JSON.stringify(state))
45 | if (success) {
46 | _lastState.value = JSON.parse(JSON.stringify(state))
47 | }
48 | }
49 | }
50 |
51 | function registerWatcher() {
52 | cancelWatcherCallback.value = store.$subscribe(() => {
53 | saveState()
54 | })
55 | }
56 |
57 | function cancelWatcher() {
58 | cancelWatcherCallback.value()
59 | }
60 |
61 | return {
62 | loadState,
63 | saveState,
64 | registerWatcher,
65 | cancelWatcher
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/composables/Dialog.ts:
--------------------------------------------------------------------------------
1 | import { useDialogStore } from '@/stores/dialog'
2 | import { computed, onUnmounted, ref } from 'vue'
3 |
4 | export function useDialog(guid: string) {
5 | const hndlDialog = ref(true)
6 | const dialogStore = useDialogStore()
7 |
8 | const isOpened = computed({
9 | get: () => hndlDialog.value,
10 | set: v => {
11 | hndlDialog.value = v
12 | if (!v) deleteDialog()
13 | }
14 | })
15 |
16 | function deleteDialog() {
17 | setTimeout(() => dialogStore.deleteDialog(guid), 300)
18 | }
19 |
20 | onUnmounted(deleteDialog)
21 |
22 | return { isOpened }
23 | }
24 |
--------------------------------------------------------------------------------
/src/composables/SearchQuery.ts:
--------------------------------------------------------------------------------
1 | import { computed, MaybeRefOrGetter, toValue } from 'vue'
2 |
3 | export function useSearchQuery(
4 | items: MaybeRefOrGetter,
5 | searchQuery: MaybeRefOrGetter,
6 | getter: (item: T) => string | string[],
7 | postProcess?: (items: T[]) => T[]
8 | ) {
9 | const results = computed(() => {
10 | const searchItems = toValue(items) ?? []
11 | const tokens = (toValue(searchQuery) ?? '').trim().toLowerCase().split(/[ ,]/i).filter(Boolean)
12 | const inclusionTokens = tokens.filter(token => !token.startsWith('-'))
13 | const exclusionTokens = tokens.filter(token => token.startsWith('-')).map(token => token.slice(1))
14 | const res = searchItems.filter(item => handleIncludeTokens(item, inclusionTokens) && handleExcludeTokens(item, exclusionTokens))
15 | return postProcess ? postProcess(res) : res
16 | })
17 |
18 | function handleIncludeTokens(item: T, tokens: string[]) {
19 | return tokens.every(token => {
20 | let value = getter(item)
21 | if (!Array.isArray(value)) value = [value]
22 | return value.some(v => v.toLowerCase().indexOf(token) !== -1)
23 | })
24 | }
25 |
26 | function handleExcludeTokens(item: T, tokens: string[]) {
27 | return !tokens.some(token => {
28 | let value = getter(item)
29 | if (!Array.isArray(value)) value = [value]
30 | return value.some(v => v.toLowerCase().indexOf(token) !== -1)
31 | })
32 | }
33 |
34 | return { results }
35 | }
36 |
--------------------------------------------------------------------------------
/src/composables/TreeBuilder.ts:
--------------------------------------------------------------------------------
1 | import { comparators } from '@/helpers'
2 | import { TorrentFile } from '@/types/qbit/models'
3 | import { TreeFile, TreeFolder, TreeNode } from '@/types/vuetorrent'
4 | import toSorted from 'array.prototype.tosorted'
5 | import { computed, MaybeRefOrGetter, shallowRef, toValue, watchEffect } from 'vue'
6 |
7 | function getEmptyRoot() {
8 | return new TreeFolder('(root)', '')
9 | }
10 |
11 | export function useTreeBuilder(items: MaybeRefOrGetter, openedItems: MaybeRefOrGetter>) {
12 | const tree = shallowRef(getEmptyRoot())
13 |
14 | const flatTree = computed(() => {
15 | const flatten = (node: TreeNode, parentPath: string): TreeNode[] => {
16 | const path = parentPath === '' ? node.name : parentPath + '/' + node.name
17 |
18 | if (node.type === 'folder' && toValue(openedItems).has(node.fullName)) {
19 | const children = toSorted(node.children, (a: TreeNode, b: TreeNode) => {
20 | if (a.type === 'folder' && b.type === 'file') return -1
21 | if (a.type === 'file' && b.type === 'folder') return 1
22 | return comparators.textWithNumbers.asc(a.name, b.name)
23 | }).flatMap(el => flatten(el, path))
24 | return [node, ...children]
25 | } else {
26 | return [node]
27 | }
28 | }
29 |
30 | return flatten(tree.value, '')
31 | })
32 |
33 | function buildTree() {
34 | const rootNode = getEmptyRoot()
35 | const files = toValue(items) ?? []
36 |
37 | for (const file of files) {
38 | let cursor = rootNode
39 | file.name
40 | .replace(/\\/g, '/')
41 | .split('/')
42 | .reduce((parentPath, nodeName) => {
43 | const nextPath = parentPath === '' ? nodeName : parentPath + '/' + nodeName
44 |
45 | if (parentPath === file.name.substring(0, file.name.lastIndexOf('/'))) {
46 | cursor.children.push(new TreeFile(file, nodeName))
47 | } else {
48 | const folder = cursor.children.find(el => el.name === nodeName) as TreeFolder | undefined
49 | if (folder) {
50 | cursor = folder
51 | } else {
52 | const newFolder = new TreeFolder(nodeName, nextPath)
53 | cursor.children.push(newFolder)
54 | cursor = newFolder
55 | }
56 | }
57 |
58 | return nextPath
59 | }, '')
60 | }
61 |
62 | tree.value = rootNode
63 |
64 | rootNode.buildCache()
65 | }
66 |
67 | watchEffect(() => {
68 | buildTree()
69 | })
70 |
71 | return { tree, flatTree }
72 | }
73 |
--------------------------------------------------------------------------------
/src/composables/i18n.ts:
--------------------------------------------------------------------------------
1 | import { emojiStateMap, TorrentState } from '@/constants/vuetorrent'
2 | import { getTorrentStateValue } from '@/helpers'
3 | import { useVueTorrentStore } from '@/stores'
4 | import { storeToRefs } from 'pinia'
5 | import { useI18n } from 'vue-i18n'
6 |
7 | export function useI18nUtils() {
8 | const i18n = useI18n()
9 | const { useEmojiState } = storeToRefs(useVueTorrentStore())
10 |
11 | function getTorrentStateString(state: TorrentState) {
12 | const stateString = i18n.t(`torrent.state.${getTorrentStateValue(state)}`)
13 | if (useEmojiState.value) {
14 | return [emojiStateMap[state], stateString].join(' ')
15 | } else {
16 | return stateString
17 | }
18 | }
19 |
20 | return {
21 | ...i18n,
22 | getTorrentStateString
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/composables/index.ts:
--------------------------------------------------------------------------------
1 | import { useArrayPagination } from './ArrayPagination'
2 | import { useBackendSync } from './BackendSync'
3 | import { useDialog } from './Dialog'
4 | import { useI18nUtils } from './i18n'
5 | import { useSearchQuery } from './SearchQuery'
6 | import { useTorrentBuilder } from './TorrentBuilder'
7 | import { useTreeBuilder } from './TreeBuilder'
8 |
9 | export { useArrayPagination, useBackendSync, useDialog, useI18nUtils, useSearchQuery, useTorrentBuilder, useTreeBuilder }
10 |
--------------------------------------------------------------------------------
/src/constants/qbit/AppPreferences.ts:
--------------------------------------------------------------------------------
1 | export enum AutoDeleteMode {
2 | NEVER,
3 | IF_ADDED,
4 | ALWAYS
5 | }
6 |
7 | export enum BitTorrentProtocol {
8 | TCP_uTP,
9 | TCP,
10 | uTP
11 | }
12 |
13 | export enum ContentLayout {
14 | ORIGINAL = 'Original',
15 | SUBFOLDER = 'Subfolder',
16 | NO_SUBFOLDER = 'NoSubfolder'
17 | }
18 |
19 | export enum DynDnsService {
20 | USE_DYNDNS,
21 | USE_NOIP,
22 | USE_NONE = -1
23 | }
24 |
25 | export enum Encryption {
26 | PREFER_ENCRYPTION,
27 | FORCE_ON,
28 | FORCE_OFF
29 | }
30 |
31 | export enum FileLogAgeType {
32 | DAYS,
33 | MONTHS,
34 | YEARS
35 | }
36 |
37 | export enum ShareLimitAction {
38 | DEFAULT = -1,
39 | STOP_TORRENT = 0,
40 | REMOVE_TORRENT = 1,
41 | ENABLE_SUPERSEEDING = 2,
42 | REMOVE_TORRENT_AND_FILES = 3
43 | }
44 |
45 | export enum ProxyType {
46 | NONE = 'None',
47 | SOCKS4 = 'SOCKS4',
48 | SOCKS5 = 'SOCKS5',
49 | HTTP = 'HTTP'
50 | }
51 |
52 | export enum ResumeDataStorageType {
53 | LEGACY = 'Legacy',
54 | SQLITE = 'SQLite'
55 | }
56 |
57 | export enum ScanDirsEnum {
58 | MONITORED_FOLDER,
59 | DEFAULT_SAVE_PATH
60 | }
61 |
62 | export type ScanDirs = ScanDirsEnum | string
63 |
64 | export enum SchedulerDays {
65 | EVERY_DAY,
66 | EVERY_WEEKDAY,
67 | EVERY_WEEKEND,
68 | EVERY_MONDAY,
69 | EVERY_TUESDAY,
70 | EVERY_WEDNESDAY,
71 | EVERY_THURSDAY,
72 | EVERY_FRIDAY,
73 | EVERY_SATURDAY,
74 | EVERY_SUNDAY
75 | }
76 |
77 | export enum StopCondition {
78 | NONE = 'None',
79 | METADATA_RECEIVED = 'MetadataReceived',
80 | FILES_CHECKED = 'FilesChecked'
81 | }
82 |
83 | export enum TorrentContentRemoveOption {
84 | DELETE = 'Delete',
85 | MOVE_TO_TRASH = 'MoveToTrash'
86 | }
87 |
88 | export enum UploadChokingAlgorithm {
89 | ROUND_ROBIN,
90 | FASTEST_UPLOAD,
91 | ANTI_LEECH
92 | }
93 |
94 | export enum UploadSlotsBehavior {
95 | FIXED_SLOTS,
96 | UPLOAD_RATE_BASED
97 | }
98 |
99 | export enum UtpTcpMixedMode {
100 | PREFER_TCP,
101 | PEER_PROPORTIONAL
102 | }
103 |
104 | export enum DiskIOType {
105 | DEFAULT,
106 | MEMORY_MAPPED_FILES,
107 | POSIX_COMPLIANT,
108 | SIMPLE_PREAD_PWRITE
109 | }
110 |
111 | export enum DiskIOMode {
112 | DISABLE_OS_CACHE,
113 | ENABLE_OS_CACHE,
114 | WRITE_THROUGH
115 | }
116 |
--------------------------------------------------------------------------------
/src/constants/qbit/ConnectionStatus.ts:
--------------------------------------------------------------------------------
1 | export enum ConnectionStatus {
2 | CONNECTED = 'connected',
3 | FIREWALLED = 'firewalled',
4 | DISCONNECTED = 'disconnected',
5 | UNKNOWN = 'unknown'
6 | }
7 |
--------------------------------------------------------------------------------
/src/constants/qbit/DirectoryContentMode.ts:
--------------------------------------------------------------------------------
1 | export enum DirectoryContentMode {
2 | ALL = 'all',
3 | ONLY_FILES = 'files',
4 | ONLY_DIRECTORIES = 'dirs'
5 | }
6 |
--------------------------------------------------------------------------------
/src/constants/qbit/FilePriority.ts:
--------------------------------------------------------------------------------
1 | export enum FilePriority {
2 | MIXED = -1,
3 | DO_NOT_DOWNLOAD = 0,
4 | NORMAL = 1,
5 | HIGH = 6,
6 | MAXIMAL = 7
7 | }
8 |
--------------------------------------------------------------------------------
/src/constants/qbit/FilterState.ts:
--------------------------------------------------------------------------------
1 | export enum FilterState {
2 | ALL = 'all',
3 | DOWNLOADING = 'downloading',
4 | SEEDING = 'seeding',
5 | COMPLETED = 'completed',
6 | /** @deprecated since 5.X, use stopped instead */
7 | PAUSED = 'paused',
8 | STOPPED = 'stopped',
9 | /** @deprecated since 5.X, use running instead */
10 | RESUMED = 'resumed',
11 | RUNNING = 'running',
12 | ACTIVE = 'active',
13 | INACTIVE = 'inactive',
14 | STALLED = 'stalled',
15 | STALLED_UPLOADING = 'stalled_uploading',
16 | STALLED_DOWNLOADING = 'stalled_downloading',
17 | CHECKING = 'checking',
18 | MOVING = 'moving',
19 | ERRORED = 'errored'
20 | }
21 |
--------------------------------------------------------------------------------
/src/constants/qbit/LogType.ts:
--------------------------------------------------------------------------------
1 | export enum LogType {
2 | NONE = 0,
3 | NORMAL = 1 << 0,
4 | INFO = 1 << 1,
5 | WARNING = 1 << 2,
6 | CRITICAL = 1 << 3,
7 | ALL = NORMAL | INFO | WARNING | CRITICAL
8 | }
9 |
--------------------------------------------------------------------------------
/src/constants/qbit/PieceState.ts:
--------------------------------------------------------------------------------
1 | export enum PieceState {
2 | MISSING = 0,
3 | DOWNLOADING = 1,
4 | DOWNLOADED = 2
5 | }
6 |
--------------------------------------------------------------------------------
/src/constants/qbit/TorrentCreatorTaskStatus.ts:
--------------------------------------------------------------------------------
1 | export enum TorrentCreatorTaskStatus {
2 | FAILED = 'Failed',
3 | QUEUED = 'Queued',
4 | RUNNING = 'Running',
5 | FINISHED = 'Finished'
6 | }
7 |
--------------------------------------------------------------------------------
/src/constants/qbit/TorrentFormat.ts:
--------------------------------------------------------------------------------
1 | export enum TorrentFormat {
2 | V1 = 'v1',
3 | V2 = 'v2',
4 | HYBRID = 'hybrid'
5 | }
6 |
--------------------------------------------------------------------------------
/src/constants/qbit/TorrentOperatingMode.ts:
--------------------------------------------------------------------------------
1 | export enum TorrentOperatingMode {
2 | AUTO_MANAGED = 'AutoManaged',
3 | FORCED = 'Forced'
4 | }
5 |
--------------------------------------------------------------------------------
/src/constants/qbit/TorrentState.ts:
--------------------------------------------------------------------------------
1 | export enum TorrentState {
2 | /** Torrent has just started downloading and is fetching metadata */
3 | META_DL = 'metaDL',
4 | /** Torrent is forced to fetch metadata */
5 | FORCED_META_DL = 'forcedMetaDL',
6 | /** Torrent is forced to downloading to ignore queue limit */
7 | FORCED_DL = 'forcedDL',
8 | /** Torrent is being downloaded and data is being transferred */
9 | DOWNLOADING = 'downloading',
10 | /** Torrent is being downloaded, but no connection were made */
11 | STALLED_DL = 'stalledDL',
12 | /** Torrent is stopped and has NOT finished downloading
13 | * @deprecated since 5.X, use STOPPED_DL instead
14 | */
15 | PAUSED_DL = 'pausedDL',
16 | /** Torrent is stopped and has NOT finished downloading */
17 | STOPPED_DL = 'stoppedDL',
18 | /** Queuing is enabled and torrent is queued for download */
19 | QUEUED_DL = 'queuedDL',
20 | /** Torrent is forced to uploading and ignore queue limit */
21 | FORCED_UP = 'forcedUP',
22 | /** Torrent is being seeded and data is being transferred */
23 | UPLOADING = 'uploading',
24 | /** Torrent is being seeded, but no connection were made */
25 | STALLED_UP = 'stalledUP',
26 | /**
27 | * Torrent is stopped and has finished downloading
28 | * @deprecated since 5.X, use `STOPPED_UP` instead
29 | */
30 | PAUSED_UP = 'pausedUP',
31 | /** Torrent is stopped and has finished downloading */
32 | STOPPED_UP = 'stoppedUP',
33 | /** Queuing is enabled and torrent is queued for upload */
34 | QUEUED_UP = 'queuedUP',
35 | /** Same as checkingUP, but torrent has NOT finished downloading */
36 | CHECKING_DL = 'checkingDL',
37 | /** Torrent has finished downloading and is being checked */
38 | CHECKING_UP = 'checkingUP',
39 | /** Checking resume data on qBt startup */
40 | CHECKING_RESUME_DATA = 'checkingResumeData',
41 | /** Torrent is allocating disk space for download
42 | * @deprecated since 4.4.0, libtorrent 2.X
43 | */
44 | ALLOCATING = 'allocating',
45 | /** Torrent is moving to another location */
46 | MOVING = 'moving',
47 | /** Torrent data files is missing */
48 | MISSING_FILES = 'missingFiles',
49 | /** Some error occurred, applies to stopped torrents */
50 | ERROR = 'error',
51 | /** Unknown status */
52 | UNKNOWN = 'unknown'
53 | }
54 |
--------------------------------------------------------------------------------
/src/constants/qbit/TrackerStatus.ts:
--------------------------------------------------------------------------------
1 | export enum TrackerStatus {
2 | /** Tracker is disabled (used for DHT, PeX, and LSD) */
3 | DISABLED = 0,
4 | /** Tracker has not been contacted yet */
5 | NOT_YET_CONTACTED = 1,
6 | /** Tracker has been contacted and is working */
7 | WORKING = 2,
8 | /** Tracker is updating */
9 | UPDATING = 3,
10 | /** Tracker has been contacted, but it is not working (or doesn't send proper replies) */
11 | NOT_WORKING = 4
12 | }
13 |
--------------------------------------------------------------------------------
/src/constants/qbit/index.ts:
--------------------------------------------------------------------------------
1 | import * as AppPreferences from './AppPreferences'
2 | import { ConnectionStatus } from './ConnectionStatus'
3 | import { DirectoryContentMode } from './DirectoryContentMode'
4 | import { FilePriority } from './FilePriority'
5 | import { FilterState } from './FilterState'
6 | import { LogType } from './LogType'
7 | import { PieceState } from './PieceState'
8 | import { TorrentCreatorTaskStatus } from './TorrentCreatorTaskStatus'
9 | import { TorrentFormat } from './TorrentFormat'
10 | import { TorrentOperatingMode } from './TorrentOperatingMode'
11 | import { TorrentState } from './TorrentState'
12 | import { TrackerStatus } from './TrackerStatus'
13 |
14 | export {
15 | AppPreferences,
16 | ConnectionStatus,
17 | DirectoryContentMode,
18 | FilterState,
19 | LogType,
20 | PieceState,
21 | FilePriority,
22 | TrackerStatus,
23 | TorrentCreatorTaskStatus,
24 | TorrentFormat,
25 | TorrentOperatingMode,
26 | TorrentState
27 | }
28 |
--------------------------------------------------------------------------------
/src/constants/vuetorrent/DashboardDisplayMode.ts:
--------------------------------------------------------------------------------
1 | export enum DashboardDisplayMode {
2 | LIST = 'list',
3 | GRID = 'grid',
4 | TABLE = 'table'
5 | }
6 |
--------------------------------------------------------------------------------
/src/constants/vuetorrent/DashboardProperty.ts:
--------------------------------------------------------------------------------
1 | export enum DashboardProperty {
2 | ADDED_ON = 'added_on',
3 | AMOUNT_LEFT = 'amount_left',
4 | AUTO_TMM = 'auto_tmm',
5 | AVAILABILITY = 'availability',
6 | AVG_DOWNLOAD_SPEED = 'avg_download_speed',
7 | AVG_UPLOAD_SPEED = 'avg_upload_speed',
8 | BASENAME_CONTENT_PATH = 'basename_content_path',
9 | BASENAME_DOWNLOAD_PATH = 'basename_download_path',
10 | BASENAME_SAVE_PATH = 'basename_save_path',
11 | CATEGORY = 'category',
12 | COMMENT = 'comment',
13 | COMPLETED_ON = 'completed_on',
14 | CONTENT_PATH = 'content_path',
15 | DOWNLOAD_LIMIT = 'download_limit',
16 | DOWNLOAD_PATH = 'download_path',
17 | DOWNLOAD_SPEED = 'download_speed',
18 | DOWNLOADED = 'downloaded',
19 | DOWNLOADED_SESSION = 'downloaded_session',
20 | ETA = 'eta',
21 | FIRST_LAST_PIECE_PRIORITY = 'f_l_piece_prio',
22 | FORCED = 'forced',
23 | GLOBAL_SPEED = 'global_speed',
24 | GLOBAL_VOLUME = 'global_volume',
25 | HAS_METADATA = 'has_metadata',
26 | HASH = 'hash',
27 | INACTIVE_SEEDING_TIME_LIMIT = 'inactive_seeding_time_limit',
28 | INFOHASH_V1 = 'infohash_v1',
29 | INFOHASH_V2 = 'infohash_v2',
30 | LAST_ACTIVITY = 'last_activity',
31 | MAGNET = 'magnet',
32 | PEERS = 'peers',
33 | POPULARITY = 'popularity',
34 | PRIORITY = 'priority',
35 | PRIVATE = 'private',
36 | PROGRESS = 'progress',
37 | RATIO = 'ratio',
38 | RATIO_LIMIT = 'ratio_limit',
39 | REANNOUNCE = 'reannounce',
40 | ROOT_PATH = 'root_path',
41 | SAVE_PATH = 'save_path',
42 | SEEDING_TIME = 'seeding_time',
43 | SEEDING_TIME_LIMIT = 'seeding_time_limit',
44 | SEEDS = 'seeds',
45 | SEEN_COMPLETE = 'seen_complete',
46 | SEQUENTIAL_DOWNLOADS = 'seq_dl',
47 | SIZE = 'size',
48 | STATE = 'state',
49 | SUPER_SEEDING = 'super_seeding',
50 | TAGS = 'tags',
51 | TIME_ACTIVE = 'time_active',
52 | TOTAL_SIZE = 'total_size',
53 | TRACKER = 'tracker',
54 | TRACKERS_COUNT = 'trackers_count',
55 | TRUNCATED_HASH = 'truncated_hash',
56 | UPLOAD_LIMIT = 'upload_limit',
57 | UPLOAD_SPEED = 'upload_speed',
58 | UPLOADED = 'uploaded',
59 | UPLOADED_SESSION = 'uploaded_session'
60 | }
61 |
--------------------------------------------------------------------------------
/src/constants/vuetorrent/DashboardPropertyType.ts:
--------------------------------------------------------------------------------
1 | export enum DashboardPropertyType {
2 | AMOUNT = 'amount',
3 | BOOLEAN = 'boolean',
4 | CHIP = 'chip',
5 | DATA = 'data',
6 | DATETIME = 'datetime',
7 | DURATION = 'duration',
8 | PERCENT = 'percent',
9 | RELATIVE = 'relative',
10 | SPEED = 'speed',
11 | TEXT = 'text'
12 | }
13 |
--------------------------------------------------------------------------------
/src/constants/vuetorrent/FeedState.ts:
--------------------------------------------------------------------------------
1 | /** RSS Feed state values ordered by priority of display */
2 | export enum FeedState {
3 | /** Feed is updating */
4 | LOADING,
5 | /** Feed has encountered an error */
6 | ERROR,
7 | /** At least one article is unread */
8 | UNREAD,
9 | /** All articles are read */
10 | READ
11 | }
12 |
--------------------------------------------------------------------------------
/src/constants/vuetorrent/FileIcon.ts:
--------------------------------------------------------------------------------
1 | enum FileIcon {
2 | PDF = 'mdi-file-pdf-box',
3 | IMAGE = 'mdi-file-image',
4 | DOCUMENT = 'mdi-file-document',
5 | INFORMATION = 'mdi-information-variant-box',
6 | MUSIC = 'mdi-music',
7 | VIDEO = 'mdi-movie',
8 | SUBTITLE = 'mdi-subtitles',
9 | ARCHIVE = 'mdi-zip-box-outline',
10 | EXECUTABLE = 'mdi-application-brackets'
11 | }
12 |
13 | export const typesMap: Record = {
14 | pdf: FileIcon.PDF,
15 |
16 | png: FileIcon.IMAGE,
17 | jpg: FileIcon.IMAGE,
18 | jpeg: FileIcon.IMAGE,
19 | tiff: FileIcon.IMAGE,
20 |
21 | doc: FileIcon.DOCUMENT,
22 | docx: FileIcon.DOCUMENT,
23 | txt: FileIcon.DOCUMENT,
24 |
25 | nfo: FileIcon.INFORMATION,
26 |
27 | mp3: FileIcon.MUSIC,
28 | wav: FileIcon.MUSIC,
29 | flac: FileIcon.MUSIC,
30 |
31 | avi: FileIcon.VIDEO,
32 | mp4: FileIcon.VIDEO,
33 | mkv: FileIcon.VIDEO,
34 | mov: FileIcon.VIDEO,
35 | wmv: FileIcon.VIDEO,
36 |
37 | srt: FileIcon.SUBTITLE,
38 | idx: FileIcon.SUBTITLE,
39 | sub: FileIcon.SUBTITLE,
40 |
41 | rar: FileIcon.ARCHIVE,
42 | zip: FileIcon.ARCHIVE,
43 | gz: FileIcon.ARCHIVE,
44 | '7z': FileIcon.ARCHIVE,
45 | iso: FileIcon.ARCHIVE,
46 |
47 | exe: FileIcon.EXECUTABLE,
48 | msi: FileIcon.EXECUTABLE,
49 | dmg: FileIcon.EXECUTABLE,
50 | deb: FileIcon.EXECUTABLE,
51 | jar: FileIcon.EXECUTABLE
52 | }
53 |
54 | export function getFileIcon(filename: string) {
55 | const type = filename.split('.').pop()?.toLowerCase() || ''
56 | return typesMap[type] || 'mdi-file'
57 | }
58 |
--------------------------------------------------------------------------------
/src/constants/vuetorrent/FilterType.ts:
--------------------------------------------------------------------------------
1 | export enum FilterType {
2 | /**
3 | * Applies conjunctive filtering
4 | *
5 | * Conjunctive refers to a connection or "and" relation between statements.
6 | * Every filters MUST match for the data to match
7 | */
8 | CONJUNCTIVE,
9 | /**
10 | * Applies disjunctive filtering
11 | *
12 | * Disjunctive denotes an "either/or" condition.
13 | * Any filter must match for the data to match
14 | */
15 | DISJUNCTIVE
16 | }
17 |
--------------------------------------------------------------------------------
/src/constants/vuetorrent/HistoryKey.ts:
--------------------------------------------------------------------------------
1 | export enum HistoryKey {
2 | COOKIE = 'cookie',
3 | SEARCH_ENGINE_QUERY = 'searchEngineQuery',
4 | TORRENT_PATH = 'torrentPath',
5 | BULK_RENAME_REGEXP = 'bulkRenameRegexp',
6 | BULK_RENAME_TARGET = 'bulkRenameTarget'
7 | }
8 |
--------------------------------------------------------------------------------
/src/constants/vuetorrent/ThemeMode.ts:
--------------------------------------------------------------------------------
1 | export enum ThemeMode {
2 | LIGHT = 'light',
3 | DARK = 'dark',
4 | SYSTEM = 'system'
5 | }
6 |
--------------------------------------------------------------------------------
/src/constants/vuetorrent/TitleOptions.ts:
--------------------------------------------------------------------------------
1 | export enum TitleOptions {
2 | DEFAULT,
3 | GLOBAL_SPEED,
4 | FIRST_TORRENT_STATUS,
5 | CUSTOM
6 | }
7 |
--------------------------------------------------------------------------------
/src/constants/vuetorrent/TrackerSpecialFilter.ts:
--------------------------------------------------------------------------------
1 | export enum TrackerSpecialFilter {
2 | UNTRACKED = 0,
3 | NOT_WORKING = 1
4 | }
5 |
--------------------------------------------------------------------------------
/src/constants/vuetorrent/index.ts:
--------------------------------------------------------------------------------
1 | import comparatorMap from './Comparators'
2 | import type { PropertyData, PropertyMetadata, TorrentProperty } from './DashboardDefaults'
3 | import { propsData, propsMetadata } from './DashboardDefaults'
4 | import { DashboardDisplayMode } from './DashboardDisplayMode'
5 | import { DashboardProperty } from './DashboardProperty'
6 | import { DashboardPropertyType } from './DashboardPropertyType'
7 | import { FeedState } from './FeedState'
8 | import { getFileIcon, typesMap } from './FileIcon'
9 | import { FilterType } from './FilterType'
10 | import { HistoryKey } from './HistoryKey'
11 | import { ThemeMode } from './ThemeMode'
12 | import { TitleOptions } from './TitleOptions'
13 | import { TorrentState, emojiStateMap, stateQbitToVt, stateVtToQbit } from './TorrentState'
14 | import { TrackerSpecialFilter } from './TrackerSpecialFilter'
15 |
16 | const defaultDateFormat = 'YYYY-MM-DD HH:mm:ss'
17 | const defaultDurationFormat = 'Y[Y] M[M] D[d] H[h] m[m] s[s]'
18 |
19 | export {
20 | comparatorMap,
21 | TorrentProperty,
22 | PropertyData,
23 | PropertyMetadata,
24 | propsData,
25 | propsMetadata,
26 | DashboardDisplayMode,
27 | DashboardProperty,
28 | DashboardPropertyType,
29 | FeedState,
30 | getFileIcon,
31 | typesMap,
32 | FilterType,
33 | HistoryKey,
34 | ThemeMode,
35 | TitleOptions,
36 | TorrentState,
37 | emojiStateMap,
38 | stateQbitToVt,
39 | stateVtToQbit,
40 | TrackerSpecialFilter,
41 | defaultDateFormat,
42 | defaultDurationFormat
43 | }
44 |
--------------------------------------------------------------------------------
/src/globalErrorHandler.ts:
--------------------------------------------------------------------------------
1 | window.onerror = function (msg, url, line, col, error) {
2 | let extra = ''
3 |
4 | // only add 'col' and 'error' if they are available
5 | col && (extra += '\ncolumn: ' + col)
6 | error && (extra += '\nerror: ' + error)
7 |
8 | const final_message = 'Error: ' + msg + '\nurl: ' + url + '\nline: ' + line + extra
9 |
10 | if (sessionStorage.getItem('vuetorrent_mounted') === 'true') {
11 | console.error(final_message)
12 | } else {
13 | alert(final_message)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/helpers/colors.ts:
--------------------------------------------------------------------------------
1 | import { TorrentState } from '@/constants/vuetorrent'
2 | import { TinyColor, random } from '@ctrl/tinycolor'
3 |
4 | function djb2Hash(str: string): number {
5 | let hash = 5381
6 | for (let i = 0; i < str.length; i++) {
7 | hash = (hash * 33) ^ str.charCodeAt(i)
8 | }
9 | return hash >>> 0 // ensure non-negative integer
10 | }
11 |
12 | export function getColorFromName(name: string, transform?: (color: TinyColor) => TinyColor) {
13 | const color = random({
14 | seed: djb2Hash(name)
15 | })
16 |
17 | if (transform) return transform(color).toHexString()
18 | else return color.toHexString()
19 | }
20 |
21 | export function getRatioColor(ratio: number) {
22 | if (ratio < 0.5) return 'text-ratio-bad'
23 | if (ratio < 1) return 'text-ratio-almost'
24 | if (ratio < 5) return 'text-ratio-good'
25 | return 'text-ratio-best'
26 | }
27 |
28 | export function getTorrentStateValue(state: TorrentState) {
29 | return TorrentState[state].toLowerCase()
30 | }
31 |
32 | export function getTorrentStateColor(state: TorrentState) {
33 | return `torrent-${getTorrentStateValue(state)}`
34 | }
35 |
--------------------------------------------------------------------------------
/src/helpers/data.ts:
--------------------------------------------------------------------------------
1 | import { toPrecision } from './number'
2 |
3 | const units = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
4 |
5 | export function formatDataValue(data: number, isBinary: boolean, precision?: number) {
6 | const base = isBinary ? 1024 : 1000
7 | if (!data || data === 0) return '0'
8 |
9 | let i = 1
10 | while (data >= base ** i && i < units.length) {
11 | i++
12 | }
13 | return toPrecision(data / base ** (i - 1), precision ?? (i > 1 ? 3 : 1))
14 | }
15 |
16 | export function formatDataUnit(data: number, isBinary: boolean) {
17 | const base = isBinary ? 1024 : 1000
18 |
19 | let i = 1
20 | while (data >= base ** i && i < units.length) {
21 | i++
22 | }
23 | return `${units[i - 1]}${isBinary && i > 1 ? 'i' : ''}B`
24 | }
25 |
26 | export function formatData(data: number, isBinary: boolean, precision?: number) {
27 | return `${formatDataValue(data, isBinary, precision)} ${formatDataUnit(data, isBinary)}`
28 | }
29 |
--------------------------------------------------------------------------------
/src/helpers/datetime.spec.ts:
--------------------------------------------------------------------------------
1 | import timezoneMock from 'timezone-mock'
2 | import { describe, expect, test } from 'vitest'
3 | import { formatDuration, formatEta, formatTimeMs, formatTimeSec, INFINITY_SYMBOL, QBIT_MAX_ETA } from './datetime'
4 |
5 | beforeAll(() => {
6 | timezoneMock.register('UTC')
7 | })
8 |
9 | afterAll(() => {
10 | timezoneMock.unregister()
11 | })
12 |
13 | describe('helpers/datetime/formatEta', () => {
14 | test('seconds', () => {
15 | expect(formatEta(1)).toBe('1s')
16 | expect(formatEta(59)).toBe('59s')
17 | })
18 |
19 | test('minutes', () => {
20 | expect(formatEta(60)).toBe('1m')
21 | expect(formatEta(61)).toBe('1m 1s')
22 | expect(formatEta(3599)).toBe('59m 59s')
23 | })
24 |
25 | test('hours', () => {
26 | expect(formatEta(3600)).toBe('1h')
27 | expect(formatEta(3601)).toBe('1h 1s')
28 | expect(formatEta(3660)).toBe('1h 1m')
29 | expect(formatEta(3661)).toBe('1h 1m')
30 | expect(formatEta(86399)).toBe('23h 59m')
31 | })
32 |
33 | test('days', () => {
34 | expect(formatEta(86400)).toBe('1d')
35 | expect(formatEta(86401)).toBe('1d 1s')
36 | expect(formatEta(86460)).toBe('1d 1m')
37 | expect(formatEta(86461)).toBe('1d 1m')
38 | expect(formatEta(90000)).toBe('1d 1h')
39 | expect(formatEta(90001)).toBe('1d 1h')
40 | expect(formatEta(90060)).toBe('1d 1h')
41 | })
42 |
43 | test('infinity', () => {
44 | expect(formatEta(0)).toBe('0s')
45 | expect(formatEta(0, false)).toBe('0s')
46 | expect(formatEta(0, true)).toBe(INFINITY_SYMBOL)
47 | expect(formatEta(QBIT_MAX_ETA)).toBe(INFINITY_SYMBOL)
48 | })
49 | })
50 |
51 | test('helpers/datetime/formatTimeMs', () => {
52 | expect(formatTimeMs(1626739200000, 'YYYY-MM-DD')).toBe('2021-07-20')
53 | })
54 |
55 | test('helpers/datetime/formatTimeSec', () => {
56 | expect(formatTimeSec(1626739200, 'YYYY-MM-DD')).toBe('2021-07-20')
57 | })
58 |
59 | test('helpers/datetime/formatDuration', () => {
60 | expect(formatDuration(60, 's', 'HH:mm:ss')).toBe('00:01:00')
61 | expect(formatDuration(1, 'm', 'HH:mm:ss')).toBe('00:01:00')
62 | expect(formatDuration(1, 'h', 'HH:mm:ss')).toBe('01:00:00')
63 | expect(formatDuration(1, 'd', 'D [days], HH:mm:ss')).toBe('1 days, 00:00:00')
64 | expect(formatDuration(0, 's', 'HH:mm:ss')).toBe('00:00:00')
65 | expect(formatDuration(1000000, 's', 'D [days], HH:mm:ss')).toBe('11 days, 13:46:40')
66 | })
67 |
--------------------------------------------------------------------------------
/src/helpers/datetime.ts:
--------------------------------------------------------------------------------
1 | import dayjs from '@/plugins/dayjs'
2 | import { DurationUnitType } from 'dayjs/plugin/duration'
3 |
4 | export const QBIT_MAX_ETA = 8_640_000 // 100 days
5 | export const INFINITY_SYMBOL = '∞'
6 |
7 | export function formatEta(value: number, isForced: boolean = false): string {
8 | const MAX_UNITS = 2 // Will display 2 units max, from highest to lowest
9 |
10 | if (value >= QBIT_MAX_ETA || (isForced && value === 0)) {
11 | return INFINITY_SYMBOL
12 | }
13 |
14 | const minute = 60
15 | const hour = minute * 60
16 | const day = hour * 24
17 | const year = day * 365
18 |
19 | const durations = [year, day, hour, minute, 1]
20 | const units = 'ydhms'
21 |
22 | let index = 0
23 | let unitSize = 0
24 | const parts = []
25 |
26 | while (unitSize < MAX_UNITS && index !== durations.length) {
27 | const duration = durations[index]
28 | if (value < duration) {
29 | index++
30 | continue
31 | }
32 |
33 | const result = Math.floor(value / duration)
34 | parts.push(result + units[index])
35 |
36 | value %= duration
37 | index++
38 | unitSize++
39 | }
40 |
41 | if (!parts.length) {
42 | return '0' + units[durations.length - 1]
43 | }
44 |
45 | return parts.join(' ')
46 | }
47 |
48 | export function formatTimeMs(value: number, format: string): string {
49 | return dayjs(value).format(format)
50 | }
51 |
52 | export function formatTimeSec(value: number, format: string): string {
53 | return formatTimeMs(value * 1000, format)
54 | }
55 |
56 | export function formatDuration(value: number, unit: DurationUnitType, format: string): string {
57 | return dayjs.duration(value, unit).format(format)
58 | }
59 |
--------------------------------------------------------------------------------
/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | import { getColorFromName, getRatioColor, getTorrentStateColor, getTorrentStateValue } from './colors'
2 | import comparators, { Comparator, isObjectEqual } from './comparators'
3 | import { formatDataValue, formatDataUnit, formatData } from './data'
4 | import { QBIT_MAX_ETA, INFINITY_SYMBOL, formatEta, formatTimeMs, formatTimeSec, formatDuration } from './datetime'
5 | import { toPrecision, formatPercent } from './number'
6 | import { basename } from './path'
7 | import { formatSpeedValue, formatSpeedUnit, formatSpeed } from './speed'
8 | import { isWindows, isMac, doesCommand, openLink, downloadFile } from './system'
9 | import { titleCase, capitalize, extractHostname, getDomainBody, splitByUrl, containsUrl, isValidUri, codeToFlag } from './text'
10 |
11 | export {
12 | getColorFromName,
13 | getRatioColor,
14 | getTorrentStateColor,
15 | getTorrentStateValue,
16 | comparators,
17 | isObjectEqual,
18 | formatDataValue,
19 | formatDataUnit,
20 | formatData,
21 | QBIT_MAX_ETA,
22 | INFINITY_SYMBOL,
23 | formatEta,
24 | formatTimeMs,
25 | formatTimeSec,
26 | formatDuration,
27 | toPrecision,
28 | formatPercent,
29 | basename,
30 | formatSpeedValue,
31 | formatSpeedUnit,
32 | formatSpeed,
33 | isWindows,
34 | isMac,
35 | doesCommand,
36 | openLink,
37 | downloadFile,
38 | titleCase,
39 | capitalize,
40 | extractHostname,
41 | getDomainBody,
42 | splitByUrl,
43 | containsUrl,
44 | isValidUri,
45 | codeToFlag
46 | }
47 |
48 | export type { Comparator }
49 |
--------------------------------------------------------------------------------
/src/helpers/number.spec.ts:
--------------------------------------------------------------------------------
1 | import { formatPercent, toPrecision } from './number'
2 | import { expect, test } from 'vitest'
3 |
4 | test('helpers/number/toPrecision', () => {
5 | expect(toPrecision(0, 3)).toBe('0.00')
6 | expect(toPrecision(0.1, 3)).toBe('0.10')
7 |
8 | expect(toPrecision(1, 3)).toBe('1.00')
9 |
10 | expect(toPrecision(10, 3)).toBe('10.0')
11 | expect(toPrecision(10, 2)).toBe('10')
12 | expect(toPrecision(10, 1)).toBe('10')
13 |
14 | expect(toPrecision(99.99, 3)).toBe('99.9')
15 |
16 | expect(toPrecision(100, 3)).toBe('100')
17 | })
18 |
19 | test('helpers/number/formatPercent', () => {
20 | expect(formatPercent(0)).toBe('0.00 %')
21 | expect(formatPercent(0.1)).toBe('10.0 %')
22 | expect(formatPercent(1)).toBe('100 %')
23 |
24 | expect(formatPercent(0.999942870757758)).toBe('99.9 %')
25 | })
26 |
--------------------------------------------------------------------------------
/src/helpers/number.ts:
--------------------------------------------------------------------------------
1 | export function toPrecision(value: number, precision: number): string {
2 | if (value >= 10 ** precision) {
3 | return Math.floor(value).toString()
4 | }
5 |
6 | const strValue = value.toFixed(precision)
7 | if (strValue.length < Math.floor(Math.log10(value)) + 1) {
8 | return strValue
9 | } else {
10 | const result = strValue.substring(0, precision + 1)
11 | if (result.endsWith('.')) {
12 | return result.slice(0, -1)
13 | }
14 | return result
15 | }
16 | }
17 |
18 | /** Formats a percentage value between 0 and 1 */
19 | export function formatPercent(progress: number): string {
20 | return `${toPrecision(progress * 100, 3)} %`
21 | }
22 |
--------------------------------------------------------------------------------
/src/helpers/path.ts:
--------------------------------------------------------------------------------
1 | export function basename(path: string | null | undefined) {
2 | if (!path) return ''
3 |
4 | const uniPath = path.replace(/\\/g, '/')
5 | if (uniPath.indexOf('/') === -1) return ''
6 |
7 | return uniPath.split('/').reverse()[0]
8 | }
9 |
10 | export function splitExt(path: string | null | undefined): [string, string] {
11 | if (!path) return ['', '']
12 |
13 | const uniPath = path.replace(/\\/g, '/')
14 | if (!uniPath.includes('.', 1)) return [uniPath, '']
15 |
16 | const groups = uniPath.split('.')
17 | const ext = groups.pop()!
18 | return [groups.join('.'), ext]
19 | }
20 |
--------------------------------------------------------------------------------
/src/helpers/speed.ts:
--------------------------------------------------------------------------------
1 | import { formatDataUnit, formatDataValue } from './data'
2 |
3 | export function formatSpeedValue(speed: number, isBits: boolean) {
4 | if (isBits) speed *= 8
5 | return formatDataValue(speed, false)
6 | }
7 |
8 | export function formatSpeedUnit(speed: number, isBits: boolean) {
9 | if (isBits) speed *= 8
10 | const unit = formatDataUnit(speed, false).slice(0, -1)
11 | return `${unit}${isBits ? 'bps' : 'B/s'}`
12 | }
13 |
14 | export function formatSpeed(speed: number, isBits: boolean) {
15 | return `${formatSpeedValue(speed, isBits)} ${formatSpeedUnit(speed, isBits)}`
16 | }
17 |
--------------------------------------------------------------------------------
/src/helpers/system.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if user is on Windows
3 | */
4 | export const isWindows = window.navigator.userAgent.toLowerCase().includes('windows')
5 |
6 | /**
7 | * Check if user is on MAC
8 | */
9 | export const isMac = window.navigator.userAgent.toLowerCase().includes('mac')
10 |
11 | /**
12 | * Check Ctrl/Cmd key
13 | */
14 | export function doesCommand(e: { metaKey: boolean; ctrlKey: boolean }): boolean {
15 | return isMac ? e.metaKey : e.ctrlKey
16 | }
17 |
18 | export function openLink(link: string) {
19 | window.open(link, '_blank', 'noreferrer')
20 | }
21 |
22 | export function downloadFile(filename: string, blob: Blob) {
23 | const href = window.URL.createObjectURL(blob)
24 | const el = Object.assign(document.createElement('a'), { href, download: filename, style: { opacity: '0' } })
25 | document.body.appendChild(el)
26 | el.click()
27 | el.remove()
28 | }
29 |
--------------------------------------------------------------------------------
/src/locales/index.ts:
--------------------------------------------------------------------------------
1 | import cs from './cs.json'
2 | import en from './en.json'
3 | import es from './es.json'
4 | import fr from './fr.json'
5 | import hu from './hu.json'
6 | import it from './it.json'
7 | import ja from './ja.json'
8 | import ko from './ko.json'
9 | import nl from './nl.json'
10 | import pl from './pl.json'
11 | import pt_br from './pt-BR.json'
12 | import ru from './ru.json'
13 | import tr from './tr.json'
14 | import uk from './uk.json'
15 | import zh_hans from './zh-Hans.json'
16 | import zh_hant from './zh-Hant.json'
17 |
18 | type LocaleDef = { title: string; value: Locales }
19 |
20 | export enum Locales {
21 | CS = 'cs',
22 | EN = 'en',
23 | ES = 'es',
24 | FR = 'fr',
25 | HU = 'hu',
26 | IT = 'it',
27 | JA = 'ja',
28 | KO = 'ko',
29 | NL = 'nl',
30 | PL = 'pl',
31 | PT_BR = 'pt-BR',
32 | RU = 'ru',
33 | TR = 'tr',
34 | UK = 'uk',
35 | ZH_HANS = 'zh-Hans',
36 | ZH_HANT = 'zh-Hant'
37 | }
38 |
39 | export const LOCALES: LocaleDef[] = [
40 | { title: 'čeština', value: Locales.CS },
41 | { title: 'English', value: Locales.EN },
42 | { title: 'español', value: Locales.ES },
43 | { title: 'Français', value: Locales.FR },
44 | { title: 'magyar', value: Locales.HU },
45 | { title: 'italiano', value: Locales.IT },
46 | { title: '日本語', value: Locales.JA },
47 | { title: '한국어', value: Locales.KO },
48 | { title: 'Nederlands', value: Locales.NL },
49 | { title: 'polski', value: Locales.PL },
50 | { title: 'português (Brasil)', value: Locales.PT_BR },
51 | { title: 'Русский', value: Locales.RU },
52 | { title: 'Türkçe', value: Locales.TR },
53 | { title: 'українська', value: Locales.UK },
54 | { title: '简体中文', value: Locales.ZH_HANS },
55 | { title: '繁體中文', value: Locales.ZH_HANT }
56 | ]
57 |
58 | export const messages: Record = {
59 | [Locales.CS]: cs,
60 | [Locales.EN]: en,
61 | [Locales.ES]: es,
62 | [Locales.FR]: fr,
63 | [Locales.HU]: hu,
64 | [Locales.IT]: it,
65 | [Locales.JA]: ja,
66 | [Locales.KO]: ko,
67 | [Locales.NL]: nl,
68 | [Locales.PL]: pl,
69 | [Locales.PT_BR]: pt_br,
70 | [Locales.RU]: ru,
71 | [Locales.TR]: tr,
72 | [Locales.UK]: uk,
73 | [Locales.ZH_HANS]: zh_hans,
74 | [Locales.ZH_HANT]: zh_hant
75 | }
76 |
77 | export const defaultLocale = Locales.EN
78 | export const fallbackLocale = Locales.EN
79 |
--------------------------------------------------------------------------------
/src/locales/nl.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "cancel": "Annuleren",
4 | "close": "Sluiten",
5 | "delete": "Verwijderen",
6 | "disable": "Uitschakelen",
7 | "emptyList": "Niets te zien hier!",
8 | "none": "(Geen)",
9 | "save": "Opslaan",
10 | "selectAll": "Alles selecteren"
11 | },
12 | "constants": {
13 | "bittorrentProtocols": {
14 | "tcp_utp": "TCP en μTP"
15 | },
16 | "connectionStatus": {
17 | "connected": "Verbonden"
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import '@/styles/styles.scss'
3 | import App from './App.vue'
4 |
5 | // Vue-Router
6 | import router from '@/plugins/router'
7 |
8 | // Vuetify
9 | import vuetify from '@/plugins/vuetify'
10 |
11 | // Vue-i18n
12 | import i18n from '@/plugins/i18n'
13 |
14 | // Vue-Toastify
15 | import Vue3Toastify from 'vue3-toastify'
16 | import options from '@/plugins/toastify'
17 |
18 | // Pinia
19 | import pinia from '@/plugins/pinia'
20 |
21 | // Font
22 | import '@fontsource/roboto'
23 |
24 | const app = createApp(App)
25 |
26 | app.config.performance = true
27 |
28 | app.use(router).use(vuetify).use(i18n).use(Vue3Toastify, options).use(pinia).mount('#app')
29 |
--------------------------------------------------------------------------------
/src/pages/MagnetHandler.vue:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/src/pages/index.ts:
--------------------------------------------------------------------------------
1 | import { RouteRecordRaw } from 'vue-router'
2 |
3 | export const routes: RouteRecordRaw[] = [
4 | {
5 | name: 'dashboard',
6 | path: '/',
7 | component: () => import('./Dashboard.vue')
8 | },
9 | {
10 | name: 'settings',
11 | path: '/settings/:tab?/:subtab?',
12 | component: () => import('./Settings.vue')
13 | },
14 | {
15 | name: 'rssArticles',
16 | path: '/rss/:tab?/:feedId?',
17 | component: () => import('./RssArticles.vue')
18 | },
19 | {
20 | name: 'logs',
21 | path: '/logs',
22 | component: () => import('./Logs.vue')
23 | },
24 | {
25 | name: 'searchEngine',
26 | path: '/search',
27 | component: () => import('./SearchEngine.vue')
28 | },
29 | {
30 | name: 'torrentCreator',
31 | path: '/torrentCreator',
32 | component: () => import('./TorrentCreator.vue')
33 | },
34 | {
35 | name: 'torrentDetail',
36 | path: '/torrent/:hash/:tab?',
37 | component: () => import('./TorrentDetail.vue')
38 | },
39 | {
40 | name: 'magnetHandler',
41 | path: '/magnet/:url',
42 | alias: '/download=:url',
43 | component: () => import('./MagnetHandler.vue')
44 | },
45 | {
46 | name: 'login',
47 | path: '/login',
48 | component: () => import('./Login.vue'),
49 | meta: {
50 | public: true // Allow access even if not logged in
51 | }
52 | },
53 | {
54 | path: '/:any+', // Catch all, prevent empty page if URL is invalid
55 | redirect: '/'
56 | }
57 | ]
58 |
--------------------------------------------------------------------------------
/src/plugins/dayjs.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 | import duration from 'dayjs/plugin/duration'
3 | import relativeTime from 'dayjs/plugin/relativeTime'
4 | import updateLocale from 'dayjs/plugin/updateLocale'
5 |
6 | const config = {
7 | thresholds: [
8 | { l: 's', r: 1 },
9 | { l: 'ss', r: 59, d: 'second' },
10 | { l: 'm', r: 1 },
11 | { l: 'mm', r: 59, d: 'minute' },
12 | { l: 'h', r: 1 },
13 | { l: 'hh', r: 23, d: 'hour' },
14 | { l: 'd', r: 1 },
15 | { l: 'dd', r: 29, d: 'day' },
16 | { l: 'M', r: 1 },
17 | { l: 'MM', r: 11, d: 'month' },
18 | { l: 'y', r: 1 },
19 | { l: 'yy', d: 'year' }
20 | ],
21 | rounding: Math.floor
22 | }
23 |
24 | dayjs.extend(duration)
25 | dayjs.extend(relativeTime, config)
26 | dayjs.extend(updateLocale)
27 |
28 | dayjs.updateLocale('en', {
29 | relativeTime: {
30 | future: 'in %s',
31 | past: '%s ago',
32 | s: 'a few seconds',
33 | ss: '%d seconds',
34 | m: 'a minute',
35 | mm: '%d minutes',
36 | h: 'an hour',
37 | hh: '%d hours',
38 | d: 'a day',
39 | dd: '%d days',
40 | M: 'a month',
41 | MM: '%d months',
42 | y: 'a year',
43 | yy: '%d years'
44 | }
45 | })
46 |
47 | export default dayjs
48 |
--------------------------------------------------------------------------------
/src/plugins/i18n.ts:
--------------------------------------------------------------------------------
1 | import { createI18n } from 'vue-i18n'
2 | import { defaultLocale, fallbackLocale, messages } from '@/locales'
3 |
4 | export default createI18n({
5 | legacy: false,
6 | locale: defaultLocale,
7 | fallbackLocale,
8 | messages,
9 | pluralRules: {
10 | ru: (choice, choicesLength) => {
11 | if (choice === 0) {
12 | return 0
13 | }
14 | const teen = choice > 10 && choice < 20
15 | const endsWithOne = choice % 10 === 1
16 | if (choicesLength == 2) {
17 | return choice === 1 ? 0 : 1
18 | }
19 | if (choicesLength < 4) {
20 | return !teen && endsWithOne ? 1 : 2
21 | }
22 | if (!teen && endsWithOne) {
23 | return 1
24 | }
25 | if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
26 | return 2
27 | }
28 | return choicesLength < 4 ? 2 : 3
29 | }
30 | }
31 | })
32 |
--------------------------------------------------------------------------------
/src/plugins/pinia.ts:
--------------------------------------------------------------------------------
1 | import { createPinia } from 'pinia'
2 | import { persistencePlugin } from 'pinia-persistence-plugin'
3 |
4 | const pinia = createPinia()
5 | pinia.use(
6 | persistencePlugin({
7 | assertStorage: () => {},
8 | storeKeysPrefix: 'vuetorrent',
9 | persistenceDefault: false,
10 | ensureAsyncStorageUpdateOrder: true,
11 | debug: import.meta.env.DEV
12 | })
13 | )
14 |
15 | export default pinia
16 |
--------------------------------------------------------------------------------
/src/plugins/router.ts:
--------------------------------------------------------------------------------
1 | import { routes } from '@/pages'
2 | import { useAppStore } from '@/stores'
3 | import { storeToRefs } from 'pinia'
4 | import { createRouter, createWebHashHistory } from 'vue-router'
5 |
6 | const router = createRouter({
7 | history: createWebHashHistory(process.env.BASE_URL),
8 | routes
9 | })
10 |
11 | router.beforeResolve((to, _, next) => {
12 | const { isAuthenticated } = storeToRefs(useAppStore())
13 | const isPublic = to.meta.public === true
14 |
15 | if (!isPublic && !isAuthenticated.value) {
16 | return next({ name: 'login', query: { redirect: location.hash.slice(1) } })
17 | }
18 |
19 | return next()
20 | })
21 |
22 | export default router
23 |
--------------------------------------------------------------------------------
/src/plugins/toastify.ts:
--------------------------------------------------------------------------------
1 | import 'vue3-toastify/dist/index.css'
2 | import { toast, type ToastContainerOptions } from 'vue3-toastify'
3 |
4 | export default {
5 | autoClose: 1500,
6 | clearOnUrlChange: false,
7 | containerId: toast.POSITION.BOTTOM_RIGHT,
8 | limit: 5,
9 | position: toast.POSITION.BOTTOM_RIGHT,
10 | theme: toast.THEME.COLORED
11 | } as ToastContainerOptions
12 |
--------------------------------------------------------------------------------
/src/plugins/vuetify.ts:
--------------------------------------------------------------------------------
1 | import 'vuetify/styles'
2 | import themes, { DarkRedesigned } from '@/themes'
3 | import { createVuetify } from 'vuetify'
4 | import * as components from 'vuetify/components'
5 | import * as directives from 'vuetify/directives'
6 | import '@mdi/font/css/materialdesignicons.css'
7 |
8 | export default createVuetify({
9 | components,
10 | directives,
11 | display: {
12 | mobileBreakpoint: 'sm'
13 | },
14 | icons: {
15 | defaultSet: 'mdi'
16 | },
17 | theme: {
18 | defaultTheme: DarkRedesigned.id,
19 | variations: {
20 | colors: [
21 | 'primary',
22 | 'secondary',
23 | 'torrent-allocating',
24 | 'torrent-checking_disk',
25 | 'torrent-checking_resume_data',
26 | 'torrent-dl_forced',
27 | 'torrent-dl_stopped',
28 | 'torrent-dl_queued',
29 | 'torrent-dl_stalled',
30 | 'torrent-downloading',
31 | 'torrent-error',
32 | 'torrent-forced_meta_download',
33 | 'torrent-meta_download',
34 | 'torrent-missing_files',
35 | 'torrent-moving',
36 | 'torrent-ul_forced',
37 | 'torrent-ul_stopped',
38 | 'torrent-ul_queued',
39 | 'torrent-ul_stalled',
40 | 'torrent-unknown',
41 | 'torrent-uploading'
42 | ],
43 | lighten: 3,
44 | darken: 3
45 | },
46 | themes
47 | }
48 | })
49 |
--------------------------------------------------------------------------------
/src/polyfills.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'array.prototype.tosorted' {
2 | export default function toSorted(arr: T[], compareFn: (a: T, b: T) => number): T[]
3 | }
4 |
--------------------------------------------------------------------------------
/src/services/Github.ts:
--------------------------------------------------------------------------------
1 | import { AxiosInstance } from 'axios'
2 | import axios from 'axios'
3 |
4 | export class Github {
5 | private axios: AxiosInstance
6 |
7 | constructor() {
8 | this.axios = axios.create()
9 | }
10 |
11 | /**
12 | * Fetches the latest version of VueTorrent
13 | */
14 | async getVersion(): Promise {
15 | const { data } = await this.axios.get('https://api.github.com/repos/vuetorrent/vuetorrent/releases/latest')
16 | return data.tag_name
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/services/backend.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosInstance } from 'axios'
2 |
3 | class BackendProvider {
4 | private axios: AxiosInstance
5 | private up: boolean = false
6 |
7 | constructor() {
8 | let baseURL = `${location.origin}${location.pathname}`
9 | if (!baseURL.endsWith('/')) baseURL += '/'
10 | baseURL += 'backend'
11 |
12 | this.axios = axios.create({
13 | baseURL,
14 | withCredentials: true,
15 | headers: {
16 | put: { 'Content-Type': 'application/json' }
17 | }
18 | })
19 | }
20 |
21 | /**
22 | * Ping the backend to check if it's up
23 | * @returns true if backend is up, false otherwise
24 | */
25 | async ping(): Promise {
26 | return await this.axios
27 | .get('/ping')
28 | .then(
29 | res => res.data === 'pong',
30 | () => false
31 | )
32 | .then(ok => {
33 | this.up = ok
34 |
35 | return ok
36 | })
37 | }
38 |
39 | /**
40 | * Get all values
41 | */
42 | async getAll(): Promise | null> {
43 | if (!this.up) return null
44 |
45 | return this.axios.get('/config').then(res => res.data)
46 | }
47 |
48 | /**
49 | * Get a single value
50 | * @param key
51 | * @returns string or null if key doesn't exists
52 | */
53 | async get(key: string): Promise {
54 | if (!this.up) return null
55 |
56 | return this.axios.get(`/config/${key}`).then(
57 | res => res.data[key],
58 | () => null
59 | )
60 | }
61 |
62 | /**
63 | * Set a value
64 | * @param key
65 | * @param value
66 | * @returns true if value was set, false otherwise
67 | */
68 | async set(key: string, value: string): Promise {
69 | if (!this.up) return false
70 |
71 | return this.axios.put(`/config/${key}`, { value }).then(
72 | () => true,
73 | () => false
74 | )
75 | }
76 |
77 | /**
78 | * Delete a value
79 | * @param key
80 | * @returns true if value was deleted, false otherwise
81 | */
82 | async del(key: string): Promise {
83 | if (!this.up) return false
84 |
85 | return this.axios.delete(`/config/${key}`).then(
86 | () => true,
87 | () => false
88 | )
89 | }
90 |
91 | async update() {
92 | return this.axios.get('/update')
93 | }
94 | }
95 |
96 | export const backend = new BackendProvider()
97 |
--------------------------------------------------------------------------------
/src/services/qbit/index.ts:
--------------------------------------------------------------------------------
1 | import IProvider from './IProvider'
2 | import MockProvider from './MockProvider'
3 | import QBitProvider from './QbitProvider'
4 |
5 | const qbit: IProvider =
6 | import.meta.env.MODE === 'demo' || (import.meta.env.DEV && import.meta.env.VITE_USE_MOCK_PROVIDER === 'true') ? MockProvider.getInstance() : QBitProvider.getInstance()
7 |
8 | export default qbit
9 |
--------------------------------------------------------------------------------
/src/stores/dialog.ts:
--------------------------------------------------------------------------------
1 | import { acceptHMRUpdate, defineStore } from 'pinia'
2 | import { v4 as uuidv4 } from 'uuid'
3 | import { AllowedComponentProps, Component, computed, shallowRef, triggerRef, VNodeProps } from 'vue'
4 |
5 | type ComponentProps = C extends new (...args: any) => any ? Omit['$props'], keyof VNodeProps | keyof AllowedComponentProps> : never
6 |
7 | type DialogTemplate = {
8 | component: C
9 | props: ComponentProps
10 | guid: string
11 | onClose?: () => any | Promise
12 | }
13 |
14 | export const useDialogStore = defineStore('dialogs', () => {
15 | const dialogs = shallowRef