A modern, responsive alternative Web UI for qBittorrent. Built as a single-page application with Vue 3, TypeScript, and Vuetify 3.
- Modern UI — Material Design via Vuetify 3 with multiple themes (Light, Dark, Grey, Luxury, Modern Dark, Crypto, Cyberpunk, Natural, Technology)
- Responsive — Works on desktop and mobile
- i18n — English, 中文 (Simplified & Traditional), Русский, Türkçe, Nederlands
- RSS — Full feed and download rule management
- Search — Integrated torrent search
- Real-time sync — Incremental polling via qBittorrent's sync API
| Light theme | Dark theme |
|---|---|
![]() |
![]() |
![]() |
![]() |
The latest build artifacts are available from the Actions tab on GitHub.
- Go to Releases and download the latest
qb-web.zip. - Extract all files.
- In qBittorrent Web UI Options, enable Use alternative Web UI and set Files location to the
publicfolder inside the extracted zip.
yarn install
yarn build
# The output will be in the `dist/public/` folder.Then point qBittorrent's alternative Web UI to the dist/public/ folder.
Recovery: If something goes wrong, append /api/v2/app/setPreferences?json=%7B%22alternative_webui_enabled%22:false%7D to the URL in your browser to disable the alternative Web UI.
See DEV.md for setup and development commands.
To connect the dev server to a running qBittorrent instance:
QB_WEBUI_URL=http://<your-qbittorrent-ip>:<port> yarn devThis sets a Vite proxy to forward /api/v2 requests to your qBittorrent instance, avoiding CORS issues during development.
| Layer | Technology |
|---|---|
| Framework | Vue 3.5 (TypeScript) |
| UI | Vuetify 3.7 (Material Design) |
| State | Pinia 2 |
| HTTP | Axios |
| i18n | node-polyglot |
| Build | Vite 6 |
| Test | Vitest 2 + Vue Test Utils 2 |
| Package | Yarn 3 |
src/
├── Api.ts # qBittorrent Web API v2 client
├── App.vue # Root component, layout, polling, themes
├── main.ts # Entry point
├── types.ts # TypeScript interfaces (Torrent, MainData, etc.)
├── consts.ts # StateType enum
├── filters.ts # Vue filters (size, duration, time)
├── directives.ts # Custom directives (v-class)
├── router.ts # Vue Router
├── components/
│ ├── Torrents.vue # Main torrent table
│ ├── AddForm.vue # Add torrent dialog
│ ├── Drawer.vue # Sidebar with filters
│ ├── MainToolbar.vue # Top toolbar
│ ├── dialogs/ # Settings, RSS, Search, Info, Peers, Logs dialogs
│ └── drawer/ # FilterGroup, DrawerFooter
├── store/ # Pinia stores
│ ├── index.ts # Main store — torrent data, filtering, polling state
│ ├── config.ts # User preferences (persisted to localStorage)
│ ├── types.ts # Store-specific type definitions
│ ├── addForm.ts # Add torrent form state
│ ├── dialog.ts # Global dialog state
│ ├── snackBar.ts # Snackbar notifications
│ └── searchEngine.ts # Torrent search state
├── locale/ # Translations (en, zh-CN, zh-TW, ru, tr, nl)
├── plugins/ # Vuetify, i18n
├── utils/ # Helpers (vue-object-merge, siteMap)
├── sites.ts # Tracker-to-icon mapping
├── protocolHandler.ts # magnet: protocol handling
└── assets/ # Icons, styles
- On startup,
App.vueregisters amagnet:protocol handler, reads#download=hash params, and resolves the base URL. - Fetches initial data via
Api.getMainData()andApi.getAppPreferences(). - Polls
/sync/maindatawith arid(response ID) for incremental updates at a configurable interval (default 2000ms). - Partial updates are merged into Pinia state using
vue-object-merge. - User actions (pause, resume, delete, add) call the corresponding qBittorrent API endpoints.
- If
baseUrlis configured, attemptgetMainData(). - On 401/403, show the login form.
- After login, fetch main data and preferences, then start polling.
Components use vue-facing-decorator for class-style component syntax:
import { Vue, Component, Watch } from 'vue-facing-decorator';
@Component({ components: { ... } })
export default class MyComponent extends Vue {
// reactive state, computed via getters, methods, lifecycle hooks
}-
Add the tracker domain and display name to
SITE_MAPinsrc/utils/siteMap.ts:'new-site.com': { en: 'NewSite', zh: '新站点' },
-
Register the icon mapping in
src/sites.ts:'new-site.com': { name: 'NewSite', icon: getSiteIcon('nexusphp') },
Use
getSiteIcon('filename')for a specific icon (file must exist insrc/assets/site_icons/), orgetSiteIcon('nexusphp')as the default.
Icon resolution order: exact match → suffix match (subdomains) → base domain fallback → mdi-server font icon.
Add a language: Create src/locale/[code].ts, import in src/locale/index.ts, add to the translations object.
Add a torrent state filter: Add to StateType in src/consts.ts, update AllStateTypes, add filter logic in filteredTorrents getter, add translation key.
Add an API endpoint: Add method to Api class in src/Api.ts, define types in src/types.ts, use this.axios.get/post.
Add a dialog: Create component in src/components/dialogs/, add to App.vue with v-if, add state to drawerOptions or a Pinia store.



