mirror of
https://github.com/VueTorrent/VueTorrent.git
synced 2025-03-24 02:20:44 +03:00
feat(Content/Filter): Group extensions of same type (#2204)
This commit is contained in:
parent
18eb8975ea
commit
ddbafbef68
9 changed files with 243 additions and 79 deletions
src
components
constants/vuetorrent
helpers
locales
|
@ -1,7 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import HistoryField from '@/components/Core/HistoryField.vue'
|
||||
import { useDialog } from '@/composables'
|
||||
import { getFileIcon, HistoryKey } from '@/constants/vuetorrent'
|
||||
import { HistoryKey } from '@/constants/vuetorrent'
|
||||
import { getFileIcon } from '@/helpers'
|
||||
import { useContentStore } from '@/stores'
|
||||
import { TreeFolder, TreeNode } from '@/types/vuetorrent'
|
||||
import { computed, onMounted, reactive, readonly, ref, watch } from 'vue'
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { useDialog } from '@/composables'
|
||||
import { useDialog, useI18nUtils } from '@/composables'
|
||||
import { FilePriority } from '@/constants/qbit'
|
||||
import { comparators, formatData } from '@/helpers'
|
||||
import { splitExt } from '@/helpers/path.ts'
|
||||
import { FileType } from '@/constants/vuetorrent'
|
||||
import { comparators, formatData, getExtType, splitExt } from '@/helpers'
|
||||
import { useContentStore, useVueTorrentStore } from '@/stores'
|
||||
import { computed, reactive } from 'vue'
|
||||
import { useI18nUtils } from '@/composables'
|
||||
|
||||
const props = defineProps<{
|
||||
guid: string
|
||||
|
@ -21,15 +20,35 @@ const sizeBoundaries = computed<[number, number]>(() =>
|
|||
.map(file => file.size)
|
||||
.reduce((prev, curr) => [prev[0] === -1 || curr < prev[0] ? curr : prev[0], prev[1] === -1 || curr > prev[1] ? curr : prev[1]], [-1, -1])
|
||||
)
|
||||
const fileExtensions = computed(() => Array.from(new Set<string>(contentStore.cachedFiles.map(file => splitExt(file.name)[1])).values()))
|
||||
const extensionItems = computed(() =>
|
||||
fileExtensions.value
|
||||
.map(ext => {
|
||||
if (ext === '') return { title: t('common.none'), value: '' }
|
||||
else return { title: `.${ext}`, value: ext }
|
||||
})
|
||||
.sort((a, b) => comparators.text.asc(a.title, b.title))
|
||||
|
||||
const fileExtensionsByType = computed(() =>
|
||||
new Set<string>(contentStore.cachedFiles.map(file => splitExt(file.name)[1])).values().reduce(
|
||||
(prev, ext) => {
|
||||
const type = getExtType(ext)
|
||||
if (Object.keys(prev).includes(type)) {
|
||||
prev[type].push(ext)
|
||||
} else {
|
||||
prev[type] = [ext]
|
||||
}
|
||||
return prev
|
||||
},
|
||||
{} as Record<FileType, string[]>
|
||||
)
|
||||
)
|
||||
const extensionItems = computed(() =>
|
||||
Object.entries(fileExtensionsByType.value)
|
||||
.sort(([typeA, _1], [typeB, _2]) => {
|
||||
if (typeA === FileType.UNKNOWN) return 1
|
||||
if (typeB === FileType.UNKNOWN) return -1
|
||||
return comparators.text.asc(typeA, typeB)
|
||||
})
|
||||
.flatMap(([type, extensions]) => [
|
||||
{ props: { header: t(`constants.file_type.${type}`) } },
|
||||
...extensions.map(ext => ({ title: `.${ext}`, value: ext })),
|
||||
{ props: { divider: true } }
|
||||
])
|
||||
)
|
||||
|
||||
const priorityOptions = [
|
||||
{
|
||||
title: t('constants.file_priority.unwanted'),
|
||||
|
@ -91,30 +110,38 @@ function close() {
|
|||
<v-card>
|
||||
<v-card-title class="ios-margin">
|
||||
<v-toolbar color="transparent">
|
||||
<v-toolbar-title>{{ $t('torrentDetail.content.filter.title') }}</v-toolbar-title>
|
||||
<v-toolbar-title>{{ t('torrentDetail.content.filter.title') }}</v-toolbar-title>
|
||||
<v-btn icon="mdi-close" @click="close" />
|
||||
</v-toolbar>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="4" class="d-flex align-center">
|
||||
{{ $t('torrentDetail.content.filter.extensions') }}
|
||||
{{ t('torrentDetail.content.filter.extensions') }}
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-select v-model="filters.extensions" :items="extensionItems" :placeholder="$t('common.disabled')" persistent-placeholder multiple hide-details />
|
||||
<v-select v-model="filters.extensions" :items="extensionItems" :placeholder="t('common.disabled')" persistent-placeholder multiple hide-details>
|
||||
<template #item="data">
|
||||
<v-list-subheader v-if="data.props.header">
|
||||
{{ data.props.header }}
|
||||
</v-list-subheader>
|
||||
<v-divider v-else-if="data.props.divider" />
|
||||
<v-list-item v-else v-bind="data.props" />
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="4" class="d-flex align-center">
|
||||
{{ $t('torrentDetail.content.filter.priority') }}
|
||||
{{ t('torrentDetail.content.filter.priority') }}
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-select v-model="filters.priority" :items="priorityOptions" :placeholder="$t('common.disabled')" persistent-placeholder multiple hide-details />
|
||||
<v-select v-model="filters.priority" :items="priorityOptions" :placeholder="t('common.disabled')" persistent-placeholder multiple hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="4" class="d-flex align-center">
|
||||
{{ $t('torrentDetail.content.filter.size') }}
|
||||
{{ t('torrentDetail.content.filter.size') }}
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-range-slider
|
||||
|
@ -145,7 +172,7 @@ function close() {
|
|||
<v-row>
|
||||
<v-col cols="12">
|
||||
{{
|
||||
$t('torrentDetail.content.filter.preview', {
|
||||
t('torrentDetail.content.filter.preview', {
|
||||
count: filterPreview.length,
|
||||
total: contentStore.cachedFiles.length,
|
||||
size: formatData(filterPreviewSize, vuetorrentStore.useBinarySize)
|
||||
|
@ -155,8 +182,8 @@ function close() {
|
|||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="error" @click="exclude">{{ $t('torrentDetail.content.filter.exclude') }}</v-btn>
|
||||
<v-btn color="success" @click="include">{{ $t('torrentDetail.content.filter.include') }}</v-btn>
|
||||
<v-btn color="error" @click="exclude">{{ t('torrentDetail.content.filter.exclude') }}</v-btn>
|
||||
<v-btn color="success" @click="include">{{ t('torrentDetail.content.filter.include') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18nUtils } from '@/composables'
|
||||
import { FilePriority } from '@/constants/qbit'
|
||||
import { getFileIcon } from '@/constants/vuetorrent'
|
||||
import { doesCommand, formatData } from '@/helpers'
|
||||
import { doesCommand, formatData, getFileIcon } from '@/helpers'
|
||||
import { useContentStore, useVueTorrentStore } from '@/stores'
|
||||
import { TreeNode } from '@/types/vuetorrent'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
|
|
@ -1,57 +1,122 @@
|
|||
enum FileIcon {
|
||||
PDF = 'mdi-file-pdf-box',
|
||||
IMAGE = 'mdi-file-image',
|
||||
DOCUMENT = 'mdi-file-document',
|
||||
INFORMATION = 'mdi-information-variant-box',
|
||||
MUSIC = 'mdi-music',
|
||||
VIDEO = 'mdi-movie',
|
||||
SUBTITLE = 'mdi-subtitles',
|
||||
ARCHIVE = 'mdi-zip-box-outline',
|
||||
EXECUTABLE = 'mdi-application-brackets'
|
||||
export enum FileType {
|
||||
ARCHIVE = 'archive',
|
||||
AUDIO = 'audio',
|
||||
BOOK = 'book',
|
||||
DOCUMENT = 'document',
|
||||
EXECUTABLE = 'executable',
|
||||
IMAGE = 'image',
|
||||
INFORMATION = 'information',
|
||||
SCRIPT = 'script',
|
||||
SUBTITLE = 'subtitle',
|
||||
VIDEO = 'video',
|
||||
|
||||
UNKNOWN = 'unknown'
|
||||
}
|
||||
|
||||
export const typesMap: Record<string, FileIcon> = {
|
||||
pdf: FileIcon.PDF,
|
||||
export const typesMap: Record<FileType, string> = {
|
||||
[FileType.ARCHIVE]: 'mdi-zip-box-outline',
|
||||
[FileType.AUDIO]: 'mdi-audio',
|
||||
[FileType.BOOK]: 'mdi-book-open-blank-variant',
|
||||
[FileType.DOCUMENT]: 'mdi-file-document',
|
||||
[FileType.EXECUTABLE]: 'mdi-application-brackets',
|
||||
[FileType.IMAGE]: 'mdi-file-image',
|
||||
[FileType.INFORMATION]: 'mdi-information-variant-box',
|
||||
[FileType.SCRIPT]: 'mdi-script-text',
|
||||
[FileType.SUBTITLE]: 'mdi-subtitles',
|
||||
[FileType.VIDEO]: 'mdi-movie',
|
||||
|
||||
png: FileIcon.IMAGE,
|
||||
jpg: FileIcon.IMAGE,
|
||||
jpeg: FileIcon.IMAGE,
|
||||
tiff: FileIcon.IMAGE,
|
||||
|
||||
doc: FileIcon.DOCUMENT,
|
||||
docx: FileIcon.DOCUMENT,
|
||||
txt: FileIcon.DOCUMENT,
|
||||
|
||||
nfo: FileIcon.INFORMATION,
|
||||
|
||||
mp3: FileIcon.MUSIC,
|
||||
wav: FileIcon.MUSIC,
|
||||
flac: FileIcon.MUSIC,
|
||||
|
||||
avi: FileIcon.VIDEO,
|
||||
mp4: FileIcon.VIDEO,
|
||||
mkv: FileIcon.VIDEO,
|
||||
mov: FileIcon.VIDEO,
|
||||
wmv: FileIcon.VIDEO,
|
||||
|
||||
srt: FileIcon.SUBTITLE,
|
||||
idx: FileIcon.SUBTITLE,
|
||||
sub: FileIcon.SUBTITLE,
|
||||
|
||||
rar: FileIcon.ARCHIVE,
|
||||
zip: FileIcon.ARCHIVE,
|
||||
gz: FileIcon.ARCHIVE,
|
||||
'7z': FileIcon.ARCHIVE,
|
||||
iso: FileIcon.ARCHIVE,
|
||||
|
||||
exe: FileIcon.EXECUTABLE,
|
||||
msi: FileIcon.EXECUTABLE,
|
||||
dmg: FileIcon.EXECUTABLE,
|
||||
deb: FileIcon.EXECUTABLE,
|
||||
jar: FileIcon.EXECUTABLE
|
||||
[FileType.UNKNOWN]: 'mdi-file'
|
||||
}
|
||||
|
||||
export function getFileIcon(filename: string) {
|
||||
const type = filename.split('.').pop()?.toLowerCase() || ''
|
||||
return typesMap[type] || 'mdi-file'
|
||||
export const extMap: Record<string, FileType> = {
|
||||
'7z': FileType.ARCHIVE,
|
||||
bz2: FileType.ARCHIVE,
|
||||
cab: FileType.ARCHIVE,
|
||||
gz: FileType.ARCHIVE,
|
||||
iso: FileType.ARCHIVE,
|
||||
rar: FileType.ARCHIVE,
|
||||
sfx: FileType.ARCHIVE,
|
||||
tar: FileType.ARCHIVE,
|
||||
tgz: FileType.ARCHIVE,
|
||||
xz: FileType.ARCHIVE,
|
||||
zip: FileType.ARCHIVE,
|
||||
|
||||
alac: FileType.AUDIO,
|
||||
flac: FileType.AUDIO,
|
||||
mp3: FileType.AUDIO,
|
||||
ogg: FileType.AUDIO,
|
||||
wav: FileType.AUDIO,
|
||||
wma: FileType.AUDIO,
|
||||
|
||||
cb7: FileType.BOOK,
|
||||
cbr: FileType.BOOK,
|
||||
cbt: FileType.BOOK,
|
||||
cbz: FileType.BOOK,
|
||||
epub: FileType.BOOK,
|
||||
mobi: FileType.BOOK,
|
||||
|
||||
doc: FileType.DOCUMENT,
|
||||
docx: FileType.DOCUMENT,
|
||||
htm: FileType.DOCUMENT,
|
||||
html: FileType.DOCUMENT,
|
||||
pdf: FileType.DOCUMENT,
|
||||
rtf: FileType.DOCUMENT,
|
||||
txt: FileType.DOCUMENT,
|
||||
xhtml: FileType.DOCUMENT,
|
||||
|
||||
apk: FileType.EXECUTABLE,
|
||||
app: FileType.EXECUTABLE,
|
||||
bin: FileType.EXECUTABLE,
|
||||
deb: FileType.EXECUTABLE,
|
||||
dmg: FileType.EXECUTABLE,
|
||||
exe: FileType.EXECUTABLE,
|
||||
jar: FileType.EXECUTABLE,
|
||||
msi: FileType.EXECUTABLE,
|
||||
|
||||
avif: FileType.IMAGE,
|
||||
bmp: FileType.IMAGE,
|
||||
gif: FileType.IMAGE,
|
||||
heif: FileType.IMAGE,
|
||||
jfif: FileType.IMAGE,
|
||||
jpeg: FileType.IMAGE,
|
||||
jpg: FileType.IMAGE,
|
||||
png: FileType.IMAGE,
|
||||
svg: FileType.IMAGE,
|
||||
tiff: FileType.IMAGE,
|
||||
webp: FileType.IMAGE,
|
||||
|
||||
nfo: FileType.INFORMATION,
|
||||
|
||||
bat: FileType.SCRIPT,
|
||||
c: FileType.SCRIPT,
|
||||
cmd: FileType.SCRIPT,
|
||||
com: FileType.SCRIPT,
|
||||
cpp: FileType.SCRIPT,
|
||||
cs: FileType.SCRIPT,
|
||||
css: FileType.SCRIPT,
|
||||
h: FileType.SCRIPT,
|
||||
hpp: FileType.SCRIPT,
|
||||
java: FileType.SCRIPT,
|
||||
js: FileType.SCRIPT,
|
||||
py: FileType.SCRIPT,
|
||||
vbs: FileType.SCRIPT,
|
||||
|
||||
idx: FileType.SUBTITLE,
|
||||
srt: FileType.SUBTITLE,
|
||||
sub: FileType.SUBTITLE,
|
||||
|
||||
'3gp': FileType.VIDEO,
|
||||
avi: FileType.VIDEO,
|
||||
flv: FileType.VIDEO,
|
||||
gifv: FileType.VIDEO,
|
||||
m2ts: FileType.VIDEO,
|
||||
m4v: FileType.VIDEO,
|
||||
mkv: FileType.VIDEO,
|
||||
mov: FileType.VIDEO,
|
||||
mp4: FileType.VIDEO,
|
||||
mpeg: FileType.VIDEO,
|
||||
mpg: FileType.VIDEO,
|
||||
mts: FileType.VIDEO,
|
||||
ts: FileType.VIDEO,
|
||||
wmv: FileType.VIDEO,
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { DashboardDisplayMode } from './DashboardDisplayMode'
|
|||
import { DashboardProperty } from './DashboardProperty'
|
||||
import { DashboardPropertyType } from './DashboardPropertyType'
|
||||
import { FeedState } from './FeedState'
|
||||
import { getFileIcon, typesMap } from './FileIcon'
|
||||
import { FileType, extMap, typesMap } from './FileIcon'
|
||||
import { FilterType } from './FilterType'
|
||||
import { HistoryKey } from './HistoryKey'
|
||||
import { ThemeMode } from './ThemeMode'
|
||||
|
@ -27,8 +27,9 @@ export {
|
|||
DashboardProperty,
|
||||
DashboardPropertyType,
|
||||
FeedState,
|
||||
getFileIcon,
|
||||
FileType,
|
||||
typesMap,
|
||||
extMap,
|
||||
FilterType,
|
||||
HistoryKey,
|
||||
ThemeMode,
|
||||
|
|
|
@ -3,7 +3,7 @@ import comparators, { Comparator, isObjectEqual } from './comparators'
|
|||
import { formatDataValue, formatDataUnit, formatData } from './data'
|
||||
import { QBIT_MAX_ETA, INFINITY_SYMBOL, formatEta, formatTimeMs, formatTimeSec, formatDuration } from './datetime'
|
||||
import { toPrecision, formatPercent } from './number'
|
||||
import { basename } from './path'
|
||||
import { basename, splitExt, getFileIcon, getExtType } from './path'
|
||||
import { formatSpeedValue, formatSpeedUnit, formatSpeed } from './speed'
|
||||
import { isWindows, isMac, doesCommand, openLink, downloadFile } from './system'
|
||||
import { titleCase, capitalize, extractHostname, getDomainBody, splitByUrl, containsUrl, isValidUri, codeToFlag } from './text'
|
||||
|
@ -27,6 +27,9 @@ export {
|
|||
toPrecision,
|
||||
formatPercent,
|
||||
basename,
|
||||
splitExt,
|
||||
getFileIcon,
|
||||
getExtType,
|
||||
formatSpeedValue,
|
||||
formatSpeedUnit,
|
||||
formatSpeed,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { basename, splitExt } from './path'
|
||||
import { FileType } from '@/constants/vuetorrent'
|
||||
import { basename, splitExt, getExtType, getFileIcon } from './path'
|
||||
|
||||
describe('helpers/path/basename', () => {
|
||||
test('*NIX :: should return basename on files', () => {
|
||||
|
@ -73,3 +74,47 @@ describe('helpers/path/splitExt', () => {
|
|||
expect(splitExt('.txt')).toEqual(['.txt', ''])
|
||||
})
|
||||
})
|
||||
|
||||
describe('helpers/path/getExtType', () => {
|
||||
it('should return the correct type for a known extension', () => {
|
||||
expect(getExtType('txt')).toEqual(FileType.DOCUMENT)
|
||||
})
|
||||
|
||||
it('should return unknown for an unknown extension', () => {
|
||||
expect(getExtType('unknown')).toBe(FileType.UNKNOWN)
|
||||
})
|
||||
|
||||
it('should return unknown for files without extension', () => {
|
||||
expect(getExtType('file')).toBe(FileType.UNKNOWN)
|
||||
})
|
||||
|
||||
it('should handle files starting with a dot', () => {
|
||||
expect(getExtType('.hiddenfile')).toBe(FileType.UNKNOWN)
|
||||
})
|
||||
})
|
||||
|
||||
describe('helpers/path/getFileIcon', () => {
|
||||
it('should return the correct icon for a known file type', () => {
|
||||
expect(getFileIcon('file.txt')).toEqual('mdi-file-document')
|
||||
})
|
||||
|
||||
it('should return the default icon for an unknown file type', () => {
|
||||
expect(getFileIcon('file.unknown')).toEqual('mdi-file')
|
||||
})
|
||||
|
||||
it('should handle filenames with multiple dots correctly', () => {
|
||||
expect(getFileIcon('my.file.name.txt')).toEqual('mdi-file-document')
|
||||
})
|
||||
|
||||
it('should return the default icon for files without extension', () => {
|
||||
expect(getFileIcon('file')).toEqual('mdi-file')
|
||||
})
|
||||
|
||||
it('should handle files starting with a dot', () => {
|
||||
expect(getFileIcon('.hiddenfile')).toEqual('mdi-file')
|
||||
})
|
||||
|
||||
it('should treat files with only extension as hidden', () => {
|
||||
expect(getFileIcon('.txt')).toEqual('mdi-file')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { extMap, FileType, typesMap } from '@/constants/vuetorrent'
|
||||
|
||||
export function basename(path: string | null | undefined) {
|
||||
if (!path) return ''
|
||||
|
||||
|
@ -17,3 +19,11 @@ export function splitExt(path: string | null | undefined): [string, string] {
|
|||
const ext = groups.pop()!
|
||||
return [groups.join('.'), ext]
|
||||
}
|
||||
|
||||
export function getExtType(ext: string) {
|
||||
return extMap[ext] || FileType.UNKNOWN
|
||||
}
|
||||
|
||||
export function getFileIcon(filename: string) {
|
||||
return typesMap[getExtType(splitExt(filename)[1])]
|
||||
}
|
||||
|
|
|
@ -80,6 +80,19 @@
|
|||
"normal": "Normal",
|
||||
"unwanted": "Unwanted"
|
||||
},
|
||||
"file_type": {
|
||||
"archive": "Archives",
|
||||
"audio": "Audio",
|
||||
"book": "Books",
|
||||
"document": "Documents",
|
||||
"executable": "Executables",
|
||||
"image": "Images",
|
||||
"information": "Information",
|
||||
"script": "Scripts",
|
||||
"subtitle": "Subtitles",
|
||||
"unknown": "Unknown",
|
||||
"video": "Video"
|
||||
},
|
||||
"filter_type": {
|
||||
"conjunctive": "Conjunctive filtering (AND)",
|
||||
"disjunctive": "Disjunctive filtering (OR)"
|
||||
|
|
Loading…
Add table
Reference in a new issue