1
0
Fork 0
mirror of https://github.com/VueTorrent/VueTorrent.git synced 2025-03-26 11:30:37 +03:00
VueTorrent/src/components/TorrentDetail/Overview.vue
2023-11-29 08:25:43 +01:00

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>