mirror of
https://github.com/VueTorrent/VueTorrent.git
synced 2025-03-14 12:10:18 +03:00
Merge pull request #1455 from VueTorrent/fixes
This commit is contained in:
commit
cdb04179c8
13 changed files with 1037 additions and 820 deletions
|
@ -100,8 +100,7 @@ If you like to always have the latest and greatest, please sync to the `nightly-
|
|||
|
||||
## Contributing
|
||||
|
||||
Open up a PR or create an issue to discuss.
|
||||
Reach out on Discord if you need help getting started!
|
||||
Open up a PR or create an issue to discuss. Reach out on Discord if you need help getting started!
|
||||
|
||||
[FAQ](../../wiki/FAQ)
|
||||
|
||||
|
|
1262
package-lock.json
generated
1262
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,25 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { TorrentState } from '@/constants/qbit'
|
||||
import { formatPercent } from '@/helpers'
|
||||
import { Torrent } from '@/types/vuetorrent'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ torrent: Torrent; title: string; value: string }>()
|
||||
|
||||
const isTorrentActive = computed(() => {
|
||||
return [
|
||||
TorrentState.UPLOADING,
|
||||
TorrentState.CHECKING_UP,
|
||||
TorrentState.FORCED_UP,
|
||||
TorrentState.ALLOCATING,
|
||||
TorrentState.DOWNLOADING,
|
||||
TorrentState.META_DL,
|
||||
TorrentState.CHECKING_DL,
|
||||
TorrentState.FORCED_DL,
|
||||
TorrentState.CHECKING_RESUME_DATA,
|
||||
TorrentState.MOVING
|
||||
].includes(props.torrent.state)
|
||||
})
|
||||
defineProps<{ torrent: Torrent; title: string; value: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -27,7 +10,7 @@ const isTorrentActive = computed(() => {
|
|||
<div class="text-caption text-grey">
|
||||
{{ $t(`torrent.properties.${title}`) }}
|
||||
</div>
|
||||
<v-progress-linear :model-value="torrent[value]" :max="1" :striped="isTorrentActive" :height="20" :color="`torrent-${torrent.state}`" rounded="sm" style="width: 10em">
|
||||
<v-progress-linear :model-value="torrent[value]" :max="1" :height="20" :color="`torrent-${torrent.state}`" rounded="sm" style="width: 10em">
|
||||
{{ formatPercent(torrent[value]) }}
|
||||
</v-progress-linear>
|
||||
</div>
|
||||
|
|
164
src/components/Dashboard/Toolbar.vue
Normal file
164
src/components/Dashboard/Toolbar.vue
Normal file
|
@ -0,0 +1,164 @@
|
|||
<script setup lang="ts">
|
||||
import { SortOptions } from '@/constants/qbit'
|
||||
import { DashboardDisplayMode } from '@/constants/vuetorrent'
|
||||
import { useDashboardStore, useTorrentStore } from '@/stores'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { mergeProps } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const torrentStore = useTorrentStore()
|
||||
const { sortOptions } = storeToRefs(torrentStore)
|
||||
|
||||
const dashboardStore = useDashboardStore()
|
||||
const { torrentCountString, isSelectionMultiple, displayMode } = storeToRefs(dashboardStore)
|
||||
|
||||
const torrentSortOptions = [
|
||||
{ value: SortOptions.ADDED_ON, title: t('dashboard.sortBy.added_on') },
|
||||
{ value: SortOptions.AMOUNT_LEFT, title: t('dashboard.sortBy.amount_left') },
|
||||
{ value: SortOptions.AUTO_TMM, title: t('dashboard.sortBy.auto_tmm') },
|
||||
{ value: SortOptions.AVAILABILITY, title: t('dashboard.sortBy.availability') },
|
||||
{ value: SortOptions.AVG_DOWNLOAD_SPEED, title: t('dashboard.sortBy.avg_download_speed') },
|
||||
{ value: SortOptions.AVG_UPLOAD_SPEED, title: t('dashboard.sortBy.avg_upload_speed') },
|
||||
{ value: SortOptions.CATEGORY, title: t('dashboard.sortBy.category') },
|
||||
{ value: SortOptions.COMPLETED, title: t('dashboard.sortBy.completed') },
|
||||
{ value: SortOptions.COMPLETION_ON, title: t('dashboard.sortBy.completion_on') },
|
||||
{ value: SortOptions.CONTENT_PATH, title: t('dashboard.sortBy.content_path') },
|
||||
{ value: SortOptions.DL_LIMIT, title: t('dashboard.sortBy.dl_limit') },
|
||||
{ value: SortOptions.DLSPEED, title: t('dashboard.sortBy.dlspeed') },
|
||||
{ value: SortOptions.DOWNLOAD_PATH, title: t('dashboard.sortBy.download_path') },
|
||||
{ value: SortOptions.DOWNLOADED, title: t('dashboard.sortBy.downloaded') },
|
||||
{ value: SortOptions.DOWNLOADED_SESSION, title: t('dashboard.sortBy.downloaded_session') },
|
||||
{ value: SortOptions.ETA, title: t('dashboard.sortBy.eta') },
|
||||
{ value: SortOptions.F_L_PIECE_PRIO, title: t('dashboard.sortBy.f_l_piece_prio') },
|
||||
{ value: SortOptions.FORCE_START, title: t('dashboard.sortBy.force_start') },
|
||||
{ value: SortOptions.GLOBALSPEED, title: t('dashboard.sortBy.globalSpeed') },
|
||||
{ value: SortOptions.GLOBALVOLUME, title: t('dashboard.sortBy.globalVolume') },
|
||||
{ value: SortOptions.HASH, title: t('dashboard.sortBy.hash') },
|
||||
{ value: SortOptions.INFOHASH_V1, title: t('dashboard.sortBy.infohash_v1') },
|
||||
{ value: SortOptions.INFOHASH_V2, title: t('dashboard.sortBy.infohash_v2') },
|
||||
{ value: SortOptions.LAST_ACTIVITY, title: t('dashboard.sortBy.last_activity') },
|
||||
{ value: SortOptions.MAGNET_URI, title: t('dashboard.sortBy.magnet_uri') },
|
||||
{ value: SortOptions.MAX_RATIO, title: t('dashboard.sortBy.max_ratio') },
|
||||
{ value: SortOptions.MAX_SEEDING_TIME, title: t('dashboard.sortBy.max_seeding_time') },
|
||||
{ value: SortOptions.NAME, title: t('dashboard.sortBy.name') },
|
||||
{ value: SortOptions.NUM_COMPLETE, title: t('dashboard.sortBy.num_complete') },
|
||||
{ value: SortOptions.NUM_INCOMPLETE, title: t('dashboard.sortBy.num_incomplete') },
|
||||
{ value: SortOptions.NUM_LEECHS, title: t('dashboard.sortBy.num_leechs') },
|
||||
{ value: SortOptions.NUM_SEEDS, title: t('dashboard.sortBy.num_seeds') },
|
||||
{ value: SortOptions.PRIORITY, title: t('dashboard.sortBy.priority') },
|
||||
{ value: SortOptions.PROGRESS, title: t('dashboard.sortBy.progress') },
|
||||
{ value: SortOptions.RATIO, title: t('dashboard.sortBy.ratio') },
|
||||
{ value: SortOptions.RATIO_LIMIT, title: t('dashboard.sortBy.ratio_limit') },
|
||||
{ value: SortOptions.SAVE_PATH, title: t('dashboard.sortBy.save_path') },
|
||||
{ value: SortOptions.SEEDING_TIME, title: t('dashboard.sortBy.seeding_time') },
|
||||
{ value: SortOptions.SEEDING_TIME_LIMIT, title: t('dashboard.sortBy.seeding_time_limit') },
|
||||
{ value: SortOptions.SEEN_COMPLETE, title: t('dashboard.sortBy.seen_complete') },
|
||||
{ value: SortOptions.SEQ_DL, title: t('dashboard.sortBy.seq_dl') },
|
||||
{ value: SortOptions.SIZE, title: t('dashboard.sortBy.size') },
|
||||
{ value: SortOptions.STATE, title: t('dashboard.sortBy.state') },
|
||||
{ value: SortOptions.SUPER_SEEDING, title: t('dashboard.sortBy.super_seeding') },
|
||||
{ value: SortOptions.TAGS, title: t('dashboard.sortBy.tags') },
|
||||
{ value: SortOptions.TIME_ACTIVE, title: t('dashboard.sortBy.time_active') },
|
||||
{ value: SortOptions.TOTAL_SIZE, title: t('dashboard.sortBy.total_size') },
|
||||
{ value: SortOptions.TRACKER, title: t('dashboard.sortBy.tracker') },
|
||||
{ value: SortOptions.TRACKERS_COUNT, title: t('dashboard.sortBy.trackers_count') },
|
||||
{ value: SortOptions.UP_LIMIT, title: t('dashboard.sortBy.up_limit') },
|
||||
{ value: SortOptions.UPLOADED, title: t('dashboard.sortBy.uploaded') },
|
||||
{ value: SortOptions.UPLOADED_SESSION, title: t('dashboard.sortBy.uploaded_session') },
|
||||
{ value: SortOptions.UPSPEED, title: t('dashboard.sortBy.upspeed') }
|
||||
].sort((a, b) => a.title.localeCompare(b.title))
|
||||
torrentSortOptions.splice(0, 0, { value: SortOptions.DEFAULT, title: t('dashboard.sortBy.default') })
|
||||
|
||||
function resetInput() {
|
||||
torrentStore.textFilter = ''
|
||||
}
|
||||
|
||||
const torrentTitleFilter = computed({
|
||||
get: () => torrentStore.textFilter,
|
||||
set: debounce((newValue: string | null) => {
|
||||
torrentStore.textFilter = newValue ?? ''
|
||||
}, 300)
|
||||
})
|
||||
|
||||
function toggleSelectMode() {
|
||||
if (isSelectionMultiple.value) {
|
||||
dashboardStore.unselectAllTorrents()
|
||||
}
|
||||
isSelectionMultiple.value = !isSelectionMultiple.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row class="ma-0 pa-0 mb-2">
|
||||
<v-col cols="9" md="4" order="2" order-md="1" class="ma-0 pa-0">
|
||||
<v-row class="ma-0 pa-0">
|
||||
<v-tooltip :text="t('dashboard.toggleSelectMode')" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn :icon="isSelectionMultiple ? 'mdi-checkbox-marked' : 'mdi-checkbox-blank-outline'" v-bind="props" variant="plain" @click="toggleSelectMode" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props: menu }">
|
||||
<v-tooltip :text="$t('dashboard.displayMode.title')" location="top">
|
||||
<template v-slot:activator="{ props: tooltip }">
|
||||
<v-btn icon v-bind="mergeProps(menu, tooltip)" variant="plain">
|
||||
<v-icon v-if="displayMode === DashboardDisplayMode.LIST" icon="mdi-view-list" />
|
||||
<v-icon v-if="displayMode === DashboardDisplayMode.GRID" icon="mdi-view-grid" />
|
||||
<v-icon v-if="displayMode === DashboardDisplayMode.TABLE" icon="mdi-table" />
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item :title="$t('dashboard.displayMode.list')" prepend-icon="mdi-view-list" @click="displayMode = DashboardDisplayMode.LIST" />
|
||||
<v-list-item :title="$t('dashboard.displayMode.grid')" prepend-icon="mdi-view-grid" @click="displayMode = DashboardDisplayMode.GRID" />
|
||||
<v-list-item :title="$t('dashboard.displayMode.table')" prepend-icon="mdi-table" @click="displayMode = DashboardDisplayMode.TABLE" />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-tooltip :text="t('dashboard.toggleSortOrder')" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
:icon="sortOptions.reverseOrder ? 'mdi-arrow-up-thin' : 'mdi-arrow-down-thin'"
|
||||
v-bind="props"
|
||||
variant="plain"
|
||||
@click="sortOptions.reverseOrder = !sortOptions.reverseOrder" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<div class="pa-0" style="width: 8em">
|
||||
<v-autocomplete
|
||||
v-model="sortOptions.sortBy"
|
||||
:items="torrentSortOptions"
|
||||
:label="t('dashboard.sortLabel')"
|
||||
auto-select-first
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="solo-filled" />
|
||||
</div>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col cols="10" md="6" order="1" order-md="2" :class="$vuetify.display.smAndDown ? 'mb-4 mx-auto' : ''" class="ma-0 pa-0">
|
||||
<v-card color="transparent" max-width="300">
|
||||
<v-text-field
|
||||
id="searchInput"
|
||||
v-model="torrentTitleFilter"
|
||||
:label="t('dashboard.searchInputLabel')"
|
||||
clearable
|
||||
density="compact"
|
||||
single-line
|
||||
hide-details
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="solo"
|
||||
@click:clear="resetInput()" />
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="3" md="2" order="3" class="align-center justify-center">
|
||||
<span class="text-uppercase" style="float: right; font-size: 0.8em">
|
||||
{{ torrentCountString }}
|
||||
</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
59
src/components/Dialogs/ImportSettingsDialog.vue
Normal file
59
src/components/Dialogs/ImportSettingsDialog.vue
Normal file
|
@ -0,0 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import { useDialog } from '@/composables'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
guid: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { isOpened } = useDialog(props.guid)
|
||||
|
||||
const isFormValid = ref(false)
|
||||
const settings = ref('')
|
||||
|
||||
const rules = [
|
||||
(v: string) => {
|
||||
if (!v) return t('dialogs.importSettings.required')
|
||||
|
||||
try {
|
||||
const settings = JSON.parse(v)
|
||||
|
||||
// naive 1 key check
|
||||
if (!settings.darkMode) return t('dialogs.importSettings.required')
|
||||
} catch (e) {
|
||||
return t('dialogs.importSettings.valid')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
]
|
||||
|
||||
function close() {
|
||||
isOpened.value = false
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
window.localStorage.setItem('vuetorrent_webuiSettings', settings.value)
|
||||
window.location.reload()
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="isOpened" max-width="500">
|
||||
<v-card :title="$t('dialogs.importSettings.title')">
|
||||
<v-card-text>
|
||||
<v-form v-model="isFormValid" @submit.prevent @keydown.enter.prevent="submit">
|
||||
<v-textarea v-model="settings" clearable auto-grow :rules="rules" />
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="error" @click="close">{{ $t('common.cancel') }}</v-btn>
|
||||
<v-btn color="accent" :disabled="!isFormValid" @click="submit">{{ $t('common.save') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
|
@ -128,5 +128,3 @@ onBeforeMount(async () => {
|
|||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -19,6 +19,21 @@ function selectAllStatuses() {
|
|||
statusFilter.value = []
|
||||
}
|
||||
|
||||
function selectActive() {
|
||||
statusFilter.value = [
|
||||
TorrentState.UPLOADING,
|
||||
TorrentState.CHECKING_UP,
|
||||
TorrentState.FORCED_UP,
|
||||
TorrentState.ALLOCATING,
|
||||
TorrentState.DOWNLOADING,
|
||||
TorrentState.META_DL,
|
||||
TorrentState.CHECKING_DL,
|
||||
TorrentState.FORCED_DL,
|
||||
TorrentState.CHECKING_RESUME_DATA,
|
||||
TorrentState.MOVING
|
||||
]
|
||||
}
|
||||
|
||||
function selectAllCategories() {
|
||||
categoryFilter.value = []
|
||||
}
|
||||
|
@ -40,7 +55,7 @@ function selectAllTrackers() {
|
|||
</v-list-item-title>
|
||||
<v-select
|
||||
v-model="statusFilter"
|
||||
:items="statuses"
|
||||
:items="statuses.sort((a, b) => a.title.localeCompare(b.title))"
|
||||
:placeholder="t('navbar.side.filters.disabled')"
|
||||
bg-color="secondary"
|
||||
class="text-accent pt-1"
|
||||
|
@ -50,6 +65,7 @@ function selectAllTrackers() {
|
|||
variant="solo">
|
||||
<template v-slot:prepend-item>
|
||||
<v-list-item :title="$t('common.disable')" @click="selectAllStatuses" />
|
||||
<v-list-item :title="$t('common.active')" @click="selectActive" />
|
||||
<v-divider />
|
||||
</template>
|
||||
<template v-slot:selection="{ item, index }">
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import ImportSettingsDialog from '@/components/Dialogs/ImportSettingsDialog.vue'
|
||||
import { TitleOptions } from '@/constants/vuetorrent'
|
||||
import { LOCALES } from '@/locales'
|
||||
import { useAppStore, useHistoryStore, useVueTorrentStore } from '@/stores'
|
||||
import { useAppStore, useDialogStore, useHistoryStore, useVueTorrentStore } from '@/stores'
|
||||
import { computed, onBeforeMount, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue3-toastify'
|
||||
import { Github } from '@/services/Github'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const historyStore = useHistoryStore()
|
||||
const vueTorrentStore = useVueTorrentStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const github = new Github()
|
||||
|
||||
const titleOptionsList = [
|
||||
{ title: t('constants.titleOptions.default'), value: TitleOptions.DEFAULT },
|
||||
|
@ -57,6 +62,25 @@ const resetSettings = () => {
|
|||
location.reload()
|
||||
}
|
||||
|
||||
const downloadSettings = () => {
|
||||
const settings = window.localStorage.getItem('vuetorrent_webuiSettings')
|
||||
if (!settings) return
|
||||
const blob = new Blob([settings], { type: 'application/json' })
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = blobUrl
|
||||
a.download = 'settings.json'
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
const importSettings = () => {
|
||||
dialogStore.createDialog(ImportSettingsDialog)
|
||||
}
|
||||
|
||||
const registerMagnetHandler = () => {
|
||||
if (typeof navigator.registerProtocolHandler !== 'function') {
|
||||
toast.error(t('toast.magnet_handler.not_supported'))
|
||||
|
@ -68,6 +92,15 @@ const registerMagnetHandler = () => {
|
|||
toast.success(t('toast.magnet_handler.registered'))
|
||||
}
|
||||
|
||||
const checkNewVersion = async () => {
|
||||
if (vueTorrentVersion.value === 'DEV') return
|
||||
|
||||
const latest = await github.getVersion()
|
||||
if ('vueTorrentVersion.value' === latest) return
|
||||
|
||||
toast.info(t('toast.new_version'))
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
appStore.fetchQbitVersion()
|
||||
})
|
||||
|
@ -149,51 +182,32 @@ onBeforeMount(() => {
|
|||
</v-row>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select v-model="vueTorrentStore.language" flat hide-details :items="LOCALES" :label="t('settings.vuetorrent.general.language')" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select v-model="vueTorrentStore.paginationSize" flat hide-details :items="paginationSizes" :label="t('settings.vuetorrent.general.paginationSize.label')" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select v-model="vueTorrentStore.uiTitleType" flat hide-details :items="titleOptionsList" :label="t('settings.vuetorrent.general.vueTorrentTitle')" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-text-field
|
||||
:disabled="vueTorrentStore.uiTitleType !== TitleOptions.CUSTOM"
|
||||
v-model="vueTorrentStore.uiTitleCustom"
|
||||
:label="t('settings.vuetorrent.general.customTitle')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-list-item :class="$vuetify.display.mdAndDown ? 'w-100' : 'w-50'" class="mx-auto">
|
||||
<v-select v-model="vueTorrentStore.language" flat hide-details :items="LOCALES" :label="t('settings.vuetorrent.general.language')" />
|
||||
<v-select v-model="vueTorrentStore.paginationSize" flat hide-details :items="paginationSizes" :label="t('settings.vuetorrent.general.paginationSize.label')" />
|
||||
<v-select v-model="vueTorrentStore.uiTitleType" flat hide-details :items="titleOptionsList" :label="t('settings.vuetorrent.general.vueTorrentTitle')" />
|
||||
<v-text-field :disabled="vueTorrentStore.uiTitleType !== TitleOptions.CUSTOM" v-model="vueTorrentStore.uiTitleCustom" :label="t('settings.vuetorrent.general.customTitle')" />
|
||||
<v-select v-model="theme" :items="themeOptions" :label="t('settings.vuetorrent.general.theme')" />
|
||||
<v-text-field v-model="vueTorrentStore.dateFormat" placeholder="DD/MM/YYYY, HH:mm:ss" hint="using Dayjs" :label="t('settings.vuetorrent.general.dateFormat')" />
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-row>
|
||||
<v-col cols="12" md="3" class="d-flex align-center justify-center">
|
||||
<v-col cols="4" class="d-flex align-center justify-center">
|
||||
<h3>
|
||||
{{ t('settings.vuetorrent.general.currentVersion') }}
|
||||
<span v-if="!vueTorrentVersion">undefined</span>
|
||||
<a v-else-if="vueTorrentVersion === 'DEV'" target="_blank" href="https://github.com/WDaan/VueTorrent/">{{ vueTorrentVersion }}</a>
|
||||
<a v-else target="_blank" :href="`https://github.com/WDaan/VueTorrent/releases/tag/v${vueTorrentVersion}`">{{ vueTorrentVersion }}</a>
|
||||
<a v-else-if="vueTorrentVersion === 'DEV'" target="_blank" href="https://github.com/VueTorrent/VueTorrent/">{{ vueTorrentVersion }}</a>
|
||||
<a v-else target="_blank" :href="`https://github.com/VueTorrent/VueTorrent/releases/tag/v${vueTorrentVersion}`">{{ vueTorrentVersion }}</a>
|
||||
</h3>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="3" class="d-flex align-center justify-center">
|
||||
<v-col cols="4" class="d-flex align-center justify-center">
|
||||
<h3>
|
||||
{{ t('settings.vuetorrent.general.qbittorrentVersion') }}
|
||||
<a target="_blank" :href="`https://github.com/qbittorrent/qBittorrent/releases/tag/release-${appStore.version}`">{{ appStore.version }}</a>
|
||||
</h3>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="3">
|
||||
<v-select v-model="theme" :items="themeOptions" :label="t('settings.vuetorrent.general.theme')" />
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="3">
|
||||
<v-text-field v-model="vueTorrentStore.dateFormat" placeholder="DD/MM/YYYY, HH:mm:ss" hint="using Dayjs" :label="t('settings.vuetorrent.general.dateFormat')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-list-item>
|
||||
|
||||
|
@ -202,8 +216,21 @@ onBeforeMount(() => {
|
|||
<v-col cols="12" sm="6" class="d-flex align-center justify-center">
|
||||
<v-btn color="primary" @click="registerMagnetHandler">{{ t('settings.vuetorrent.general.registerMagnet') }} </v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" class="d-flex align-center justify-center">
|
||||
<v-btn color="primary" @click="checkNewVersion">{{ t('settings.vuetorrent.general.check_new') }} </v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="4" class="d-flex align-center justify-center">
|
||||
<v-btn color="primary" @click="importSettings">{{ t('settings.vuetorrent.general.import') }}</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4" class="d-flex align-center justify-center">
|
||||
<v-btn color="primary" @click="downloadSettings">{{ t('settings.vuetorrent.general.download') }}</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4" class="d-flex align-center justify-center">
|
||||
<v-btn color="red" @click="resetSettings">{{ t('settings.vuetorrent.general.resetSettings') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"_comments": "Generated by Tolgee app. NEVER EDIT MANUALLY AS IT WILL BE OVERRIDDEN BY TOLGEE",
|
||||
"common": {
|
||||
"active": "Active",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"delete": "Delete",
|
||||
|
@ -910,13 +911,16 @@
|
|||
"general": {
|
||||
"canvasRefreshThreshold": "Piece count to disable canvas auto-refresh",
|
||||
"canvasRenderThreshold": "Piece count to disable canvas rendering",
|
||||
"check_new": "Check for new version",
|
||||
"currentVersion": "Current Version",
|
||||
"customTitle": "Custom title",
|
||||
"dateFormat": "Date Format",
|
||||
"download": "Download settings",
|
||||
"exportSettings": "Export Settings",
|
||||
"fileContentInterval": "Torrent file content refresh interval",
|
||||
"hideChipIfUnset": "Hide chips if unset",
|
||||
"historySize": "History size on eligible fields",
|
||||
"import": "Import Settings",
|
||||
"importSettings": "Import Settings",
|
||||
"isDrawerRight": "Right Drawer",
|
||||
"isPaginationOnTop": "Top Pagination",
|
||||
|
@ -1023,6 +1027,7 @@
|
|||
"not_supported": "Current context isn't secure. Unable to register handler.",
|
||||
"registered": "Magnet handler registered."
|
||||
},
|
||||
"new_version": "New version available!",
|
||||
"qbit": {
|
||||
"not_supported": "Only available for qbit >= {version}"
|
||||
}
|
||||
|
|
|
@ -4,130 +4,55 @@ import GridView from '@/components/Dashboard/Views/Grid/GridView.vue'
|
|||
import ListView from '@/components/Dashboard/Views/List/ListView.vue'
|
||||
import TableView from '@/components/Dashboard/Views/Table/TableView.vue'
|
||||
import ConfirmDeleteDialog from '@/components/Dialogs/ConfirmDeleteDialog.vue'
|
||||
import Toolbar from '@/components/Dashboard/Toolbar.vue'
|
||||
import { useArrayPagination } from '@/composables'
|
||||
import { SortOptions } from '@/constants/qbit'
|
||||
import { DashboardDisplayMode } from '@/constants/vuetorrent'
|
||||
import { doesCommand } from '@/helpers'
|
||||
import { useDashboardStore, useDialogStore, useMaindataStore, useTorrentStore, useVueTorrentStore } from '@/stores'
|
||||
import { Torrent as TorrentType } from '@/types/vuetorrent'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, mergeProps, nextTick, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const dashboardStore = useDashboardStore()
|
||||
const { currentPage: dashboardPage, isSelectionMultiple, selectedTorrents, displayMode, torrentCountString } = storeToRefs(dashboardStore)
|
||||
const { currentPage: dashboardPage, isSelectionMultiple, selectedTorrents, displayMode } = storeToRefs(dashboardStore)
|
||||
const dialogStore = useDialogStore()
|
||||
const maindataStore = useMaindataStore()
|
||||
const torrentStore = useTorrentStore()
|
||||
const { filteredTorrents, sortOptions } = storeToRefs(torrentStore)
|
||||
const { filteredTorrents } = storeToRefs(torrentStore)
|
||||
const vuetorrentStore = useVueTorrentStore()
|
||||
|
||||
const torrentSortOptions = [
|
||||
{ value: SortOptions.ADDED_ON, title: t('dashboard.sortBy.added_on') },
|
||||
{ value: SortOptions.AMOUNT_LEFT, title: t('dashboard.sortBy.amount_left') },
|
||||
{ value: SortOptions.AUTO_TMM, title: t('dashboard.sortBy.auto_tmm') },
|
||||
{ value: SortOptions.AVAILABILITY, title: t('dashboard.sortBy.availability') },
|
||||
{ value: SortOptions.AVG_DOWNLOAD_SPEED, title: t('dashboard.sortBy.avg_download_speed') },
|
||||
{ value: SortOptions.AVG_UPLOAD_SPEED, title: t('dashboard.sortBy.avg_upload_speed') },
|
||||
{ value: SortOptions.CATEGORY, title: t('dashboard.sortBy.category') },
|
||||
{ value: SortOptions.COMPLETED, title: t('dashboard.sortBy.completed') },
|
||||
{ value: SortOptions.COMPLETION_ON, title: t('dashboard.sortBy.completion_on') },
|
||||
{ value: SortOptions.CONTENT_PATH, title: t('dashboard.sortBy.content_path') },
|
||||
{ value: SortOptions.DL_LIMIT, title: t('dashboard.sortBy.dl_limit') },
|
||||
{ value: SortOptions.DLSPEED, title: t('dashboard.sortBy.dlspeed') },
|
||||
{ value: SortOptions.DOWNLOAD_PATH, title: t('dashboard.sortBy.download_path') },
|
||||
{ value: SortOptions.DOWNLOADED, title: t('dashboard.sortBy.downloaded') },
|
||||
{ value: SortOptions.DOWNLOADED_SESSION, title: t('dashboard.sortBy.downloaded_session') },
|
||||
{ value: SortOptions.ETA, title: t('dashboard.sortBy.eta') },
|
||||
{ value: SortOptions.F_L_PIECE_PRIO, title: t('dashboard.sortBy.f_l_piece_prio') },
|
||||
{ value: SortOptions.FORCE_START, title: t('dashboard.sortBy.force_start') },
|
||||
{ value: SortOptions.GLOBALSPEED, title: t('dashboard.sortBy.globalSpeed') },
|
||||
{ value: SortOptions.GLOBALVOLUME, title: t('dashboard.sortBy.globalVolume') },
|
||||
{ value: SortOptions.HASH, title: t('dashboard.sortBy.hash') },
|
||||
{ value: SortOptions.INFOHASH_V1, title: t('dashboard.sortBy.infohash_v1') },
|
||||
{ value: SortOptions.INFOHASH_V2, title: t('dashboard.sortBy.infohash_v2') },
|
||||
{ value: SortOptions.LAST_ACTIVITY, title: t('dashboard.sortBy.last_activity') },
|
||||
{ value: SortOptions.MAGNET_URI, title: t('dashboard.sortBy.magnet_uri') },
|
||||
{ value: SortOptions.MAX_RATIO, title: t('dashboard.sortBy.max_ratio') },
|
||||
{ value: SortOptions.MAX_SEEDING_TIME, title: t('dashboard.sortBy.max_seeding_time') },
|
||||
{ value: SortOptions.NAME, title: t('dashboard.sortBy.name') },
|
||||
{ value: SortOptions.NUM_COMPLETE, title: t('dashboard.sortBy.num_complete') },
|
||||
{ value: SortOptions.NUM_INCOMPLETE, title: t('dashboard.sortBy.num_incomplete') },
|
||||
{ value: SortOptions.NUM_LEECHS, title: t('dashboard.sortBy.num_leechs') },
|
||||
{ value: SortOptions.NUM_SEEDS, title: t('dashboard.sortBy.num_seeds') },
|
||||
{ value: SortOptions.PRIORITY, title: t('dashboard.sortBy.priority') },
|
||||
{ value: SortOptions.PROGRESS, title: t('dashboard.sortBy.progress') },
|
||||
{ value: SortOptions.RATIO, title: t('dashboard.sortBy.ratio') },
|
||||
{ value: SortOptions.RATIO_LIMIT, title: t('dashboard.sortBy.ratio_limit') },
|
||||
{ value: SortOptions.SAVE_PATH, title: t('dashboard.sortBy.save_path') },
|
||||
{ value: SortOptions.SEEDING_TIME, title: t('dashboard.sortBy.seeding_time') },
|
||||
{ value: SortOptions.SEEDING_TIME_LIMIT, title: t('dashboard.sortBy.seeding_time_limit') },
|
||||
{ value: SortOptions.SEEN_COMPLETE, title: t('dashboard.sortBy.seen_complete') },
|
||||
{ value: SortOptions.SEQ_DL, title: t('dashboard.sortBy.seq_dl') },
|
||||
{ value: SortOptions.SIZE, title: t('dashboard.sortBy.size') },
|
||||
{ value: SortOptions.STATE, title: t('dashboard.sortBy.state') },
|
||||
{ value: SortOptions.SUPER_SEEDING, title: t('dashboard.sortBy.super_seeding') },
|
||||
{ value: SortOptions.TAGS, title: t('dashboard.sortBy.tags') },
|
||||
{ value: SortOptions.TIME_ACTIVE, title: t('dashboard.sortBy.time_active') },
|
||||
{ value: SortOptions.TOTAL_SIZE, title: t('dashboard.sortBy.total_size') },
|
||||
{ value: SortOptions.TRACKER, title: t('dashboard.sortBy.tracker') },
|
||||
{ value: SortOptions.TRACKERS_COUNT, title: t('dashboard.sortBy.trackers_count') },
|
||||
{ value: SortOptions.UP_LIMIT, title: t('dashboard.sortBy.up_limit') },
|
||||
{ value: SortOptions.UPLOADED, title: t('dashboard.sortBy.uploaded') },
|
||||
{ value: SortOptions.UPLOADED_SESSION, title: t('dashboard.sortBy.uploaded_session') },
|
||||
{ value: SortOptions.UPSPEED, title: t('dashboard.sortBy.upspeed') }
|
||||
].sort((a, b) => a.title.localeCompare(b.title))
|
||||
torrentSortOptions.splice(0, 0, { value: SortOptions.DEFAULT, title: t('dashboard.sortBy.default') })
|
||||
|
||||
const isSearchFilterVisible = ref(false)
|
||||
const trcProperties = reactive({
|
||||
isVisible: false,
|
||||
offset: [0, 0]
|
||||
})
|
||||
|
||||
const torrentTitleFilter = computed({
|
||||
get: () => torrentStore.textFilter,
|
||||
set: debounce((newValue: string | null) => {
|
||||
torrentStore.textFilter = newValue ?? ''
|
||||
}, 300)
|
||||
})
|
||||
|
||||
const isListView = computed(() => displayMode.value === DashboardDisplayMode.LIST)
|
||||
const isGridView = computed(() => displayMode.value === DashboardDisplayMode.GRID)
|
||||
const isTableView = computed(() => displayMode.value === DashboardDisplayMode.TABLE)
|
||||
|
||||
const { paginatedResults: paginatedTorrents, currentPage, pageCount } = useArrayPagination(filteredTorrents, vuetorrentStore.paginationSize, dashboardPage)
|
||||
const hasSearchFilter = computed(() => !!torrentStore.textFilter && torrentStore.textFilter.length > 0)
|
||||
|
||||
const isAllTorrentsSelected = computed(() => filteredTorrents.value.length <= selectedTorrents.value.length)
|
||||
|
||||
function toggleSearchFilter(forceState?: boolean) {
|
||||
isSearchFilterVisible.value = forceState ?? !isSearchFilterVisible.value
|
||||
if (isSearchFilterVisible.value) {
|
||||
nextTick(() => {
|
||||
const searchInput = document.getElementById('searchInput')
|
||||
searchInput?.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function resetInput() {
|
||||
torrentStore.textFilter = ''
|
||||
}
|
||||
const trcProperties = reactive({
|
||||
isVisible: false,
|
||||
offset: [0, 0]
|
||||
})
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
function toggleSelectMode() {
|
||||
if (isSelectionMultiple.value) {
|
||||
dashboardStore.unselectAllTorrents()
|
||||
function toggleSearchFilter(forceState?: boolean) {
|
||||
if (forceState) {
|
||||
return nextTick(() => {
|
||||
const searchInput = document.getElementById('searchInput')
|
||||
searchInput?.focus()
|
||||
})
|
||||
}
|
||||
isSelectionMultiple.value = !isSelectionMultiple.value
|
||||
nextTick(() => {
|
||||
const searchInput = document.getElementById('searchInput')
|
||||
searchInput?.blur()
|
||||
})
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
|
@ -216,7 +141,7 @@ function handleKeyboardShortcuts(e: KeyboardEvent) {
|
|||
// 'Escape' => Remove focus from search field / unselect all torrents
|
||||
if (e.key === 'Escape') {
|
||||
const searchInput = document.getElementById('searchInput')
|
||||
if (document.activeElement === searchInput || isSearchFilterVisible.value) {
|
||||
if (document.activeElement === searchInput) {
|
||||
toggleSearchFilter(false)
|
||||
} else {
|
||||
isSelectionMultiple.value = false
|
||||
|
@ -238,7 +163,7 @@ function handleKeyboardShortcuts(e: KeyboardEvent) {
|
|||
// 'Search' => Search view
|
||||
if (e.key === '/') {
|
||||
const searchInput = document.getElementById('searchInput')
|
||||
if (document.activeElement !== searchInput && !isSearchFilterVisible.value) {
|
||||
if (document.activeElement !== searchInput) {
|
||||
router.push({ name: 'searchEngine' })
|
||||
e.preventDefault()
|
||||
}
|
||||
|
@ -258,7 +183,6 @@ watch(
|
|||
onBeforeMount(async () => {
|
||||
await maindataStore.fetchCategories()
|
||||
await maindataStore.fetchTags()
|
||||
toggleSearchFilter(hasSearchFilter.value)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -274,77 +198,7 @@ onBeforeUnmount(() => {
|
|||
|
||||
<template>
|
||||
<div class="pt-4 px-1 px-sm-5">
|
||||
<v-row class="ma-0 pa-0 mb-2">
|
||||
<v-expand-x-transition>
|
||||
<v-card v-show="isSearchFilterVisible" color="transparent">
|
||||
<v-text-field
|
||||
id="searchInput"
|
||||
v-model="torrentTitleFilter"
|
||||
:label="t('dashboard.searchInputLabel')"
|
||||
clearable
|
||||
density="compact"
|
||||
hide-details
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
rounded="pill"
|
||||
single-line
|
||||
style="width: 200px"
|
||||
variant="solo"
|
||||
@click:clear="resetInput()" />
|
||||
</v-card>
|
||||
</v-expand-x-transition>
|
||||
<v-tooltip :text="t('dashboard.toggleSearchFilter')" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn :icon="isSearchFilterVisible ? 'mdi-chevron-left-circle' : 'mdi-text-box-search'" v-bind="props" variant="plain" @click="toggleSearchFilter()" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip :text="t('dashboard.toggleSelectMode')" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn :icon="isSelectionMultiple ? 'mdi-checkbox-marked' : 'mdi-checkbox-blank-outline'" v-bind="props" variant="plain" @click="toggleSelectMode" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props: menu }">
|
||||
<v-tooltip :text="$t('dashboard.displayMode.title')" location="top">
|
||||
<template v-slot:activator="{ props: tooltip }">
|
||||
<v-btn icon v-bind="mergeProps(menu, tooltip)" variant="plain">
|
||||
<v-icon v-if="displayMode === DashboardDisplayMode.LIST" icon="mdi-view-list" />
|
||||
<v-icon v-if="displayMode === DashboardDisplayMode.GRID" icon="mdi-view-grid" />
|
||||
<v-icon v-if="displayMode === DashboardDisplayMode.TABLE" icon="mdi-table" />
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item :title="$t('dashboard.displayMode.list')" prepend-icon="mdi-view-list" @click="displayMode = DashboardDisplayMode.LIST" />
|
||||
<v-list-item :title="$t('dashboard.displayMode.grid')" prepend-icon="mdi-view-grid" @click="displayMode = DashboardDisplayMode.GRID" />
|
||||
<v-list-item :title="$t('dashboard.displayMode.table')" prepend-icon="mdi-table" @click="displayMode = DashboardDisplayMode.TABLE" />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-tooltip :text="t('dashboard.toggleSortOrder')" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
:icon="sortOptions.reverseOrder ? 'mdi-arrow-up-thin' : 'mdi-arrow-down-thin'"
|
||||
v-bind="props"
|
||||
variant="plain"
|
||||
@click="sortOptions.reverseOrder = !sortOptions.reverseOrder" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<div class="pa-0" style="width: 10em">
|
||||
<v-autocomplete
|
||||
v-model="sortOptions.sortBy"
|
||||
:items="torrentSortOptions"
|
||||
:label="t('dashboard.sortLabel')"
|
||||
auto-select-first
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="solo-filled" />
|
||||
</div>
|
||||
<v-col class="align-center justify-center">
|
||||
<span class="text-uppercase" style="float: right; font-size: 0.8em">
|
||||
{{ torrentCountString }}
|
||||
</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Toolbar />
|
||||
<v-row class="ma-0 pa-0">
|
||||
<v-expand-transition>
|
||||
<v-card v-show="isSelectionMultiple" color="transparent">
|
||||
|
|
|
@ -115,9 +115,19 @@ onUnmounted(() => {
|
|||
<template v-for="(log, index) in paginatedResults">
|
||||
<v-divider v-if="index > 0" />
|
||||
|
||||
<v-list-item :class="getLogTypeClassName(log)">
|
||||
<v-list-item-title>{{ log.id }}) {{ log.message }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ getLogTypeName(log) }} | {{ formatLogTimestamp(log) }}</v-list-item-subtitle>
|
||||
<v-list-item :class="getLogTypeClassName(log)" class="pa-0">
|
||||
<v-expansion-panels class="p-0">
|
||||
<v-expansion-panel class="pa-0">
|
||||
<v-expansion-panel-title>
|
||||
<template v-slot:default>
|
||||
<v-list-item-title>[{{ log.id }}] {{ log.message }}</v-list-item-title>
|
||||
<v-spacer />
|
||||
<v-list-item-subtitle>{{ getLogTypeName(log) }} | {{ formatLogTimestamp(log) }}</v-list-item-subtitle>
|
||||
</template>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>{{ log.message }}</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
|
|
18
src/services/Github.ts
Normal file
18
src/services/Github.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { AxiosInstance } from 'axios'
|
||||
import axios from 'axios'
|
||||
|
||||
export class Github {
|
||||
private axios: AxiosInstance
|
||||
|
||||
constructor() {
|
||||
this.axios = axios.create()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the latest version of VueTorrent
|
||||
*/
|
||||
async getVersion(): Promise<string> {
|
||||
const { data } = await this.axios.get('https://api.github.com/repos/vuetorrent/vuetorrent/releases/latest')
|
||||
return data.tag_name
|
||||
}
|
||||
}
|
|
@ -99,3 +99,13 @@ $sideborder-margin: 6px;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
background: background-color;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 16px;
|
||||
background: #6b7280;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue