mirror of
https://github.com/CzBiX/qb-web.git
synced 2026-04-05 11:58:07 +08:00
Refine to use Vue class component
This commit is contained in:
172
src/App.vue
172
src/App.vue
@@ -77,12 +77,14 @@ import LogsDialog from './components/dialogs/LogsDialog.vue';
|
||||
import RssDialog from './components/dialogs/RssDialog.vue';
|
||||
|
||||
import api from './Api';
|
||||
import { sleep } from './utils';
|
||||
import { timeout } from './utils';
|
||||
import Component from 'vue-class-component';
|
||||
import { Watch } from 'vue-property-decorator';
|
||||
import { MainData } from './types';
|
||||
|
||||
let appWrapEl: HTMLElement;
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'app',
|
||||
@Component({
|
||||
components: {
|
||||
AddForm,
|
||||
Drawer,
|
||||
@@ -95,28 +97,6 @@ export default Vue.extend({
|
||||
GlobalSnackBar,
|
||||
RssDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
needAuth: false,
|
||||
drawer: true,
|
||||
drawerOptions: {
|
||||
showLogs: false,
|
||||
showRss: false,
|
||||
},
|
||||
task: 0,
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
await this.getInitData();
|
||||
appWrapEl = (this.$refs.app as any).$el.querySelector('.v-application--wrap');
|
||||
appWrapEl.addEventListener('paste', this.onPaste);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.task) {
|
||||
clearTimeout(this.task);
|
||||
}
|
||||
appWrapEl.removeEventListener('paste', this.onPaste);
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'mainData',
|
||||
@@ -124,9 +104,6 @@ export default Vue.extend({
|
||||
'preferences',
|
||||
]),
|
||||
...mapGetters(['config']),
|
||||
phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
@@ -134,64 +111,105 @@ export default Vue.extend({
|
||||
'updatePreferences',
|
||||
'setPasteUrl',
|
||||
]),
|
||||
async getInitData() {
|
||||
try {
|
||||
await this.getMainData();
|
||||
} catch (e) {
|
||||
if (e.response.status === 403) {
|
||||
this.needAuth = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class App extends Vue {
|
||||
needAuth = false
|
||||
drawer = true
|
||||
drawerOptions = {
|
||||
showLogs: false,
|
||||
showRss: false,
|
||||
}
|
||||
task = 0
|
||||
|
||||
return;
|
||||
mainData!: MainData
|
||||
rid!: number
|
||||
preferences!: any
|
||||
config!: any
|
||||
|
||||
updateMainData!: (_: any) => void
|
||||
updatePreferences!: (_: any) => void
|
||||
setPasteUrl!: (_: any) => void
|
||||
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
}
|
||||
|
||||
async created() {
|
||||
await this.getInitData();
|
||||
appWrapEl = (this.$refs.app as any).$el.querySelector('.v-application--wrap');
|
||||
appWrapEl.addEventListener('paste', this.onPaste);
|
||||
}
|
||||
|
||||
beforeDestroy() {
|
||||
if (this.task) {
|
||||
clearTimeout(this.task);
|
||||
}
|
||||
appWrapEl.removeEventListener('paste', this.onPaste);
|
||||
}
|
||||
|
||||
async getInitData() {
|
||||
try {
|
||||
await this.getMainData();
|
||||
} catch (e) {
|
||||
if (e.response.status === 403) {
|
||||
this.needAuth = true;
|
||||
}
|
||||
|
||||
await this.getPreferences();
|
||||
},
|
||||
async getPreferences() {
|
||||
const resp = await api.getAppPreferences();
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatePreferences(resp.data);
|
||||
},
|
||||
async getMainData() {
|
||||
const rid = this.rid ? this.rid : null;
|
||||
const resp = await api.getMainData(rid);
|
||||
const mainData = resp.data;
|
||||
await this.getPreferences();
|
||||
}
|
||||
|
||||
this.updateMainData(mainData);
|
||||
async getPreferences() {
|
||||
const resp = await api.getAppPreferences();
|
||||
|
||||
this.task = setTimeout(this.getMainData, this.config.updateInterval);
|
||||
},
|
||||
async drawerFooterOpen(v: boolean) {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
await sleep(3000);
|
||||
this.updatePreferences(resp.data);
|
||||
}
|
||||
|
||||
(this.$refs.end as HTMLElement).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
async getMainData() {
|
||||
const rid = this.rid ? this.rid : undefined;
|
||||
const resp = await api.getMainData(rid);
|
||||
const mainData = resp.data;
|
||||
|
||||
this.updateMainData(mainData);
|
||||
|
||||
this.task = setTimeout(this.getMainData, this.config.updateInterval);
|
||||
}
|
||||
|
||||
async drawerFooterOpen(v: boolean) {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
await timeout(3000);
|
||||
|
||||
(this.$refs.end as HTMLElement).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
onPaste(e: ClipboardEvent) {
|
||||
if ((e.target as HTMLElement).tagName === 'INPUT') {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = e.clipboardData!.getData('text');
|
||||
if (text) {
|
||||
this.setPasteUrl({
|
||||
url: text,
|
||||
});
|
||||
},
|
||||
onPaste(e: ClipboardEvent) {
|
||||
if ((e.target as HTMLElement).tagName === 'INPUT') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const text = e.clipboardData!.getData('text');
|
||||
if (text) {
|
||||
this.setPasteUrl({
|
||||
url: text,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async needAuth(v) {
|
||||
if (!v) {
|
||||
await this.getInitData();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@Watch('needAuth')
|
||||
onNeedAuth(v: boolean) {
|
||||
if (!v) {
|
||||
this.getInitData();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -151,6 +151,8 @@ import { mapState } from 'vuex';
|
||||
|
||||
import { tr } from '@/locale';
|
||||
import api from '../Api';
|
||||
import Component from 'vue-class-component';
|
||||
import { Watch } from 'vue-property-decorator';
|
||||
|
||||
const defaultParams = {
|
||||
urls: null,
|
||||
@@ -162,30 +164,7 @@ const defaultParams = {
|
||||
firstLastPiecePrio: false,
|
||||
};
|
||||
|
||||
interface Data {
|
||||
tr: any,
|
||||
dialog: boolean,
|
||||
valid: boolean,
|
||||
files: any[],
|
||||
userParams: any,
|
||||
error: string | null,
|
||||
submitting: boolean,
|
||||
showMore: boolean,
|
||||
}
|
||||
|
||||
export default {
|
||||
data(): Data {
|
||||
return {
|
||||
tr,
|
||||
dialog: false,
|
||||
valid: false,
|
||||
files: [],
|
||||
userParams: {},
|
||||
error: null,
|
||||
submitting: false,
|
||||
showMore: false,
|
||||
};
|
||||
},
|
||||
@Component({
|
||||
computed: {
|
||||
...mapState({
|
||||
pasteUrl: 'pasteUrl',
|
||||
@@ -196,113 +175,136 @@ export default {
|
||||
return getters.allCategories.map((c: any) => ({ text: c.name, value: c.key }));
|
||||
},
|
||||
}),
|
||||
params() {
|
||||
return Object.assign({}, defaultParams, this.userParams);
|
||||
},
|
||||
phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
},
|
||||
autoStart: {
|
||||
get(): boolean {
|
||||
return !this.params.paused;
|
||||
},
|
||||
set(value: boolean) {
|
||||
const paused = !value;
|
||||
const tmp = defaultParams.paused === paused ? null : paused;
|
||||
this.setParams('paused', tmp);
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class AddForm extends Vue {
|
||||
dialog = false
|
||||
valid = false
|
||||
files: FileList | [] = []
|
||||
userParams = {}
|
||||
error: string | null = null
|
||||
submitting = false
|
||||
showMore = false
|
||||
|
||||
pasteUrl!: string | null
|
||||
prefs!: any
|
||||
$refs!: {
|
||||
form: any,
|
||||
file: any,
|
||||
fileZone: HTMLElement,
|
||||
}
|
||||
|
||||
get params() {
|
||||
return Object.assign({}, defaultParams, this.userParams);
|
||||
}
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
}
|
||||
|
||||
get autoStart() {
|
||||
return !this.params.paused;
|
||||
}
|
||||
|
||||
set autoStart(value: boolean) {
|
||||
const paused = !value;
|
||||
const tmp = defaultParams.paused === paused ? null : paused;
|
||||
this.setParams('paused', tmp);
|
||||
}
|
||||
|
||||
created() {
|
||||
defaultParams.paused = this.prefs.start_paused_enabled;
|
||||
defaultParams.root_path = this.prefs.create_subfolder_enabled;
|
||||
this.showMore = !this.phoneLayout;
|
||||
},
|
||||
}
|
||||
|
||||
mounted() {
|
||||
(this.$refs.fileZone as HTMLElement).addEventListener('drop', this.onDrop, true);
|
||||
},
|
||||
this.$refs.fileZone.addEventListener('drop', this.onDrop, true);
|
||||
}
|
||||
|
||||
beforeDestroy() {
|
||||
(this.$refs.fileZone as HTMLElement).removeEventListener('drop', this.onDrop, true);
|
||||
},
|
||||
this.$refs.fileZone.removeEventListener('drop', this.onDrop, true);
|
||||
}
|
||||
|
||||
methods: {
|
||||
setParams(key: string, value: any) {
|
||||
if (_.isNil(value)) {
|
||||
Vue.delete(this.userParams, key);
|
||||
} else {
|
||||
Vue.set(this.userParams, key, value);
|
||||
}
|
||||
},
|
||||
async submit() {
|
||||
if (this.submitting) {
|
||||
return;
|
||||
}
|
||||
setParams(key: string, value: any) {
|
||||
if (_.isNil(value)) {
|
||||
Vue.delete(this.userParams, key);
|
||||
} else {
|
||||
Vue.set(this.userParams, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
this.error = null;
|
||||
let files;
|
||||
if (this.files.length) {
|
||||
({ files } = this);
|
||||
Vue.delete(this.userParams, 'urls');
|
||||
} else {
|
||||
files = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await api.addTorrents(this.userParams, files);
|
||||
|
||||
if (resp !== 'Ok.') {
|
||||
this.error = resp;
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
}
|
||||
|
||||
this.submitting = false;
|
||||
|
||||
if (this.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialog = false;
|
||||
async submit() {
|
||||
if (this.submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
this.error = null;
|
||||
let files;
|
||||
if (this.files.length) {
|
||||
({ files } = this);
|
||||
Vue.delete(this.userParams, 'urls');
|
||||
this.files = [];
|
||||
} else {
|
||||
files = null;
|
||||
}
|
||||
|
||||
this.$refs.form.resetValidation();
|
||||
},
|
||||
selectFiles() {
|
||||
const input = this.$refs.file.$el.querySelector('input[type=file]');
|
||||
input.click();
|
||||
},
|
||||
onDrop(e: DragEvent) {
|
||||
const transfer = e.dataTransfer!;
|
||||
const { files } = transfer;
|
||||
if (!files.length) {
|
||||
return;
|
||||
try {
|
||||
const resp = await api.addTorrents(this.userParams, files);
|
||||
|
||||
if (resp !== 'Ok.') {
|
||||
this.error = resp;
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
this.files = files;
|
||||
},
|
||||
},
|
||||
this.submitting = false;
|
||||
|
||||
watch: {
|
||||
pasteUrl(v) {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
if (this.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.dialog) {
|
||||
Vue.set(this.userParams, 'urls', v);
|
||||
this.dialog = true;
|
||||
}
|
||||
},
|
||||
files(v) {
|
||||
this.$refs.form.validate();
|
||||
},
|
||||
},
|
||||
};
|
||||
this.dialog = false;
|
||||
|
||||
Vue.delete(this.userParams, 'urls');
|
||||
this.files = [];
|
||||
|
||||
this.$refs.form.resetValidation();
|
||||
}
|
||||
|
||||
selectFiles() {
|
||||
const input = this.$refs.file.$el.querySelector('input[type=file]');
|
||||
input.click();
|
||||
}
|
||||
|
||||
onDrop(e: DragEvent) {
|
||||
const transfer = e.dataTransfer!;
|
||||
const { files } = transfer;
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
@Watch('pasteUrl')
|
||||
onPasteUrl(v: string) {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.dialog) {
|
||||
Vue.set(this.userParams, 'urls', v);
|
||||
this.dialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('files')
|
||||
onFilesChange(v: FileList) {
|
||||
this.$refs.form.validate();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -56,16 +56,18 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { sortBy, sumBy, defaultTo, isUndefined } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import { mapState, mapMutations, mapGetters } from 'vuex';
|
||||
|
||||
import { tr } from '@/locale';
|
||||
import { Torrent } from '@/types';
|
||||
import { Torrent, Category } from '@/types';
|
||||
import FilterGroup from './drawer/FilterGroup.vue';
|
||||
import api from '../Api';
|
||||
import { formatSize } from '../filters';
|
||||
import { SiteMap, StateType, AllStateTypes } from '../consts';
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Emit } from 'vue-property-decorator';
|
||||
|
||||
const stateList = [
|
||||
{
|
||||
@@ -131,40 +133,10 @@ interface Data {
|
||||
endItems: MenuItem[],
|
||||
}
|
||||
|
||||
export default {
|
||||
@Component({
|
||||
components: {
|
||||
FilterGroup,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
},
|
||||
|
||||
data(): Data {
|
||||
const basicItems = [
|
||||
{ icon: 'mdi-settings', title: tr('settings'), click: () => alert(tr('todo')) },
|
||||
];
|
||||
const endItems = [
|
||||
{ icon: 'mdi-delta', title: tr('logs'), click: () => this.updateOptions('showLogs', true) },
|
||||
];
|
||||
|
||||
return {
|
||||
tr,
|
||||
basicItems,
|
||||
endItems,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.phoneLayout) {
|
||||
return
|
||||
}
|
||||
|
||||
this.endItems = this.endItems.concat([
|
||||
{ icon: 'mdi-rss-box', title: 'RSS', click: () => this.updateOptions('showRss', true) },
|
||||
{ icon: 'mdi-history', title: tr('label.switch_to_old_ui'), click: this.switchUi },
|
||||
])
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'isDataReady',
|
||||
@@ -174,111 +146,152 @@ export default {
|
||||
'torrentGroupBySite',
|
||||
'torrentGroupByState',
|
||||
]),
|
||||
phoneLayout() {
|
||||
return this.$vuetify.breakpoint.smAndDown;
|
||||
},
|
||||
items(): Array<any> {
|
||||
if (!this.isDataReady) {
|
||||
return _.concat(this.basicItems, this.endItems);
|
||||
},
|
||||
})
|
||||
export default class Drawer extends Vue {
|
||||
@Prop()
|
||||
readonly value: any
|
||||
|
||||
basicItems: MenuItem[] = [
|
||||
{ icon: 'mdi-settings', title: tr('settings'), click: () => alert(tr('todo')) },
|
||||
]
|
||||
|
||||
endItems: MenuItem[] = [
|
||||
{ icon: 'mdi-delta', title: tr('logs'), click: () => this.updateOptions('showLogs', true) },
|
||||
]
|
||||
|
||||
isDataReady!: boolean
|
||||
allTorrents!: Torrent[]
|
||||
allCategories!: Category[]
|
||||
torrentGroupByCategory!: {[category: string]: Torrent[]}
|
||||
torrentGroupBySite!: {[site: string]: Torrent[]}
|
||||
torrentGroupByState!: {[state: string]: Torrent[]}
|
||||
|
||||
created() {
|
||||
if (this.phoneLayout) {
|
||||
return
|
||||
}
|
||||
|
||||
this.endItems = this.endItems.concat([
|
||||
{ icon: 'mdi-rss-box', title: 'RSS', click: () => this.updateOptions('showRss', true) },
|
||||
{ icon: 'mdi-history', title: tr('label.switch_to_old_ui'), click: this.switchUi },
|
||||
])
|
||||
}
|
||||
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.smAndDown;
|
||||
}
|
||||
|
||||
buildStateGroup(): MenuChildrenItem[] {
|
||||
return stateList.map((item) => {
|
||||
let value = this.torrentGroupByState[item.state];
|
||||
if (isUndefined(value)) {
|
||||
value = [];
|
||||
}
|
||||
const size = formatSize(sumBy(value, 'size'));
|
||||
const title = `${item.title} (${value.length})`;
|
||||
const append = `[${size}]`;
|
||||
return {
|
||||
icon: `mdi-${item.icon}`, title, key: item.state, append,
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const filterGroups: MenuItem[] = [];
|
||||
const totalSize = formatSize(_.sumBy(this.allTorrents, 'size'));
|
||||
buildCategoryGroup(): MenuChildrenItem[] {
|
||||
return [{
|
||||
key: '',
|
||||
name: tr('uncategorized'),
|
||||
}].concat(this.allCategories).map((category) => {
|
||||
let value = this.torrentGroupByCategory[category.key];
|
||||
if (isUndefined(value)) {
|
||||
value = [];
|
||||
}
|
||||
const size = formatSize(sumBy(value, 'size'));
|
||||
const title = `${category.name} (${value.length})`;
|
||||
const append = `[${size}]`;
|
||||
return {
|
||||
icon: 'mdi-folder-open', title, key: category.key, append,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const states = stateList.map((item) => {
|
||||
let value = this.torrentGroupByState[item.state];
|
||||
if (_.isUndefined(value)) {
|
||||
value = [];
|
||||
}
|
||||
const size = formatSize(_.sumBy(value, 'size'));
|
||||
const title = `${item.title} (${value.length})`;
|
||||
const append = `[${size}]`;
|
||||
return {
|
||||
icon: `mdi-${item.icon}`, title, key: item.state, append,
|
||||
};
|
||||
});
|
||||
filterGroups.push({
|
||||
icon: 'mdi-menu-up',
|
||||
'icon-alt': 'mdi-menu-down',
|
||||
title: tr('state._'),
|
||||
model: false,
|
||||
select: 'state',
|
||||
children: [
|
||||
{
|
||||
icon: 'mdi-filter-remove', title: `${tr('all')} (${this.allTorrents.length})`, key: null, append: `[${totalSize}]`,
|
||||
},
|
||||
...states,
|
||||
],
|
||||
});
|
||||
buildSiteGroup(): MenuChildrenItem[] {
|
||||
return sortBy(Object.entries(this.torrentGroupBySite).map(([key, value]) => {
|
||||
const size = formatSize(sumBy(value, 'size'));
|
||||
const site = (SiteMap as any)[key];
|
||||
const title = `${site ? site.name : (key || tr('others'))} (${value.length})`;
|
||||
const icon = defaultTo(site ? site.icon : null, 'mdi-server');
|
||||
const append = `[${size}]`;
|
||||
return {
|
||||
icon, title, key, append,
|
||||
};
|
||||
}), 'title');
|
||||
}
|
||||
|
||||
const categories: any[] = [{
|
||||
key: '',
|
||||
name: tr('uncategorized'),
|
||||
}].concat(this.allCategories).map((category) => {
|
||||
let value = this.torrentGroupByCategory[category.key];
|
||||
if (_.isUndefined(value)) {
|
||||
value = [];
|
||||
}
|
||||
const size = formatSize(_.sumBy(value, 'size'));
|
||||
const title = `${category.name} (${value.length})`;
|
||||
const append = `[${size}]`;
|
||||
return {
|
||||
icon: 'mdi-folder-open', title, key: category.key, append,
|
||||
};
|
||||
});
|
||||
filterGroups.push({
|
||||
icon: 'mdi-menu-up',
|
||||
'icon-alt': 'mdi-menu-down',
|
||||
title: tr('category', 0),
|
||||
model: !this.$vuetify.breakpoint.xsOnly,
|
||||
select: 'category',
|
||||
children: [
|
||||
{
|
||||
icon: 'mdi-folder-open', title: `${tr('all')} (${this.allTorrents.length})`, key: null, append: `[${totalSize}]`,
|
||||
},
|
||||
...categories,
|
||||
],
|
||||
});
|
||||
get items() {
|
||||
if (!this.isDataReady) {
|
||||
return this.basicItems.concat(this.endItems);
|
||||
}
|
||||
|
||||
const sites: any[] = _.sortBy(Object.entries(this.torrentGroupBySite).map(([key, value]) => {
|
||||
const size = formatSize(_.sumBy(value, 'size'));
|
||||
const site = (SiteMap as any)[key];
|
||||
const title = `${site ? site.name : (key || tr('others'))} (${value.length})`;
|
||||
const icon = _.defaultTo(site ? site.icon : null, 'mdi-server');
|
||||
const append = `[${size}]`;
|
||||
return {
|
||||
icon, title, key, append,
|
||||
};
|
||||
}), 'title');
|
||||
filterGroups.push({
|
||||
icon: 'mdi-menu-up',
|
||||
'icon-alt': 'mdi-menu-down',
|
||||
title: tr('sites'),
|
||||
model: false,
|
||||
select: 'site',
|
||||
children: [
|
||||
{
|
||||
icon: 'mdi-server', title: `${tr('all')} (${this.allTorrents.length})`, key: null, append: `[${totalSize}]`,
|
||||
},
|
||||
...sites,
|
||||
],
|
||||
});
|
||||
const filterGroups: MenuItem[] = [];
|
||||
const totalSize = formatSize(sumBy(this.allTorrents, 'size'));
|
||||
|
||||
return _.concat(this.basicItems, [{filterGroups}] as any, this.endItems);
|
||||
},
|
||||
},
|
||||
filterGroups.push({
|
||||
icon: 'mdi-menu-up',
|
||||
'icon-alt': 'mdi-menu-down',
|
||||
title: tr('state._'),
|
||||
model: false,
|
||||
select: 'state',
|
||||
children: [
|
||||
{
|
||||
icon: 'mdi-filter-remove', title: `${tr('all')} (${this.allTorrents.length})`, key: null, append: `[${totalSize}]`,
|
||||
},
|
||||
...this.buildStateGroup(),
|
||||
],
|
||||
});
|
||||
|
||||
methods: {
|
||||
async switchUi() {
|
||||
await api.switchToOldUi();
|
||||
filterGroups.push({
|
||||
icon: 'mdi-menu-up',
|
||||
'icon-alt': 'mdi-menu-down',
|
||||
title: tr('category', 0),
|
||||
model: !this.$vuetify.breakpoint.xsOnly,
|
||||
select: 'category',
|
||||
children: [
|
||||
{
|
||||
icon: 'mdi-folder-open', title: `${tr('all')} (${this.allTorrents.length})`, key: null, append: `[${totalSize}]`,
|
||||
},
|
||||
...this.buildCategoryGroup(),
|
||||
],
|
||||
});
|
||||
|
||||
window.location.reload(true);
|
||||
},
|
||||
updateOptions(key: string, value: any) {
|
||||
this.$emit('input', Object.assign({}, this.value, { [key]: value }));
|
||||
},
|
||||
},
|
||||
};
|
||||
filterGroups.push({
|
||||
icon: 'mdi-menu-up',
|
||||
'icon-alt': 'mdi-menu-down',
|
||||
title: tr('sites'),
|
||||
model: false,
|
||||
select: 'site',
|
||||
children: [
|
||||
{
|
||||
icon: 'mdi-server', title: `${tr('all')} (${this.allTorrents.length})`, key: null, append: `[${totalSize}]`,
|
||||
},
|
||||
...this.buildSiteGroup(),
|
||||
],
|
||||
});
|
||||
|
||||
return this.basicItems.concat([{filterGroups}] as any, this.endItems);
|
||||
}
|
||||
|
||||
async switchUi() {
|
||||
await api.switchToOldUi();
|
||||
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
@Emit('input')
|
||||
updateOptions(key: string, value: any) {
|
||||
return Object.assign({}, this.value, { [key]: value })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -132,19 +132,12 @@ import _ from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import api from '../Api';
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Watch } from 'vue-property-decorator';
|
||||
import { Torrent, ServerState } from '../types';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
phoneLayout: Boolean,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
app: null,
|
||||
speedLimited: false,
|
||||
};
|
||||
},
|
||||
|
||||
@Component({
|
||||
filters: {
|
||||
connectionIcon(status: string) {
|
||||
const statusMap: any = {
|
||||
@@ -163,7 +156,6 @@ export default Vue.extend({
|
||||
return statusMap[status];
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState({
|
||||
info(state: any) {
|
||||
@@ -174,69 +166,84 @@ export default Vue.extend({
|
||||
'isDataReady',
|
||||
'allTorrents',
|
||||
]),
|
||||
totalSize() {
|
||||
return _.sumBy(this.allTorrents, 'size');
|
||||
},
|
||||
speedModeBind() {
|
||||
if (this.speedLimited) {
|
||||
return {
|
||||
class: 'speed-limited',
|
||||
color: 'warning',
|
||||
};
|
||||
}
|
||||
},
|
||||
})
|
||||
export default class Footer extends Vue {
|
||||
@Prop(Boolean)
|
||||
readonly phoneLayout!: boolean
|
||||
|
||||
app: any = null
|
||||
speedLimited = false
|
||||
|
||||
info!: ServerState
|
||||
isDataReady!: boolean
|
||||
allTorrents!: Torrent[]
|
||||
|
||||
get totalSize() {
|
||||
return _.sumBy(this.allTorrents, 'size');
|
||||
}
|
||||
|
||||
get speedModeBind() {
|
||||
if (this.speedLimited) {
|
||||
return {
|
||||
class: null,
|
||||
color: 'success',
|
||||
class: 'speed-limited',
|
||||
color: 'warning',
|
||||
};
|
||||
},
|
||||
topLayoutClass() {
|
||||
const v = this.phoneLayout;
|
||||
if (v) {
|
||||
return ['in-drawer', 'flex-column'];
|
||||
}
|
||||
}
|
||||
|
||||
return ['mx-4', 'justify-space-between'];
|
||||
},
|
||||
},
|
||||
return {
|
||||
class: null,
|
||||
color: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
methods: {
|
||||
async getAppInfo() {
|
||||
let resp = await api.getAppVersion();
|
||||
const version = resp.data;
|
||||
get topLayoutClass() {
|
||||
const v = this.phoneLayout;
|
||||
if (v) {
|
||||
return ['in-drawer', 'flex-column'];
|
||||
}
|
||||
|
||||
resp = await api.getApiVersion();
|
||||
const apiVersion = resp.data;
|
||||
return ['mx-4', 'justify-space-between'];
|
||||
}
|
||||
|
||||
this.app = {
|
||||
version, apiVersion,
|
||||
};
|
||||
},
|
||||
async toggleSpeedLimitsMode() {
|
||||
this.speedLimited = !this.speedLimited;
|
||||
await api.toggleSpeedLimitsMode();
|
||||
},
|
||||
},
|
||||
async getAppInfo() {
|
||||
let resp = await api.getAppVersion();
|
||||
const version = resp.data;
|
||||
|
||||
async created() {
|
||||
resp = await api.getApiVersion();
|
||||
const apiVersion = resp.data;
|
||||
|
||||
this.app = {
|
||||
version, apiVersion,
|
||||
};
|
||||
}
|
||||
|
||||
async toggleSpeedLimitsMode() {
|
||||
this.speedLimited = !this.speedLimited;
|
||||
await api.toggleSpeedLimitsMode();
|
||||
}
|
||||
|
||||
created() {
|
||||
if (!this.isDataReady) {
|
||||
return;
|
||||
}
|
||||
this.speedLimited = this.info.use_alt_speed_limits;
|
||||
await this.getAppInfo();
|
||||
},
|
||||
|
||||
watch: {
|
||||
async isDataReady(v) {
|
||||
if (v && this.app === null) {
|
||||
await this.getAppInfo();
|
||||
}
|
||||
},
|
||||
'info.use_alt_speed_limits': function (v) {
|
||||
this.speedLimited = v;
|
||||
},
|
||||
},
|
||||
});
|
||||
this.speedLimited = this.info.use_alt_speed_limits;
|
||||
this.getAppInfo();
|
||||
}
|
||||
|
||||
@Watch('isDataReady')
|
||||
onDataReady(v: boolean) {
|
||||
if (v && this.app === null) {
|
||||
this.getAppInfo();
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('info.use_alt_speed_limits')
|
||||
onSpeedLimitChanged (v: boolean) {
|
||||
this.speedLimited = v;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -148,20 +148,23 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { mapState, mapGetters, mapMutations } from 'vuex';
|
||||
import _ from 'lodash';
|
||||
import Vue from 'vue'
|
||||
import { mapState, mapGetters, mapMutations } from 'vuex'
|
||||
import { intersection, difference } from 'lodash'
|
||||
|
||||
import { tr } from '@/locale';
|
||||
import ConfirmDeleteDialog from './dialogs/ConfirmDeleteDialog.vue';
|
||||
import ConfirmSetCategoryDialog from './dialogs/ConfirmSetCategoryDialog.vue';
|
||||
import EditTrackerDialog from './dialogs/EditTrackerDialog.vue';
|
||||
import InfoDialog from './dialogs/InfoDialog.vue';
|
||||
import api from '../Api';
|
||||
import { formatSize, formatDuration } from '../filters';
|
||||
import { torrentIsState } from '../utils';
|
||||
import { StateType } from '../consts';
|
||||
import { DialogType } from '../store/types';
|
||||
import { tr } from '@/locale'
|
||||
import ConfirmDeleteDialog from './dialogs/ConfirmDeleteDialog.vue'
|
||||
import ConfirmSetCategoryDialog from './dialogs/ConfirmSetCategoryDialog.vue'
|
||||
import EditTrackerDialog from './dialogs/EditTrackerDialog.vue'
|
||||
import InfoDialog from './dialogs/InfoDialog.vue'
|
||||
import api from '../Api'
|
||||
import { formatSize, formatDuration } from '../filters'
|
||||
import { torrentIsState } from '../utils'
|
||||
import { StateType } from '../consts'
|
||||
import { DialogType, TorrentFilter, ConfigPayload, DialogConfig, SnackBarConfig } from '../store/types'
|
||||
import Component from 'vue-class-component'
|
||||
import { Torrent, Category } from '../types'
|
||||
import { Watch } from 'vue-property-decorator'
|
||||
|
||||
function getStateInfo(state: string) {
|
||||
let icon;
|
||||
@@ -238,54 +241,13 @@ function getStateInfo(state: string) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'torrents',
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ConfirmDeleteDialog,
|
||||
ConfirmSetCategoryDialog,
|
||||
EditTrackerDialog,
|
||||
InfoDialog,
|
||||
},
|
||||
|
||||
data() {
|
||||
const headers = [
|
||||
{ text: tr('name'), value: 'name' },
|
||||
{ text: tr('size'), value: 'size' },
|
||||
{ text: tr('progress'), value: 'progress' },
|
||||
{ text: tr('status'), value: 'state' },
|
||||
{ text: tr('seeds'), value: 'num_complete' },
|
||||
{ text: tr('peers'), value: 'num_incomplete' },
|
||||
{ text: tr('dl_speed'), value: 'dlspeed' },
|
||||
{ text: tr('up_speed'), value: 'upspeed' },
|
||||
{ text: tr('eta'), value: 'eta' },
|
||||
{ text: tr('ratio'), value: 'ratio' },
|
||||
{ text: tr('added_on'), value: 'added_on' },
|
||||
];
|
||||
|
||||
const footerProps = {
|
||||
'items-per-page-options': [10, 20, 50, -1],
|
||||
};
|
||||
|
||||
return {
|
||||
tr,
|
||||
headers,
|
||||
selectedRows: [],
|
||||
toDelete: [],
|
||||
toSetCategory: [],
|
||||
categoryToSet: null,
|
||||
toShowInfo: [],
|
||||
toEditTracker: [],
|
||||
infoTab: null,
|
||||
pageOptions: null,
|
||||
footerProps,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.pageOptions = this.$store.getters.config.pageOptions;
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'isDataReady',
|
||||
@@ -300,38 +262,7 @@ export default Vue.extend({
|
||||
return getters.config.filter;
|
||||
},
|
||||
}),
|
||||
loading() {
|
||||
return !this.isDataReady;
|
||||
},
|
||||
hasSelected() {
|
||||
return !!this.selectedRows.length;
|
||||
},
|
||||
selectedHashes() {
|
||||
return this.selectedRows.map(_.property('hash'));
|
||||
},
|
||||
torrents() {
|
||||
if (!this.isDataReady) {
|
||||
return [];
|
||||
}
|
||||
let list = this.allTorrents;
|
||||
if (this.filter.site !== null) {
|
||||
list = _.intersection(list, this.torrentGroupBySite[this.filter.site]);
|
||||
}
|
||||
if (this.filter.category !== null) {
|
||||
list = _.intersection(list, this.torrentGroupByCategory[this.filter.category]);
|
||||
}
|
||||
if (this.filter.state !== null) {
|
||||
list = _.intersection(list, this.torrentGroupByState[this.filter.state]);
|
||||
}
|
||||
|
||||
return list;
|
||||
},
|
||||
hasSelectedAll() {
|
||||
return this.hasSelected && this.selectedRows.length
|
||||
=== Math.min(this.torrents.length, this.pageOptions.rowsPerPage);
|
||||
},
|
||||
},
|
||||
|
||||
filters: {
|
||||
progressColorClass(progress: number) {
|
||||
const color = progress >= 0.5 ? 'white' : 'black';
|
||||
@@ -357,107 +288,191 @@ export default Vue.extend({
|
||||
return item.color || '#0008';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'updateConfig',
|
||||
'showDialog',
|
||||
'showSnackBar',
|
||||
]),
|
||||
confirmDelete() {
|
||||
this.toDelete = this.selectedRows;
|
||||
},
|
||||
showInfo(row?: any) {
|
||||
this.toShowInfo = row ? [row] : this.selectedRows;
|
||||
},
|
||||
async resumeTorrents() {
|
||||
await api.resumeTorrents(this.selectedHashes);
|
||||
},
|
||||
async pauseTorrents() {
|
||||
await api.pauseTorrents(this.selectedHashes);
|
||||
},
|
||||
async reannounceTorrents() {
|
||||
if (!this.hasSelected) {
|
||||
this.selectedRows = this.allTorrents;
|
||||
}
|
||||
const v = await new Promise((resolve) => {
|
||||
this.showDialog({
|
||||
content: {
|
||||
title: 'Reannounce Torrents',
|
||||
text: 'Are you sure want to reannounce torrents?',
|
||||
type: DialogType.OkCancel,
|
||||
callback: resolve,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
await api.reannounceTorrents(this.selectedHashes);
|
||||
|
||||
this.showSnackBar('Reannounced');
|
||||
},
|
||||
async recheckTorrents() {
|
||||
const v = await new Promise((resolve) => {
|
||||
this.showDialog({
|
||||
content: {
|
||||
title: 'Recheck Torrents',
|
||||
text: 'Are you sure want to recheck torrents?',
|
||||
type: DialogType.OkCancel,
|
||||
callback: resolve,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
await api.recheckTorrents(this.selectedHashes);
|
||||
|
||||
this.showSnackBar('Rechecking');
|
||||
},
|
||||
setTorrentsCategory(category: string) {
|
||||
this.categoryToSet = category;
|
||||
this.toSetCategory = this.selectedRows;
|
||||
},
|
||||
editTracker() {
|
||||
if (this.hasSelected) {
|
||||
this.selectedRows = this.allTorrents;
|
||||
}
|
||||
this.toEditTracker = this.selectedRows;
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class Torrents extends Vue {
|
||||
readonly headers = [
|
||||
{ text: tr('name'), value: 'name' },
|
||||
{ text: tr('size'), value: 'size' },
|
||||
{ text: tr('progress'), value: 'progress' },
|
||||
{ text: tr('status'), value: 'state' },
|
||||
{ text: tr('seeds'), value: 'num_complete' },
|
||||
{ text: tr('peers'), value: 'num_incomplete' },
|
||||
{ text: tr('dl_speed'), value: 'dlspeed' },
|
||||
{ text: tr('up_speed'), value: 'upspeed' },
|
||||
{ text: tr('eta'), value: 'eta' },
|
||||
{ text: tr('ratio'), value: 'ratio' },
|
||||
{ text: tr('added_on'), value: 'added_on' },
|
||||
]
|
||||
|
||||
watch: {
|
||||
pageOptions: {
|
||||
handler() {
|
||||
this.updateConfig({
|
||||
key: 'pageOptions',
|
||||
value: this.pageOptions,
|
||||
});
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
filter() {
|
||||
this.selectedRows = [];
|
||||
},
|
||||
torrents(v) {
|
||||
if (!this.hasSelected) {
|
||||
return;
|
||||
}
|
||||
readonly footerProps = {
|
||||
'items-per-page-options': [10, 20, 50, -1],
|
||||
}
|
||||
|
||||
const torrentHashs = v.map(_.property('hash'));
|
||||
const toRemove = _.difference(this.selectedHashes, torrentHashs);
|
||||
if (!toRemove) {
|
||||
return;
|
||||
}
|
||||
selectedRows: Torrent[] = []
|
||||
toDelete: Torrent[] = []
|
||||
toSetCategory: Torrent[] = []
|
||||
categoryToSet: string | null = null
|
||||
toShowInfo: Torrent[] = []
|
||||
toEditTracker: Torrent[] = []
|
||||
infoTab = null
|
||||
pageOptions: any = null
|
||||
|
||||
this.selectedRows = this.selectedRows.filter(r => !toRemove.includes(r.hash));
|
||||
},
|
||||
},
|
||||
});
|
||||
isDataReady!: boolean
|
||||
allTorrents!: Torrent[]
|
||||
allCategories!: Category[]
|
||||
torrentGroupByCategory!: {[category: string]: Torrent[]}
|
||||
torrentGroupBySite!: {[site: string]: Torrent[]}
|
||||
torrentGroupByState!: {[state: string]: Torrent[]}
|
||||
filter!: TorrentFilter
|
||||
|
||||
updateConfig!: (_: ConfigPayload) => void
|
||||
showDialog!: (_: DialogConfig) => void
|
||||
showSnackBar!: (_: SnackBarConfig) => void
|
||||
|
||||
get loading() {
|
||||
return !this.isDataReady;
|
||||
}
|
||||
get hasSelected() {
|
||||
return !!this.selectedRows.length;
|
||||
}
|
||||
get selectedHashes() {
|
||||
return this.selectedRows.map(r => r.hash);
|
||||
}
|
||||
|
||||
get torrents() {
|
||||
if (!this.isDataReady) {
|
||||
return [];
|
||||
}
|
||||
let list = this.allTorrents;
|
||||
if (this.filter.site !== null) {
|
||||
list = intersection(list, this.torrentGroupBySite[this.filter.site]);
|
||||
}
|
||||
if (this.filter.category !== null) {
|
||||
list = intersection(list, this.torrentGroupByCategory[this.filter.category]);
|
||||
}
|
||||
if (this.filter.state !== null) {
|
||||
list = intersection(list, this.torrentGroupByState[this.filter.state]);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
get hasSelectedAll() {
|
||||
return this.hasSelected && this.selectedRows.length
|
||||
=== Math.min(this.torrents.length, this.pageOptions.rowsPerPage);
|
||||
}
|
||||
|
||||
created() {
|
||||
this.pageOptions = this.$store.getters.config.pageOptions;
|
||||
}
|
||||
|
||||
confirmDelete() {
|
||||
this.toDelete = this.selectedRows;
|
||||
}
|
||||
|
||||
showInfo(row?: any) {
|
||||
this.toShowInfo = row ? [row] : this.selectedRows;
|
||||
}
|
||||
|
||||
async resumeTorrents() {
|
||||
await api.resumeTorrents(this.selectedHashes);
|
||||
}
|
||||
|
||||
async pauseTorrents() {
|
||||
await api.pauseTorrents(this.selectedHashes);
|
||||
}
|
||||
|
||||
async reannounceTorrents() {
|
||||
if (!this.hasSelected) {
|
||||
this.selectedRows = this.allTorrents;
|
||||
}
|
||||
const v = await new Promise((resolve) => {
|
||||
this.showDialog({
|
||||
content: {
|
||||
title: 'Reannounce Torrents',
|
||||
text: 'Are you sure want to reannounce torrents?',
|
||||
type: DialogType.OkCancel,
|
||||
callback: resolve,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
await api.reannounceTorrents(this.selectedHashes);
|
||||
|
||||
this.showSnackBar({text: 'Reannounced'});
|
||||
}
|
||||
|
||||
async recheckTorrents() {
|
||||
const v = await new Promise((resolve) => {
|
||||
this.showDialog({
|
||||
content: {
|
||||
title: 'Recheck Torrents',
|
||||
text: 'Are you sure want to recheck torrents?',
|
||||
type: DialogType.OkCancel,
|
||||
callback: resolve,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
await api.recheckTorrents(this.selectedHashes);
|
||||
|
||||
this.showSnackBar({text: 'Rechecking'});
|
||||
}
|
||||
|
||||
setTorrentsCategory(category: string) {
|
||||
this.categoryToSet = category;
|
||||
this.toSetCategory = this.selectedRows;
|
||||
}
|
||||
|
||||
editTracker() {
|
||||
if (this.hasSelected) {
|
||||
this.selectedRows = this.allTorrents;
|
||||
}
|
||||
this.toEditTracker = this.selectedRows;
|
||||
}
|
||||
|
||||
@Watch('pageOptions', { deep: true})
|
||||
onPageOptionsChanged() {
|
||||
this.updateConfig({
|
||||
key: 'pageOptions',
|
||||
value: this.pageOptions,
|
||||
})
|
||||
}
|
||||
|
||||
@Watch('filter')
|
||||
onFilterChanged() {
|
||||
this.selectedRows = []
|
||||
}
|
||||
|
||||
@Watch('torrents')
|
||||
onTorrentsChanged(v: Torrent[]) {
|
||||
if (!this.hasSelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const torrentHashs = v.map(t => t.hash);
|
||||
const toRemove = difference(this.selectedHashes, torrentHashs);
|
||||
if (!toRemove) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedRows = this.selectedRows.filter(r => !toRemove.includes(r.hash));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -534,30 +549,8 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
|
||||
// .toolbar {
|
||||
// position: sticky;
|
||||
// top: 0;
|
||||
// z-index: 2;
|
||||
// }
|
||||
|
||||
// .category-actions .v-list__tile__action {
|
||||
// min-width: 40px;
|
||||
// }
|
||||
|
||||
// .menu-check {
|
||||
// padding: 0;
|
||||
// }
|
||||
|
||||
.icon-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// ::v-deep .v-datatable {
|
||||
// // table-layout: fixed;
|
||||
// }
|
||||
|
||||
// ::v-deep.hide-headers .v-datatable thead {
|
||||
// display: none;
|
||||
// }
|
||||
</style>
|
||||
|
||||
@@ -46,62 +46,66 @@
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import Vue, { PropType } from 'vue';
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { tr } from '@/locale';
|
||||
import api from '@/Api';
|
||||
import { findSameNamedTorrents } from '@/utils';
|
||||
import { Torrent } from '../../types';
|
||||
import Component from 'vue-class-component';
|
||||
import { Emit, Prop } from 'vue-property-decorator';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
value: Array as PropType<Torrent[]>,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tr,
|
||||
deleteFiles: false,
|
||||
deleteSameNamed: false,
|
||||
submitting: false,
|
||||
torrents: [],
|
||||
sameNamedTorrents: [],
|
||||
};
|
||||
@Component({
|
||||
computed: {
|
||||
...mapGetters(['allTorrents']),
|
||||
},
|
||||
})
|
||||
export default class ConfirmDeleteDialog extends Vue {
|
||||
@Prop(Array)
|
||||
readonly value!: Torrent[]
|
||||
|
||||
deleteFiles = false
|
||||
deleteSameNamed = false
|
||||
moveSameNamed = false
|
||||
submitting = false
|
||||
torrents: Torrent[] = []
|
||||
sameNamedTorrents: Torrent[] = []
|
||||
|
||||
allTorrents!: Torrent[]
|
||||
|
||||
created() {
|
||||
this.torrents = this.value;
|
||||
this.sameNamedTorrents = findSameNamedTorrents(this.allTorrents, this.torrents);
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['allTorrents']),
|
||||
phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
this.$emit('input', []);
|
||||
},
|
||||
async submit() {
|
||||
if (this.submitting) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
}
|
||||
|
||||
let torrentsToDelete;
|
||||
if (this.deleteSameNamed) {
|
||||
torrentsToDelete = this.torrents.concat(this.sameNamedTorrents);
|
||||
} else {
|
||||
torrentsToDelete = this.torrents;
|
||||
}
|
||||
const hashes = torrentsToDelete.map((t: any) => t.hash);
|
||||
await api.deleteTorrents(hashes, this.deleteFiles);
|
||||
@Emit('input')
|
||||
closeDialog() {
|
||||
return []
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
},
|
||||
},
|
||||
});
|
||||
async submit() {
|
||||
if (this.submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
|
||||
let torrentsToDelete;
|
||||
if (this.deleteSameNamed) {
|
||||
torrentsToDelete = this.torrents.concat(this.sameNamedTorrents);
|
||||
} else {
|
||||
torrentsToDelete = this.torrents;
|
||||
}
|
||||
const hashes = torrentsToDelete.map((t: any) => t.hash);
|
||||
await api.deleteTorrents(hashes, this.deleteFiles);
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -49,57 +49,64 @@ import _ from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { tr } from '@/locale';
|
||||
import api from '@/Api';
|
||||
import { findSameNamedTorrents } from '@/utils';
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Emit } from 'vue-property-decorator';
|
||||
import { Torrent } from '../../types';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
value: Array,
|
||||
category: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
moveSameNamed: false,
|
||||
submitting: false,
|
||||
torrents: [],
|
||||
sameNamedTorrents: [],
|
||||
};
|
||||
@Component({
|
||||
computed: {
|
||||
...mapGetters(['allTorrents']),
|
||||
},
|
||||
})
|
||||
export default class ConfirmSetCategoryDialog extends Vue {
|
||||
@Prop(Array)
|
||||
readonly value!: Torrent[]
|
||||
|
||||
@Prop(String)
|
||||
readonly category!: string
|
||||
|
||||
moveSameNamed = false
|
||||
submitting = false
|
||||
torrents: Torrent[] = []
|
||||
sameNamedTorrents: Torrent[] = []
|
||||
|
||||
allTorrents!: Torrent[]
|
||||
|
||||
created() {
|
||||
this.torrents = this.value;
|
||||
this.sameNamedTorrents = findSameNamedTorrents(this.allTorrents, this.torrents);
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['allTorrents']),
|
||||
phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
this.$emit('input', []);
|
||||
},
|
||||
async submit() {
|
||||
if (this.submitting) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
}
|
||||
|
||||
let torrentsToMove;
|
||||
if (this.moveSameNamed) {
|
||||
torrentsToMove = this.torrents.concat(this.sameNamedTorrents);
|
||||
} else {
|
||||
torrentsToMove = this.torrents;
|
||||
}
|
||||
const hashes = torrentsToMove.map((t: any) => t.hash);
|
||||
await api.setTorrentsCategory(hashes, this.category);
|
||||
@Emit('input')
|
||||
closeDialog() {
|
||||
return []
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
},
|
||||
},
|
||||
});
|
||||
async submit() {
|
||||
if (this.submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
|
||||
let torrentsToMove;
|
||||
if (this.moveSameNamed) {
|
||||
torrentsToMove = this.torrents.concat(this.sameNamedTorrents);
|
||||
} else {
|
||||
torrentsToMove = this.torrents;
|
||||
}
|
||||
const hashes = torrentsToMove.map((t: any) => t.hash);
|
||||
await api.setTorrentsCategory(hashes, this.category);
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -83,103 +83,112 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { chain } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import api from '@/Api';
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Emit } from 'vue-property-decorator';
|
||||
import { Torrent } from '../../types';
|
||||
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
value: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
step: 1,
|
||||
valid: false,
|
||||
submitting: false,
|
||||
torrents: [],
|
||||
search: '',
|
||||
replace: '',
|
||||
toEdit: [],
|
||||
currentIndex: 0,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.torrents = this.value;
|
||||
},
|
||||
@Component({
|
||||
computed: {
|
||||
...mapGetters(['allTorrents']),
|
||||
phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
},
|
||||
canNext() {
|
||||
if (this.step === 1 && this.valid) {
|
||||
return true;
|
||||
}
|
||||
if (this.step === 2 && this.toEdit.length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (this.step === 3 && !this.submitting) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
this.$emit('input', []);
|
||||
},
|
||||
calcResults() {
|
||||
const regex = new RegExp(this.search);
|
||||
})
|
||||
export default class EditTrackerDialog extends Vue {
|
||||
@Prop(Array)
|
||||
readonly value!: Torrent[]
|
||||
|
||||
return _.chain(this.torrents)
|
||||
.map(({ tracker, hash, name }) => {
|
||||
const newUrl = tracker.replace(regex, this.replace);
|
||||
return newUrl === tracker ? null : {
|
||||
hash,
|
||||
name,
|
||||
origUrl: tracker,
|
||||
newUrl,
|
||||
};
|
||||
}).compact().value();
|
||||
},
|
||||
back() {
|
||||
if (this.step === 1) {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
this.step--;
|
||||
},
|
||||
async foward() {
|
||||
if (this.step === 1) {
|
||||
this.toEdit = this.calcResults();
|
||||
this.step++;
|
||||
return;
|
||||
}
|
||||
if (this.step === 3) {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
step = 1
|
||||
valid = false
|
||||
submitting = false
|
||||
torrents: Torrent[] = []
|
||||
search = ''
|
||||
replace = ''
|
||||
toEdit: any[] = []
|
||||
currentIndex = 0
|
||||
|
||||
if (this.submitting) {
|
||||
return;
|
||||
}
|
||||
allTorrents!: Torrent[]
|
||||
|
||||
this.submitting = true;
|
||||
created() {
|
||||
this.torrents = this.value
|
||||
}
|
||||
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
}
|
||||
get canNext() {
|
||||
if (this.step === 1 && this.valid) {
|
||||
return true;
|
||||
}
|
||||
if (this.step === 2 && this.toEdit.length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (this.step === 3 && !this.submitting) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Emit('input')
|
||||
closeDialog() {
|
||||
return []
|
||||
}
|
||||
|
||||
calcResults(): any[] {
|
||||
const regex = new RegExp(this.search);
|
||||
|
||||
return chain(this.torrents)
|
||||
.map(({ tracker, hash, name }) => {
|
||||
const newUrl = tracker.replace(regex, this.replace);
|
||||
return newUrl === tracker ? null : {
|
||||
hash,
|
||||
name,
|
||||
origUrl: tracker,
|
||||
newUrl,
|
||||
};
|
||||
}).compact().value();
|
||||
}
|
||||
|
||||
back() {
|
||||
if (this.step === 1) {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
this.step--;
|
||||
}
|
||||
|
||||
async foward() {
|
||||
if (this.step === 1) {
|
||||
this.toEdit = this.calcResults();
|
||||
this.step++;
|
||||
return;
|
||||
}
|
||||
if (this.step === 3) {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentIndex = 0;
|
||||
if (this.submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of this.toEdit) {
|
||||
await api.editTracker(item.hash, item.origUrl, item.newUrl);
|
||||
this.currentIndex++;
|
||||
}
|
||||
this.submitting = true;
|
||||
this.step++;
|
||||
|
||||
this.submitting = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
this.currentIndex = 0;
|
||||
|
||||
for (const item of this.toEdit) {
|
||||
await api.editTracker(item.hash, item.origUrl, item.newUrl);
|
||||
this.currentIndex++;
|
||||
}
|
||||
|
||||
this.submitting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<span>Info</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-tabs v-model="mTab">
|
||||
<v-tabs v-model="tab">
|
||||
<v-tab href="#general">
|
||||
General
|
||||
</v-tab>
|
||||
@@ -27,7 +27,7 @@
|
||||
Content
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-items :value="mTab" touchless>
|
||||
<v-tabs-items :value="tab" touchless>
|
||||
<v-tab-item value="general">
|
||||
<panel
|
||||
v-for="torrent in torrents"
|
||||
@@ -50,7 +50,7 @@
|
||||
>
|
||||
<trackers
|
||||
:hash="torrent.hash"
|
||||
:isActive="mTab === 'trackers'"
|
||||
:isActive="tab === 'trackers'"
|
||||
/>
|
||||
</panel>
|
||||
</v-tab-item>
|
||||
@@ -63,7 +63,7 @@
|
||||
>
|
||||
<peers
|
||||
:hash="torrent.hash"
|
||||
:isActive="mTab === 'peers'"
|
||||
:isActive="tab === 'peers'"
|
||||
/>
|
||||
</panel>
|
||||
</v-tab-item>
|
||||
@@ -76,7 +76,7 @@
|
||||
>
|
||||
<torrent-content
|
||||
:hash="torrent.hash"
|
||||
:isActive="mTab === 'content'"
|
||||
:isActive="tab === 'content'"
|
||||
/>
|
||||
</panel>
|
||||
</v-tab-item>
|
||||
@@ -98,8 +98,11 @@ import TorrentContent from './TorrentContent.vue';
|
||||
import Trackers from './Trackers.vue';
|
||||
import Peers from './Peers.vue';
|
||||
import Panel from './Panel.vue';
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Emit, Watch, PropSync } from 'vue-property-decorator';
|
||||
import { Torrent } from '../../types';
|
||||
|
||||
export default Vue.extend({
|
||||
@Component({
|
||||
components: {
|
||||
TorrentInfo,
|
||||
TorrentContent,
|
||||
@@ -107,40 +110,32 @@ export default Vue.extend({
|
||||
Peers,
|
||||
Panel,
|
||||
},
|
||||
})
|
||||
export default class InfoDialog extends Vue {
|
||||
@Prop(Array)
|
||||
readonly value!: Torrent[]
|
||||
|
||||
@PropSync('tab', String)
|
||||
tab!: string
|
||||
|
||||
torrents!: Torrent[]
|
||||
|
||||
props: {
|
||||
value: Array,
|
||||
tab: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
torrents: [],
|
||||
mTab: null,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.torrents = this.value;
|
||||
this.mTab = this.tab;
|
||||
},
|
||||
computed: {
|
||||
phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
},
|
||||
dialogWidth() {
|
||||
return this.phoneLayout ? '100%' : '80%';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
this.$emit('input', []);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
mTab(v) {
|
||||
this.$emit('change', v);
|
||||
},
|
||||
},
|
||||
});
|
||||
this.torrents = this.value
|
||||
}
|
||||
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
}
|
||||
get dialogWidth() {
|
||||
return this.phoneLayout ? '100%' : '80%';
|
||||
}
|
||||
|
||||
@Emit('input')
|
||||
closeDialog() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -38,20 +38,11 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import api from '@/Api';
|
||||
import { sleep } from '@/utils';
|
||||
import Taskable from '@/mixins/taskable';
|
||||
import Component from 'vue-class-component';
|
||||
import HasTask from '../../mixins/hasTask';
|
||||
import { Prop, Emit } from 'vue-property-decorator';
|
||||
|
||||
export default Vue.extend({
|
||||
mixins: [Taskable],
|
||||
|
||||
props: {
|
||||
value: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
logs: [],
|
||||
};
|
||||
},
|
||||
@Component({
|
||||
filters: {
|
||||
formatType(type: number) {
|
||||
const map: any = {
|
||||
@@ -72,41 +63,46 @@ export default Vue.extend({
|
||||
return map[type];
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dialogWidth() {
|
||||
return this.$vuetify.breakpoint.smAndDown ? '100%' : '70%';
|
||||
},
|
||||
phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
this.$emit('input', false);
|
||||
},
|
||||
async getLogs() {
|
||||
const lastId = this.logs.length ? this.logs[this.logs.length - 1].id : -1;
|
||||
const logs = await api.getLogs(lastId);
|
||||
})
|
||||
export default class LogsDialog extends HasTask {
|
||||
@Prop(Boolean)
|
||||
readonly value!: boolean
|
||||
|
||||
if (this.destory) {
|
||||
return;
|
||||
}
|
||||
logs: any[] = []
|
||||
|
||||
if (logs.length) {
|
||||
this.logs = this.logs.concat(logs);
|
||||
get dialogWidth() {
|
||||
return this.$vuetify.breakpoint.smAndDown ? '100%' : '70%';
|
||||
}
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
}
|
||||
|
||||
await this.$nextTick();
|
||||
@Emit('input')
|
||||
closeDialog() {
|
||||
return false
|
||||
}
|
||||
|
||||
this.$refs.end.scrollIntoView();
|
||||
}
|
||||
async getLogs() {
|
||||
const lastId = this.logs.length ? this.logs[this.logs.length - 1].id : -1;
|
||||
const logs = await api.getLogs(lastId);
|
||||
|
||||
this.task = setTimeout(this.getLogs, 2000);
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.getLogs();
|
||||
},
|
||||
});
|
||||
if (this.destroy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (logs.length) {
|
||||
this.logs = this.logs.concat(logs);
|
||||
|
||||
await this.$nextTick();
|
||||
|
||||
(this.$refs.end as HTMLElement).scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
created() {
|
||||
this.setTaskAndRun(this.getLogs)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -39,42 +39,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { map, merge, cloneDeep } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import { codeToFlag, isWindows } from '../../utils';
|
||||
import Taskable from '@/mixins/taskable';
|
||||
import api from '../../Api';
|
||||
import { formatSize } from '../../filters';
|
||||
import BaseTorrentInfo from './baseTorrentInfo';
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop } from 'vue-property-decorator';
|
||||
|
||||
export default Vue.extend({
|
||||
mixins: [Taskable],
|
||||
|
||||
props: {
|
||||
hash: String,
|
||||
isActive: Boolean,
|
||||
},
|
||||
data() {
|
||||
const headers = [
|
||||
{ text: 'IP', value: 'ip' },
|
||||
{ text: 'Connection', value: 'connection' },
|
||||
{ text: 'Flags', value: 'flags' },
|
||||
{ text: 'Client', value: 'client' },
|
||||
{ text: 'Progress', value: 'progress' },
|
||||
{ text: 'DL Speed', value: 'dl_speed' },
|
||||
{ text: 'Downloaded', value: 'downloaded' },
|
||||
{ text: 'UP Speed', value: 'up_speed' },
|
||||
{ text: 'Uploaded', value: 'uploaded' },
|
||||
{ text: 'Relevance', value: 'relevance' },
|
||||
{ text: 'Files', value: 'files' },
|
||||
];
|
||||
|
||||
return {
|
||||
headers,
|
||||
peersObj: null,
|
||||
rid: null,
|
||||
isWindows,
|
||||
};
|
||||
},
|
||||
@Component({
|
||||
filters: {
|
||||
networkSpeed(speed: number) {
|
||||
if (speed === 0) {
|
||||
@@ -91,57 +65,62 @@ export default Vue.extend({
|
||||
return formatSize(size);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
peers() {
|
||||
return _.map(this.peersObj, (value, key) => _.merge({}, value, { key }));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
codeToFlag(code: string) {
|
||||
if (code) {
|
||||
return codeToFlag(code);
|
||||
}
|
||||
})
|
||||
export default class Peers extends BaseTorrentInfo {
|
||||
@Prop(String)
|
||||
readonly hash!: string
|
||||
|
||||
return {};
|
||||
},
|
||||
async getPeers() {
|
||||
const resp = await api.getTorrentPeers(this.hash, this.rid);
|
||||
this.rid = resp.rid;
|
||||
headers = [
|
||||
{ text: 'IP', value: 'ip' },
|
||||
{ text: 'Connection', value: 'connection' },
|
||||
{ text: 'Flags', value: 'flags' },
|
||||
{ text: 'Client', value: 'client' },
|
||||
{ text: 'Progress', value: 'progress' },
|
||||
{ text: 'DL Speed', value: 'dl_speed' },
|
||||
{ text: 'Downloaded', value: 'downloaded' },
|
||||
{ text: 'UP Speed', value: 'up_speed' },
|
||||
{ text: 'Uploaded', value: 'uploaded' },
|
||||
{ text: 'Relevance', value: 'relevance' },
|
||||
{ text: 'Files', value: 'files' },
|
||||
]
|
||||
|
||||
if (resp.full_update) {
|
||||
this.peersObj = resp.peers;
|
||||
} else {
|
||||
const tmp: any = _.cloneDeep(this.peersObj);
|
||||
if (resp.peers_removed) {
|
||||
for (const key of resp.peers_removed) {
|
||||
delete tmp[key];
|
||||
}
|
||||
}
|
||||
this.peersObj = _.merge(tmp, resp.peers);
|
||||
}
|
||||
peersObj: any = null
|
||||
rid: number | null = null
|
||||
isWindows: boolean = isWindows
|
||||
|
||||
if (!this.isActive || this.destroy) {
|
||||
return;
|
||||
}
|
||||
get peers() {
|
||||
return map(this.peersObj, (value, key) => merge({}, value, { key }));
|
||||
}
|
||||
|
||||
this.task = setTimeout(this.getPeers, 2000);
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
if (this.isActive) {
|
||||
await this.getPeers();
|
||||
codeToFlag(code: string) {
|
||||
if (code) {
|
||||
return codeToFlag(code);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async isActive(v) {
|
||||
if (v) {
|
||||
await this.getPeers();
|
||||
} else {
|
||||
this.cancelTask();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
async getPeers() {
|
||||
const resp = await api.getTorrentPeers(this.hash, this.rid || undefined);
|
||||
this.rid = resp.rid;
|
||||
|
||||
if (resp.full_update) {
|
||||
this.peersObj = resp.peers;
|
||||
} else {
|
||||
const tmp: any = cloneDeep(this.peersObj);
|
||||
if (resp.peers_removed) {
|
||||
for (const key of resp.peers_removed) {
|
||||
delete tmp[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
this.peersObj = merge(tmp, resp.peers);
|
||||
}
|
||||
}
|
||||
|
||||
startTask() {
|
||||
this.setTaskAndRun(this.doTask, 2000)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { groupBy } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import api from '../../Api';
|
||||
import Taskable from '@/mixins/taskable';
|
||||
import BaseTorrentInfo from './baseTorrentInfo'
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Watch } from 'vue-property-decorator';
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface File {
|
||||
@@ -49,122 +51,104 @@ interface Data {
|
||||
|
||||
const FILE_KEY = '/FILE/';
|
||||
|
||||
export default Vue.extend({
|
||||
mixins: [Taskable],
|
||||
@Component
|
||||
export default class TorrentContent extends BaseTorrentInfo {
|
||||
@Prop(String)
|
||||
readonly hash!: string
|
||||
|
||||
props: {
|
||||
hash: String,
|
||||
isActive: Boolean,
|
||||
},
|
||||
data(): Data {
|
||||
return {
|
||||
files: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
fileTree() {
|
||||
return this.buildTree(this.files, 0);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getFiles() {
|
||||
this.files = await api.getTorrentFiles(this.hash);
|
||||
if (!this.isActive || this.destroy) {
|
||||
return;
|
||||
}
|
||||
files: File[] = []
|
||||
|
||||
this.task = setTimeout(this.getFiles, 5000);
|
||||
},
|
||||
getRowIcon(row: any) {
|
||||
if (row.item.item) {
|
||||
return 'mdi-file';
|
||||
}
|
||||
get fileTree() {
|
||||
return this.buildTree(this.files, 0);
|
||||
}
|
||||
|
||||
return row.open ? 'mdi-folder-open' : 'mdi-folder';
|
||||
},
|
||||
getTotalSize(item: TreeItem) {
|
||||
if (item.item) {
|
||||
return item.item.size;
|
||||
}
|
||||
async getFiles() {
|
||||
this.files = await api.getTorrentFiles(this.hash);
|
||||
}
|
||||
|
||||
let size = 0;
|
||||
for (const child of item.children!) {
|
||||
size += this.getTotalSize(child);
|
||||
}
|
||||
|
||||
return size;
|
||||
},
|
||||
getTotalProgress(item: TreeItem) {
|
||||
if (item.item) {
|
||||
return item.item.progress;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
let progress = 0;
|
||||
for (const child of item.children!) {
|
||||
count++;
|
||||
progress += this.getTotalProgress(child);
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return progress / count;
|
||||
},
|
||||
getFileFolder(item: File, start: number) {
|
||||
const { name } = item;
|
||||
const index = name.indexOf('/', start);
|
||||
if (index === -1) {
|
||||
return FILE_KEY;
|
||||
}
|
||||
|
||||
return name.substring(start, index);
|
||||
},
|
||||
buildTree(files: Array<File>, start: number): Array<TreeItem> {
|
||||
if (!files.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = _.groupBy(files, item => this.getFileFolder(item, start));
|
||||
|
||||
const result = [];
|
||||
for (const [folder, values] of _.entries(entries)) {
|
||||
if (folder !== FILE_KEY) {
|
||||
const subTree = this.buildTree(values, start + folder.length + 1);
|
||||
result.push({
|
||||
name: folder,
|
||||
children: subTree,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const item of values) {
|
||||
result.push({
|
||||
name: item.name.substring(start),
|
||||
item,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
if (this.isActive) {
|
||||
await this.getFiles();
|
||||
getRowIcon(row: any) {
|
||||
if (row.item.item) {
|
||||
return 'mdi-file';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async isActive(v) {
|
||||
if (v) {
|
||||
await this.getFiles();
|
||||
} else {
|
||||
this.cancelTask();
|
||||
|
||||
return row.open ? 'mdi-folder-open' : 'mdi-folder';
|
||||
}
|
||||
|
||||
getTotalSize(item: TreeItem) {
|
||||
if (item.item) {
|
||||
return item.item.size;
|
||||
}
|
||||
|
||||
let size = 0;
|
||||
for (const child of item.children!) {
|
||||
size += this.getTotalSize(child);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
getTotalProgress(item: TreeItem) {
|
||||
if (item.item) {
|
||||
return item.item.progress;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
let progress = 0;
|
||||
for (const child of item.children!) {
|
||||
count++;
|
||||
progress += this.getTotalProgress(child);
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return progress / count;
|
||||
}
|
||||
|
||||
getFileFolder(item: File, start: number) {
|
||||
const { name } = item;
|
||||
const index = name.indexOf('/', start);
|
||||
if (index === -1) {
|
||||
return FILE_KEY;
|
||||
}
|
||||
|
||||
return name.substring(start, index);
|
||||
}
|
||||
|
||||
buildTree(files: Array<File>, start: number): TreeItem[] {
|
||||
if (!files.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = groupBy(files, item => this.getFileFolder(item, start));
|
||||
|
||||
const result = [];
|
||||
for (const [folder, values] of Object.entries(entries)) {
|
||||
if (folder !== FILE_KEY) {
|
||||
const subTree = this.buildTree(values, start + folder.length + 1);
|
||||
result.push({
|
||||
name: folder,
|
||||
children: subTree,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const item of values) {
|
||||
result.push({
|
||||
name: item.name.substring(start),
|
||||
item,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fetchInfo() {
|
||||
return this.getFiles()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -43,64 +43,22 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { chunk, countBy } from 'lodash';
|
||||
|
||||
import Vue from 'vue';
|
||||
import api from '../../Api';
|
||||
import Taskable from '@/mixins/taskable';
|
||||
import {
|
||||
formatDuration, formatSize, formatTimestamp, toPrecision,
|
||||
} from '@/filters';
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface Properties {
|
||||
addition_date: number;
|
||||
comment: string;
|
||||
completion_date: number;
|
||||
created_by: string;
|
||||
creation_date: number;
|
||||
dl_limit: number;
|
||||
dl_speed: number;
|
||||
dl_speed_avg: number;
|
||||
eta: number;
|
||||
last_seen: number;
|
||||
nb_connections: number;
|
||||
nb_connections_limit: number;
|
||||
peers: number;
|
||||
peers_total: number;
|
||||
piece_size: number;
|
||||
pieces_have: number;
|
||||
pieces_num: number;
|
||||
reannounce: number;
|
||||
save_path: string;
|
||||
seeding_time: number;
|
||||
seeds: number;
|
||||
seeds_total: number;
|
||||
share_ratio: number;
|
||||
time_elapsed: number;
|
||||
total_downloaded: number;
|
||||
total_downloaded_session: number;
|
||||
total_size: number;
|
||||
total_uploaded: number;
|
||||
total_uploaded_session: number;
|
||||
total_wasted: number;
|
||||
up_limit: number;
|
||||
up_speed: number;
|
||||
up_speed_avg: number;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
import { TorrentProperties, Torrent } from '@/types'
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Watch } from 'vue-property-decorator';
|
||||
import BaseTorrentInfo from './baseTorrentInfo';
|
||||
|
||||
interface Item {
|
||||
label: string;
|
||||
value: (prop: Properties) => string;
|
||||
}
|
||||
|
||||
interface Data {
|
||||
properties?: Properties;
|
||||
transfer: Array<Item>;
|
||||
information: Array<Item>;
|
||||
pieces: Array<PieceState>;
|
||||
canvas: CanvasRenderingContext2D | null;
|
||||
value: (prop: TorrentProperties) => string;
|
||||
}
|
||||
|
||||
enum PieceState {
|
||||
@@ -109,120 +67,105 @@ enum PieceState {
|
||||
Downloaded,
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
mixins: [Taskable],
|
||||
@Component({
|
||||
|
||||
props: {
|
||||
torrent: Object,
|
||||
isActive: Boolean,
|
||||
},
|
||||
data(): Data {
|
||||
return {
|
||||
properties: undefined,
|
||||
transfer: [
|
||||
{
|
||||
label: 'Time active',
|
||||
value: prop => formatDuration(prop.time_elapsed) + (prop.seeding_time ? ` (seeded ${formatDuration(prop.seeding_time)})` : ''),
|
||||
},
|
||||
{ label: 'ETA', value: prop => formatDuration(prop.eta, { dayLimit: 100 }) },
|
||||
{ label: 'Connections', value: prop => `${prop.nb_connections} (${prop.nb_connections_limit} max)` },
|
||||
{ label: 'Downloaded', value: prop => `${formatSize(prop.total_downloaded_session)}/${formatSize(prop.total_downloaded)}` },
|
||||
{ label: 'Uploaded', value: prop => `${formatSize(prop.total_uploaded_session)}/${formatSize(prop.total_uploaded)}` },
|
||||
{ label: 'Seeds', value: prop => `${prop.seeds} (${prop.seeds_total} total)` },
|
||||
{ label: 'DL speed', value: prop => `${formatSize(prop.dl_speed)}/s` },
|
||||
{ label: 'UP speed', value: prop => `${formatSize(prop.up_speed)}/s` },
|
||||
{ label: 'Peers', value: prop => `${prop.peers} (${prop.peers_total} total)` },
|
||||
{ label: 'Wasted', value: prop => formatSize(prop.total_wasted) },
|
||||
{ label: 'Share ratio', value: prop => toPrecision(prop.share_ratio, 3) },
|
||||
{ label: 'Reannounce', value: prop => formatDuration(prop.reannounce) },
|
||||
{ label: 'Last seen', value: prop => formatTimestamp(prop.last_seen) },
|
||||
],
|
||||
information: [
|
||||
{ label: 'Total size', value: prop => formatSize(prop.total_size) },
|
||||
{ label: 'Pieces', value: prop => `${prop.pieces_num} x ${formatSize(prop.piece_size)} (have ${prop.pieces_have})` },
|
||||
{ label: 'Created by', value: prop => prop.created_by },
|
||||
{ label: 'Created on', value: prop => formatTimestamp(prop.creation_date) },
|
||||
{ label: 'Added on', value: prop => formatTimestamp(prop.addition_date) },
|
||||
{ label: 'Completed on', value: prop => formatTimestamp(prop.completion_date) },
|
||||
{ label: 'Torrent hash', value: prop => this.torrent.hash },
|
||||
{ label: 'Save path', value: prop => prop.save_path },
|
||||
{ label: 'Comment', value: prop => prop.comment },
|
||||
],
|
||||
pieces: [],
|
||||
canvas: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async getData() {
|
||||
this.properties = await api.getTorrentProperties(this.torrent.hash);
|
||||
this.pieces = await api.getTorrentPieceStates(this.torrent.hash);
|
||||
if (!this.isActive || this.destroy) {
|
||||
return;
|
||||
}
|
||||
})
|
||||
export default class TorrentInfo extends BaseTorrentInfo {
|
||||
@Prop()
|
||||
readonly torrent!: Torrent
|
||||
|
||||
this.task = setTimeout(this.getData, 5000);
|
||||
properties: TorrentProperties | null = null
|
||||
|
||||
transfer: Item[] = [
|
||||
{
|
||||
label: 'Time active',
|
||||
value: prop => formatDuration(prop.time_elapsed) + (prop.seeding_time ? ` (seeded ${formatDuration(prop.seeding_time)})` : ''),
|
||||
},
|
||||
initCanvas(el: HTMLCanvasElement) {
|
||||
const { clientWidth, clientHeight } = el;
|
||||
/* eslint-disable no-param-reassign */
|
||||
el.width = clientWidth;
|
||||
el.height = clientHeight;
|
||||
/* eslint-enable no-param-reassign */
|
||||
{ label: 'ETA', value: prop => formatDuration(prop.eta, { dayLimit: 100 }) },
|
||||
{ label: 'Connections', value: prop => `${prop.nb_connections} (${prop.nb_connections_limit} max)` },
|
||||
{ label: 'Downloaded', value: prop => `${formatSize(prop.total_downloaded_session)}/${formatSize(prop.total_downloaded)}` },
|
||||
{ label: 'Uploaded', value: prop => `${formatSize(prop.total_uploaded_session)}/${formatSize(prop.total_uploaded)}` },
|
||||
{ label: 'Seeds', value: prop => `${prop.seeds} (${prop.seeds_total} total)` },
|
||||
{ label: 'DL speed', value: prop => `${formatSize(prop.dl_speed)}/s` },
|
||||
{ label: 'UP speed', value: prop => `${formatSize(prop.up_speed)}/s` },
|
||||
{ label: 'Peers', value: prop => `${prop.peers} (${prop.peers_total} total)` },
|
||||
{ label: 'Wasted', value: prop => formatSize(prop.total_wasted) },
|
||||
{ label: 'Share ratio', value: prop => toPrecision(prop.share_ratio, 3) },
|
||||
{ label: 'Reannounce', value: prop => formatDuration(prop.reannounce) },
|
||||
{ label: 'Last seen', value: prop => formatTimestamp(prop.last_seen) },
|
||||
]
|
||||
|
||||
const ctx = el.getContext('2d')!;
|
||||
return ctx;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
if (this.isActive) {
|
||||
await this.getData();
|
||||
information: Item[] = [
|
||||
{ label: 'Total size', value: prop => formatSize(prop.total_size) },
|
||||
{ label: 'Pieces', value: prop => `${prop.pieces_num} x ${formatSize(prop.piece_size)} (have ${prop.pieces_have})` },
|
||||
{ label: 'Created by', value: prop => prop.created_by },
|
||||
{ label: 'Created on', value: prop => formatTimestamp(prop.creation_date) },
|
||||
{ label: 'Added on', value: prop => formatTimestamp(prop.addition_date) },
|
||||
{ label: 'Completed on', value: prop => formatTimestamp(prop.completion_date) },
|
||||
{ label: 'Torrent hash', value: prop => this.torrent.hash },
|
||||
{ label: 'Save path', value: prop => prop.save_path },
|
||||
{ label: 'Comment', value: prop => prop.comment },
|
||||
]
|
||||
pieces: PieceState[] = []
|
||||
canvas: CanvasRenderingContext2D | null = null
|
||||
|
||||
async getData() {
|
||||
this.properties = await api.getTorrentProperties(this.torrent.hash);
|
||||
this.pieces = await api.getTorrentPieceStates(this.torrent.hash);
|
||||
}
|
||||
|
||||
initCanvas(el: HTMLCanvasElement) {
|
||||
const { clientWidth, clientHeight } = el;
|
||||
/* eslint-disable no-param-reassign */
|
||||
el.width = clientWidth;
|
||||
el.height = clientHeight;
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
||||
const ctx = el.getContext('2d')!;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
fetchInfo() {
|
||||
return this.getData()
|
||||
}
|
||||
|
||||
@Watch('pieces')
|
||||
onPiecesChanged(v: PieceState[]) {
|
||||
let ctx;
|
||||
if (this.canvas) {
|
||||
ctx = this.canvas
|
||||
} else {
|
||||
ctx = this.initCanvas(this.$refs.canvas as HTMLCanvasElement)
|
||||
this.canvas = ctx
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async isActive(v) {
|
||||
if (v) {
|
||||
await this.getData();
|
||||
|
||||
const { clientHeight, clientWidth } = ctx.canvas;
|
||||
const partNum = clientWidth / 2;
|
||||
ctx.clearRect(0, 0, clientWidth, clientHeight);
|
||||
|
||||
const offset = clientWidth / partNum;
|
||||
const chunkSize = v.length / partNum;
|
||||
|
||||
const chunks = chunk(v, chunkSize);
|
||||
for (let i = 0; i < partNum; i++) {
|
||||
const states = countBy(chunks[i]);
|
||||
const downloading = states[PieceState.Downloading];
|
||||
const empty = states[PieceState.Empty];
|
||||
const downloaded = states[PieceState.Downloaded];
|
||||
let color;
|
||||
if (downloading) {
|
||||
color = 'green';
|
||||
} else if (downloaded >= empty) {
|
||||
color = 'blue';
|
||||
} else {
|
||||
this.cancelTask();
|
||||
}
|
||||
},
|
||||
pieces(v) {
|
||||
let ctx;
|
||||
if (this.canvas) {
|
||||
ctx = this.canvas!;
|
||||
} else {
|
||||
ctx = this.initCanvas(this.$refs.canvas as HTMLCanvasElement);
|
||||
this.canvas = ctx;
|
||||
continue;
|
||||
}
|
||||
|
||||
const { clientHeight, clientWidth } = ctx.canvas;
|
||||
const partNum = clientWidth / 2;
|
||||
ctx.clearRect(0, 0, clientWidth, clientHeight);
|
||||
|
||||
const offset = clientWidth / partNum;
|
||||
const chunkSize = v.length / partNum;
|
||||
|
||||
const chunks = _.chunk(v, chunkSize);
|
||||
for (let i = 0; i < partNum; i++) {
|
||||
const states = _.countBy(chunks[i]);
|
||||
const downloading = states[PieceState.Downloading];
|
||||
const empty = states[PieceState.Empty];
|
||||
const downloaded = states[PieceState.Downloaded];
|
||||
let color;
|
||||
if (downloading) {
|
||||
color = 'green';
|
||||
} else if (downloaded >= empty) {
|
||||
color = 'blue';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(i * offset, 0, offset, clientHeight);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(i * offset, 0, offset, clientHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -23,32 +23,11 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import api from '../../Api';
|
||||
import Taskable from '@/mixins/taskable';
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Watch } from 'vue-property-decorator';
|
||||
import BaseTorrentInfo from './baseTorrentInfo';
|
||||
|
||||
export default Vue.extend({
|
||||
mixins: [Taskable],
|
||||
|
||||
props: {
|
||||
hash: String,
|
||||
isActive: Boolean,
|
||||
},
|
||||
data() {
|
||||
const headers = [
|
||||
{ text: '#', value: 'tier' },
|
||||
{ text: 'URL', value: 'url' },
|
||||
{ text: 'Status', value: 'status' },
|
||||
{ text: 'Peers', value: 'num_peers' },
|
||||
{ text: 'Seeds', value: 'num_seeds' },
|
||||
{ text: 'Leeches', value: 'num_leeches' },
|
||||
{ text: 'Downloaded', value: 'num_downloaded' },
|
||||
{ text: 'Message', value: 'msg' },
|
||||
];
|
||||
|
||||
return {
|
||||
headers,
|
||||
trackers: [],
|
||||
};
|
||||
},
|
||||
@Component({
|
||||
filters: {
|
||||
formatTrackerStatus(status: number) {
|
||||
const map = [
|
||||
@@ -69,29 +48,30 @@ export default Vue.extend({
|
||||
return num.toString();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getTracker() {
|
||||
this.trackers = await api.getTorrentTracker(this.hash);
|
||||
if (!this.isActive || this.destroy) {
|
||||
return;
|
||||
}
|
||||
})
|
||||
export default class Trackers extends BaseTorrentInfo {
|
||||
@Prop(String)
|
||||
readonly hash!: string
|
||||
|
||||
this.task = setTimeout(this.getTracker, 5000);
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
if (this.isActive) {
|
||||
await this.getTracker();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async isActive(v) {
|
||||
if (v) {
|
||||
await this.getTracker();
|
||||
} else {
|
||||
this.cancelTask();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
readonly headers = [
|
||||
{ text: '#', value: 'tier' },
|
||||
{ text: 'URL', value: 'url' },
|
||||
{ text: 'Status', value: 'status' },
|
||||
{ text: 'Peers', value: 'num_peers' },
|
||||
{ text: 'Seeds', value: 'num_seeds' },
|
||||
{ text: 'Leeches', value: 'num_leeches' },
|
||||
{ text: 'Downloaded', value: 'num_downloaded' },
|
||||
{ text: 'Message', value: 'msg' },
|
||||
]
|
||||
|
||||
trackers = []
|
||||
|
||||
async getTracker() {
|
||||
this.trackers = await api.getTorrentTracker(this.hash);
|
||||
}
|
||||
|
||||
fetchInfo() {
|
||||
return this.getTracker()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
37
src/components/dialogs/baseTorrentInfo.ts
Normal file
37
src/components/dialogs/baseTorrentInfo.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Prop, Watch, Component } from 'vue-property-decorator'
|
||||
import HasTask from '@/mixins/hasTask'
|
||||
|
||||
@Component
|
||||
export default class BaseTorrentInfo extends HasTask {
|
||||
@Prop(Boolean)
|
||||
readonly isActive!: boolean
|
||||
|
||||
protected fetchInfo(): Promise<void> {
|
||||
throw 'Not implement'
|
||||
}
|
||||
|
||||
protected async doTask() {
|
||||
await this.fetchInfo()
|
||||
|
||||
return !this.isActive
|
||||
}
|
||||
|
||||
startTask() {
|
||||
this.setTaskAndRun(this.doTask, 5000)
|
||||
}
|
||||
|
||||
created() {
|
||||
if (this.isActive) {
|
||||
this.startTask()
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('isActive')
|
||||
async onActived(v: boolean) {
|
||||
if (v) {
|
||||
this.startTask()
|
||||
} else {
|
||||
this.cancelTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,12 @@ export default class HasTask extends Vue {
|
||||
async runTask() {
|
||||
this.cancelTask()
|
||||
|
||||
const r = this.call!()
|
||||
let r = this.call!()
|
||||
if (r instanceof Promise) {
|
||||
await r
|
||||
r = await r
|
||||
}
|
||||
|
||||
if (this.destroy) {
|
||||
if (this.destroy || r) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
destory: false,
|
||||
task: 0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
cancelTask() {
|
||||
if (this.task) {
|
||||
clearTimeout(this.task);
|
||||
this.task = 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destory = true;
|
||||
this.cancelTask();
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import { Module } from 'vuex';
|
||||
import { ConfigState } from './types';
|
||||
import { ConfigState, ConfigPayload } from './types';
|
||||
|
||||
const configKey = 'qb-config';
|
||||
|
||||
@@ -38,7 +38,7 @@ export const configStore : Module<ConfigState, any> = {
|
||||
};
|
||||
},
|
||||
mutations: {
|
||||
updateConfig(state, payload) {
|
||||
updateConfig(state, payload: ConfigPayload) {
|
||||
const { key, value } = payload;
|
||||
if (_.isPlainObject(value)) {
|
||||
const tmp = _.merge({}, state.userConfig[key], value);
|
||||
|
||||
@@ -7,10 +7,21 @@ export interface RootState {
|
||||
pasteUrl: string | null;
|
||||
}
|
||||
|
||||
export interface TorrentFilter {
|
||||
state: string
|
||||
category: string
|
||||
site: string
|
||||
}
|
||||
|
||||
export interface ConfigState {
|
||||
userConfig: any;
|
||||
}
|
||||
|
||||
export interface ConfigPayload {
|
||||
key: string,
|
||||
value: any,
|
||||
}
|
||||
|
||||
export enum DialogType {
|
||||
Alert,
|
||||
YesNo,
|
||||
|
||||
41
src/types.ts
41
src/types.ts
@@ -49,8 +49,9 @@ export interface Torrent extends BaseTorrent {
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
name: string;
|
||||
savePath: string;
|
||||
key: string
|
||||
name: string
|
||||
savePath?: string
|
||||
}
|
||||
|
||||
export interface ServerState {
|
||||
@@ -126,3 +127,39 @@ export interface RssRule {
|
||||
assignedCategory: string,
|
||||
savepath: string,
|
||||
}
|
||||
|
||||
export interface TorrentProperties {
|
||||
addition_date: number;
|
||||
comment: string;
|
||||
completion_date: number;
|
||||
created_by: string;
|
||||
creation_date: number;
|
||||
dl_limit: number;
|
||||
dl_speed: number;
|
||||
dl_speed_avg: number;
|
||||
eta: number;
|
||||
last_seen: number;
|
||||
nb_connections: number;
|
||||
nb_connections_limit: number;
|
||||
peers: number;
|
||||
peers_total: number;
|
||||
piece_size: number;
|
||||
pieces_have: number;
|
||||
pieces_num: number;
|
||||
reannounce: number;
|
||||
save_path: string;
|
||||
seeding_time: number;
|
||||
seeds: number;
|
||||
seeds_total: number;
|
||||
share_ratio: number;
|
||||
time_elapsed: number;
|
||||
total_downloaded: number;
|
||||
total_downloaded_session: number;
|
||||
total_size: number;
|
||||
total_uploaded: number;
|
||||
total_uploaded_session: number;
|
||||
total_wasted: number;
|
||||
up_limit: number;
|
||||
up_speed: number;
|
||||
up_speed_avg: number;
|
||||
}
|
||||
Reference in New Issue
Block a user