feat(filters): Allow to disable filters temporarily

This commit is contained in:
Larsluph 2023-11-05 21:04:25 +01:00 committed by Daan Wijns
parent 55c2ef8115
commit 6f9ee5d0e3
8 changed files with 262 additions and 91 deletions

View file

@ -12,7 +12,7 @@
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="theme-color" content="#000" />
<title>qBittorrent</title>
<title>VueTorrent</title>
</head>
<body>
<noscript>

View file

@ -3,12 +3,19 @@ import { TorrentState } from '@/constants/qbit'
import { useMaindataStore } from '@/stores/maindata'
import { useVueTorrentStore } from '@/stores/vuetorrent'
import { storeToRefs } from 'pinia'
import { computed, toRefs } from 'vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const { categories: _categories, tags: _tags, trackers: _trackers, filters } = storeToRefs(useMaindataStore())
const { statusFilter, categoryFilter, tagFilter, trackerFilter } = toRefs(filters.value)
const {
categories: _categories,
tags: _tags,
trackers: _trackers,
statusFilter,
categoryFilter,
tagFilter,
trackerFilter
} = storeToRefs(useMaindataStore())
const vueTorrentStore = useVueTorrentStore()
const statuses = computed(() => Object.values(TorrentState).map(state => (

View file

@ -1,97 +1,225 @@
<script setup lang="ts">
import { useDashboardStore } from '@/stores/dashboard'
import { useMaindataStore } from '@/stores/maindata.ts'
import { storeToRefs } from 'pinia'
import { computed, toRefs } from 'vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const dashboardStore = useDashboardStore()
const { filters } = storeToRefs(useMaindataStore())
const { statusFilter, categoryFilter, tagFilter, trackerFilter } = toRefs(filters.value)
const {
isTextFilterActive,
textFilter,
isStatusFilterActive,
statusFilter,
isCategoryFilterActive,
categoryFilter,
isTagFilterActive,
tagFilter,
isTrackerFilterActive,
trackerFilter,
} = storeToRefs(useMaindataStore())
const isTextFilterActive = computed(() => dashboardStore.searchFilter?.length > 0)
const isStatusFilterActive = computed(() => statusFilter.value.length > 0)
const isCategoryFilterActive = computed(() => categoryFilter.value.length > 0)
const isTagFilterActive = computed(() => tagFilter.value.length > 0)
const isTrackerFilterActive = computed(() => trackerFilter.value.length > 0)
const filterCount = computed(
const globalFilterActive = computed(
() =>
Number(isTextFilterActive.value) +
Number(isStatusFilterActive.value) +
Number(isCategoryFilterActive.value) +
Number(isTagFilterActive.value) +
Number(isTrackerFilterActive.value)
isTextFilterActive.value && isTextFilterPresent.value ||
isStatusFilterActive.value && isStatusFilterPresent.value ||
isCategoryFilterActive.value && isCategoryFilterPresent.value ||
isTagFilterActive.value && isTagFilterPresent.value ||
isTrackerFilterActive.value && isTrackerFilterPresent.value
)
// TODO
const isTextFilterPresent = computed(() => textFilter.value.length > 0)
const isStatusFilterPresent = computed(() => statusFilter.value.length > 0)
const isCategoryFilterPresent = computed(() => categoryFilter.value.length > 0)
const isTagFilterPresent = computed(() => tagFilter.value.length > 0)
const isTrackerFilterPresent = computed(() => trackerFilter.value.length > 0)
const globalFilterColor = computed(() => globalFilterActive.value ? 'active-global' : 'active-global-disabled')
const textFilterColor = computed(() => isTextFilterActive.value ? 'active-text' : 'active-text-disabled')
const singleStatusFilterColor = computed(() => isStatusFilterActive.value ? `torrent-${statusFilter.value[0]}` : `torrent-${statusFilter.value[0]}-darken-2`)
const statusFilterColor = computed(() => isStatusFilterActive.value ? 'active-status' : 'active-status-disabled')
const categoryFilterColor = computed(() => isCategoryFilterActive.value ? 'active-category' : 'active-category-disabled')
const tagFilterColor = computed(() => isTagFilterActive.value ? 'active-tag' : 'active-tag-disabled')
const trackerFilterColor = computed(() => isTrackerFilterActive.value ? 'active-tracker' : 'active-tracker-disabled')
const filterPresentCount = computed(
() =>
Number(isTextFilterPresent.value) +
Number(isStatusFilterPresent.value) +
Number(isCategoryFilterPresent.value) +
Number(isTagFilterPresent.value) +
Number(isTrackerFilterPresent.value)
)
const filterActiveCount = computed(
() =>
Number(isTextFilterPresent.value && isTextFilterActive.value) +
Number(isStatusFilterPresent.value && isStatusFilterActive.value) +
Number(isCategoryFilterPresent.value && isCategoryFilterActive.value) +
Number(isTagFilterPresent.value && isTagFilterActive.value) +
Number(isTrackerFilterPresent.value && isTrackerFilterActive.value)
)
function toggleAllFilters() {
if (globalFilterActive.value) {
isTextFilterActive.value = false
isStatusFilterActive.value = false
isCategoryFilterActive.value = false
isTagFilterActive.value = false
isTrackerFilterActive.value = false
} else {
isTextFilterActive.value = true
isStatusFilterActive.value = true
isCategoryFilterActive.value = true
isTagFilterActive.value = true
isTrackerFilterActive.value = true
}
}
function resetAllFilters() {
dashboardStore.searchFilter = ''
statusFilter.value = []
categoryFilter.value = []
tagFilter.value = []
trackerFilter.value = []
resetTextFilter()
resetStatusFilter()
resetCategoryFilter()
resetTagFilter()
resetTrackerFilter()
}
function toggleTextFilter() {
isTextFilterActive.value = !isTextFilterActive.value
}
function resetTextFilter() {
dashboardStore.searchFilter = ''
textFilter.value = ''
}
function toggleStatusFilter() {
isStatusFilterActive.value = !isStatusFilterActive.value
}
function resetStatusFilter() {
statusFilter.value = []
}
function toggleCategoryFilter() {
isCategoryFilterActive.value = !isCategoryFilterActive.value
}
function resetCategoryFilter() {
categoryFilter.value = []
}
function toggleTagFilter() {
isTagFilterActive.value = !isTagFilterActive.value
}
function resetTagFilter() {
tagFilter.value = []
}
function toggleTrackerFilter() {
isTrackerFilterActive.value = !isTrackerFilterActive.value
}
function resetTrackerFilter() {
trackerFilter.value = []
}
</script>
<template>
<v-menu close-delay="0" open-delay="0" open-on-click open-on-hover>
<v-menu close-delay="0" open-delay="0" open-on-click open-on-hover open-on-focus>
<template v-slot:activator="{ props }">
<v-slide-x-transition>
<v-chip v-if="filterCount > 0" v-bind="props" class="ml-6" color="primary" variant="elevated" closable @click:close="resetAllFilters()">
{{ t('navbar.top.active_filters.menu_label', filterCount) }}
<v-chip v-if="filterPresentCount > 0" v-bind="props" class="ml-6" :color="globalFilterColor" variant="elevated"
closable @click:close="resetAllFilters()">
<template v-slot:prepend>
<v-icon class="mr-1" @click="toggleAllFilters()">{{ globalFilterActive ? 'mdi-filter' : 'mdi-filter-off' }}</v-icon>
</template>
{{ t('navbar.top.active_filters.menu_label', filterActiveCount) }}
</v-chip>
</v-slide-x-transition>
</template>
<div class="d-flex flex-column gap mt-3">
<v-chip v-if="isTextFilterActive" variant="elevated" color="grey" closable @click:close="resetTextFilter()">
{{ t('navbar.top.active_filters.text', { value: dashboardStore.searchFilter }) }}
<v-chip v-if="isTextFilterPresent" :color="textFilterColor" variant="elevated"
closable @click:close="resetTextFilter()">
<template v-slot:prepend>
<v-icon class="mr-1" @click="toggleTextFilter()">{{ isTextFilterActive ? 'mdi-filter' : 'mdi-filter-off' }}</v-icon>
</template>
{{ t('navbar.top.active_filters.text', { value: textFilter }) }}
</v-chip>
<v-chip v-if="isStatusFilterActive && statusFilter.length === 1" :color="'torrent-' + statusFilter[0]" variant="elevated" closable @click:close="resetStatusFilter()">
{{ t('navbar.top.active_filters.state', { value: t(`torrent.state.${statusFilter[0]}`) }) }}
</v-chip>
<v-chip v-else-if="isStatusFilterActive" variant="elevated" closable @click:close="resetStatusFilter()">
{{ t('navbar.top.active_filters.multiple_state', statusFilter.length) }}
</v-chip>
<template v-if="isStatusFilterPresent">
<v-chip v-if="statusFilter.length === 1" :color="singleStatusFilterColor" variant="elevated"
closable @click:close="resetStatusFilter()">
<template v-slot:prepend>
<v-icon class="mr-1" @click="toggleStatusFilter()">{{ isStatusFilterActive ? 'mdi-filter' : 'mdi-filter-off' }}</v-icon>
</template>
{{ t('navbar.top.active_filters.state', { value: t(`torrent.state.${ statusFilter[0] }`) }) }}
</v-chip>
<v-chip v-else :color="statusFilterColor" variant="elevated"
closable @click:close="resetStatusFilter()">
<template v-slot:prepend>
<v-icon class="mr-1" @click="toggleStatusFilter()">{{ isStatusFilterActive ? 'mdi-filter' : 'mdi-filter-off' }}</v-icon>
</template>
{{ t('navbar.top.active_filters.multiple_state', statusFilter.length) }}
</v-chip>
</template>
<v-chip v-if="isCategoryFilterActive && categoryFilter.length === 1" color="category" variant="elevated" closable @click:close="resetCategoryFilter()">
{{ t('navbar.top.active_filters.category', { value: categoryFilter[0] === '' ? t('navbar.side.filters.uncategorized') : categoryFilter[0] }) }}
</v-chip>
<v-chip v-else-if="isCategoryFilterActive" color="category" variant="elevated" closable @click:close="resetCategoryFilter()">
{{ t('navbar.top.active_filters.multiple_category', categoryFilter.length) }}
</v-chip>
<template v-if="isCategoryFilterPresent">
<v-chip v-if="categoryFilter.length === 1" :color="categoryFilterColor" variant="elevated"
closable @click:close="resetCategoryFilter()">
<template v-slot:prepend>
<v-icon class="mr-1" @click="toggleCategoryFilter()">{{ isCategoryFilterActive ? 'mdi-filter' : 'mdi-filter-off' }}</v-icon>
</template>
{{
t('navbar.top.active_filters.category', { value: categoryFilter[0] === '' ? t('navbar.side.filters.uncategorized') : categoryFilter[0] })
}}
</v-chip>
<v-chip v-else :color="categoryFilterColor" variant="elevated"
closable @click:close="resetCategoryFilter()">
<template v-slot:prepend>
<v-icon class="mr-1" @click="toggleCategoryFilter()">{{ isCategoryFilterActive ? 'mdi-filter' : 'mdi-filter-off' }}</v-icon>
</template>
{{ t('navbar.top.active_filters.multiple_category', categoryFilter.length) }}
</v-chip>
</template>
<v-chip v-if="isTagFilterActive && tagFilter.length === 1" color="tag" variant="elevated" closable @click:close="resetTagFilter()">
{{ t('navbar.top.active_filters.tag', { value: tagFilter[0] === null ? t('navbar.side.filters.untagged') : tagFilter[0] }) }}
</v-chip>
<v-chip v-else-if="isTagFilterActive" color="tag" variant="elevated" closable @click:close="resetTagFilter()">
{{ t('navbar.top.active_filters.multiple_tag', tagFilter.length) }}
</v-chip>
<template v-if="isTagFilterPresent">
<v-chip v-if="tagFilter.length === 1" :color="tagFilterColor" variant="elevated"
closable @click:close="resetTagFilter()">
<template v-slot:prepend>
<v-icon class="mr-1" @click="toggleTagFilter()">{{ isTagFilterActive ? 'mdi-filter' : 'mdi-filter-off' }}</v-icon>
</template>
{{
t('navbar.top.active_filters.tag', { value: tagFilter[0] === null ? t('navbar.side.filters.untagged') : tagFilter[0] })
}}
</v-chip>
<v-chip v-else :color="tagFilterColor" variant="elevated"
closable @click:close="resetTagFilter()">
<template v-slot:prepend>
<v-icon class="mr-1" @click="toggleTagFilter()">{{ isTagFilterActive ? 'mdi-filter' : 'mdi-filter-off' }}</v-icon>
</template>
{{ t('navbar.top.active_filters.multiple_tag', tagFilter.length) }}
</v-chip>
</template>
<v-chip v-if="isTrackerFilterActive && trackerFilter.length === 1" color="tracker" variant="elevated" closable @click:close="resetTrackerFilter()">
{{ t('navbar.top.active_filters.tracker', { value: trackerFilter[0] === '' ? t('navbar.side.filters.untracked') : trackerFilter[0] }) }}
</v-chip>
<v-chip v-else-if="isTrackerFilterActive" color="tracker" variant="elevated" closable @click:close="resetTrackerFilter()">
{{ t('navbar.top.active_filters.multiple_tracker', trackerFilter.length) }}
</v-chip>
<template v-if="isTrackerFilterPresent">
<v-chip v-if="trackerFilter.length === 1" :color="trackerFilterColor" variant="elevated"
closable @click:close="resetTrackerFilter()">
<template v-slot:prepend>
<v-icon class="mr-1" @click="toggleTrackerFilter()">{{ isTrackerFilterActive ? 'mdi-filter' : 'mdi-filter-off' }}</v-icon>
</template>
{{
t('navbar.top.active_filters.tracker', { value: trackerFilter[0] === '' ? t('navbar.side.filters.untracked') : trackerFilter[0] })
}}
</v-chip>
<v-chip v-else :color="trackerFilterColor" variant="elevated"
closable @click:close="resetTrackerFilter()">
<template v-slot:prepend>
<v-icon class="mr-1" @click="toggleTrackerFilter()">{{ isTrackerFilterActive ? 'mdi-filter' : 'mdi-filter-off' }}</v-icon>
</template>
{{ t('navbar.top.active_filters.multiple_tracker', trackerFilter.length) }}
</v-chip>
</template>
</div>
</v-menu>
</template>

View file

@ -1,6 +1,6 @@
import { MaybeRefOrGetter, ref, toValue, watchEffect } from 'vue'
export function useSearchQuery<T>(items: MaybeRefOrGetter<T[]>, searchQuery: MaybeRefOrGetter<string>, getter: (item: T) => string, postProcess?: (items: T[]) => T[]) {
export function useSearchQuery<T>(items: MaybeRefOrGetter<T[]>, searchQuery: MaybeRefOrGetter<string | null>, getter: (item: T) => string, postProcess?: (items: T[]) => T[]) {
const results = ref<T[]>([])
watchEffect(() => {

View file

@ -21,7 +21,7 @@ const router = useRouter()
const display = useDisplay()
const dashboardStore = useDashboardStore()
const dialogStore = useDialogStore()
const { currentPage: dashboardPage, filteredTorrents, isSelectionMultiple, searchFilter, selectedTorrents, sortOptions, torrentCountString } = storeToRefs(useDashboardStore())
const { currentPage: dashboardPage, filteredTorrents, isSelectionMultiple, selectedTorrents, sortOptions, torrentCountString } = storeToRefs(useDashboardStore())
const maindataStore = useMaindataStore()
const vuetorrentStore = useVueTorrentStore()
@ -89,9 +89,9 @@ const trcProperties = reactive({
})
const torrentTitleFilter = computed({
get: () => searchFilter.value,
get: () => maindataStore.textFilter,
set: debounce((newValue: string) => {
searchFilter.value = newValue
maindataStore.textFilter = newValue
}, 300)
})
@ -100,7 +100,7 @@ const {
currentPage,
pageCount
} = useArrayPagination(filteredTorrents, vuetorrentStore.paginationSize, dashboardPage)
const hasSearchFilter = computed(() => !!searchFilter.value && searchFilter.value.length > 0)
const hasSearchFilter = computed(() => !!maindataStore.textFilter && maindataStore.textFilter.length > 0)
const isAllTorrentsSelected = computed(() => filteredTorrents.value.length <= selectedTorrents.value.length)
@ -125,7 +125,7 @@ function goToInfo(hash: string) {
}
function resetInput() {
searchFilter.value = ''
maindataStore.textFilter = ''
}
function scrollToTop() {

View file

@ -15,11 +15,24 @@ const variables = {
download: '#5BB974',
upload: '#00B3FA',
ratio: '#00B2F8',
state: '#1E9367',
category: '#04669A',
tag: '#048B9A',
tracker: '#C97D09',
// Active filters chip colors
'active-global': '#4f738d',
'active-global-disabled': '#35495E',
'active-text': '#4f738d',
'active-text-disabled': '#35495E',
'active-status': '#4f738d',
'active-status-disabled': '#35495E',
'active-category': '#04669A',
'active-category-disabled': '#02334d',
'active-tag': '#048B9A',
'active-tag-disabled': '#03464f',
'active-tracker': '#C97D09',
'active-tracker-disabled': '#6d4504',
// Torrent state colors
'torrent-error': '#F83E70',
'torrent-missingFiles': '#F83E70',

View file

@ -12,7 +12,6 @@ export const useDashboardStore = defineStore(
'dashboard',
() => {
const currentPage = ref(1)
const searchFilter = ref('')
const filteredTorrents = computed(() => searchQuery.results.value)
const isSelectionMultiple = ref(false)
const selectedTorrents = ref<string[]>([])
@ -28,7 +27,7 @@ export const useDashboardStore = defineStore(
const vuetorrentStore = useVueTorrentStore()
const searchQuery = useSearchQuery(
() => maindataStore.torrentsWithFilters,
searchFilter,
() => maindataStore.isTextFilterActive ? maindataStore.textFilter : null,
torrent => torrent.name,
results => {
if (sortOptions.isCustomSortEnabled) {
@ -51,10 +50,10 @@ export const useDashboardStore = defineStore(
const torrentCountString = computed(() => {
if (selectedTorrents.value.length) {
const selectedSize = selectedTorrents.value
.map(hash => maindataStore.getTorrentByHash(hash))
.filter(torrent => torrent !== undefined)
.map(torrent => torrent!.size)
.reduce((partial, size) => partial + size, 0)
.map(hash => maindataStore.getTorrentByHash(hash))
.filter(torrent => torrent !== undefined)
.map(torrent => torrent!.size)
.reduce((partial, size) => partial + size, 0)
return t('dashboard.selectedTorrentsCount', {
count: selectedTorrents.value.length,
@ -135,7 +134,6 @@ export const useDashboardStore = defineStore(
return {
currentPage,
searchFilter,
filteredTorrents,
isSelectionMultiple,
selectedTorrents,
@ -160,7 +158,7 @@ export const useDashboardStore = defineStore(
{
storage: localStorage,
key: 'vuetorrent_dashboard',
paths: ['searchFilter', 'sortOptions']
paths: ['sortOptions']
}
]
}

View file

@ -12,7 +12,7 @@ import { AddTorrentPayload } from '@/types/qbit/payloads'
import { Torrent } from '@/types/vuetorrent'
import { generateMultiple } from '@/utils/faker'
import { defineStore } from 'pinia'
import { computed, MaybeRefOrGetter, reactive, ref, toValue } from 'vue'
import { computed, MaybeRefOrGetter, ref, toValue } from 'vue'
const isProduction = computed(() => process.env.NODE_ENV === 'production')
@ -25,22 +25,27 @@ export const useMaindataStore = defineStore('maindata', () => {
const torrents = ref<Torrent[]>([])
const trackers = ref<string[]>([])
const filters = reactive({
statusFilter: [] as TorrentState[],
categoryFilter: [] as string[],
tagFilter: [] as (string | null)[],
trackerFilter: [] as (string | null)[]
})
const isTextFilterActive = ref(true)
const isStatusFilterActive = ref(true)
const isCategoryFilterActive = ref(true)
const isTagFilterActive = ref(true)
const isTrackerFilterActive = ref(true)
const textFilter = ref('')
const statusFilter = ref<TorrentState[]>([])
const categoryFilter = ref<string[]>([])
const tagFilter = ref<(string | null)[]>([])
const trackerFilter = ref<(string | null)[]>([])
const torrentsWithFilters = computed(() => {
return torrents.value.filter(torrent => {
if (filters.statusFilter.length > 0 && !filters.statusFilter.includes(torrent.state)) return false
if (filters.categoryFilter.length > 0 && !filters.categoryFilter.includes(torrent.category)) return false
if (filters.tagFilter.length > 0) {
if (torrent.tags.length === 0 && filters.tagFilter.includes(null)) return true
if (!torrent.tags.some(tag => filters.tagFilter.includes(tag))) return false
if (statusFilter.value.length > 0 && isStatusFilterActive.value && !statusFilter.value.includes(torrent.state)) return false
if (categoryFilter.value.length > 0 && isCategoryFilterActive.value && !categoryFilter.value.includes(torrent.category)) return false
if (tagFilter.value.length > 0 && isTagFilterActive.value) {
if (torrent.tags.length === 0 && tagFilter.value.includes(null)) return true
if (!torrent.tags.some(tag => tagFilter.value.includes(tag))) return false
}
if (filters.trackerFilter.length > 0 && !filters.trackerFilter.includes(extractHostname(torrent.tracker))) return false
if (trackerFilter.value.length > 0 && isTrackerFilterActive.value && !trackerFilter.value.includes(extractHostname(torrent.tracker))) return false
return true
})
})
@ -167,10 +172,10 @@ export const useMaindataStore = defineStore('maindata', () => {
if (vueTorrentStore.showTrackerFilter) {
trackers.value = data
.map(t => t.tracker)
.map(url => extractHostname(url))
.filter((domain, index, self) => index === self.indexOf(domain) && domain)
.sort()
.map(t => t.tracker)
.map(url => extractHostname(url))
.filter((domain, index, self) => index === self.indexOf(domain) && domain)
.sort()
}
// update torrents
@ -324,7 +329,16 @@ export const useMaindataStore = defineStore('maindata', () => {
torrents,
torrentsWithFilters,
trackers,
filters,
isTextFilterActive,
textFilter,
isStatusFilterActive,
statusFilter,
isCategoryFilterActive,
categoryFilter,
isTagFilterActive,
tagFilter,
isTrackerFilterActive,
trackerFilter,
getTorrentByHash,
getTorrentIndexByHash,
deleteTorrents,
@ -378,7 +392,18 @@ export const useMaindataStore = defineStore('maindata', () => {
{
storage: localStorage,
key: 'vuetorrent_maindata',
paths: ['filters']
paths: [
'isTextFilterActive',
'textFilter',
'isStatusFilterActive',
'statusFilter',
'isCategoryFilterActive',
'categoryFilter',
'isTagFilterActive',
'tagFilter',
'isTrackerFilterActive',
'trackerFilter',
]
}
]
}