mirror of
https://github.com/CzBiX/qb-web.git
synced 2026-04-14 02:10:31 +08:00
Add RSS rule dialog
This commit is contained in:
26
src/Api.ts
26
src/Api.ts
@@ -1,6 +1,5 @@
|
||||
import Axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';
|
||||
import { RssNode } from '@/types';
|
||||
|
||||
import { RssNode, RssRule } from '@/types';
|
||||
|
||||
class Api {
|
||||
private axios: AxiosInstance;
|
||||
@@ -213,6 +212,29 @@ class Api {
|
||||
return this.axios.post('/rss/removeItem', data).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
public getRssRules(): Promise<{[key: string]: RssRule}> {
|
||||
return this.axios.get('/rss/rules').then(Api.handleResponse);
|
||||
}
|
||||
|
||||
public setRssRule(name: string, def: any = {}) {
|
||||
const params: any = {
|
||||
ruleName: name,
|
||||
ruleDef: JSON.stringify(def),
|
||||
}
|
||||
|
||||
const data = new URLSearchParams(params)
|
||||
return this.axios.post('/rss/setRule', data).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
public removeRssRule(name: string) {
|
||||
const params: any = {
|
||||
ruleName: name,
|
||||
}
|
||||
|
||||
const data = new URLSearchParams(params)
|
||||
return this.axios.post('/rss/removeRule', data).then(Api.handleResponse);
|
||||
}
|
||||
|
||||
private actionTorrents(action: string, hashes: string[], extra?: any) {
|
||||
const params: any = {
|
||||
hashes: hashes.join('|'),
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-if="!!config"
|
||||
v-bind="config"
|
||||
:value="true"
|
||||
:value="config"
|
||||
@input="changed"
|
||||
>
|
||||
{{ config.text }}
|
||||
<v-btn
|
||||
text
|
||||
color="info"
|
||||
@click="clickBtn"
|
||||
>
|
||||
{{ config.btnText ? config.btnText : $t('close') }}
|
||||
</v-btn>
|
||||
<template v-if="config">
|
||||
{{ config.text }}
|
||||
<v-btn
|
||||
v-if="config.callback"
|
||||
text
|
||||
color="info"
|
||||
@click="clickBtn"
|
||||
>
|
||||
{{ config.btnText ? config.btnText : $t('close') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
@@ -32,7 +34,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
await timeout(150);
|
||||
mutations.closeSnackBar();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
@input="$emit('input', $event)"
|
||||
fullscreen
|
||||
persistent
|
||||
hide-overlay
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title
|
||||
@@ -13,7 +12,9 @@
|
||||
<v-icon class="mr-2">mdi-rss-box</v-icon>
|
||||
<span>RSS</span>
|
||||
<v-spacer />
|
||||
<v-btn text @click="closeDialog">{{ $t('close') }}</v-btn>
|
||||
<v-btn icon @click="closeDialog">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="toolbar">
|
||||
@@ -30,12 +31,16 @@
|
||||
:label="$t('dialog.rss.auto_refresh')"
|
||||
hide-details
|
||||
/>
|
||||
<v-divider vertical />
|
||||
<v-switch
|
||||
:input-value="preferences.rss_auto_downloading_enabled"
|
||||
@change="changePreference('rss_auto_downloading_enabled', $event)"
|
||||
:label="$t('dialog.rss.auto_download')"
|
||||
hide-details
|
||||
/>
|
||||
<v-btn icon @click="showRulesDialog = true" :title="$t('settings')">
|
||||
<v-icon>mdi-settings</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-divider />
|
||||
<div class="content">
|
||||
@@ -87,10 +92,11 @@
|
||||
v-if="selectItem"
|
||||
dense
|
||||
>
|
||||
<v-list-item-group v-model="selectArticleIndex" color="primary" >
|
||||
<v-list-item-group v-model="selectArticle" color="primary" >
|
||||
<v-list-item
|
||||
v-for="article in selectItem.articles"
|
||||
:key="article.id"
|
||||
:value="article"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
@@ -125,6 +131,8 @@
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<RssRulesDialog v-if="showRulesDialog" :rss-node="rssNode" v-model="showRulesDialog"/>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
@@ -141,8 +149,12 @@ import { tr } from '@/locale'
|
||||
import { RssItem, RssNode, RssTorrent } from '@/types';
|
||||
import { DialogType, DialogConfig, SnackBarConfig } from '@/store/types'
|
||||
import { parseDate, formatTimestamp, formatAsDuration } from '../../filters'
|
||||
import RssRulesDialog from './RssRulesDialog.vue'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
RssRulesDialog,
|
||||
},
|
||||
computed: mapState([
|
||||
'preferences',
|
||||
]),
|
||||
@@ -198,7 +210,8 @@ export default class RssDialog extends HasTask {
|
||||
|
||||
rssNode: RssNode | null = null
|
||||
selectNode: string | null = null
|
||||
selectArticleIndex: number | null = null
|
||||
selectArticle: RssTorrent | null = null
|
||||
showRulesDialog = false
|
||||
|
||||
preferences!: any
|
||||
asyncShowDialog!: (_: DialogConfig) => Promise<string | undefined>
|
||||
@@ -225,13 +238,6 @@ export default class RssDialog extends HasTask {
|
||||
// Folder
|
||||
return null
|
||||
}
|
||||
get selectArticle() {
|
||||
if (!this.selectItem || this.selectArticleIndex == null || this.selectArticleIndex < 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.selectItem!.articles[this.selectArticleIndex]
|
||||
}
|
||||
|
||||
isItemLoading(row: any) {
|
||||
const item = row.item.item
|
||||
@@ -345,15 +351,16 @@ export default class RssDialog extends HasTask {
|
||||
|
||||
@Watch('selectNode')
|
||||
onSelectNodeChanged() {
|
||||
this.selectArticleIndex = null
|
||||
this.selectArticle = null
|
||||
}
|
||||
|
||||
created() {
|
||||
this.setTaskAndRun(this.fetchRssItems)
|
||||
this.setTaskAndRun(this.fetchRssItems, 5000)
|
||||
}
|
||||
|
||||
@Emit('input')
|
||||
closeDialog() {
|
||||
this.$emit('input', false);
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -382,11 +389,12 @@ export default class RssDialog extends HasTask {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
padding: 0 2em;
|
||||
padding: 0 20px;
|
||||
align-items: center;
|
||||
|
||||
.v-input--switch {
|
||||
margin-left: 1em;
|
||||
margin-top: 0;
|
||||
margin: 0 0.5em;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
371
src/components/dialogs/RssRulesDialog.vue
Normal file
371
src/components/dialogs/RssRulesDialog.vue
Normal file
@@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
:value="value"
|
||||
@input="$emit('input', $event)"
|
||||
persistent
|
||||
width="50%"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title
|
||||
class="headline grey lighten-4"
|
||||
>
|
||||
<v-icon class="mr-2">mdi-filter</v-icon>
|
||||
<span v-text="$t('dialog.rss_rule.title')" />
|
||||
<v-spacer />
|
||||
<v-btn icon @click="closeDialog">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="toolbar">
|
||||
<v-btn icon @click="addRssRule" :title="$t('dialog.rss_rule.add_rule')">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon :disabled="!selectedRuleName" @click="deleteRssRule" :title="$t('delete')">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-divider />
|
||||
<div class="content">
|
||||
<div v-if="!rssRules" class="loading">
|
||||
<v-progress-circular indeterminate>
|
||||
</v-progress-circular>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="rss-rules">
|
||||
<v-list
|
||||
dense
|
||||
>
|
||||
<v-list-item-group v-model="selectedRuleName" color="primary" >
|
||||
<v-list-item
|
||||
v-for="(value, key) in rssRules"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
<v-list-item-action>
|
||||
<v-checkbox
|
||||
dense
|
||||
v-model="value.enabled"
|
||||
/>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="key" />
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</div>
|
||||
<v-divider vertical />
|
||||
<div class="rule-details">
|
||||
<v-form class="rule-form">
|
||||
<p class="form-title" v-text="$t('dialog.rss_rule.rule_settings')" />
|
||||
|
||||
<v-checkbox dense :label="$t('dialog.rss_rule.use_regex')"
|
||||
:disabled="!selectedRule.enabled"
|
||||
v-model="selectedRule.useRegex"
|
||||
/>
|
||||
<v-text-field dense :label="$t('dialog.rss_rule.must_contain')"
|
||||
:disabled="!selectedRule.enabled"
|
||||
v-model="selectedRule.mustContain"
|
||||
/>
|
||||
<v-text-field dense :label="$t('dialog.rss_rule.must_not_contain')"
|
||||
:disabled="!selectedRule.enabled"
|
||||
v-model="selectedRule.mustNotContain"
|
||||
/>
|
||||
<v-text-field dense :label="$t('dialog.rss_rule.episode_filter')"
|
||||
:disabled="!selectedRule.enabled"
|
||||
v-model="selectedRule.episodeFilter"
|
||||
/>
|
||||
<v-checkbox dense :label="$t('dialog.rss_rule.smart_episode')"
|
||||
:disabled="!selectedRule.enabled"
|
||||
v-model="selectedRule.smartFilter"
|
||||
/>
|
||||
|
||||
<v-select dense :label="$t('dialog.rss_rule.assign_category')"
|
||||
:items="categoryItems"
|
||||
:disabled="!selectedRule.enabled"
|
||||
v-model="selectedRule.assignedCategory"
|
||||
/>
|
||||
</v-form>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<p class="feeds-title" v-text="$t('dialog.rss_rule.apply_to_feeds')"/>
|
||||
<v-list
|
||||
dense
|
||||
v-if="selectedRule.enabled"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="item in rssItems"
|
||||
:key="item.value"
|
||||
>
|
||||
<v-list-item-action>
|
||||
<v-checkbox
|
||||
dense
|
||||
:input-value="hasSelectSite(item.value)"
|
||||
@change="selectSite(item.value, $event)"
|
||||
/>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.text" />
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { isEmpty, isEqual, pull, cloneDeep } from 'lodash'
|
||||
import Vue from 'vue'
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import { tr } from '@/locale'
|
||||
import { Prop, Emit, Watch } from 'vue-property-decorator';
|
||||
import { RssRule, Category, RssNode, RssItem } from '../../types';
|
||||
import api from '../../Api';
|
||||
import { mapActions, mapMutations, mapGetters } from 'vuex';
|
||||
import { DialogConfig, DialogType, SnackBarConfig } from '../../store/types';
|
||||
|
||||
@Component({
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'allCategories',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'showSnackBar',
|
||||
'closeSnackBar',
|
||||
]),
|
||||
...mapActions([
|
||||
'asyncShowDialog',
|
||||
]),
|
||||
},
|
||||
})
|
||||
export default class RssRulesDialog extends Vue {
|
||||
@Prop(Boolean)
|
||||
readonly value!: boolean
|
||||
@Prop()
|
||||
readonly rssNode!: RssNode
|
||||
|
||||
rssRules: {[key: string]: RssRule} | null = null
|
||||
selectedRuleName: string | null = null
|
||||
|
||||
allCategories!: Category[]
|
||||
|
||||
asyncShowDialog!: (_: DialogConfig) => Promise<string | undefined>
|
||||
showSnackBar!: (_: SnackBarConfig) => void
|
||||
closeSnackBar!: () => void
|
||||
|
||||
get selectedRule(): RssRule {
|
||||
if (!this.selectedRuleName || !(this.selectedRuleName in this.rssRules!)) {
|
||||
return {} as RssRule
|
||||
}
|
||||
|
||||
return this.rssRules![this.selectedRuleName]
|
||||
}
|
||||
get categoryItems() {
|
||||
const uncategory: Category = {
|
||||
key: '',
|
||||
name: tr('uncategorized'),
|
||||
}
|
||||
|
||||
return [uncategory, ...this.allCategories].map(c => {
|
||||
return {
|
||||
text: c.name,
|
||||
value: c.key,
|
||||
}
|
||||
})
|
||||
}
|
||||
get rssItems() {
|
||||
return this.buildRssItems(this.rssNode)
|
||||
}
|
||||
|
||||
hasSelectSite(url: string) {
|
||||
return this.selectedRule.affectedFeeds.includes(url)
|
||||
}
|
||||
|
||||
selectSite(url: string, enabled: boolean) {
|
||||
const rule = cloneDeep(this.selectedRule)
|
||||
const feeds = rule.affectedFeeds
|
||||
|
||||
if (enabled) {
|
||||
feeds.push(url)
|
||||
} else {
|
||||
pull(feeds, url)
|
||||
}
|
||||
|
||||
this.rssRules![this.selectedRuleName!] = rule
|
||||
}
|
||||
|
||||
buildRssItems(node: RssNode) {
|
||||
let result: any[] = []
|
||||
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
if ('uid' in value) {
|
||||
result.push({
|
||||
text: key,
|
||||
value: value.url,
|
||||
})
|
||||
} else {
|
||||
result = result.concat(this.buildRssItems(value))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async fetchRssRules() {
|
||||
this.rssRules = await api.getRssRules()
|
||||
}
|
||||
|
||||
async addRssRule() {
|
||||
const name = await this.asyncShowDialog({
|
||||
content: {
|
||||
text: tr('dialog.rss_rule.new_rule_name'),
|
||||
type: DialogType.Input,
|
||||
}
|
||||
})
|
||||
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
|
||||
this.showSnackBar({
|
||||
text: tr('label.adding'),
|
||||
})
|
||||
|
||||
await api.setRssRule(name);
|
||||
this.fetchRssRules()
|
||||
|
||||
this.closeSnackBar();
|
||||
}
|
||||
|
||||
async deleteRssRule() {
|
||||
const input = await this.asyncShowDialog({
|
||||
content: {
|
||||
text: tr('dialog.rss_rule.delete_rule'),
|
||||
type: DialogType.OkCancel,
|
||||
}
|
||||
})
|
||||
|
||||
if (!input) {
|
||||
return
|
||||
}
|
||||
|
||||
this.showSnackBar({
|
||||
text: tr('label.deleting'),
|
||||
})
|
||||
|
||||
await api.removeRssRule(this.selectedRuleName!);
|
||||
this.fetchRssRules()
|
||||
|
||||
this.closeSnackBar();
|
||||
}
|
||||
|
||||
@Emit('input')
|
||||
closeDialog() {
|
||||
return false
|
||||
}
|
||||
|
||||
created() {
|
||||
this.fetchRssRules()
|
||||
}
|
||||
|
||||
@Watch('selectedRule', {deep: true})
|
||||
async onSelectedRuleChanged(v: RssRule, old: RssRule) {
|
||||
if (isEmpty(old) || isEmpty(v)) {
|
||||
// just select rule
|
||||
return
|
||||
}
|
||||
|
||||
if (isEqual(v, old)) {
|
||||
return
|
||||
}
|
||||
|
||||
await api.setRssRule(this.selectedRuleName!, v)
|
||||
await this.fetchRssRules()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.v-card__text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
padding: 0 18px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 75vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.rss-rules {
|
||||
flex: 30%;
|
||||
|
||||
.v-list-item__action {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-details {
|
||||
flex: 70%;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
.rule-form {
|
||||
margin: 0.5em;
|
||||
|
||||
.v-divider {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.v-input--selection-controls {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.feeds-title {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.v-list-item {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.v-list-item__action {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -101,6 +101,22 @@ export default {
|
||||
delete_feeds: 'Are you sure to delete selected feeds?',
|
||||
date_format: '%{date} (%{duration} ago)',
|
||||
},
|
||||
rss_rule: {
|
||||
add_rule: 'Add Rule',
|
||||
new_rule_name: 'The name of the new rule',
|
||||
delete_rule: 'Are you sure to delete selected rule?',
|
||||
title: 'RSS Downloader',
|
||||
rule_settings: 'Rule Settings',
|
||||
|
||||
use_regex: 'Use Regex',
|
||||
must_contain: 'Must Contain',
|
||||
must_not_contain: 'Must Not Contain',
|
||||
episode_filter: 'Episode Filter',
|
||||
smart_episode: 'Use Smart Episode Filter',
|
||||
assign_category: 'Assign Category',
|
||||
|
||||
apply_to_feeds: 'Apply Rule to Feeds',
|
||||
},
|
||||
},
|
||||
|
||||
state: {
|
||||
|
||||
@@ -101,6 +101,22 @@ export default {
|
||||
delete_feeds: '确定要删除选中的订阅吗?',
|
||||
date_format: '%{date}(%{duration} 之前)',
|
||||
},
|
||||
rss_rule: {
|
||||
add_rule: '添加规则',
|
||||
new_rule_name: '新规则的名称',
|
||||
delete_rule: '确定要删除选中的规则吗?',
|
||||
title: 'RSS 自动下载',
|
||||
rule_settings: '规则设置',
|
||||
|
||||
use_regex: '使用正则',
|
||||
must_contain: '必须包含',
|
||||
must_not_contain: '必须排除',
|
||||
episode_filter: '剧集过滤',
|
||||
smart_episode: '使用智能剧集过滤',
|
||||
assign_category: '分配分类',
|
||||
|
||||
apply_to_feeds: '应用到订阅',
|
||||
},
|
||||
},
|
||||
|
||||
state: {
|
||||
|
||||
@@ -121,9 +121,10 @@ export interface RssRule {
|
||||
smartFilter: boolean,
|
||||
previouslyMatchedEpisodes: string[],
|
||||
affectedFeeds: string[],
|
||||
createSubfolder: boolean | null,
|
||||
ignoreDays: number,
|
||||
lastMatch: string,
|
||||
addPaused: boolean,
|
||||
addPaused: boolean | null,
|
||||
assignedCategory: string,
|
||||
savepath: string,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user