feeds-fun 1.14.2 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/DiscoveryForm.vue +36 -17
- package/src/components/EntriesList.vue +5 -10
- package/src/components/EntryForList.vue +20 -39
- package/src/components/FfunTag.vue +55 -30
- package/src/components/OPMLUpload.vue +7 -0
- package/src/components/RuleConstructor.vue +56 -27
- package/src/components/RuleForList.vue +14 -3
- package/src/components/TagsFilter.vue +68 -76
- package/src/components/TagsList.vue +14 -66
- package/src/components/notifications/Block.vue +7 -2
- package/src/components/notifications/CreateRuleHelp.vue +16 -9
- package/src/inputs/ScoreSelector.vue +10 -3
- package/src/layouts/SidePanelLayout.vue +2 -2
- package/src/logic/api.ts +33 -5
- package/src/logic/asserts.ts +5 -0
- package/src/logic/tagsFilterState.ts +63 -11
- package/src/logic/types.ts +34 -7
- package/src/stores/entries.ts +27 -1
- package/src/stores/globalSettings.ts +0 -2
- package/src/values/Score.vue +11 -2
- package/src/views/DiscoveryView.vue +16 -6
- package/src/views/FeedsView.vue +9 -0
- package/src/views/NewsView.vue +51 -51
- package/src/views/RulesView.vue +34 -14
|
@@ -7,26 +7,21 @@
|
|
|
7
7
|
v-for="tag of displayedSelectedTags"
|
|
8
8
|
:key="tag"
|
|
9
9
|
class="whitespace-nowrap line-clamp-1">
|
|
10
|
-
<a
|
|
11
|
-
href="#"
|
|
12
|
-
@click.prevent="deselect(tag)"
|
|
13
|
-
>[X]</a
|
|
14
|
-
>
|
|
15
10
|
<ffun-tag
|
|
16
11
|
class="ml-1"
|
|
17
12
|
:uid="tag"
|
|
18
13
|
:count="tags[tag] ?? 0"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@tag:clicked="onTagClicked">
|
|
22
|
-
</ffun-tag>
|
|
14
|
+
:showSwitch="true"
|
|
15
|
+
count-mode="no" />
|
|
23
16
|
</li>
|
|
24
17
|
</ul>
|
|
25
18
|
|
|
19
|
+
<rule-constructor v-if="showCreateRule" />
|
|
20
|
+
|
|
26
21
|
<input
|
|
27
22
|
class="ffun-input w-full"
|
|
28
23
|
type="text"
|
|
29
|
-
placeholder="Input part of tag
|
|
24
|
+
placeholder="Input part of a tag…"
|
|
30
25
|
v-model="tagNameFilter" />
|
|
31
26
|
|
|
32
27
|
<ul
|
|
@@ -39,9 +34,7 @@
|
|
|
39
34
|
<ffun-tag
|
|
40
35
|
:uid="tag"
|
|
41
36
|
:count="tags[tag]"
|
|
42
|
-
count-mode="prefix"
|
|
43
|
-
:mode="null"
|
|
44
|
-
@tag:clicked="onTagClicked" />
|
|
37
|
+
count-mode="prefix" />
|
|
45
38
|
</li>
|
|
46
39
|
</ul>
|
|
47
40
|
|
|
@@ -57,18 +50,24 @@
|
|
|
57
50
|
</template>
|
|
58
51
|
|
|
59
52
|
<script lang="ts" setup>
|
|
60
|
-
import {computed, ref} from "vue";
|
|
53
|
+
import {computed, ref, inject} from "vue";
|
|
54
|
+
import type {Ref} from "vue";
|
|
61
55
|
import {useTagsStore} from "@/stores/tags";
|
|
62
56
|
import type * as tagsFilterState from "@/logic/tagsFilterState";
|
|
57
|
+
import * as asserts from "@/logic/asserts";
|
|
58
|
+
import * as api from "@/logic/api";
|
|
59
|
+
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
60
|
+
|
|
63
61
|
const tagsStore = useTagsStore();
|
|
64
62
|
|
|
65
|
-
const
|
|
63
|
+
const globalSettings = useGlobalSettingsStore();
|
|
66
64
|
|
|
67
|
-
const
|
|
65
|
+
const currentScore = ref(1);
|
|
68
66
|
|
|
69
|
-
const
|
|
67
|
+
const tagsStates = inject<Ref<tagsFilterState.Storage>>("tagsStates");
|
|
68
|
+
asserts.defined(tagsStates);
|
|
70
69
|
|
|
71
|
-
const properties = defineProps<{tags: {[key: string]: number}}>();
|
|
70
|
+
const properties = defineProps<{tags: {[key: string]: number}; showCreateRule?: boolean}>();
|
|
72
71
|
|
|
73
72
|
const showFromStart = ref(25);
|
|
74
73
|
|
|
@@ -76,37 +75,40 @@
|
|
|
76
75
|
|
|
77
76
|
const tagNameFilter = ref("");
|
|
78
77
|
|
|
79
|
-
function
|
|
80
|
-
const
|
|
81
|
-
const bCount = properties.tags[b];
|
|
78
|
+
function createTagComparator() {
|
|
79
|
+
const counts = properties.tags;
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
function tagComparator(a: string, b: string) {
|
|
82
|
+
const aCount = counts[a];
|
|
83
|
+
const bCount = counts[b];
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
if (aCount > bCount) {
|
|
86
|
+
return -1;
|
|
87
|
+
}
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
if (aCount < bCount) {
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
if (a > b) {
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (a < b) {
|
|
98
|
+
return -1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return 0;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
return
|
|
104
|
+
return tagComparator;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
const displayedSelectedTags = computed(() => {
|
|
103
|
-
let values = Object.keys(
|
|
108
|
+
let values = Object.keys(tagsStates.value.selectedTags);
|
|
104
109
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
values.sort(tagComparator);
|
|
110
|
+
const comparator = createTagComparator();
|
|
111
|
+
values.sort(comparator);
|
|
110
112
|
|
|
111
113
|
return values;
|
|
112
114
|
});
|
|
@@ -114,22 +116,38 @@
|
|
|
114
116
|
const totalTags = computed(() => {
|
|
115
117
|
// TODO: this is not correct, because selected tags are treated differently
|
|
116
118
|
// depending on their status: required or excluded.
|
|
119
|
+
// I.e. by excluding tag `x` you exclude concreate entries => you can exclude more tags,
|
|
120
|
+
// if they only belong to the excluded entries)
|
|
117
121
|
// => value is not accurate, but it is ok for now
|
|
118
|
-
return Object.keys(properties.tags).length + Object.keys(
|
|
122
|
+
return Object.keys(properties.tags).length + Object.keys(tagsStates.value.selectedTags).length;
|
|
119
123
|
});
|
|
120
124
|
|
|
121
|
-
const
|
|
125
|
+
const _sortedTags = computed(() => {
|
|
122
126
|
let values = Object.keys(properties.tags);
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
return [];
|
|
126
|
-
}
|
|
128
|
+
const comparator = createTagComparator();
|
|
127
129
|
|
|
128
|
-
values
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
values.sort(comparator);
|
|
131
|
+
|
|
132
|
+
return values;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const _visibleTags = computed(() => {
|
|
136
|
+
let values = _sortedTags.value.slice();
|
|
137
|
+
|
|
138
|
+
const textFilter = tagNameFilter.value.trim().toLowerCase();
|
|
139
|
+
|
|
140
|
+
const selectedTags = Object.keys(tagsStates.value.selectedTags);
|
|
131
141
|
|
|
132
142
|
values = values.filter((tag) => {
|
|
143
|
+
if (selectedTags.includes(tag)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!textFilter) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
133
151
|
const tagInfo = tagsStore.tags[tag];
|
|
134
152
|
|
|
135
153
|
if (tagInfo === undefined || tagInfo.name === null) {
|
|
@@ -139,36 +157,10 @@
|
|
|
139
157
|
return tagInfo.name.includes(tagNameFilter.value);
|
|
140
158
|
});
|
|
141
159
|
|
|
142
|
-
values.sort(tagComparator);
|
|
143
|
-
|
|
144
|
-
values = values.slice(0, showEntries.value);
|
|
145
|
-
|
|
146
160
|
return values;
|
|
147
161
|
});
|
|
148
162
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (state === "none") {
|
|
153
|
-
tagStates.value[tag] = "required";
|
|
154
|
-
selectedTags.value[tag] = true;
|
|
155
|
-
} else if (state === "required") {
|
|
156
|
-
tagStates.value[tag] = "excluded";
|
|
157
|
-
selectedTags.value[tag] = true;
|
|
158
|
-
} else if (state === "excluded") {
|
|
159
|
-
tagStates.value[tag] = "required";
|
|
160
|
-
selectedTags.value[tag] = true;
|
|
161
|
-
} else {
|
|
162
|
-
throw new Error(`Unknown tag state: ${state}`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
emit("tag:stateChanged", {tag: tag, state: tagStates.value[tag]});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function deselect(tag: string) {
|
|
169
|
-
selectedTags.value[tag] = false;
|
|
170
|
-
tagStates.value[tag] = "none";
|
|
171
|
-
|
|
172
|
-
emit("tag:stateChanged", {tag: tag, state: tagStates.value[tag]});
|
|
173
|
-
}
|
|
163
|
+
const displayedTags = computed(() => {
|
|
164
|
+
return _visibleTags.value.slice(0, showEntries.value);
|
|
165
|
+
});
|
|
174
166
|
</script>
|
|
@@ -1,72 +1,53 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
<div class="text-
|
|
3
|
+
<div class="text-base">
|
|
4
4
|
<ffun-tag
|
|
5
5
|
v-for="tag of displayedTags"
|
|
6
6
|
:key="tag"
|
|
7
7
|
:uid="tag"
|
|
8
|
-
:mode="tagMode(tag)"
|
|
9
8
|
:count="tagsCount[tag]"
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
:secondary-mode="tagMode(tag)"
|
|
10
|
+
count-mode="tooltip" />
|
|
12
11
|
|
|
13
12
|
<a
|
|
14
|
-
class="
|
|
13
|
+
class=""
|
|
14
|
+
title="Click on the news title to open it and see all tags"
|
|
15
15
|
href="#"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
>{{ tagsNumber - showLimit }} more</a
|
|
19
|
-
>
|
|
20
|
-
|
|
21
|
-
<a
|
|
22
|
-
class="ffun-normal-link"
|
|
23
|
-
href="#"
|
|
24
|
-
v-if="canHide"
|
|
25
|
-
@click.prevent="showAll = false"
|
|
26
|
-
>hide</a
|
|
16
|
+
@click.prevent="emit('request-to-show-all')"
|
|
17
|
+
v-if="!showAll && tagsNumber - showLimit > 0"
|
|
18
|
+
>[{{ tagsNumber - showLimit }} more]</a
|
|
27
19
|
>
|
|
28
20
|
</div>
|
|
29
|
-
|
|
30
|
-
<rule-constructor
|
|
31
|
-
v-if="selectedTagsList.length > 0"
|
|
32
|
-
:tags="selectedTagsList"
|
|
33
|
-
@rule-constructor:created="onRuleCreated" />
|
|
34
21
|
</div>
|
|
35
22
|
</template>
|
|
36
23
|
|
|
37
24
|
<script lang="ts" setup>
|
|
38
25
|
import {computed, ref} from "vue";
|
|
39
26
|
|
|
40
|
-
const
|
|
41
|
-
const showLimit = ref(5);
|
|
42
|
-
|
|
43
|
-
const selectedTags = ref<{[key: string]: boolean}>({});
|
|
27
|
+
const showLimit = 5;
|
|
44
28
|
|
|
45
29
|
const properties = defineProps<{
|
|
46
30
|
tags: string[];
|
|
47
31
|
contributions: {[key: string]: number};
|
|
48
32
|
tagsCount: {[key: string]: number};
|
|
33
|
+
showAll: boolean;
|
|
49
34
|
}>();
|
|
50
35
|
|
|
36
|
+
const emit = defineEmits(["request-to-show-all"]);
|
|
37
|
+
|
|
51
38
|
const tagsNumber = computed(() => {
|
|
52
39
|
return properties.tags.length;
|
|
53
40
|
});
|
|
54
41
|
|
|
55
42
|
const displayedTags = computed(() => {
|
|
56
|
-
if (showAll
|
|
43
|
+
if (properties.showAll) {
|
|
57
44
|
return preparedTags.value;
|
|
58
45
|
}
|
|
59
46
|
|
|
60
|
-
return preparedTags.value.slice(0, showLimit
|
|
47
|
+
return preparedTags.value.slice(0, showLimit);
|
|
61
48
|
});
|
|
62
49
|
|
|
63
50
|
function tagMode(tag: string) {
|
|
64
|
-
if (!!selectedTags.value[tag]) {
|
|
65
|
-
return "selected";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// return null;
|
|
69
|
-
|
|
70
51
|
if (!properties.contributions) {
|
|
71
52
|
return null;
|
|
72
53
|
}
|
|
@@ -129,37 +110,4 @@
|
|
|
129
110
|
|
|
130
111
|
return values;
|
|
131
112
|
});
|
|
132
|
-
|
|
133
|
-
const canShowAll = computed(() => {
|
|
134
|
-
return !showAll.value && showLimit.value < preparedTags.value.length;
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
const canHide = computed(() => {
|
|
138
|
-
return showAll.value && showLimit.value < preparedTags.value.length;
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
function onTagClicked(tag: string) {
|
|
142
|
-
if (!!selectedTags.value[tag]) {
|
|
143
|
-
delete selectedTags.value[tag];
|
|
144
|
-
} else {
|
|
145
|
-
selectedTags.value[tag] = true;
|
|
146
|
-
showAll.value = true;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const selectedTagsList = computed(() => {
|
|
151
|
-
const values = [];
|
|
152
|
-
|
|
153
|
-
for (const tag in selectedTags.value) {
|
|
154
|
-
values.push(tag);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
values.sort();
|
|
158
|
-
|
|
159
|
-
return values;
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
function onRuleCreated() {
|
|
163
|
-
selectedTags.value = {};
|
|
164
|
-
}
|
|
165
113
|
</script>
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
</template>
|
|
7
7
|
|
|
8
8
|
<script lang="ts" setup>
|
|
9
|
-
import {computed, ref, onUnmounted, watch} from "vue";
|
|
9
|
+
import {computed, ref, onUnmounted, watch, inject} from "vue";
|
|
10
|
+
import type {Ref} from "vue";
|
|
10
11
|
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
11
12
|
import {useCollectionsStore} from "@/stores/collections";
|
|
13
|
+
import * as tagsFilterState from "@/logic/tagsFilterState";
|
|
12
14
|
|
|
13
15
|
const properties = defineProps<{
|
|
14
16
|
apiKey: boolean;
|
|
@@ -20,6 +22,8 @@
|
|
|
20
22
|
const collections = useCollectionsStore();
|
|
21
23
|
const globalSettings = useGlobalSettingsStore();
|
|
22
24
|
|
|
25
|
+
const tagsStates = inject<Ref<tagsFilterState.Storage> | null>("tagsStates", null);
|
|
26
|
+
|
|
23
27
|
const showApiKeyMessage = computed(() => {
|
|
24
28
|
return (
|
|
25
29
|
globalSettings.userSettings &&
|
|
@@ -33,7 +37,8 @@
|
|
|
33
37
|
return (
|
|
34
38
|
properties.collectionsNotification_ &&
|
|
35
39
|
globalSettings.userSettings &&
|
|
36
|
-
!globalSettings.userSettings.hide_message_about_adding_collections.value
|
|
40
|
+
!globalSettings.userSettings.hide_message_about_adding_collections.value &&
|
|
41
|
+
!tagsStates?.value.hasSelectedTags
|
|
37
42
|
);
|
|
38
43
|
});
|
|
39
44
|
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="ffun-info-attention">
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
<li>Click any tag
|
|
7
|
-
<li>
|
|
8
|
-
<li>
|
|
9
|
-
<li>
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
<h3>Create your first news score rule</h3>
|
|
4
|
+
<p>Set up a rule to filter your news:</p>
|
|
5
|
+
<ol class="list-decimal list-inside">
|
|
6
|
+
<li><strong>Click</strong> any tag on a news item.</li>
|
|
7
|
+
<li>See how news is filtered by the chosen tag.</li>
|
|
8
|
+
<li>Locate the <strong>Create Rule</strong> panel in the left sidebar.</li>
|
|
9
|
+
<li>
|
|
10
|
+
Assign a numeric score for filtered news:
|
|
11
|
+
<ul>
|
|
12
|
+
<li>Use higher scores (above zero) for important or engaging content.</li>
|
|
13
|
+
<li>Use lower (below zero) scores for uninteresting or irrelevant content.</li>
|
|
14
|
+
</ul>
|
|
15
|
+
</li>
|
|
16
|
+
<li>Click <strong>Create Rule</strong>.</li>
|
|
17
|
+
<li>Watch the news reorganize based on your scores.</li>
|
|
18
|
+
</ol>
|
|
12
19
|
</div>
|
|
13
20
|
</template>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
v-for="score of scores"
|
|
8
8
|
:value="score"
|
|
9
9
|
:selected="modelValue === score">
|
|
10
|
-
{{ score }}
|
|
10
|
+
{{ addSign(score) }}
|
|
11
11
|
</option>
|
|
12
12
|
</select>
|
|
13
13
|
</template>
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
const properties = withDefaults(defineProps<{scores?: number[]; modelValue: number}>(), {
|
|
20
20
|
scores: () => [
|
|
21
|
-
1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,
|
|
22
|
-
-377, -610
|
|
21
|
+
1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, -1, -2, -3, -5, -8, -13, -21, -34, -55, -89, -144, -233
|
|
23
22
|
],
|
|
24
23
|
modelValue: 1
|
|
25
24
|
});
|
|
@@ -32,6 +31,14 @@
|
|
|
32
31
|
const newScore = Number(target.value);
|
|
33
32
|
emit("update:modelValue", newScore);
|
|
34
33
|
}
|
|
34
|
+
|
|
35
|
+
function addSign(score: number) {
|
|
36
|
+
if (score > 0) {
|
|
37
|
+
return `+${score}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return score.toString();
|
|
41
|
+
}
|
|
35
42
|
</script>
|
|
36
43
|
|
|
37
44
|
<style scoped></style>
|
package/src/logic/api.ts
CHANGED
|
@@ -99,10 +99,22 @@ export async function getEntriesByIds({ids}: {ids: t.EntryId[]}) {
|
|
|
99
99
|
return entries;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
export async function createOrUpdateRule({
|
|
102
|
+
export async function createOrUpdateRule({
|
|
103
|
+
requiredTags,
|
|
104
|
+
excludedTags,
|
|
105
|
+
score
|
|
106
|
+
}: {
|
|
107
|
+
requiredTags: string[];
|
|
108
|
+
excludedTags: string[];
|
|
109
|
+
score: number;
|
|
110
|
+
}) {
|
|
103
111
|
const response = await post({
|
|
104
112
|
url: API_CREATE_OR_UPDATE_RULE,
|
|
105
|
-
data: {
|
|
113
|
+
data: {
|
|
114
|
+
requiredTags: requiredTags,
|
|
115
|
+
excludedTags: excludedTags,
|
|
116
|
+
score: score
|
|
117
|
+
}
|
|
106
118
|
});
|
|
107
119
|
return response;
|
|
108
120
|
}
|
|
@@ -112,10 +124,20 @@ export async function deleteRule({id}: {id: t.RuleId}) {
|
|
|
112
124
|
return response;
|
|
113
125
|
}
|
|
114
126
|
|
|
115
|
-
export async function updateRule({
|
|
127
|
+
export async function updateRule({
|
|
128
|
+
id,
|
|
129
|
+
requiredTags,
|
|
130
|
+
excludedTags,
|
|
131
|
+
score
|
|
132
|
+
}: {
|
|
133
|
+
id: t.RuleId;
|
|
134
|
+
requiredTags: string[];
|
|
135
|
+
excludedTags: string[];
|
|
136
|
+
score: number;
|
|
137
|
+
}) {
|
|
116
138
|
const response = await post({
|
|
117
139
|
url: API_UPDATE_RULE,
|
|
118
|
-
data: {id: id,
|
|
140
|
+
data: {id: id, score: score, requiredTags: requiredTags, excludedTags: excludedTags}
|
|
119
141
|
});
|
|
120
142
|
return response;
|
|
121
143
|
}
|
|
@@ -167,13 +189,19 @@ export async function discoverFeeds({url}: {url: string}) {
|
|
|
167
189
|
const response = await post({url: API_DISCOVER_FEEDS, data: {url: url}});
|
|
168
190
|
|
|
169
191
|
const feeds = [];
|
|
192
|
+
const messages = [];
|
|
170
193
|
|
|
171
194
|
for (let rawFeed of response.feeds) {
|
|
172
195
|
const feed = t.feedInfoFromJSON(rawFeed);
|
|
173
196
|
feeds.push(feed);
|
|
174
197
|
}
|
|
175
198
|
|
|
176
|
-
|
|
199
|
+
for (let rawMessage of response.messages) {
|
|
200
|
+
const message = t.apiMessageFromJSON(rawMessage);
|
|
201
|
+
messages.push(message);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {feeds, messages};
|
|
177
205
|
}
|
|
178
206
|
|
|
179
207
|
export async function addFeed({url}: {url: string}) {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {ref, computed, reactive} from "vue";
|
|
2
|
+
import type {ComputedRef} from "vue";
|
|
3
|
+
|
|
1
4
|
export type State = "required" | "excluded" | "none";
|
|
2
5
|
|
|
3
6
|
interface ReturnTagsForEntity {
|
|
@@ -7,48 +10,97 @@ interface ReturnTagsForEntity {
|
|
|
7
10
|
export class Storage {
|
|
8
11
|
requiredTags: {[key: string]: boolean};
|
|
9
12
|
excludedTags: {[key: string]: boolean};
|
|
13
|
+
selectedTags: ComputedRef<{[key: string]: boolean}>;
|
|
14
|
+
hasSelectedTags: ComputedRef<boolean>;
|
|
10
15
|
|
|
11
16
|
constructor() {
|
|
12
|
-
this.requiredTags = {};
|
|
13
|
-
this.excludedTags = {};
|
|
17
|
+
this.requiredTags = reactive({});
|
|
18
|
+
this.excludedTags = reactive({});
|
|
19
|
+
|
|
20
|
+
this.selectedTags = computed(() => {
|
|
21
|
+
return {...this.requiredTags, ...this.excludedTags};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
this.hasSelectedTags = computed(() => {
|
|
25
|
+
return Object.keys(this.selectedTags.value).length > 0;
|
|
26
|
+
});
|
|
14
27
|
}
|
|
15
28
|
|
|
16
29
|
onTagStateChanged({tag, state}: {tag: string; state: State}) {
|
|
17
30
|
if (state === "required") {
|
|
18
31
|
this.requiredTags[tag] = true;
|
|
19
|
-
this.excludedTags[tag]
|
|
32
|
+
if (this.excludedTags[tag]) {
|
|
33
|
+
delete this.excludedTags[tag];
|
|
34
|
+
}
|
|
20
35
|
} else if (state === "excluded") {
|
|
21
36
|
this.excludedTags[tag] = true;
|
|
22
|
-
this.requiredTags[tag]
|
|
37
|
+
if (this.requiredTags[tag]) {
|
|
38
|
+
delete this.requiredTags[tag];
|
|
39
|
+
}
|
|
23
40
|
} else if (state === "none") {
|
|
24
|
-
this.
|
|
25
|
-
|
|
41
|
+
if (this.requiredTags[tag]) {
|
|
42
|
+
delete this.requiredTags[tag];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (this.excludedTags[tag]) {
|
|
46
|
+
delete this.excludedTags[tag];
|
|
47
|
+
}
|
|
26
48
|
} else {
|
|
27
49
|
throw new Error(`Unknown tag state: ${state}`);
|
|
28
50
|
}
|
|
29
51
|
}
|
|
30
52
|
|
|
53
|
+
onTagReversed({tag}: {tag: string}) {
|
|
54
|
+
if (!(tag in this.selectedTags)) {
|
|
55
|
+
this.onTagStateChanged({tag: tag, state: "required"});
|
|
56
|
+
} else if (this.requiredTags[tag]) {
|
|
57
|
+
this.onTagStateChanged({tag: tag, state: "excluded"});
|
|
58
|
+
} else if (this.excludedTags[tag]) {
|
|
59
|
+
this.onTagStateChanged({tag: tag, state: "required"});
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error(`Unknown tag state: ${tag}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onTagClicked({tag}: {tag: string}) {
|
|
66
|
+
if (tag in this.selectedTags) {
|
|
67
|
+
this.onTagStateChanged({tag: tag, state: "none"});
|
|
68
|
+
} else {
|
|
69
|
+
this.onTagStateChanged({tag: tag, state: "required"});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
31
73
|
filterByTags(entities: any[], getTags: ReturnTagsForEntity) {
|
|
32
74
|
let report = entities.slice();
|
|
33
75
|
|
|
76
|
+
const requiredTags = Object.keys(this.requiredTags);
|
|
77
|
+
|
|
34
78
|
report = report.filter((entity) => {
|
|
35
79
|
for (const tag of getTags(entity)) {
|
|
36
80
|
if (this.excludedTags[tag]) {
|
|
37
81
|
return false;
|
|
38
82
|
}
|
|
39
83
|
}
|
|
40
|
-
return true;
|
|
41
|
-
});
|
|
42
84
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (this.requiredTags[tag] && !getTags(entity).includes(tag)) {
|
|
85
|
+
for (const tag of requiredTags) {
|
|
86
|
+
if (!getTags(entity).includes(tag)) {
|
|
46
87
|
return false;
|
|
47
88
|
}
|
|
48
89
|
}
|
|
90
|
+
|
|
49
91
|
return true;
|
|
50
92
|
});
|
|
51
93
|
|
|
52
94
|
return report;
|
|
53
95
|
}
|
|
96
|
+
|
|
97
|
+
clear() {
|
|
98
|
+
Object.keys(this.requiredTags).forEach((key) => {
|
|
99
|
+
delete this.requiredTags[key];
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
Object.keys(this.excludedTags).forEach((key) => {
|
|
103
|
+
delete this.excludedTags[key];
|
|
104
|
+
});
|
|
105
|
+
}
|
|
54
106
|
}
|