Add RSS rule dialog

This commit is contained in:
CzBiX
2020-03-30 12:57:52 +08:00
parent 428328c29b
commit 72d38f9fd5
7 changed files with 466 additions and 31 deletions

View File

@@ -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('|'),

View File

@@ -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();
}

View File

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

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

View File

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

View File

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

View File

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