Merge pull request #1455 from VueTorrent/fixes

This commit is contained in:
Daan Wijns 2024-01-13 12:16:20 +01:00 committed by GitHub
commit cdb04179c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1037 additions and 820 deletions

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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>

View 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>

View 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>

View file

@ -128,5 +128,3 @@ onBeforeMount(async () => {
</v-card>
</v-dialog>
</template>
<style scoped></style>

View file

@ -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 }">

View file

@ -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>

View file

@ -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}"
}

View file

@ -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">

View file

@ -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
View 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
}
}

View file

@ -99,3 +99,13 @@ $sideborder-margin: 6px;
}
}
}
/* scrollbar */
::-webkit-scrollbar {
width: 6px;
background: background-color;
}
::-webkit-scrollbar-thumb {
border-radius: 16px;
background: #6b7280;
}