mirror of
https://github.com/VueTorrent/VueTorrent.git
synced 2025-03-14 12:10:18 +03:00
perf: Rework Content tab (#940)
This commit is contained in:
parent
373e825cca
commit
516f1d7913
17 changed files with 499 additions and 287 deletions
91
src/components/Modals/RenameTorrentFileModal.vue
Normal file
91
src/components/Modals/RenameTorrentFileModal.vue
Normal file
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<v-dialog v-model="dialog" scrollable max-width="750px" :content-class="phoneLayout ? 'rounded-0' : 'rounded-form'" :fullscreen="phoneLayout" @keydown.enter.prevent="rename">
|
||||
<v-card>
|
||||
<v-card-title class="pa-0">
|
||||
<v-toolbar-title class="ma-4 primarytext--text">
|
||||
<h3>{{ $t('modals.rename.title') }}</h3>
|
||||
</v-toolbar-title>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="caption">{{ $t('modals.rename.oldPath') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ oldName }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-text-field v-model="newName" id="newPathInput" :label="$t('modals.rename.newPath')" autofocus :prepend-inner-icon="mdiFile" />
|
||||
</v-list-item>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn class="accent white--text elevation-0 px-4" @click="rename">
|
||||
{{ $t('save') }}
|
||||
</v-btn>
|
||||
<v-btn class="error white--text elevation-0 px-4" @click="close">
|
||||
{{ $t('cancel') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Modal from '@/mixins/Modal'
|
||||
import { mdiFile } from '@mdi/js'
|
||||
import { FullScreenModal } from '@/mixins'
|
||||
import qbit from '@/services/qbit'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RenameTorrentFileModal',
|
||||
mixins: [Modal, FullScreenModal],
|
||||
props: {
|
||||
hash: String,
|
||||
isFolder: Boolean,
|
||||
oldName: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newName: '',
|
||||
mdiFile
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
torrentHash() {
|
||||
return this.hash as string
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.newName = this.oldName as string
|
||||
},
|
||||
mounted() {
|
||||
const input = document.getElementById('newPathInput') as HTMLInputElement
|
||||
|
||||
const startIndex = this.newName.lastIndexOf('/')
|
||||
const endIndex = this.newName.lastIndexOf('.')
|
||||
|
||||
if (input) {
|
||||
input.setSelectionRange(startIndex + 1, endIndex == -1 ? this.newName.length : endIndex)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async rename() {
|
||||
let result: Promise<void>
|
||||
if (this.isFolder) {
|
||||
result = qbit.renameFolder(this.torrentHash, this.oldName as string, this.newName)
|
||||
} else {
|
||||
result = qbit.renameFile(this.torrentHash, this.oldName as string, this.newName)
|
||||
}
|
||||
|
||||
result.then(
|
||||
() => this.close(),
|
||||
() => this.$toast.error(this.$t('modals.rename.errorConflict'))
|
||||
)
|
||||
},
|
||||
close() {
|
||||
this.dialog = false
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -10,7 +10,7 @@
|
|||
<v-container>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field v-model="name" clearable :label="$t('modals.rename.torrentName')" autofocus :prepend-inner-icon="mdiFile" />
|
||||
<v-text-field v-model="name" id="torrentNameInput" clearable :label="$t('modals.rename.torrentName')" autofocus :prepend-inner-icon="mdiFile" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
@ -37,7 +37,7 @@ import qbit from '@/services/qbit'
|
|||
import {defineComponent} from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RenameModal',
|
||||
name: 'RenameTorrentModal',
|
||||
mixins: [Modal, FullScreenModal],
|
||||
props: {
|
||||
hash: String
|
||||
|
@ -58,10 +58,13 @@ export default defineComponent({
|
|||
created() {
|
||||
this.name = this.torrent.name
|
||||
},
|
||||
mounted() {
|
||||
const input = document.getElementById('torrentNameInput') as HTMLInputElement
|
||||
if (input) {
|
||||
input.select()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
urlDecode() {
|
||||
this.name = decodeURIComponent(this.name)
|
||||
},
|
||||
async rename() {
|
||||
await qbit.setTorrentName(this.hash, this.name)
|
||||
this.close()
|
|
@ -44,9 +44,11 @@
|
|||
<v-checkbox v-model="settings.showShutdownButton" hide-details class="ma-0 pa-0" :label="$t('modals.settings.vueTorrent.general.showShutdownButton')" />
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="mb-5" />
|
||||
|
||||
<v-list-item class="my-2">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" class="mb-n4">
|
||||
<v-col cols="12" sm="6" md="4" class="mb-n4">
|
||||
<v-text-field
|
||||
v-model="settings.refreshInterval"
|
||||
type="number"
|
||||
|
@ -55,18 +57,29 @@
|
|||
:hint="$t('modals.settings.vueTorrent.general.refreshIntervalHint')"
|
||||
:label="$t('modals.settings.vueTorrent.general.refreshInterval')" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-col cols="12" sm="6" md="4" class="mb-n4">
|
||||
<v-text-field
|
||||
v-model="settings.contentInterval"
|
||||
type="number"
|
||||
dense
|
||||
outlined
|
||||
:hint="$t('modals.settings.vueTorrent.general.contentIntervalHint')"
|
||||
:label="$t('modals.settings.vueTorrent.general.contentInterval')" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model="settings.torrentPieceCountRenderThreshold"
|
||||
type="number"
|
||||
dense
|
||||
outlined
|
||||
:hint="$t('modals.settings.vueTorrent.general.torrentPieceCountRenderThresholdHint')"
|
||||
hide-details
|
||||
:label="$t('modals.settings.vueTorrent.general.torrentPieceCountRenderThreshold')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="my-5" />
|
||||
|
||||
<v-list-item class="mb-3">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
|
|
|
@ -397,7 +397,7 @@ export default {
|
|||
this.createModal('ChangeLocationModal', { hashes: this.multiple ? this.selected_torrents : [this.torrent.hash] })
|
||||
},
|
||||
rename() {
|
||||
this.createModal('RenameModal', { hash: this.torrent.hash })
|
||||
this.createModal('RenameTorrentModal', { hash: this.torrent.hash })
|
||||
},
|
||||
async reannounce() {
|
||||
await qbit.reannounceTorrents(this.hashes)
|
||||
|
|
|
@ -1,35 +1,42 @@
|
|||
<template>
|
||||
<v-card flat>
|
||||
<v-treeview v-model="selected" :items="fileTree" :open.sync="opened" activatable selectable item-key="fullName" open-all>
|
||||
<template #prepend="{ item, open }">
|
||||
<v-icon v-if="!item.icon">
|
||||
<v-card flat :loading="loading">
|
||||
<v-treeview
|
||||
v-model="fileSelection"
|
||||
:open.sync="openedItems"
|
||||
:items="fileTree"
|
||||
activatable
|
||||
selectable
|
||||
item-key="id"
|
||||
@input="updateSelection"
|
||||
>
|
||||
<template v-slot:prepend="{ item: node, open }">
|
||||
<v-icon v-if="node.type === 'root'">
|
||||
{{ mdiFileTree }}
|
||||
</v-icon>
|
||||
<v-icon v-if="node.type === 'folder'">
|
||||
{{ open ? mdiFolderOpen : mdiFolder }}
|
||||
</v-icon>
|
||||
<v-icon v-else>
|
||||
{{ item.icon }}
|
||||
<v-icon v-else-if="node.type === 'file'">
|
||||
{{ node | fileIcon }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<template #label="{ item }">
|
||||
<v-text-field v-if="item.editing" v-model="item.newName" autofocus />
|
||||
<span class="item-name" v-else>{{ item.name }}</span>
|
||||
<template #label="{ item: node }">
|
||||
<span class="item-name">{{ node.type === 'root' ? $t('modals.detail.pageContent.rootNode') : node.name }}</span>
|
||||
</template>
|
||||
<template #append="{ item }">
|
||||
<div v-if="!item.icon">
|
||||
<span class="ml-4">{{ item.children.length }} Files</span>
|
||||
<v-btn v-if="!item.editing" fab x-small class="accent white--text elevation-0 px-4 ml-2" @click="edit(item)">
|
||||
<template #append="{ item: node }">
|
||||
<div v-if="node.type === 'root'">
|
||||
|
||||
</div>
|
||||
<div v-else-if="node.type === 'folder'">
|
||||
<span class="ml-4">{{ node | nodeContent }}</span>
|
||||
<v-btn fab x-small class="accent white--text elevation-0 px-4 ml-2" @click="renameNode(node)">
|
||||
<v-icon>{{ mdiPencil }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="item.editing" fab x-small class="accent white--text elevation-0 px-4 ml-2" @click="renameFolder(item)">
|
||||
<v-icon>{{ mdiContentSave }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="item.editing" fab x-small class="error white--text elevation-0 px-4 ml-2" @click="toggleEditing(item)">
|
||||
<v-icon>{{ mdiClose }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span v-if="!$vuetify.breakpoint.xsOnly && !item.editing">[{{ item.size }}]</span>
|
||||
<span v-if="!$vuetify.breakpoint.xsOnly && !item.editing" class="ml-4">{{ item.progress }}%</span>
|
||||
<span v-if="!$vuetify.breakpoint.xsOnly && !item.editing" class="ml-4">[ {{ item.priority | priority }} ]</span>
|
||||
<span v-if="!$vuetify.breakpoint.xsOnly">[{{ node.size | formatSize }}]</span>
|
||||
<span v-if="!$vuetify.breakpoint.xsOnly" class="ml-4">{{ node.progress | progress }}</span>
|
||||
<span v-if="!$vuetify.breakpoint.xsOnly" class="ml-4">[ {{ getNodePriority(node) }} ]</span>
|
||||
<v-menu open-on-hover offset-y>
|
||||
<template #activator="{ on }">
|
||||
<v-btn fab x-small class="accent white--text elevation-0 px-4 ml-2" v-on="on">
|
||||
|
@ -37,7 +44,8 @@
|
|||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item v-for="prio in priority_options" :key="prio.value" link @click="setFilePrio(item.id, prio.value)">
|
||||
<v-list-item v-for="prio in filePriorityOptions" :key="prio.value" link
|
||||
@click="setFilePrio(node, prio.value)">
|
||||
<v-icon>{{ prio.icon }}</v-icon>
|
||||
<v-list-item-title class="caption">
|
||||
{{ prio.name }}
|
||||
|
@ -45,175 +53,192 @@
|
|||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn v-if="!item.editing" fab x-small class="accent white--text elevation-0 px-4 ml-2" @click="edit(item)">
|
||||
<v-btn fab x-small class="accent white--text elevation-0 px-4 ml-2" @click="renameNode(node)">
|
||||
<v-icon>{{ mdiPencil }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="item.editing" fab x-small class="accent white--text elevation-0 px-4 ml-2" @click="renameFile(item)">
|
||||
<v-icon>{{ mdiContentSave }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="item.editing" fab x-small class="error white--text elevation-0 px-4 ml-2" @click="toggleEditing(item)">
|
||||
<v-icon>{{ mdiClose }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-treeview>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue'
|
||||
import {mapGetters} from 'vuex'
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiCodeJson,
|
||||
mdiFile,
|
||||
mdiFileDocumentOutline,
|
||||
mdiFileExcel,
|
||||
mdiFileImage,
|
||||
mdiFilePdfBox,
|
||||
mdiFileTree,
|
||||
mdiFolder,
|
||||
mdiFolderOpen,
|
||||
mdiLanguageHtml5,
|
||||
mdiLanguageMarkdown,
|
||||
mdiMovie,
|
||||
mdiNodejs,
|
||||
mdiPencil,
|
||||
mdiPriorityHigh,
|
||||
mdiPriorityLow,
|
||||
mdiTrendingUp
|
||||
} from '@mdi/js'
|
||||
import {TreeFile, TreeFolder, TreeNode, TreeRoot} from '@/types/vuetorrent'
|
||||
import {Priority} from '@/enums/qbit'
|
||||
import qbit from '@/services/qbit'
|
||||
import { treeify } from '@/helpers'
|
||||
import { FullScreenModal } from '@/mixins'
|
||||
import { mdiClose, mdiContentSave, mdiPencil, mdiFolderOpen, mdiFolder, mdiFile, mdiTrendingUp, mdiPriorityHigh, mdiArrowUp, mdiArrowDown, mdiPriorityLow } from '@mdi/js'
|
||||
import Vue from 'vue'
|
||||
import i18n from "@/plugins/i18n";
|
||||
import {TorrentFile} from "@/types/qbit/models";
|
||||
import {genFileTree} from "@/helpers";
|
||||
import {General} from "@/mixins";
|
||||
|
||||
const FILE_PRIORITY_OPTIONS = [
|
||||
{ name: 'Max', icon: mdiPriorityHigh, value: 7 },
|
||||
{ name: 'High', icon: mdiArrowUp, value: 6 },
|
||||
{ name: 'Normal', icon: mdiArrowDown, value: 1 },
|
||||
{ name: 'Unwanted', icon: mdiPriorityLow, value: 0 }
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'Content',
|
||||
filters: {
|
||||
priority(value) {
|
||||
const res = FILE_PRIORITY_OPTIONS.find(el => el.value === value)
|
||||
|
||||
return res ? res.name : 'undefined'
|
||||
}
|
||||
},
|
||||
mixins: [FullScreenModal],
|
||||
export default defineComponent({
|
||||
name: "Content",
|
||||
props: {
|
||||
hash: String,
|
||||
isActive: Boolean
|
||||
},
|
||||
mixins: [General],
|
||||
data() {
|
||||
return {
|
||||
inited: false,
|
||||
opened: null,
|
||||
selected: [],
|
||||
treeData: null,
|
||||
priority_options: FILE_PRIORITY_OPTIONS,
|
||||
timer: null as NodeJS.Timeout | null,
|
||||
apiLock: false,
|
||||
loading: false,
|
||||
cachedFiles: [] as TorrentFile[],
|
||||
fileTree: [{}] as [TreeRoot],
|
||||
openedItems: [] as string[],
|
||||
fileSelection: [] as number[],
|
||||
filePriorityOptions: [
|
||||
{name: 'Max', icon: mdiPriorityHigh, value: Priority.MAXIMAL},
|
||||
{name: 'High', icon: mdiArrowUp, value: Priority.HIGH},
|
||||
{name: 'Normal', icon: mdiArrowDown, value: Priority.NORMAL},
|
||||
{name: 'Unwanted', icon: mdiPriorityLow, value: Priority.DO_NOT_DOWNLOAD}
|
||||
],
|
||||
mdiFolderOpen,
|
||||
mdiFolder,
|
||||
mdiFile,
|
||||
mdiFileTree,
|
||||
mdiTrendingUp,
|
||||
mdiPencil,
|
||||
mdiContentSave,
|
||||
mdiClose
|
||||
mdiPencil
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fileTree() {
|
||||
if (this.treeData) {
|
||||
return treeify(this.treeData)
|
||||
}
|
||||
|
||||
return []
|
||||
...mapGetters(['getContentInterval']),
|
||||
torrentHash(): string {
|
||||
return this.hash as string
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isActive(active) {
|
||||
if (active) {
|
||||
if (this.inited) {
|
||||
this.getTorrentFiles()
|
||||
} else {
|
||||
this.initFiles()
|
||||
this.inited = true
|
||||
}
|
||||
isActive(newValue: boolean) {
|
||||
if (newValue) {
|
||||
this.updateFileTree().then(() => this.openedItems.push(''))
|
||||
|
||||
this.timer = setInterval(this.updateFileTree, this.getContentInterval())
|
||||
} else {
|
||||
clearInterval(this.timer as NodeJS.Timeout)
|
||||
}
|
||||
},
|
||||
selected(newValue, oldValue) {
|
||||
this.changeFilePriorities(newValue, oldValue)
|
||||
}
|
||||
},
|
||||
created() {},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer as NodeJS.Timeout)
|
||||
},
|
||||
filters: {
|
||||
fileIcon(file: TreeFile) {
|
||||
const types: Record<string, string> = {
|
||||
html: mdiLanguageHtml5,
|
||||
js: mdiNodejs,
|
||||
json: mdiCodeJson,
|
||||
md: mdiLanguageMarkdown,
|
||||
pdf: mdiFilePdfBox,
|
||||
png: mdiFileImage,
|
||||
txt: mdiFileDocumentOutline,
|
||||
sub: mdiFileDocumentOutline,
|
||||
idx: mdiFileDocumentOutline,
|
||||
xls: mdiFileExcel,
|
||||
xlsx: mdiFileExcel,
|
||||
avi: mdiMovie,
|
||||
mp4: mdiMovie,
|
||||
mkv: mdiMovie,
|
||||
mov: mdiMovie,
|
||||
wmv: mdiMovie
|
||||
}
|
||||
|
||||
const type = file.name.split('.').pop() || ''
|
||||
return types[type] || mdiFile
|
||||
},
|
||||
nodeContent(node: TreeRoot | TreeFolder) {
|
||||
let fileCount = 0
|
||||
let folderCount = 0
|
||||
for (const child of node.children) {
|
||||
if (child.type === 'file') {
|
||||
fileCount++
|
||||
} else if (child.type === 'folder') {
|
||||
folderCount++
|
||||
}
|
||||
}
|
||||
|
||||
const res = []
|
||||
if (fileCount > 0) {
|
||||
res.push(i18n.tc('modals.detail.pageContent.fileInfo', fileCount).toString())
|
||||
}
|
||||
if (folderCount > 0) {
|
||||
res.push(i18n.tc('modals.detail.pageContent.folderInfo', folderCount).toString())
|
||||
}
|
||||
|
||||
return res.join(', ')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initFiles() {
|
||||
this.getTorrentFiles().then(() => {
|
||||
this.opened = []
|
||||
this.selected = this.treeData.filter(file => file.priority !== 0).map(file => file.name)
|
||||
})
|
||||
getNodePriority(node: TreeFile) {
|
||||
const res = this.filePriorityOptions.find(el => el.value === node.priority)
|
||||
|
||||
return res ? res.name : 'undefined'
|
||||
},
|
||||
async getTorrentFiles() {
|
||||
const data = await qbit.getTorrentFiles(this.hash)
|
||||
data.forEach((d, i) => {
|
||||
d.id = i
|
||||
d.name = d.name.replace('.unwanted/', '')
|
||||
})
|
||||
this.treeData = data
|
||||
},
|
||||
async changeFilePriorities(newValue, oldValue) {
|
||||
if (newValue.length === oldValue.length) return
|
||||
async updateSelection(newValue: number[]) {
|
||||
const oldValue = this.cachedFiles.filter(f => f.priority !== Priority.DO_NOT_DOWNLOAD).map(f => f.index)
|
||||
|
||||
const filesToExclude = oldValue
|
||||
.filter(f => !newValue.includes(f))
|
||||
.map(name => this.treeData.find(f => f.name === name))
|
||||
.filter(f => f.priority !== 0)
|
||||
.map(f => f.id)
|
||||
.filter(index => !newValue.includes(index))
|
||||
.map(index => this.cachedFiles.find(f => f.index === index))
|
||||
.filter(f => f.priority !== Priority.DO_NOT_DOWNLOAD)
|
||||
.map(f => f.index)
|
||||
const filesToInclude = newValue
|
||||
.filter(f => !oldValue.includes(f))
|
||||
.map(name => this.treeData.find(f => f.name === name))
|
||||
.filter(f => f.priority === 0)
|
||||
.map(f => f.id)
|
||||
.filter(index => !oldValue.includes(index))
|
||||
.map(index => this.cachedFiles.find(f => f.index === index))
|
||||
.filter(f => f.priority === Priority.DO_NOT_DOWNLOAD)
|
||||
.map(f => f.index)
|
||||
|
||||
if (filesToExclude.length) {
|
||||
await qbit.setTorrentFilePriority(this.hash, filesToExclude, 0)
|
||||
await qbit.setTorrentFilePriority(this.torrentHash, filesToExclude, Priority.DO_NOT_DOWNLOAD)
|
||||
}
|
||||
if (filesToInclude.length) {
|
||||
await qbit.setTorrentFilePriority(this.hash, filesToInclude, 1)
|
||||
}
|
||||
if (filesToExclude.length || filesToInclude.length) {
|
||||
await this.getTorrentFiles()
|
||||
await qbit.setTorrentFilePriority(this.torrentHash, filesToInclude, Priority.NORMAL)
|
||||
}
|
||||
},
|
||||
toggleEditing(item) {
|
||||
item.editing = !item.editing
|
||||
async renameNode(node: TreeNode) {
|
||||
this.createModal('RenameTorrentFileModal', {
|
||||
hash: this.torrentHash,
|
||||
isFolder: node.type === 'folder',
|
||||
oldName: node.fullName
|
||||
})
|
||||
},
|
||||
edit(item) {
|
||||
item.newName = item.name
|
||||
this.toggleEditing(item)
|
||||
async setFilePrio(file: TreeFile, prio: Priority) {
|
||||
await qbit.setTorrentFilePriority(this.torrentHash, [file.index], prio)
|
||||
},
|
||||
renameFile(item) {
|
||||
const lastPathSep = item.fullName.lastIndexOf('/')
|
||||
const args = [this.hash]
|
||||
async updateFileTree() {
|
||||
if (this.apiLock) return
|
||||
this.apiLock = true
|
||||
this.loading = true
|
||||
|
||||
if (lastPathSep === -1) args.push(item.name, item.newName)
|
||||
else {
|
||||
const prefix = item.fullName.substring(0, lastPathSep)
|
||||
args.push(`${prefix}/${item.name}`, `${prefix}/${item.newName}`)
|
||||
}
|
||||
this.cachedFiles = await qbit.getTorrentFiles(this.torrentHash)
|
||||
this.fileTree = [await genFileTree(this.cachedFiles)]
|
||||
this.fileSelection = this.cachedFiles.filter(f => f.priority !== Priority.DO_NOT_DOWNLOAD).map(f => f.index)
|
||||
|
||||
qbit.renameFile(...args).catch(() => Vue.$toast.error(this.$t('toast.renameFileFailed')))
|
||||
|
||||
item.name = item.newName
|
||||
this.toggleEditing(item)
|
||||
},
|
||||
renameFolder(item) {
|
||||
const lastPathSep = item.fullName.lastIndexOf('/')
|
||||
const args = [this.hash]
|
||||
|
||||
if (lastPathSep === -1) args.push(item.name, item.newName)
|
||||
else {
|
||||
const prefix = item.fullName.substring(0, lastPathSep)
|
||||
args.push(`${prefix}/${item.name}`, `${prefix}/${item.newName}`)
|
||||
}
|
||||
|
||||
qbit.renameFolder(...args).catch(() => Vue.$toast.error(this.$t('toast.renameFolderFailed')))
|
||||
|
||||
item.name = item.newName
|
||||
this.toggleEditing(item)
|
||||
},
|
||||
setFilePrio(fileId, priority) {
|
||||
qbit.setTorrentFilePriority(this.hash, [fileId], priority).then(() => this.initFiles())
|
||||
await this.$nextTick()
|
||||
this.loading = false
|
||||
this.apiLock = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item-name {
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
|
|
200
src/helpers.js
200
src/helpers.js
|
@ -1,7 +1,12 @@
|
|||
import * as _ from 'lodash'
|
||||
import { isProduction } from './utils'
|
||||
import { mdiLanguageHtml5, mdiFileDocumentOutline, mdiNodejs, mdiFilePdfBox, mdiFileExcel, mdiCodeJson, mdiFileImage, mdiMovie, mdiLanguageMarkdown, mdiFile } from '@mdi/js'
|
||||
|
||||
/**
|
||||
* Format bytes to human readable string
|
||||
* @param a {number}
|
||||
* @param b {number}
|
||||
* @return {string}
|
||||
*/
|
||||
export function formatBytes(a, b) {
|
||||
if (a === 0) return '0 B'
|
||||
const c = 1024
|
||||
|
@ -12,32 +17,11 @@ export function formatBytes(a, b) {
|
|||
return `${parseFloat((a / Math.pow(c, f)).toFixed(d))} ${e[f]}`
|
||||
}
|
||||
|
||||
export function getIconForFileType(type) {
|
||||
const types = {
|
||||
html: mdiLanguageHtml5,
|
||||
js: mdiNodejs,
|
||||
json: mdiCodeJson,
|
||||
md: mdiLanguageMarkdown,
|
||||
pdf: mdiFilePdfBox,
|
||||
png: mdiFileImage,
|
||||
txt: mdiFileDocumentOutline,
|
||||
sub: mdiFileDocumentOutline,
|
||||
idx: mdiFileDocumentOutline,
|
||||
xls: mdiFileExcel,
|
||||
avi: mdiMovie,
|
||||
mp4: mdiMovie,
|
||||
mkv: mdiMovie
|
||||
}
|
||||
|
||||
if (!types[type]) return mdiFile
|
||||
|
||||
return types[type]
|
||||
}
|
||||
|
||||
export const isWindows = navigator.userAgent.includes('Windows')
|
||||
export const isWindows = navigator.userAgent.includes('Windows');
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* Convert code to flag
|
||||
* @param code {string}
|
||||
* @return {{char: string, url: string}}
|
||||
*/
|
||||
export function codeToFlag(code) {
|
||||
|
@ -55,77 +39,81 @@ export function codeToFlag(code) {
|
|||
}
|
||||
}
|
||||
|
||||
export function treeify(paths) {
|
||||
let result = []
|
||||
const level = { result }
|
||||
/**
|
||||
* Generates a file tree from a list of files
|
||||
* @param files {TorrentFile[]}
|
||||
* @return {TreeRoot}
|
||||
*/
|
||||
export function genFileTree(files) {
|
||||
/** @type {TreeRoot} */
|
||||
const rootNode = {
|
||||
type: 'root',
|
||||
name: '',
|
||||
fullName: '',
|
||||
id: '',
|
||||
children: []
|
||||
}
|
||||
|
||||
paths.forEach(path => {
|
||||
path.name
|
||||
.split('\\')
|
||||
.join('/')
|
||||
.split('/')
|
||||
.reduce((r, name) => {
|
||||
if (!r[name]) {
|
||||
r[name] = { result: [] }
|
||||
r.result.push(createFile(path, name, r[name].result))
|
||||
for (const file of files) {
|
||||
/** @type {TreeRoot | TreeFolder} */
|
||||
let cursor = rootNode
|
||||
file.name.replace('\\', '/').split('/').reduce((parentPath, nodeName) => {
|
||||
const nextPath = parentPath === '' ? nodeName : parentPath + '/' + nodeName
|
||||
|
||||
if (file.name.endsWith(nodeName)) {
|
||||
/** @type {TreeFile} */
|
||||
const newFile = {
|
||||
type: 'file',
|
||||
name: nodeName,
|
||||
fullName: nextPath,
|
||||
id: file.index,
|
||||
availability: file.availability,
|
||||
index: file.index,
|
||||
is_seed: file.is_seed,
|
||||
priority: file.priority,
|
||||
progress: file.progress,
|
||||
size: file.size
|
||||
}
|
||||
cursor.children.push(newFile)
|
||||
} else {
|
||||
/** @type {TreeFolder | undefined} */
|
||||
const folder = cursor.children.find(el => el.name === nodeName)
|
||||
if (folder) {
|
||||
cursor = folder
|
||||
} else {
|
||||
// if not found, create folder and set cursor to folder
|
||||
/** @type {TreeFolder} */
|
||||
const newFolder = {
|
||||
type: 'folder',
|
||||
name: nodeName,
|
||||
fullName: nextPath,
|
||||
id: nextPath,
|
||||
children: []
|
||||
}
|
||||
cursor.children.push(newFolder)
|
||||
cursor = newFolder
|
||||
}
|
||||
}
|
||||
|
||||
return r[name]
|
||||
}, level)
|
||||
})
|
||||
|
||||
// parse folders
|
||||
result = result.map(el => parseFolder(el))
|
||||
|
||||
function parseFolder(el, parent) {
|
||||
if (el.children.length !== 0) {
|
||||
const folder = createFolder(parent, el.name, el.children)
|
||||
folder.children = folder.children.map(child => parseFolder(child, folder))
|
||||
|
||||
return folder
|
||||
}
|
||||
|
||||
return el
|
||||
return nextPath
|
||||
}, '')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function createFile(data, name, children) {
|
||||
return {
|
||||
id: data.id,
|
||||
name: name,
|
||||
fullName: data.name,
|
||||
progress: Math.round(data.progress * 100),
|
||||
size: formatBytes(data.size),
|
||||
icon: getIconForFileType(name.split('.').pop()),
|
||||
priority: data.priority,
|
||||
children: children,
|
||||
type: 'file'
|
||||
}
|
||||
}
|
||||
|
||||
function createFolder(parent, name, children) {
|
||||
children.sort((a, b) => {
|
||||
if (a.type === b.type) return a.name.localeCompare(b.name)
|
||||
else if (a.type === 'directory') return -1
|
||||
else return 1
|
||||
})
|
||||
|
||||
return {
|
||||
name: name,
|
||||
fullName: parent === undefined ? name : `${parent.fullName}/${name}`,
|
||||
type: 'directory',
|
||||
children: children
|
||||
}
|
||||
return rootNode
|
||||
}
|
||||
|
||||
const urlRegExp = new RegExp(
|
||||
/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.\S{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.\S{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.\S{2,}|www\.[a-zA-Z0-9]+\.\S{2,})/gi
|
||||
)
|
||||
|
||||
/**
|
||||
* Split string by separating urls
|
||||
* @param string
|
||||
* @return {string[]}
|
||||
*/
|
||||
export function splitByUrl(string) {
|
||||
const urls = string.match(urlRegExp)
|
||||
/** @type {string[]} */
|
||||
let resultArray = []
|
||||
|
||||
if (urls) {
|
||||
|
@ -152,10 +140,19 @@ export function splitByUrl(string) {
|
|||
return resultArray
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if string contains url
|
||||
* @param string {string}
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function stringContainsUrl(string) {
|
||||
return urlRegExp.test(string)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get version from import.meta
|
||||
* @return {string}
|
||||
*/
|
||||
export function getVersion() {
|
||||
if (isProduction()) {
|
||||
return 'import.meta.env.VITE_PACKAGE_VERSION'
|
||||
|
@ -165,6 +162,10 @@ export function getVersion() {
|
|||
return 'undefined'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base url from import.meta
|
||||
* @return {string}
|
||||
*/
|
||||
export function getBaseURL() {
|
||||
if (isProduction()) {
|
||||
return 'import.meta.env.BASE_URL'
|
||||
|
@ -174,6 +175,11 @@ export function getBaseURL() {
|
|||
return 'undefined'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain from url
|
||||
* @param string {string}
|
||||
* @return {string}
|
||||
*/
|
||||
export function getDomainBody(string) {
|
||||
const match = string.match(/:\/\/([^\/]+\.)?([^\/.]+)\.[^\/.:]+/i)
|
||||
if (match != null && match.length > 2 && typeof match[2] === 'string' && match[2].length > 0) {
|
||||
|
@ -184,6 +190,13 @@ export function getDomainBody(string) {
|
|||
}
|
||||
|
||||
export class ArrayHelper {
|
||||
/**
|
||||
* Remove item from array
|
||||
* @typedef T
|
||||
* @param array {T[]}
|
||||
* @param item {T | T[]} Item or array of items to remove
|
||||
* @return {T[]}
|
||||
*/
|
||||
static remove(array, item) {
|
||||
const toRemove = Array.isArray(item) ? item : [item]
|
||||
_.remove(array, item => toRemove.indexOf(item) > -1)
|
||||
|
@ -191,12 +204,24 @@ export class ArrayHelper {
|
|||
return array
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate two arrays together
|
||||
* @typedef T
|
||||
* @param a {T[]} First array
|
||||
* @param b {T[]} Second array
|
||||
* @return {T[]}
|
||||
*/
|
||||
static concat(a, b) {
|
||||
return _.concat(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
export class Hostname {
|
||||
/**
|
||||
* Get hostname from url
|
||||
* @param url {string}
|
||||
* @return {string}
|
||||
*/
|
||||
static get(url) {
|
||||
const match = url.match(/:\/\/(www[0-9]?\.)?(.[^/:]+)/i)
|
||||
if (match != null && match.length > 2 && typeof match[2] === 'string' && match[2].length > 0) {
|
||||
|
@ -207,10 +232,19 @@ export class Hostname {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is on mac
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function isMac() {
|
||||
return window.navigator.userAgent.toUpperCase().indexOf('MAC') >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Check ctrl/cmd key
|
||||
* @param e {KeyboardEvent}
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function doesCommand(e) {
|
||||
return isMac() ? e.metaKey : e.ctrlKey
|
||||
}
|
||||
|
|
|
@ -271,8 +271,9 @@
|
|||
"showShutdownButton": "Show shutdown button",
|
||||
"refreshInterval": "qBittorrent API refresh interval",
|
||||
"refreshIntervalHint": "In milliseconds",
|
||||
"contentInterval": "Torrent file content refresh interval",
|
||||
"contentIntervalHint": "In milliseconds",
|
||||
"torrentPieceCountRenderThreshold": "Torrent piece count to disable rendering",
|
||||
"torrentPieceCountRenderThresholdHint": "In milliseconds",
|
||||
"currentVersion": "Current Version",
|
||||
"qbittorrentVersion": "QBittorrent Version",
|
||||
"registerMagnet": "Register magnet links",
|
||||
|
@ -684,6 +685,15 @@
|
|||
"is_private": "Torrent Private",
|
||||
"wasted_size": "Wasted"
|
||||
},
|
||||
"pageTrackers": {
|
||||
"url": "URL",
|
||||
"status": "Status",
|
||||
"peers": "Peers",
|
||||
"seeds": "Seeds",
|
||||
"leeches": "Leeches",
|
||||
"downloaded": "Downloaded",
|
||||
"message": "Message"
|
||||
},
|
||||
"pagePeers": {
|
||||
"ip": "IP",
|
||||
"connection": "Connection",
|
||||
|
@ -697,18 +707,14 @@
|
|||
"relevance": "Relevance",
|
||||
"files": "Files"
|
||||
},
|
||||
"pageContent": {
|
||||
"rootNode": "(Root Node)",
|
||||
"fileInfo": "No file | {n} file | {n} files",
|
||||
"folderInfo": "No folder | {n} folder | {n} folders"
|
||||
},
|
||||
"tagsAndCategories": {
|
||||
"subHeaderTag": "Available Tags:",
|
||||
"subHeaderCategories": "Available Categories:"
|
||||
},
|
||||
"pageTrackers": {
|
||||
"url": "URL",
|
||||
"status": "Status",
|
||||
"peers": "Peers",
|
||||
"seeds": "Seeds",
|
||||
"leeches": "Leeches",
|
||||
"downloaded": "Downloaded",
|
||||
"message": "Message"
|
||||
}
|
||||
},
|
||||
"add": {
|
||||
|
@ -727,7 +733,10 @@
|
|||
},
|
||||
"rename": {
|
||||
"title": "Rename",
|
||||
"torrentName": "Torrent Name"
|
||||
"torrentName": "Torrent Name",
|
||||
"oldPath": "Old Path",
|
||||
"newPath": "New Path",
|
||||
"errorConflict": "A file or folder with the same name already exists"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Sort Torrents",
|
||||
|
@ -805,8 +814,7 @@
|
|||
"categorySaved": "Category edited successfully!",
|
||||
"feedSaved": "Feed saved successfully!",
|
||||
"ruleSaved": "Rule saved!",
|
||||
"renameFileFailed": "Unable to rename file",
|
||||
"renameFolderFailed": "Unable to rename file",
|
||||
"renameFailed": "Unable to rename node",
|
||||
"copySuccess": "Text copied!",
|
||||
"copyNotSupported": "Unable to copy, context isn't secured",
|
||||
"pasteSuccess": "Text pasted!",
|
||||
|
|
|
@ -43,6 +43,7 @@ export const StoreStateSchema: JSONSchemaType<PersistentStoreState> = {
|
|||
openSideBarOnStart: { type: 'boolean' },
|
||||
showShutdownButton: { type: 'boolean' },
|
||||
refreshInterval: { type: 'number' },
|
||||
contentInterval: { type: 'number' },
|
||||
torrentPieceCountRenderThreshold: { type: 'number' },
|
||||
busyDesktopTorrentProperties: { $ref: '/schemas/desktopDashboardProperties' },
|
||||
doneDesktopTorrentProperties: { $ref: '/schemas/desktopDashboardProperties' },
|
||||
|
@ -68,6 +69,7 @@ export const StoreStateSchema: JSONSchemaType<PersistentStoreState> = {
|
|||
'openSideBarOnStart',
|
||||
'showShutdownButton',
|
||||
'refreshInterval',
|
||||
'contentInterval',
|
||||
'torrentPieceCountRenderThreshold',
|
||||
'busyDesktopTorrentProperties',
|
||||
'doneDesktopTorrentProperties',
|
||||
|
|
|
@ -9,7 +9,7 @@ export default {
|
|||
INIT_INTERVALS: async (store: Store<StoreState>) => {
|
||||
store.state.intervals[0] = setInterval(() => {
|
||||
store.commit('updateMainData')
|
||||
}, store.state.webuiSettings.refreshInterval)
|
||||
}, store.getters.getApiRefreshInterval())
|
||||
},
|
||||
LOGIN: async (store: Store<StoreState>, payload: LoginPayload) => {
|
||||
const res = await qbit.login(payload)
|
||||
|
|
|
@ -37,5 +37,7 @@ export default {
|
|||
return i18n.tc('dashboard.torrentsCount', state.filteredTorrentsCount)
|
||||
}
|
||||
},
|
||||
getSearchPlugins: (state: StoreState) => () => state.searchPlugins
|
||||
getSearchPlugins: (state: StoreState) => () => state.searchPlugins,
|
||||
getApiRefreshInterval: (state: StoreState) => () => state.webuiSettings.refreshInterval,
|
||||
getContentInterval: (state: StoreState) => () => state.webuiSettings.contentInterval,
|
||||
}
|
||||
|
|
|
@ -136,6 +136,7 @@ export default new Vuex.Store<StoreState>({
|
|||
openSideBarOnStart: true,
|
||||
showShutdownButton: true,
|
||||
refreshInterval: 2000,
|
||||
contentInterval: 5000,
|
||||
torrentPieceCountRenderThreshold: 5000,
|
||||
busyDesktopTorrentProperties: JSON.parse(JSON.stringify(desktopPropertiesTemplate)),
|
||||
doneDesktopTorrentProperties: JSON.parse(JSON.stringify(desktopPropertiesTemplate)),
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { Priority } from '@/enums/qbit'
|
|||
export default interface TorrentFile {
|
||||
/** Percentage of file pieces currently available (percentage/100) */
|
||||
availability: number
|
||||
/** File index */
|
||||
/** File index (starting at 0) */
|
||||
index: number
|
||||
/** True if file is seeding/complete */
|
||||
is_seed: boolean
|
||||
|
@ -15,6 +15,6 @@ export default interface TorrentFile {
|
|||
priority: Priority
|
||||
/** File progress (percentage/100) */
|
||||
progress: number
|
||||
/** File size (bytes) */
|
||||
/** File size in bytes */
|
||||
size: number
|
||||
}
|
||||
|
|
|
@ -1,19 +1,46 @@
|
|||
import {Priority} from "@/enums/qbit";
|
||||
|
||||
export interface TreeNode {
|
||||
/** Represents the type of the node */
|
||||
type: 'file' | 'folder' | 'root'
|
||||
/** Represents only the file/folder name */
|
||||
name: string
|
||||
/** Represents the full path of the file/folder (name included) */
|
||||
fullName: string
|
||||
children: TreeNode[]
|
||||
type: 'file' | 'directory'
|
||||
/** Represents the ID of the node
|
||||
* the ID is the file index for a TreeFile
|
||||
* otherwise it's the node's fullName */
|
||||
id: string | number
|
||||
}
|
||||
|
||||
export interface TreeFile extends TreeNode {
|
||||
id: number
|
||||
progress: number
|
||||
size: string
|
||||
icon: string
|
||||
priority: number
|
||||
type: 'file'
|
||||
/** File index */
|
||||
id: number
|
||||
/** file pieces currently available (percentage/100) */
|
||||
availability: number
|
||||
/** File index */
|
||||
index: number
|
||||
/** True if file is seeding/complete */
|
||||
is_seed: boolean
|
||||
/** File priority */
|
||||
priority: Priority
|
||||
/** File progress */
|
||||
progress: number
|
||||
/** File size in bytes */
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface TreeFolder extends TreeNode {
|
||||
type: 'directory'
|
||||
type: 'folder'
|
||||
id: string
|
||||
children: TreeNode[]
|
||||
}
|
||||
|
||||
export interface TreeRoot extends TreeNode {
|
||||
type: 'root'
|
||||
name: ''
|
||||
fullName: ''
|
||||
id: ''
|
||||
children: TreeNode[]
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ export default interface WebUISettings {
|
|||
openSideBarOnStart: boolean
|
||||
showShutdownButton: boolean
|
||||
refreshInterval: number
|
||||
contentInterval: number
|
||||
torrentPieceCountRenderThreshold: number
|
||||
busyDesktopTorrentProperties: TorrentProperty[]
|
||||
doneDesktopTorrentProperties: TorrentProperty[]
|
||||
|
|
|
@ -6,13 +6,14 @@ import type ModalTemplate from './ModalTemplate'
|
|||
import type SortOptions from './SortOptions'
|
||||
import type StoreState from './StoreState'
|
||||
import type { PersistentStoreState } from './StoreState'
|
||||
import type { TreeNode, TreeFile, TreeFolder } from './TreeObjects'
|
||||
import type { TreeNode, TreeFile, TreeFolder, TreeRoot } from './TreeObjects'
|
||||
import { TorrentProperty } from './WebUISettings'
|
||||
import type Tracker from './Tracker'
|
||||
|
||||
export {
|
||||
Category,
|
||||
Feed,
|
||||
FeedArticle,
|
||||
FeedRule,
|
||||
SearchStatus,
|
||||
SearchResult,
|
||||
|
@ -23,6 +24,7 @@ export {
|
|||
TreeNode,
|
||||
TreeFile,
|
||||
TreeFolder,
|
||||
TreeRoot,
|
||||
TorrentProperty,
|
||||
Tracker
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
<v-row class="ma-0 pa-0">
|
||||
<v-tabs v-model="tab" align-with-title show-arrows background-color="primary">
|
||||
<v-tabs-slider color="white"/>
|
||||
<v-tabs-slider color="white" />
|
||||
<v-tab class="white--text" href="#overview">
|
||||
<h4>{{ $t('modals.detail.tabTitleOverview') }}</h4>
|
||||
</v-tab>
|
||||
|
@ -41,23 +41,22 @@
|
|||
<v-card-text class="pa-0">
|
||||
<v-tabs-items v-model="tab" touchless>
|
||||
<v-tab-item eager value="overview">
|
||||
<Overview v-if="torrent" :is-active="tab === 'overview'" :torrent="torrent"/>
|
||||
<Overview v-if="torrent" :is-active="tab === 'overview'" :torrent="torrent" />
|
||||
</v-tab-item>
|
||||
<v-tab-item eager value="info">
|
||||
<Info v-if="torrent" :is-active="tab === 'info'" :torrent="torrent"/>
|
||||
<Info v-if="torrent" :is-active="tab === 'info'" :torrent="torrent" />
|
||||
</v-tab-item>
|
||||
<v-tab-item eager value="trackers">
|
||||
<Trackers :is-active="tab === 'trackers'" :hash="hash"/>
|
||||
<Trackers :is-active="tab === 'trackers'" :hash="hash" />
|
||||
</v-tab-item>
|
||||
<v-tab-item eager value="peers">
|
||||
<DetailPeers :is-active="tab === 'peers'" :hash="hash"/>
|
||||
<DetailPeers :is-active="tab === 'peers'" :hash="hash" />
|
||||
</v-tab-item>
|
||||
<v-tab-item eager value="content">
|
||||
<Content :is-active="tab === 'content'" :hash="hash"/>
|
||||
<Content :is-active="tab === 'content'" :hash="hash" />
|
||||
</v-tab-item>
|
||||
<v-tab-item eager value="tagsAndCategories">
|
||||
<TorrentTagsAndCategories v-if="torrent" :torrent="torrent" :is-active="tab === 'tagsAndCategories'"
|
||||
:hash="hash"/>
|
||||
<TorrentTagsAndCategories v-if="torrent" :torrent="torrent" :is-active="tab === 'tagsAndCategories'" :hash="hash" />
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-card-text>
|
||||
|
@ -66,15 +65,15 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue'
|
||||
import {mapGetters} from 'vuex'
|
||||
import {Content, DetailPeers, Info, TorrentTagsAndCategories, Trackers} from '../components/TorrentDetail/Tabs'
|
||||
import {mdiClose} from '@mdi/js'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { Content, Info, DetailPeers, Trackers, TorrentTagsAndCategories } from '../components/TorrentDetail/Tabs'
|
||||
import { mdiClose } from '@mdi/js'
|
||||
import Overview from "@/components/TorrentDetail/Tabs/Overview.vue";
|
||||
import {defineComponent} from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TorrentDetail',
|
||||
components: {Overview, Content, Info, DetailPeers, Trackers, TorrentTagsAndCategories},
|
||||
components: { Overview, Content, Info, DetailPeers, Trackers, TorrentTagsAndCategories },
|
||||
data() {
|
||||
return {
|
||||
tab: null,
|
||||
|
@ -82,10 +81,10 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getTorrent', 'getModals']),
|
||||
...mapGetters(['getTorrent']),
|
||||
torrent() {
|
||||
//@ts-expect-error: TS2339: Property 'getTorrent' does not exist on type 'CreateComponentPublicInstance...'
|
||||
return this.getTorrent(this.hash as string)
|
||||
//@ts-expect-error: TS2339: Property 'getTorrent' does not exist on type 'CreateComponentPublicInstance...'.
|
||||
return this.getTorrent(this.hash)
|
||||
},
|
||||
hash(): string {
|
||||
return this.$route.params.hash
|
||||
|
@ -104,8 +103,7 @@ export default defineComponent({
|
|||
this.$router.back()
|
||||
},
|
||||
handleKeyboardShortcut(e: KeyboardEvent) {
|
||||
//@ts-expect-error: TS2339: Property 'getModals' does not exist on type 'CreateComponentPublicInstance...'
|
||||
if (e.key === 'Escape' && this.getModals().length === 0) {
|
||||
if (e.key === 'Escape') {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,16 +33,21 @@ exports[`General > render correctly 1`] = `
|
|||
<v-list-item-stub data-v-7da6d3e2=\\"\\" activeclass=\\"\\" tag=\\"div\\">
|
||||
<v-checkbox-stub data-v-7da6d3e2=\\"\\" errorcount=\\"1\\" errormessages=\\"\\" messages=\\"\\" rules=\\"\\" successmessages=\\"\\" backgroundcolor=\\"\\" hidedetails=\\"true\\" ripple=\\"true\\" valuecomparator=\\"[Function]\\" indeterminateicon=\\"$checkboxIndeterminate\\" officon=\\"$checkboxOff\\" onicon=\\"$checkboxOn\\" class=\\"ma-0 pa-0\\"></v-checkbox-stub>
|
||||
</v-list-item-stub>
|
||||
<v-divider-stub data-v-7da6d3e2=\\"\\" class=\\"mb-5\\"></v-divider-stub>
|
||||
<v-list-item-stub data-v-7da6d3e2=\\"\\" activeclass=\\"\\" tag=\\"div\\" class=\\"my-2\\">
|
||||
<v-row-stub data-v-7da6d3e2=\\"\\" tag=\\"div\\">
|
||||
<v-col-stub data-v-7da6d3e2=\\"\\" cols=\\"12\\" sm=\\"6\\" tag=\\"div\\" class=\\"mb-n4\\">
|
||||
<v-col-stub data-v-7da6d3e2=\\"\\" cols=\\"12\\" sm=\\"6\\" md=\\"4\\" tag=\\"div\\" class=\\"mb-n4\\">
|
||||
<v-text-field-stub data-v-7da6d3e2=\\"\\" errorcount=\\"1\\" errormessages=\\"\\" messages=\\"\\" rules=\\"\\" successmessages=\\"\\" backgroundcolor=\\"\\" dense=\\"true\\" loaderheight=\\"2\\" clearicon=\\"$clear\\" outlined=\\"true\\" type=\\"number\\"></v-text-field-stub>
|
||||
</v-col-stub>
|
||||
<v-col-stub data-v-7da6d3e2=\\"\\" cols=\\"12\\" sm=\\"6\\" tag=\\"div\\">
|
||||
<v-col-stub data-v-7da6d3e2=\\"\\" cols=\\"12\\" sm=\\"6\\" md=\\"4\\" tag=\\"div\\" class=\\"mb-n4\\">
|
||||
<v-text-field-stub data-v-7da6d3e2=\\"\\" errorcount=\\"1\\" errormessages=\\"\\" messages=\\"\\" rules=\\"\\" successmessages=\\"\\" backgroundcolor=\\"\\" dense=\\"true\\" loaderheight=\\"2\\" clearicon=\\"$clear\\" outlined=\\"true\\" type=\\"number\\"></v-text-field-stub>
|
||||
</v-col-stub>
|
||||
<v-col-stub data-v-7da6d3e2=\\"\\" cols=\\"12\\" sm=\\"6\\" md=\\"4\\" tag=\\"div\\">
|
||||
<v-text-field-stub data-v-7da6d3e2=\\"\\" errorcount=\\"1\\" errormessages=\\"\\" messages=\\"\\" rules=\\"\\" successmessages=\\"\\" backgroundcolor=\\"\\" dense=\\"true\\" hidedetails=\\"true\\" loaderheight=\\"2\\" clearicon=\\"$clear\\" outlined=\\"true\\" type=\\"number\\"></v-text-field-stub>
|
||||
</v-col-stub>
|
||||
</v-row-stub>
|
||||
</v-list-item-stub>
|
||||
<v-divider-stub data-v-7da6d3e2=\\"\\" class=\\"my-5\\"></v-divider-stub>
|
||||
<v-list-item-stub data-v-7da6d3e2=\\"\\" activeclass=\\"\\" tag=\\"div\\" class=\\"mb-3\\">
|
||||
<v-row-stub data-v-7da6d3e2=\\"\\" tag=\\"div\\">
|
||||
<v-col-stub data-v-7da6d3e2=\\"\\" cols=\\"12\\" sm=\\"6\\" md=\\"3\\" tag=\\"div\\">
|
||||
|
|
Loading…
Add table
Reference in a new issue