commit e315a35982e4f405a8a27fe5580626c97da350e6 Author: CzBiX Date: Fri Apr 12 12:23:33 2019 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..185e663 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw* diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4cfaac --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# qb-web + +## Project setup +``` +npm install +``` + +### Compiles and hot-reloads for development +``` +npm run serve +``` + +### Compiles and minifies for production +``` +npm run build +``` + +### Run your tests +``` +npm run test +``` + +### Lints and fixes files +``` +npm run lint +``` + +### Customize configuration +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..ba17966 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/app' + ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..56ee84d --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "qb-web", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "@mdi/font": "^3.5.95", + "axios": "^0.18.0", + "core-js": "^2.6.5", + "dayjs": "^1.8.12", + "lodash": "^4.17.11", + "register-service-worker": "^1.6.2", + "roboto-fontface": "*", + "vue": "^2.6.6", + "vuetify": "^1.5.11", + "vuex": "^3.0.1" + }, + "devDependencies": { + "@types/lodash": "^4.14.123", + "@vue/cli-plugin-babel": "^3.5.0", + "@vue/cli-plugin-typescript": "^3.5.0", + "@vue/cli-service": "^3.5.3", + "fibers": "^3.1.1", + "sass": "^1.17.2", + "sass-loader": "^7.1.0", + "stylus": "^0.54.5", + "stylus-loader": "^3.0.1", + "typescript": "^3.4.3", + "vue-cli-plugin-vuetify": "^0.5.0", + "vue-template-compiler": "^2.6.10", + "vuetify-loader": "^1.2.1" + }, + "postcss": { + "plugins": { + "autoprefixer": {} + } + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..c7b9a43 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/img/icons/android-chrome-192x192.png b/public/img/icons/android-chrome-192x192.png new file mode 100644 index 0000000..b02aa64 Binary files /dev/null and b/public/img/icons/android-chrome-192x192.png differ diff --git a/public/img/icons/android-chrome-512x512.png b/public/img/icons/android-chrome-512x512.png new file mode 100644 index 0000000..06088b0 Binary files /dev/null and b/public/img/icons/android-chrome-512x512.png differ diff --git a/public/img/icons/apple-touch-icon-120x120.png b/public/img/icons/apple-touch-icon-120x120.png new file mode 100644 index 0000000..1427cf6 Binary files /dev/null and b/public/img/icons/apple-touch-icon-120x120.png differ diff --git a/public/img/icons/apple-touch-icon-152x152.png b/public/img/icons/apple-touch-icon-152x152.png new file mode 100644 index 0000000..f24d454 Binary files /dev/null and b/public/img/icons/apple-touch-icon-152x152.png differ diff --git a/public/img/icons/apple-touch-icon-180x180.png b/public/img/icons/apple-touch-icon-180x180.png new file mode 100644 index 0000000..404e192 Binary files /dev/null and b/public/img/icons/apple-touch-icon-180x180.png differ diff --git a/public/img/icons/apple-touch-icon-60x60.png b/public/img/icons/apple-touch-icon-60x60.png new file mode 100644 index 0000000..cf10a56 Binary files /dev/null and b/public/img/icons/apple-touch-icon-60x60.png differ diff --git a/public/img/icons/apple-touch-icon-76x76.png b/public/img/icons/apple-touch-icon-76x76.png new file mode 100644 index 0000000..c500769 Binary files /dev/null and b/public/img/icons/apple-touch-icon-76x76.png differ diff --git a/public/img/icons/apple-touch-icon.png b/public/img/icons/apple-touch-icon.png new file mode 100644 index 0000000..03c0c5d Binary files /dev/null and b/public/img/icons/apple-touch-icon.png differ diff --git a/public/img/icons/favicon-16x16.png b/public/img/icons/favicon-16x16.png new file mode 100644 index 0000000..42af009 Binary files /dev/null and b/public/img/icons/favicon-16x16.png differ diff --git a/public/img/icons/favicon-32x32.png b/public/img/icons/favicon-32x32.png new file mode 100644 index 0000000..46ca04d Binary files /dev/null and b/public/img/icons/favicon-32x32.png differ diff --git a/public/img/icons/msapplication-icon-144x144.png b/public/img/icons/msapplication-icon-144x144.png new file mode 100644 index 0000000..7808237 Binary files /dev/null and b/public/img/icons/msapplication-icon-144x144.png differ diff --git a/public/img/icons/mstile-150x150.png b/public/img/icons/mstile-150x150.png new file mode 100644 index 0000000..3b37a43 Binary files /dev/null and b/public/img/icons/mstile-150x150.png differ diff --git a/public/img/icons/safari-pinned-tab.svg b/public/img/icons/safari-pinned-tab.svg new file mode 100644 index 0000000..732afd8 --- /dev/null +++ b/public/img/icons/safari-pinned-tab.svg @@ -0,0 +1,149 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..48521d9 --- /dev/null +++ b/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + qBittorrent Web UI + + + +
+ + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..580b880 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,20 @@ +{ + "name": "qBittorrent Web UI", + "short_name": "qbittorrent-web-ui", + "icons": [ + { + "src": "./img/icons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "./img/icons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "start_url": "./index.html", + "display": "standalone", + "background_color": "#000000", + "theme_color": "#4DBA87" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/src/Api.ts b/src/Api.ts new file mode 100644 index 0000000..6926ebf --- /dev/null +++ b/src/Api.ts @@ -0,0 +1,70 @@ +import 'axios'; +import Axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; + +class Api { + private axios: AxiosInstance; + + constructor() { + this.axios = Axios.create({ + baseURL: '/api/v2', + }); + + this.axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + public getAppVersion() { + return this.axios.get('/app/version'); + } + + public getApiVersion() { + return this.axios.get('/app/webapiVersion'); + } + + public login(params: any) { + const data = new URLSearchParams(params); + return this.axios.post('/auth/login', data, { + validateStatus(status) { + return status === 200 || status === 403; + }, + }).then(this.handleResponse); + } + + public getGlobalTransferInfo() { + return this.axios.get('/transfer/info'); + } + + public getAppPreferences() { + return this.axios.get('/app/preferences'); + } + + public getMainData(rid?: number) { + return this.axios.get('/sync/maindata', { + params: { + rid, + }, + }); + } + + public addTorrents(params: any) { + const data = new URLSearchParams(params); + return this.axios.post('/torrents/add', data); + } + + public switchToOldUi() { + const params = { + alternative_webui_enabled: false, + }; + + const data = new URLSearchParams({ + json: JSON.stringify(params), + }); + + return this.axios.post('/app/setPreferences', data); + } + + private handleResponse(resp: AxiosResponse) { + return resp.data; + } +} + +export const api = new Api(); diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..6c420f7 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..f3d2503 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/components/AddForm.vue b/src/components/AddForm.vue new file mode 100644 index 0000000..a9f8d4f --- /dev/null +++ b/src/components/AddForm.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/components/Drawer.vue b/src/components/Drawer.vue new file mode 100644 index 0000000..2388c69 --- /dev/null +++ b/src/components/Drawer.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/src/components/Footer.vue b/src/components/Footer.vue new file mode 100644 index 0000000..17b03ac --- /dev/null +++ b/src/components/Footer.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/src/components/LoginForm.vue b/src/components/LoginForm.vue new file mode 100644 index 0000000..cecf254 --- /dev/null +++ b/src/components/LoginForm.vue @@ -0,0 +1,114 @@ + + + diff --git a/src/components/Torrents.vue b/src/components/Torrents.vue new file mode 100644 index 0000000..18ce17d --- /dev/null +++ b/src/components/Torrents.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/src/components/drawer/FilterGroup.vue b/src/components/drawer/FilterGroup.vue new file mode 100644 index 0000000..87f10ac --- /dev/null +++ b/src/components/drawer/FilterGroup.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/directives.ts b/src/directives.ts new file mode 100644 index 0000000..aeb1aef --- /dev/null +++ b/src/directives.ts @@ -0,0 +1,6 @@ +import Vue from 'vue'; + +Vue.directive('class', (el, binding) => { + const clsName = binding.arg!; + el.classList.toggle(clsName, binding.value); +}); diff --git a/src/filters.ts b/src/filters.ts new file mode 100644 index 0000000..7892b87 --- /dev/null +++ b/src/filters.ts @@ -0,0 +1,68 @@ +import dayjs from 'dayjs'; +import Vue from 'vue'; + +export function formatSize(value: number) { + const units = 'KMGTP'; + let index = -1; + + while (value >= 1024) { + index++; + value /= 1024; + } + + const unit = index < 0 ? 'B' : units[index] + 'iB'; + + return `${value.toFixed(2)} ${unit}`; +} + +Vue.filter('formatSize', formatSize); + +Vue.filter('formatDuration', (value: number) => { + const minute = 60; + const hour = 3600; + const day = 3600 * 24; + + const durations = [day, hour, minute, 1]; + const units = 'dhms'; + + let index = 0; + let unitSize = 0; + const parts = []; + + while (true) { + if (unitSize === 2 || index === durations.length) { + break; + } + + const duration = durations[index]; + if (value < duration) { + index++; + continue; + } + const result = Math.floor(value / duration); + if (index === 0 && result >= 100) { + return '∞'; + } + parts.push(result + units[index]); + + value %= duration; + index++; + unitSize++; + } + + if (unitSize < 2 && index !== durations.length) { + const result = Math.floor(value / durations[index]); + parts.push(result + units[index]); + } + + return parts.join(' '); +}); + +Vue.filter('formatTimestamp', (timestamp: number) => { + if (timestamp === null) { + return ''; + } + + const m = dayjs.unix(timestamp); + return m.format('YYYY-MM-DD HH:mm:ss'); +}); diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..11eeccd --- /dev/null +++ b/src/main.ts @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import './plugins/vuetify'; +import store from './store'; +import './filters'; +import './directives'; +import App from './App.vue'; +// import './registerServiceWorker'; +import 'roboto-fontface/css/roboto/roboto-fontface.css'; +import '@mdi/font/css/materialdesignicons.css'; + +Vue.config.productionTip = false; + +new Vue({ + store, + render: (h) => h(App), +}).$mount('#app'); diff --git a/src/plugins/vuetify.ts b/src/plugins/vuetify.ts new file mode 100644 index 0000000..cb87688 --- /dev/null +++ b/src/plugins/vuetify.ts @@ -0,0 +1,8 @@ +import Vue from 'vue'; +// tslint:disable-next-line +import Vuetify from 'vuetify/lib'; +import 'vuetify/src/stylus/app.styl'; + +Vue.use(Vuetify, { + iconfont: 'mdi', +}); diff --git a/src/registerServiceWorker.ts b/src/registerServiceWorker.ts new file mode 100644 index 0000000..ef87577 --- /dev/null +++ b/src/registerServiceWorker.ts @@ -0,0 +1,32 @@ +/* tslint:disable:no-console */ + +import { register } from 'register-service-worker'; + +if (process.env.NODE_ENV === 'production') { + register(`${process.env.BASE_URL}service-worker.js`, { + ready() { + console.log( + 'App is being served from cache by a service worker.\n' + + 'For more details, visit https://goo.gl/AFskqB', + ); + }, + registered() { + console.log('Service worker has been registered.'); + }, + cached() { + console.log('Content has been cached for offline use.'); + }, + updatefound() { + console.log('New content is downloading.'); + }, + updated() { + console.log('New content is available; please refresh.'); + }, + offline() { + console.log('No internet connection found. App is running in offline mode.'); + }, + error(error) { + console.error('Error during service worker registration:', error); + }, + }); +} diff --git a/src/shims-tsx.d.ts b/src/shims-tsx.d.ts new file mode 100644 index 0000000..3b88b58 --- /dev/null +++ b/src/shims-tsx.d.ts @@ -0,0 +1,13 @@ +import Vue, { VNode } from 'vue'; + +declare global { + namespace JSX { + // tslint:disable no-empty-interface + interface Element extends VNode {} + // tslint:disable no-empty-interface + interface ElementClass extends Vue {} + interface IntrinsicElements { + [elem: string]: any; + } + } +} diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts new file mode 100644 index 0000000..8f6f410 --- /dev/null +++ b/src/shims-vue.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + import Vue from 'vue'; + export default Vue; +} diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..1f2af09 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,64 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import _ from 'lodash'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + state: { + rid: 0, + mainData: null, + filter: { + type: null, + category: null, + site: null, + }, + config: { + updateInterval: 2000, + }, + preferences: null, + }, + mutations: { + updateMainData(state, payload) { + state.rid = payload.rid; + if (payload.full_update) { + state.mainData = payload; + } else { + state.mainData = _.merge({}, state.mainData, payload); + } + }, + updatePreferences(state, payload) { + state.preferences = payload; + }, + updateFilter(state, payload) { + state.filter = _.clone(payload); + }, + }, + getters: { + isDataReady(state) { + return !!state.mainData; + }, + allTorrents(state) { + if (!state.mainData) { + return []; + } + + return _.map(state.mainData.torrents, (value, key) => { + return _.merge({}, value, { hash: key }); + }); + }, + torrentGroupByCategory(state, getters) { + return _.groupBy(getters.allTorrents, (torrent) => torrent.category); + }, + torrentGroupBySite(state, getters) { + return _.groupBy(getters.allTorrents, (torrent) => { + if (!torrent.tracker) { + return ''; + } + + const url = new URL(torrent.tracker); + return url.hostname; + }); + }, + }, +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8eb79ec --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "strict": true, + "jsx": "preserve", + "importHelpers": true, + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "baseUrl": ".", + "types": [ + "webpack-env", + "vuetify", + ], + "paths": { + "@/*": [ + "src/*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + "tests/**/*.ts", + "tests/**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..2b37e40 --- /dev/null +++ b/tslint.json @@ -0,0 +1,19 @@ +{ + "defaultSeverity": "warning", + "extends": [ + "tslint:recommended" + ], + "linterOptions": { + "exclude": [ + "node_modules/**" + ] + }, + "rules": { + "quotemark": [true, "single"], + "indent": [true, "spaces", 2], + "interface-name": false, + "ordered-imports": false, + "object-literal-sort-keys": false, + "no-consecutive-blank-lines": false + } +} diff --git a/vue.config.js b/vue.config.js new file mode 100644 index 0000000..268e288 --- /dev/null +++ b/vue.config.js @@ -0,0 +1,13 @@ +module.exports = { + pwa: { + name: 'qBittorrent Web UI', + }, + + devServer: { + proxy: { + '/api': { + target: 'http://192.168.1.2:8080', + } + } + } +}