Refine to use Vue class component

This commit is contained in:
CzBiX
2020-03-29 18:12:53 +08:00
parent 3800055907
commit f67bda4f46
20 changed files with 1244 additions and 1251 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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();
}
}
}

View File

@@ -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
}

View File

@@ -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();
},
});

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;
}