mirror of
https://github.com/VueTorrent/VueTorrent.git
synced 2025-03-26 11:30:37 +03:00
369 lines
13 KiB
Vue
369 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import MoveTorrentDialog from '@/components/Dialogs/MoveTorrentDialog.vue'
|
|
import MoveTorrentFileDialog from '@/components/Dialogs/MoveTorrentFileDialog.vue'
|
|
import { FilePriority, PieceState, TorrentState } from '@/constants/qbit'
|
|
import { formatData, formatDataUnit, formatDataValue, formatPercent, formatSpeed, getDomainBody, splitByUrl, stringContainsUrl } from '@/helpers'
|
|
import { useDialogStore, useMaindataStore, useTorrentStore, useVueTorrentStore } from '@/stores'
|
|
import { TorrentFile } from '@/types/qbit/models'
|
|
import { Torrent } from '@/types/vuetorrent'
|
|
import { useIntervalFn } from '@vueuse/core'
|
|
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useTheme } from 'vuetify'
|
|
|
|
const props = defineProps<{ torrent: Torrent; isActive: boolean }>()
|
|
|
|
const { t } = useI18n()
|
|
const theme = useTheme()
|
|
const dialogStore = useDialogStore()
|
|
const maindataStore = useMaindataStore()
|
|
const torrentStore = useTorrentStore()
|
|
const vuetorrentStore = useVueTorrentStore()
|
|
|
|
const canvas = ref<HTMLCanvasElement>()
|
|
|
|
const comment = ref('')
|
|
const downloadSpeedAvg = ref(0)
|
|
const files = ref<TorrentFile[]>([])
|
|
const selectedFileCount = ref(0)
|
|
const torrentFileCount = ref(0)
|
|
const torrentFileName = ref('')
|
|
const torrentPieceSize = ref(0)
|
|
const torrentPieceOwned = ref(0)
|
|
const torrentPieceCount = ref(0)
|
|
const uploadSpeedAvg = ref(0)
|
|
|
|
const torrentStateColor = computed(() => `torrent-${props.torrent.state}`)
|
|
const pieceSize = computed(() => `${parseInt(formatDataValue(torrentPieceSize.value, true))} ${formatDataUnit(torrentPieceSize.value, true)}`)
|
|
const isFetchingMetadata = computed(() => props.torrent.state === TorrentState.META_DL)
|
|
const shouldRenderPieceState = computed(() => !isFetchingMetadata.value && torrentPieceCount.value > 0 && torrentPieceCount.value < vuetorrentStore.canvasRenderThreshold)
|
|
const shouldRefreshPieceState = computed(() => shouldRenderPieceState.value && torrentPieceCount.value < vuetorrentStore.canvasRefreshThreshold)
|
|
|
|
async function getTorrentProperties() {
|
|
const ppts = await torrentStore.getTorrentProperties(props.torrent.hash)
|
|
comment.value = ppts.comment
|
|
downloadSpeedAvg.value = ppts.dl_speed_avg
|
|
torrentPieceCount.value = ppts.pieces_num
|
|
torrentPieceOwned.value = ppts.pieces_have
|
|
torrentPieceSize.value = ppts.piece_size
|
|
uploadSpeedAvg.value = ppts.up_speed_avg
|
|
}
|
|
|
|
async function updateTorrentFiles() {
|
|
files.value = await maindataStore.fetchFiles(props.torrent.hash)
|
|
torrentFileCount.value = files.value.length
|
|
const selectedFiles = files.value.filter(f => f.priority !== FilePriority.DO_NOT_DOWNLOAD)
|
|
selectedFileCount.value = selectedFiles.length
|
|
if (selectedFileCount.value === 1) {
|
|
torrentFileName.value = selectedFiles[0].name
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Source:
|
|
* https://github.com/qbittorrent/qBittorrent/blob/6229b817300344759139d2fedbd59651065a561d/src/webui/www/private/scripts/prop-general.js#L230
|
|
*/
|
|
async function renderTorrentPieceStates() {
|
|
if (!canvas.value) return
|
|
|
|
const pieces = await maindataStore.fetchPieceState(props.torrent.hash)
|
|
|
|
canvas.value.width = pieces.length || -1
|
|
const ctx = canvas.value.getContext('2d') as CanvasRenderingContext2D
|
|
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height)
|
|
|
|
// Group contiguous colors together and draw as a single rectangle
|
|
let color = ''
|
|
let rectWidth = 1
|
|
|
|
for (let i = 0; i < pieces.length; ++i) {
|
|
const state = pieces[i]
|
|
let newColor = ''
|
|
|
|
if (state === PieceState.DOWNLOADING) newColor = theme.current.value.colors['torrent-downloading']
|
|
else if (state === PieceState.DOWNLOADED) newColor = theme.current.value.colors['torrent-pausedUP']
|
|
else if (state === PieceState.MISSING) {
|
|
const selected_piece_ranges = files.value.filter(file => file.priority !== FilePriority.DO_NOT_DOWNLOAD).map(file => file.piece_range)
|
|
for (const [min_piece_range, max_piece_range] of selected_piece_ranges) {
|
|
if (i > min_piece_range && i < max_piece_range) {
|
|
newColor = theme.current.value.colors['torrent-pausedDL']
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if (newColor === color) {
|
|
++rectWidth
|
|
continue
|
|
}
|
|
|
|
if (color !== '') {
|
|
ctx.fillStyle = color
|
|
ctx.fillRect(i - rectWidth, 0, rectWidth, canvas.value.height)
|
|
}
|
|
|
|
rectWidth = 1
|
|
color = newColor
|
|
}
|
|
|
|
// Fill a rect at the end of the canvas if one is needed
|
|
if (color !== '') {
|
|
ctx.fillStyle = color
|
|
ctx.fillRect(pieces.length - rectWidth, 0, rectWidth, canvas.value.height)
|
|
}
|
|
}
|
|
|
|
async function copyHash() {
|
|
await navigator.clipboard.writeText(props.torrent.hash)
|
|
}
|
|
|
|
function openMoveTorrentDialog(mode: 'dl' | 'save') {
|
|
dialogStore.createDialog(MoveTorrentDialog, { hashes: [props.torrent.hash], mode })
|
|
}
|
|
|
|
function openMoveTorrentFileDialog() {
|
|
dialogStore.createDialog(MoveTorrentFileDialog, {
|
|
hash: props.torrent.hash,
|
|
isFolder: false,
|
|
oldName: torrentFileName.value
|
|
})
|
|
}
|
|
|
|
const { resume: resumeTimer, pause: pauseTimer } = useIntervalFn(
|
|
async () => {
|
|
await updateTorrentFiles()
|
|
if (shouldRefreshPieceState.value) {
|
|
await renderTorrentPieceStates()
|
|
}
|
|
},
|
|
vuetorrentStore.fileContentInterval,
|
|
{
|
|
immediate: true,
|
|
immediateCallback: true
|
|
}
|
|
)
|
|
|
|
watch(
|
|
() => props.isActive,
|
|
newValue => {
|
|
if (newValue) {
|
|
resumeTimer()
|
|
} else {
|
|
pauseTimer()
|
|
}
|
|
}
|
|
)
|
|
|
|
watch(
|
|
() => props.torrent,
|
|
async () => {
|
|
await getTorrentProperties()
|
|
}
|
|
)
|
|
|
|
function handleKeyboardShortcuts(e: KeyboardEvent) {
|
|
if (dialogStore.hasActiveDialog) return false
|
|
|
|
if (e.key === 'd') {
|
|
e.preventDefault()
|
|
openMoveTorrentDialog('dl')
|
|
return true
|
|
}
|
|
|
|
if (e.key === 's') {
|
|
e.preventDefault()
|
|
openMoveTorrentDialog('save')
|
|
return true
|
|
}
|
|
|
|
if (e.key === 'f' && selectedFileCount.value === 1) {
|
|
e.preventDefault()
|
|
openMoveTorrentFileDialog()
|
|
return true
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('keydown', handleKeyboardShortcuts)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('keydown', handleKeyboardShortcuts)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<v-card v-if="torrent">
|
|
<v-card-title class="text-wrap">{{ torrent.name }}</v-card-title>
|
|
<v-card-subtitle>
|
|
<div>
|
|
<span v-for="commentPart in splitByUrl(comment)">
|
|
<a v-if="stringContainsUrl(commentPart)" target="_blank" :href="commentPart">{{ commentPart }}</a>
|
|
<span v-else>{{ commentPart }}</span>
|
|
</span>
|
|
</div>
|
|
<div class="my-1">
|
|
<span class="mr-2">{{ torrent.hash }}</span>
|
|
<v-btn variant="outlined" rounded @click="copyHash">{{ $t('torrentDetail.overview.copy_hash') }}</v-btn>
|
|
</div>
|
|
</v-card-subtitle>
|
|
<v-card-text>
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-row>
|
|
<v-col cols="4">
|
|
<v-progress-circular :color="torrentStateColor" :indeterminate="isFetchingMetadata" :size="100" :model-value="torrent?.progress * 100 ?? 0" :width="15">
|
|
<template v-slot>
|
|
<span v-if="isFetchingMetadata">{{ $t('torrentDetail.overview.fetchingMetadata') }}</span>
|
|
<v-icon v-else-if="torrent.progress === 1" icon="mdi-check" size="x-large" />
|
|
<span v-else>{{ formatPercent(torrent.progress) }}</span>
|
|
</template>
|
|
</v-progress-circular>
|
|
</v-col>
|
|
<v-col cols="8" class="d-flex flex-column align-center justify-center">
|
|
<div v-if="isFetchingMetadata">
|
|
<span>{{ $t('torrentDetail.overview.waitingForMetadata') }}</span>
|
|
</div>
|
|
<div v-else-if="shouldRenderPieceState">
|
|
<canvas ref="canvas" width="0" height="10" />
|
|
</div>
|
|
|
|
<div v-if="!isFetchingMetadata && !shouldRenderPieceState">
|
|
<span>{{ $t('torrentDetail.overview.canvasRenderDisabled') }}</span>
|
|
</div>
|
|
<div v-else-if="!isFetchingMetadata && !shouldRefreshPieceState">
|
|
<span>{{ $t('torrentDetail.overview.canvasRefreshDisabled') }}</span>
|
|
</div>
|
|
|
|
<div v-if="torrentPieceCount > 0">
|
|
<span>
|
|
{{
|
|
t('torrentDetail.overview.pieceCount', {
|
|
owned: torrentPieceOwned,
|
|
total: torrentPieceCount,
|
|
pieceSize
|
|
})
|
|
}}
|
|
</span>
|
|
</div>
|
|
|
|
<div>
|
|
<v-icon icon="mdi-arrow-down" />
|
|
{{ formatSpeed(torrent.dlspeed, vuetorrentStore.useBitSpeed) }}
|
|
|
|
<v-icon icon="mdi-arrow-up" />
|
|
{{ formatSpeed(torrent.upspeed, vuetorrentStore.useBitSpeed) }}
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrent.properties.download_path') }}:</div>
|
|
<div>{{ torrent.download_path }}</div>
|
|
<v-btn icon="mdi-pencil" color="accent" size="x-small" @click="openMoveTorrentDialog('dl')" />
|
|
</v-col>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrentDetail.overview.fileCount') }}:</div>
|
|
<div>{{ selectedFileCount }} / {{ torrentFileCount }}</div>
|
|
<div v-if="selectedFileCount === 1">{{ torrentFileName }}</div>
|
|
<v-btn v-if="selectedFileCount === 1" icon="mdi-pencil" color="accent" size="x-small" @click="openMoveTorrentFileDialog" />
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrent.properties.save_path') }}:</div>
|
|
<div>{{ torrent.savePath }}</div>
|
|
<v-btn icon="mdi-pencil" color="accent" size="x-small" @click="openMoveTorrentDialog('save')" />
|
|
</v-col>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrent.properties.content_path') }}:</div>
|
|
<div>{{ torrent.content_path }}</div>
|
|
</v-col>
|
|
</v-row>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<v-row>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrent.properties.state') }}:</div>
|
|
<v-chip variant="flat" :color="torrentStateColor">{{ $t(`torrent.state.${torrent.state}`) }}</v-chip>
|
|
</v-col>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrent.properties.category') }}:</div>
|
|
<v-chip variant="flat" color="category">
|
|
{{ torrent.category.length ? torrent.category : $t('navbar.side.filters.uncategorized') }}
|
|
</v-chip>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrent.properties.tracker') }}:</div>
|
|
<v-chip variant="flat" color="tracker">
|
|
{{ torrent.tracker ? getDomainBody(torrent.tracker) : $t('navbar.side.filters.untracked') }}
|
|
</v-chip>
|
|
</v-col>
|
|
<v-col cols="6" class="d-flex flex-wrap chipgap">
|
|
<div>{{ $t('torrent.properties.tags') }}:</div>
|
|
<v-chip v-if="torrent.tags" v-for="tag in torrent.tags" :key="tag" variant="flat" color="tag">
|
|
{{ tag }}
|
|
</v-chip>
|
|
<v-chip v-if="!torrent.tags || torrent.tags.length === 0" variant="flat" color="tag">
|
|
{{ $t('navbar.side.filters.untagged') }}
|
|
</v-chip>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrentDetail.overview.selectedFileSize') }}:</div>
|
|
<div>
|
|
{{ formatData(torrent.size, vuetorrentStore.useBinarySize) }} /
|
|
{{ formatData(torrent.total_size, vuetorrentStore.useBinarySize) }}
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrentDetail.overview.ratio') }}:</div>
|
|
<div>{{ torrent.ratio }}</div>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrentDetail.overview.downloaded') }}:</div>
|
|
<div>{{ formatData(torrent.downloaded, vuetorrentStore.useBinarySize) }}</div>
|
|
</v-col>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrentDetail.overview.uploaded') }}:</div>
|
|
<div>{{ formatData(torrent.uploaded, vuetorrentStore.useBinarySize) }}</div>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrentDetail.overview.dlSpeedAverage') }}:</div>
|
|
<div>{{ formatSpeed(downloadSpeedAvg, vuetorrentStore.useBitSpeed) }}</div>
|
|
</v-col>
|
|
<v-col cols="6">
|
|
<div>{{ $t('torrentDetail.overview.upSpeedAverage') }}:</div>
|
|
<div>{{ formatSpeed(uploadSpeedAvg, vuetorrentStore.useBitSpeed) }}</div>
|
|
</v-col>
|
|
</v-row>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card-text>
|
|
</v-card>
|
|
</template>
|
|
|
|
<style scoped>
|
|
canvas {
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
.chipgap {
|
|
gap: 4px;
|
|
}
|
|
</style>
|