<template> <v-dialog v-model="dialog" max-width="1000px"> <v-card flat :loading="loading"> <v-container class="pa-0 project done"> <v-card-title class="justify-center"> <v-toolbar flat dense class="transparent"> <v-toolbar-title class="mx-auto"> <h2>{{ this.lastSavedName !== '' ? $t('modals.newRule.titleEdit') : $t('modals.newRule.titleCreate') }}</h2> </v-toolbar-title> <v-btn fab small class="transparent elevation-0" @click="close"> <v-icon>{{ mdiClose }}</v-icon> </v-btn> </v-toolbar> </v-card-title> <v-card-text class="pb-0"> <v-row> <v-col cols="12" sm="6"> <v-form ref="form"> <v-container> <v-text-field v-model="rule.name" :label="$t('modals.newRule.name')" required /> <v-divider /> <v-checkbox hide-details v-model="rule.useRegex" :label="$t('modals.newRule.def.useRegex')" /> <v-text-field hide-details v-model="rule.mustContain" :label="$t('modals.newRule.def.mustContain')" /> <v-text-field hide-details v-model="rule.mustNotContain" :label="$t('modals.newRule.def.mustNotContain')" /> <v-checkbox hide-details v-model="rule.smartFilter" :label="$t('modals.newRule.def.smartFilter')" /> <v-text-field v-model="rule.episodeFilter" :label="$t('modals.newRule.def.episodeFilter')" /> <v-divider /> <v-row> <p class="subtitle-1 mb-1">{{ $t('modals.newRule.def.assignedCategory') }}</p> <v-select flat solo dense hide-details background-color="background" class="rounded-xl" v-model="rule.assignedCategory" :items="availableCategories" /> </v-row> <v-text-field hide-details v-model="rule.savePath" :label="$t('modals.newRule.def.savePath')" /> <v-text-field hide-details v-model="rule.ignoreDays" :label="$t('modals.newRule.def.ignoreDays')" required type="number" min="0" /> <v-text-field disabled :value="lastMatch" :label="$t('modals.newRule.def.lastMatch.title')" /> <v-divider /> <v-row class="my-2 flex-gap"> <v-col> <div class="d-flex flex-column align-center"> <p class="subtitle-1 mb-1">{{ $t('modals.newRule.def.addPaused.title') }}</p> <v-select v-model="rule.addPaused" flat solo dense hide-details background-color="background" class="rounded-xl" :items="addPausedOptions" /> </div> </v-col> <v-col> <div class="d-flex flex-column align-center"> <p class="subtitle-1 mb-1">{{ $t('enums.contentLayout.title') }}</p> <v-select v-model="rule.torrentContentLayout" flat solo dense hide-details background-color="background" class="rounded-xl" :items="contentLayoutOptions" /> </div> </v-col> </v-row> <v-subheader class="pa-0"> {{ $t('modals.newRule.def.affectedFeeds') }} </v-subheader> <v-row> <v-col cols="6" class="d-flex align-center justify-center"> <v-btn color="accent" @click="selectAll">{{ $t('selectAll') }}</v-btn> </v-col> <v-col cols="6" class="d-flex align-center justify-center"> <v-btn color="primary" @click="selectNone">{{ $t('selectNone') }}</v-btn> </v-col> </v-row> <v-checkbox v-for="(item, index) in availableFeeds" :key="index" v-model="rule.affectedFeeds" hide-details :label="item.name" :value="item.url" /> </v-container> </v-form> </v-col> <v-col v-if="$vuetify.breakpoint.smAndUp" cols="6"> <h2>{{ $t('modals.matchingArticles.title') }}</h2> <v-list subheader> <template v-for="item in matchingArticles"> <v-divider :key="item.value" v-if="item.type === 'divider'" /> <v-subheader inset v-else-if="item.type === 'subheader'">{{ item.value }}</v-subheader> <v-list-item v-else class="mb-3">{{ item.value }}</v-list-item> </template> </v-list> </v-col> </v-row> <v-btn fab color="accent" fixed right bottom @click="setRule"> <v-icon>{{ mdiContentSave }}</v-icon> </v-btn> </v-card-text> <v-divider /> <v-card-actions class="justify-end"> <v-btn class="accent white--text elevation-0 px-4" @click="setRule"> {{ $t('save') }} </v-btn> <v-btn color="primary" class="white--text elevation-0 px-4" @click="close"> {{ $t('close') }} </v-btn> </v-card-actions> </v-container> </v-card> </v-dialog> </template> <script lang="ts"> import { defineComponent } from 'vue' import { mapGetters } from 'vuex' import qbit from '@/services/qbit' import { Modal } from '@/mixins' import { mdiClose, mdiContentSave } from '@mdi/js' import i18n from '@/plugins/i18n' import { AppPreferences } from '@/enums/qbit' import {Category, Feed} from '@/types/vuetorrent' type FormattedArticle = { type: string; value?: string } export default defineComponent({ name: 'RuleForm', mixins: [Modal], props: ['initialRule'], data: () => ({ rule: { addPaused: null, affectedFeeds: [], assignedCategory: '', enabled: true, episodeFilter: '', ignoreDays: 0, lastMatch: '', mustContain: '', mustNotContain: '', name: '', savePath: '', smartFilter: false, torrentContentLayout: null, useRegex: false }, addPausedOptions: [ { text: i18n.t('useGlobalSettings'), value: null }, { text: i18n.t('modals.newRule.def.addPaused.always'), value: true }, { text: i18n.t('modals.newRule.def.addPaused.never'), value: false } ], contentLayoutOptions: [ { text: i18n.t('useGlobalSettings'), value: null }, { text: i18n.t('enums.contentLayout.original'), value: AppPreferences.ContentLayout.ORIGINAL }, { text: i18n.t('enums.contentLayout.subfolder'), value: AppPreferences.ContentLayout.SUBFOLDER }, { text: i18n.t('enums.contentLayout.nosubfolder'), value: AppPreferences.ContentLayout.NO_SUBFOLDER } ], loading: false, matchingArticles: [] as FormattedArticle[], lastSavedName: '', mdiClose, mdiContentSave }), computed: { ...mapGetters(['getFeeds', 'getCategories']), lastMatch() { if (this.rule.lastMatch === '') return i18n.t('modals.newRule.def.lastMatch.unknownValue').toString() const delta = new Date().getTime() - new Date(this.rule.lastMatch).getTime() return i18n .t('modals.newRule.def.lastMatch.knownValue') .toString() .replace('%1', Math.floor(delta / (1000 * 60 * 60 * 24)).toString()) }, availableFeeds() { // @ts-expect-error: TS2349: This expression is not callable. Type 'never' has no call signatures. return this.getFeeds() }, availableCategories() { // @ts-expect-error: TS2349: This expression is not callable. Type 'never' has no call signatures. return this.getCategories().map((c: Category) => c.name) }, hasInitialRule() { return !!(this.initialRule && this.initialRule.name) } }, created() { this.$store.commit('FETCH_RULES') if (this.hasInitialRule) { this.rule = { ...this.initialRule } this.lastSavedName = this.initialRule.name } }, mounted() { document.addEventListener('keydown', this.handleKeyboardShortcut) this.updateArticles() }, beforeDestroy() { document.removeEventListener('keydown', this.handleKeyboardShortcut) }, methods: { async setRule() { if ((this.hasInitialRule || this.lastSavedName !== '') && this.lastSavedName !== this.rule.name) { await qbit.renameRule(this.lastSavedName, this.rule.name) } await qbit.setRule(this.rule) this.lastSavedName = this.rule.name this.$store.commit('FETCH_RULES') await this.updateArticles() }, async updateArticles() { if (this.lastSavedName === '') return this.loading = true const formattedArticles = [] const articles = await qbit.getMatchingArticles(this.lastSavedName) for (const feedName in articles) { const feedArticles = articles[feedName] if (formattedArticles.length > 0) formattedArticles.push({ type: 'divider' }) formattedArticles.push({ type: 'subheader', value: feedName }) for (const i in feedArticles) { const article = feedArticles[i] formattedArticles.push({ type: 'item', value: article }) } } this.matchingArticles = formattedArticles this.loading = false }, selectNone() { this.rule.affectedFeeds = [] }, selectAll() { this.rule.affectedFeeds = this.availableFeeds.map((feed: Feed) => feed.url) }, close() { this.dialog = false }, handleKeyboardShortcut(e: KeyboardEvent) { if (e.key === 'Escape') { this.close() } else if (e.key === 'Enter') { this.setRule() } } } }) </script>