Add info dialog

This commit is contained in:
CzBiX
2019-05-03 18:16:02 +08:00
parent bd4d5792f6
commit 94d715376b
12 changed files with 538 additions and 28 deletions

View File

@@ -93,6 +93,27 @@ class Api {
return this.actionTorrents('setCategory', hashes, {category});
}
public getTorrentTracker(hash: string) {
const params = {
hash,
};
return this.axios.get('/torrents/trackers', {
params,
}).then(this.handleResponse);
}
public getTorrentPeers(hash: string, rid?: number) {
const params = {
hash,
rid,
};
return this.axios.get('/sync/torrentPeers', {
params,
}).then(this.handleResponse);
}
private actionTorrents(action: string, hashes: string[], extra?: any) {
const params: any = {
hashes: hashes.join('|'),

View File

@@ -55,7 +55,7 @@ import LoginForm from './components/LoginForm.vue';
import MainToolbar from './components/MainToolbar.vue';
import Torrents from './components/Torrents.vue';
import AppFooter from './components/Footer.vue';
import LogsDialog from './components/LogsDialog.vue';
import LogsDialog from './components/dialogs/LogsDialog.vue';
import { api } from './Api';
import { mapActions, mapGetters, mapState, mapMutations } from 'vuex';
import Axios, { AxiosError } from 'axios';
@@ -76,7 +76,6 @@ export default Vue.extend({
return {
needAuth: false,
drawer: true,
phoneLayout: false,
drawerOptions: {
showLogs: false,
},
@@ -84,8 +83,6 @@ export default Vue.extend({
};
},
async created() {
this.phoneLayout = this.$vuetify.breakpoint.xsOnly;
await this.getInitData();
},
beforeDestroy() {
@@ -100,6 +97,9 @@ export default Vue.extend({
'preferences',
]),
...mapGetters(['config']),
phoneLayout() {
return this.$vuetify.breakpoint.xsOnly;
},
},
methods: {
...mapMutations([

View File

@@ -29,6 +29,9 @@
<v-icon>mdi-pause</v-icon>
</v-btn>
<v-divider vertical inset />
<v-btn icon @click="showInfo" title="Info" v-if="selectedRows.length <= 4">
<v-icon>mdi-alert-circle</v-icon>
</v-btn>
<v-menu offset-y>
<template v-slot:activator="{ on }">
<v-btn icon v-on="on" title="Category">
@@ -104,7 +107,7 @@
:color="row.item.state | stateColor(true)"
class="text-xs-center ma-0"
>
<span :class="row.item.progress | progressColorClass">{{ row.item.progress | progressText }}</span>
<span :class="row.item.progress | progressColorClass">{{ row.item.progress | progress }}</span>
</v-progress-linear>
</td>
<td>{{ row.item.state }}</td>
@@ -123,13 +126,15 @@
</v-data-table>
</v-layout>
<confirm-delete-dialog v-if="deleteDialog" v-model="deleteDialog" :torrents="selectedRows" />
<confirm-delete-dialog v-if="toDelete.length" v-model="toDelete" />
<info-dialog v-if="toShowInfo.length" v-model="toShowInfo" :tab="infoTab" @change="infoTab = $event" />
</v-container>
</template>
<script lang="ts">
import Vue from 'vue';
import ConfirmDeleteDialog from './dialogs/ConfirmDeleteDialog.vue';
import InfoDialog from './dialogs/InfoDialog.vue';
import { mapState, mapGetters, mapMutations } from 'vuex';
import _ from 'lodash';
import { api } from '../Api';
@@ -218,6 +223,7 @@ export default Vue.extend({
components: {
ConfirmDeleteDialog,
InfoDialog,
},
data() {
@@ -238,7 +244,9 @@ export default Vue.extend({
return {
headers,
selectedRows: [],
deleteDialog: false,
toDelete: [],
toShowInfo: [],
infoTab: null,
pagination: null,
};
},
@@ -290,9 +298,6 @@ export default Vue.extend({
},
filters: {
progressText(progress: number) {
return Math.floor(progress * 100) + '%';
},
progressColorClass(progress: number) {
const color = progress >= 0.5 ? 'white' : 'black';
return color + '--text';
@@ -323,7 +328,10 @@ export default Vue.extend({
'updateConfig',
]),
confirmDelete() {
this.deleteDialog = true;
this.toDelete = this.selectedRows;
},
showInfo() {
this.toShowInfo = this.selectedRows;
},
async resumeTorrents() {
await api.resumeTorrents(this.selectedHashes);

View File

@@ -1,5 +1,5 @@
<template>
<v-dialog :value="value" @input="$emit('input', $event)" width="40em">
<v-dialog :value="true" @input="closeDialog" :fullscreen="phoneLayout" width="40em">
<v-card>
<v-card-title
class="headline grey lighten-4"
@@ -18,7 +18,7 @@
<v-checkbox
v-model="deleteFiles"
prepend-icon="mdi-file-cancel"
label="Also delete the files on the hard disk"
label="Also delete files"
/>
</v-card-text>
<v-card-actions>
@@ -43,18 +43,26 @@ import { api } from '@/Api';
export default Vue.extend({
props: {
torrents: Array,
value: Boolean,
value: Array,
},
data() {
return {
deleteFiles: false,
submitting: false,
torrents: [],
};
},
created() {
this.torrents = this.value;
},
computed: {
phoneLayout() {
return this.$vuetify.breakpoint.xsOnly;
}
},
methods: {
closeDialog() {
this.$emit('input', false);
this.$emit('input', []);
},
async submit() {
if (this.submitting) {
@@ -76,4 +84,15 @@ export default Vue.extend({
.torrents {
overflow: auto;
}
.v-dialog--fullscreen {
.v-card__text {
padding-bottom: 52px;
}
.v-card__actions {
position: absolute;
bottom: 0;
right: 0;
}
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<v-dialog
:value="true"
@input="closeDialog"
:width="dialogWidth"
:fullscreen="phoneLayout"
>
<v-card>
<v-card-title
class="headline grey lighten-4"
>
<v-icon class="mr-2">mdi-alert-circle</v-icon>
<span>Info</span>
</v-card-title>
<v-card-text>
<v-tabs v-model="mTab">
<!-- <v-tab>
General
</v-tab> -->
<v-tab href="#trackers">
Trackers
</v-tab>
<v-tab href="#peers">
Peers
</v-tab>
<!-- <v-tab>
Content
</v-tab> -->
</v-tabs>
<v-tabs-items :value="mTab" touchless>
<v-tab-item value="trackers">
<panel
v-for="torrent in torrents"
:key="torrent.hash"
:title="torrent.name"
:single="torrents.length === 1"
>
<trackers
:hash="torrent.hash"
:isActive="mTab === 'trackers'"
/>
</panel>
</v-tab-item>
<v-tab-item value="peers">
<panel
v-for="torrent in torrents"
:key="torrent.hash"
:title="torrent.name"
:single="torrents.length === 1"
>
<peers
:hash="torrent.hash"
:isActive="mTab === 'peers'"
/>
</panel>
</v-tab-item>
</v-tabs-items>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn flat @click="closeDialog">Close</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import _ from 'lodash';
import Vue from 'vue'
import Trackers from './Trackers.vue';
import Peers from './Peers.vue';
import Panel from './Panel.vue';
export default Vue.extend({
components: {
Trackers,
Peers,
Panel,
},
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);
},
},
});
</script>
<style lang="scss" scoped>
::v-deep .v-dialog {
max-width: 1100px;
}
::v-deep .v-datatable thead th, ::v-deep .v-datatable tbody td {
padding: 0 2px !important;
width: auto;
height: auto;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
&:first-child {
padding: 0 0 0 8px !important;
}
&:last-child {
padding-right: 8px !important;
}
}
.v-dialog--fullscreen {
.v-card__text {
padding-bottom: 52px;
}
.v-card__actions {
position: absolute;
bottom: 0;
right: 0;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<v-dialog :value="value" @input="$emit('input', $event)" scrollable :width="dialogWidth">
<v-dialog :value="value" @input="$emit('input', $event)" scrollable :fullscreen="phoneLayout" :width="dialogWidth">
<v-card>
<v-card-title
class="headline grey lighten-4"
@@ -29,17 +29,19 @@
<script lang="ts">
import Vue from 'vue'
import { api } from '../Api';
import { sleep } from '../utils';
import { api } from '@/Api';
import { sleep } from '@/utils';
import Taskable from '@/mixins/taskable';
export default Vue.extend({
mixins: [Taskable],
props: {
value: Boolean,
},
data() {
return {
logs: [],
task: 0,
destory: false,
};
},
filters: {
@@ -66,6 +68,9 @@ export default Vue.extend({
dialogWidth() {
return this.$vuetify.breakpoint.smAndDown ? '100%' : '70%';
},
phoneLayout() {
return this.$vuetify.breakpoint.xsOnly;
}
},
methods: {
closeDialog() {
@@ -93,12 +98,6 @@ export default Vue.extend({
async created() {
await this.getLogs();
},
beforeDestroy() {
this.destory = true;
if (this.task) {
clearTimeout(this.task);
}
},
});
</script>

View File

@@ -0,0 +1,41 @@
<template>
<fieldset class="panel" v-if="!single">
<legend v-text="title" />
<div class="inner">
<slot />
</div>
</fieldset>
<div v-else>
<slot />
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
props: {
single: Boolean,
title: String,
},
});
</script>
<style lang="scss" scoped>
.panel {
margin-top: 1em;
.inner {
max-height: 500px;
overflow-y: auto;
}
}
fieldset {
border-width: 1px;
legend {
margin-left: 1em;
}
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<v-data-table
:headers="headers"
:items="peers"
:hide-actions="true"
>
<template v-slot:items="row">
<td class="ip">
<img
v-if="isWindows"
class="country-flag"
:title="row.item.country"
:alt="codeToFlag(row.item.country_code).char"
:src="codeToFlag(row.item.country_code).url"
/>
<template v-else>
{{ codeToFlag(row.item.country_code).char }}
</template>
{{ row.item.ip }}
<span class="grey--text">:{{ row.item.port }}</span>
</td>
<td>{{ row.item.connection }}</td>
<td :title="row.item.flags_desc">{{ row.item.flags }}</td>
<td>{{ row.item.client }}</td>
<td>{{ row.item.progress | progress }}</td>
<td>{{ row.item.dl_speed | size }}/s</td>
<td>{{ row.item.up_speed | size }}/s</td>
<td>{{ row.item.downloaded | size }}</td>
<td>{{ row.item.uploaded | size }}</td>
<td>{{ row.item.relevance | progress }}</td>
<td>{{ row.item.files }}</td>
</template>
</v-data-table>
</template>
<script lang="ts">
import _ from 'lodash';
import Vue from 'vue';
import { codeToFlag, isWindows } from '../../utils';
import Taskable from '@/mixins/taskable';
import { api } from '../../Api';
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: 'UP Speed', value: 'up_speed' },
{ text: 'Downloaded', value: 'downloaded' },
{ text: 'Uploaded', value: 'uploaded' },
{ text: 'Relevance', value: 'relevance' },
{ text: 'Files', value: 'files' },
];
return {
headers,
peersObj: null,
rid: null,
isWindows,
};
},
computed: {
peers() {
return _.map(this.peersObj, (value, key) => {
return _.merge({}, value, { key });
});
},
},
methods: {
codeToFlag(code: string) {
return codeToFlag(code);
},
async getPeers() {
const resp = await api.getTorrentPeers(this.hash, this.rid);
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);
}
if (!this.isActive || this.destroy) {
return;
}
this.task = setTimeout(this.getPeers, 2000);
},
},
async created() {
if (this.isActive) {
await this.getPeers();
}
},
watch: {
async isActive(v) {
if (v) {
await this.getPeers();
} else {
this.cancelTask();
}
}
},
});
</script>
<style lang="scss" scoped>
::v-deep .ip {
display: flex;
align-items: center;
.country-flag {
width: 1.5em;
margin-right: 0.5em;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<v-data-table
:headers="headers"
:items="trackers"
:hide-actions="true"
>
<template v-slot:items="row">
<td>{{ row.item.tier }}</td>
<td>{{ row.item.url }}</td>
<td>{{ row.item.status | formatTrackerStatus }}</td>
<td>{{ row.item.num_peers | formatTrackerNum }}</td>
<td>{{ row.item.num_seeds | formatTrackerNum }}</td>
<td>{{ row.item.num_leeches | formatTrackerNum }}</td>
<td>{{ row.item.num_downloaded | formatTrackerNum }}</td>
<td>{{ row.item.msg }}</td>
</template>
</v-data-table>
</template>
<script lang="ts">
import Vue from 'vue'
import { api } from '../../Api';
import Taskable from '@/mixins/taskable';
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: [],
};
},
filters: {
formatTrackerStatus(status: number) {
const map = [
'Disabled',
'Contacted',
'Working',
'Not working',
'Not contacted',
];
return map[status];
},
formatTrackerNum(num: number) {
if (num === -1) {
return 'N/A';
}
return num.toString();
},
},
methods: {
async getTracker() {
this.trackers = await api.getTorrentTracker(this.hash);
if (!this.isActive || this.destroy) {
return;
}
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();
}
}
},
});
</script>

View File

@@ -16,6 +16,7 @@ export function formatSize(value: number) {
}
Vue.filter('formatSize', formatSize);
Vue.filter('size', formatSize);
export interface DurationOptions {
dayLimit?: number;
@@ -86,6 +87,12 @@ Vue.filter('formatTimestamp', (timestamp: number) => {
export function formatAsDuration(date: number, options?: DurationOptions) {
const duration = (Date.now() / 1000) - date;
return formatDuration(duration, options);
};
}
Vue.filter('formatAsDuration', formatAsDuration);
export function formatProgress(progress: number) {
return Math.floor(progress * 100) + '%';
}
Vue.filter('progress', formatProgress);

22
src/mixins/taskable.ts Normal file
View File

@@ -0,0 +1,22 @@
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

@@ -46,3 +46,20 @@ export function torrentIsState(type: StateType, state: string) {
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function codeToFlag(code: string) {
const magicNumber = 0x1F1A5;
code = code.toUpperCase();
const codePoints = [...code].map((c) => magicNumber + c.charCodeAt(0));
const char = String.fromCodePoint(...codePoints);
const url = 'https://cdn.jsdelivr.net/npm/twemoji/2/svg/' +
`${codePoints[0].toString(16)}-${codePoints[1].toString(16)}.svg`;
return {
char,
url,
};
}
export const isWindows = navigator.userAgent.includes('Windows');