1
0
Fork 0
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 ()

This commit is contained in:
Rémi Marseault 2025-03-20 11:28:21 +01:00 committed by GitHub
parent 18eb8975ea
commit ddbafbef68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 243 additions and 79 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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')
})
})

View 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])]
}

View file

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