feeds-fun 1.17.0 → 1.18.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/public/news-filtering-example.png +0 -0
- package/src/components/EntriesList.vue +11 -0
- package/src/components/EntryForList.vue +17 -6
- package/src/components/RuleForList.vue +2 -2
- package/src/components/RulesList.vue +18 -9
- package/src/components/TokensCost.vue +0 -2
- package/src/components/collections/DetailedItem.vue +9 -0
- package/src/components/collections/PublicIntro.vue +63 -0
- package/src/components/collections/PublicSelector.vue +43 -0
- package/src/components/main/Block.vue +1 -1
- package/src/components/main/Item.vue +1 -1
- package/src/components/notifications/LoadedOldNews.vue +47 -0
- package/src/components/page_header/ExternalLinks.vue +11 -6
- package/src/components/tags/EntryTag.vue +4 -3
- package/src/components/tags/FilterTag.vue +6 -3
- package/src/components/tags/RuleTag.vue +4 -3
- package/src/components/tags/TagsFilter.vue +17 -5
- package/src/css/panels.css +5 -5
- package/src/layouts/SidePanelLayout.vue +41 -22
- package/src/layouts/WideLayout.vue +0 -4
- package/src/logic/api.ts +23 -0
- package/src/logic/enums.ts +28 -12
- package/src/logic/events.ts +36 -8
- package/src/logic/tagsFilterState.ts +50 -1
- package/src/logic/types.ts +15 -2
- package/src/logic/utils.ts +104 -1
- package/src/main.ts +6 -0
- package/src/router/index.ts +16 -3
- package/src/stores/collections.ts +17 -1
- package/src/stores/entries.ts +154 -19
- package/src/stores/globalSettings.ts +6 -7
- package/src/views/AuthView.vue +3 -1
- package/src/views/CollectionsView.vue +13 -2
- package/src/views/DiscoveryView.vue +3 -1
- package/src/views/FeedsView.vue +3 -1
- package/src/views/MainView.vue +21 -2
- package/src/views/NewsView.vue +32 -98
- package/src/views/PublicCollectionView.vue +237 -0
- package/src/views/RulesView.vue +26 -29
- package/src/views/SettingsView.vue +3 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<side-panel-layout
|
|
3
|
+
:reloadButton="false"
|
|
4
|
+
:login-required="false"
|
|
5
|
+
:home-button="true">
|
|
6
|
+
<template #side-menu-item-1>
|
|
7
|
+
<collections-public-selector
|
|
8
|
+
class="min-w-full"
|
|
9
|
+
v-if="collection"
|
|
10
|
+
:collection-id="collection.id" />
|
|
11
|
+
|
|
12
|
+
<p
|
|
13
|
+
v-if="collection"
|
|
14
|
+
class="ffun-info-common my-2"
|
|
15
|
+
>{{ collection.description }}</p
|
|
16
|
+
>
|
|
17
|
+
</template>
|
|
18
|
+
<template #side-menu-item-2>
|
|
19
|
+
For
|
|
20
|
+
<config-selector
|
|
21
|
+
:values="e.LastEntriesPeriodProperties"
|
|
22
|
+
v-model:property="globalSettings.lastEntriesPeriod" />
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<template #side-menu-item-3>
|
|
26
|
+
Show read
|
|
27
|
+
|
|
28
|
+
<config-flag
|
|
29
|
+
style="min-width: 2.5rem"
|
|
30
|
+
v-model:flag="globalSettings.showRead"
|
|
31
|
+
on-text="no"
|
|
32
|
+
off-text="yes" />
|
|
33
|
+
|
|
34
|
+
<button
|
|
35
|
+
class="ffun-form-button py-0 ml-1"
|
|
36
|
+
title='Undo last "mark read" operation'
|
|
37
|
+
:disabled="!entriesStore.canUndoMarkRead"
|
|
38
|
+
@click="entriesStore.undoMarkRead()">
|
|
39
|
+
↶
|
|
40
|
+
</button>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<template #side-footer>
|
|
44
|
+
<tags-filter
|
|
45
|
+
:tags="tagsCount"
|
|
46
|
+
:show-create-rule="false"
|
|
47
|
+
:show-registration-invitation="true" />
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<template #main-header>
|
|
51
|
+
News
|
|
52
|
+
<span v-if="entriesNumber > 0">[{{ entriesNumber }}]</span>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<template #main-footer> </template>
|
|
56
|
+
|
|
57
|
+
<!-- currently we have a "nuance" with tags user experience in this block -->
|
|
58
|
+
<!-- The tags work as expected till the user selects their own tags from other places -->
|
|
59
|
+
<!-- after that we can get a situation, when, after clicking on a tag in the block, there will be no news displayed -->
|
|
60
|
+
<!-- because the user previously selected tags that have no common news with the tags in the block -->
|
|
61
|
+
<!-- That effect is possible only if the user has already interacted with the ags filter => should not be a problem -->
|
|
62
|
+
<collections-public-intro
|
|
63
|
+
v-if="collection && !globalState.isLoggedIn"
|
|
64
|
+
:collectionId="collection.id"
|
|
65
|
+
:tag1Uid="medianTag1"
|
|
66
|
+
:tag1Count="tagsCount[medianTag1] || 0"
|
|
67
|
+
:tag2Uid="medianTag2"
|
|
68
|
+
:tag2Count="tagsCount[medianTag2] || 0" />
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
v-if="collection && globalState.isLoggedIn"
|
|
72
|
+
class="ffun-info-good">
|
|
73
|
+
<p
|
|
74
|
+
>Welcome to curated <strong>{{ collection.name }}</strong> news collection!</p
|
|
75
|
+
>
|
|
76
|
+
|
|
77
|
+
<p>Collection news is always shown in the order of publication.</p>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<notifications-loaded-old-news
|
|
81
|
+
:entries="entriesStore.loadedEntriesReport || []"
|
|
82
|
+
:period="e.LastEntriesPeriodProperties.get(globalSettings.lastEntriesPeriod)" />
|
|
83
|
+
|
|
84
|
+
<entries-list
|
|
85
|
+
:loading="entriesStore.loading"
|
|
86
|
+
:entriesIds="entriesReport"
|
|
87
|
+
:time-field="entriesStore.activeOrderProperties.timeField"
|
|
88
|
+
:tags-count="tagsCount"
|
|
89
|
+
:show-score="false"
|
|
90
|
+
:showFromStart="25"
|
|
91
|
+
:showPerPage="25" />
|
|
92
|
+
</side-panel-layout>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<script lang="ts" setup>
|
|
96
|
+
import {computed, ref, onUnmounted, watch, provide} from "vue";
|
|
97
|
+
import type {ComputedRef} from "vue";
|
|
98
|
+
import {useRoute, useRouter} from "vue-router";
|
|
99
|
+
import {computedAsync} from "@vueuse/core";
|
|
100
|
+
import * as api from "@/logic/api";
|
|
101
|
+
import * as tagsFilterState from "@/logic/tagsFilterState";
|
|
102
|
+
import * as e from "@/logic/enums";
|
|
103
|
+
import * as utils from "@/logic/utils";
|
|
104
|
+
import type * as t from "@/logic/types";
|
|
105
|
+
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
106
|
+
import {useEntriesStore} from "@/stores/entries";
|
|
107
|
+
import {useCollectionsStore} from "@/stores/collections";
|
|
108
|
+
import {useGlobalState} from "@/stores/globalState";
|
|
109
|
+
import _ from "lodash";
|
|
110
|
+
import * as asserts from "@/logic/asserts";
|
|
111
|
+
|
|
112
|
+
const route = useRoute();
|
|
113
|
+
const router = useRouter();
|
|
114
|
+
|
|
115
|
+
const globalState = useGlobalState();
|
|
116
|
+
const globalSettings = useGlobalSettingsStore();
|
|
117
|
+
const entriesStore = useEntriesStore();
|
|
118
|
+
const collections = useCollectionsStore();
|
|
119
|
+
|
|
120
|
+
const collectionSlug = computed(() => route.params.collectionSlug as t.CollectionSlug);
|
|
121
|
+
|
|
122
|
+
const collection = computed(() => {
|
|
123
|
+
if (!collectionSlug.value) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = collections.getCollectionBySlug({slug: collectionSlug.value});
|
|
128
|
+
|
|
129
|
+
if (Object.keys(collections.collections).length > 0 && !result) {
|
|
130
|
+
// TODO: implement better behaviour for broken slugs
|
|
131
|
+
router.push({name: "main"});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const tagsStates = ref<tagsFilterState.Storage>(new tagsFilterState.Storage());
|
|
138
|
+
|
|
139
|
+
globalSettings.mainPanelMode = e.MainPanelMode.PublicCollection;
|
|
140
|
+
|
|
141
|
+
// Required to separate real collection change (and reset tags filter) from the collection initialization
|
|
142
|
+
const lastDefinedCollectionId = ref<t.CollectionId | null>(null);
|
|
143
|
+
|
|
144
|
+
watch(
|
|
145
|
+
collection,
|
|
146
|
+
() => {
|
|
147
|
+
if (!collection.value) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
entriesStore.setPublicCollectionMode(collection.value.slug);
|
|
152
|
+
|
|
153
|
+
if (lastDefinedCollectionId.value !== null && lastDefinedCollectionId.value !== collection.value.id) {
|
|
154
|
+
tagsStates.value.clear();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (lastDefinedCollectionId.value !== collection.value.id) {
|
|
158
|
+
lastDefinedCollectionId.value = collection.value.id;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{immediate: true}
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
provide("tagsStates", tagsStates);
|
|
165
|
+
provide("eventsViewName", "public_collections");
|
|
166
|
+
|
|
167
|
+
tagsFilterState.setSyncingTagsWithRoute({
|
|
168
|
+
tagsStates: tagsStates.value as unknown as tagsFilterState.Storage,
|
|
169
|
+
route,
|
|
170
|
+
router
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
globalSettings.updateDataVersion();
|
|
174
|
+
|
|
175
|
+
const entriesReport = computed(() => {
|
|
176
|
+
let report = entriesStore.visibleEntries.slice();
|
|
177
|
+
|
|
178
|
+
report = tagsStates.value.filterByTags(report, (entryId) => entriesStore.entries[entryId].tags);
|
|
179
|
+
return report;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const tagsCount = computed(() => {
|
|
183
|
+
const entriesToProcess = entriesReport.value.map((entryId) => entriesStore.entries[entryId]);
|
|
184
|
+
|
|
185
|
+
return utils.countTags(entriesToProcess);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const entriesNumber = computed(() => {
|
|
189
|
+
return entriesReport.value.length;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const medianTag1: ComputedRef<string> = computed(() => {
|
|
193
|
+
// do not change tag when the filter changed
|
|
194
|
+
if (tagsStates.value.hasSelectedTags) {
|
|
195
|
+
return medianTag1.value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const entriesNumber = entriesReport.value.length;
|
|
199
|
+
|
|
200
|
+
const result = utils.chooseTagByUsage({tagsCount: tagsCount.value, border: 0.5 * entriesNumber, exclude: []});
|
|
201
|
+
|
|
202
|
+
if (result === null) {
|
|
203
|
+
return "";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const medianTag2: ComputedRef<string> = computed(() => {
|
|
210
|
+
// do not change tag when the filter changed
|
|
211
|
+
if (tagsStates.value.hasSelectedTags) {
|
|
212
|
+
return medianTag2.value;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const entriesToProcess = entriesReport.value
|
|
216
|
+
.map((entryId) => entriesStore.entries[entryId])
|
|
217
|
+
.filter((entry) => entry.tags.includes(medianTag1.value));
|
|
218
|
+
|
|
219
|
+
const entriesNumber = entriesToProcess.length;
|
|
220
|
+
|
|
221
|
+
const counts = utils.countTags(entriesToProcess);
|
|
222
|
+
|
|
223
|
+
const result = utils.chooseTagByUsage({
|
|
224
|
+
tagsCount: counts,
|
|
225
|
+
border: 0.5 * entriesNumber,
|
|
226
|
+
exclude: [medianTag1.value]
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (result === null) {
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return result;
|
|
234
|
+
});
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<style></style>
|
package/src/views/RulesView.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<side-panel-layout :reload-button="true">
|
|
3
3
|
<template #main-header>
|
|
4
4
|
Rules
|
|
5
|
-
<span v-if="rules">[{{
|
|
5
|
+
<span v-if="rules">[{{ rulesNumber }}]</span>
|
|
6
6
|
</template>
|
|
7
7
|
|
|
8
8
|
<template #side-menu-item-2>
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
<template #side-footer>
|
|
16
16
|
<tags-filter
|
|
17
17
|
:tags="tagsCount"
|
|
18
|
-
:show-create-rule="false"
|
|
19
|
-
change-source="rules_tags_filter" />
|
|
18
|
+
:show-create-rule="false" />
|
|
20
19
|
</template>
|
|
21
20
|
|
|
22
21
|
<div class="ffun-info-common mb-2">
|
|
@@ -32,14 +31,14 @@
|
|
|
32
31
|
</div>
|
|
33
32
|
|
|
34
33
|
<rules-list
|
|
35
|
-
|
|
34
|
+
:loading="loading"
|
|
36
35
|
:rules="sortedRules" />
|
|
37
36
|
</side-panel-layout>
|
|
38
37
|
</template>
|
|
39
38
|
|
|
40
39
|
<script lang="ts" setup>
|
|
41
40
|
import {computed, ref, onUnmounted, watch, provide} from "vue";
|
|
42
|
-
import {useRouter} from "vue-router";
|
|
41
|
+
import {useRoute, useRouter} from "vue-router";
|
|
43
42
|
import {computedAsync} from "@vueuse/core";
|
|
44
43
|
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
45
44
|
import _ from "lodash";
|
|
@@ -49,11 +48,19 @@
|
|
|
49
48
|
import * as e from "@/logic/enums";
|
|
50
49
|
import * as tagsFilterState from "@/logic/tagsFilterState";
|
|
51
50
|
|
|
51
|
+
const route = useRoute();
|
|
52
52
|
const router = useRouter();
|
|
53
53
|
|
|
54
54
|
const tagsStates = ref<tagsFilterState.Storage>(new tagsFilterState.Storage());
|
|
55
55
|
|
|
56
56
|
provide("tagsStates", tagsStates);
|
|
57
|
+
provide("eventsViewName", "rules");
|
|
58
|
+
|
|
59
|
+
tagsFilterState.setSyncingTagsWithRoute({
|
|
60
|
+
tagsStates: tagsStates.value as unknown as tagsFilterState.Storage,
|
|
61
|
+
route,
|
|
62
|
+
router
|
|
63
|
+
});
|
|
57
64
|
|
|
58
65
|
const globalSettings = useGlobalSettingsStore();
|
|
59
66
|
|
|
@@ -63,32 +70,14 @@
|
|
|
63
70
|
router.push({name: e.MainPanelMode.Entries, params: {}});
|
|
64
71
|
}
|
|
65
72
|
|
|
73
|
+
const loading = computed(() => rules.value === null);
|
|
74
|
+
|
|
66
75
|
const rules = computedAsync(async () => {
|
|
67
76
|
// force refresh
|
|
68
77
|
globalSettings.dataVersion;
|
|
69
78
|
return await api.getRules();
|
|
70
79
|
}, null);
|
|
71
80
|
|
|
72
|
-
const tagsCount = computed(() => {
|
|
73
|
-
if (!rules.value) {
|
|
74
|
-
return {};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const tags: {[key: string]: number} = {};
|
|
78
|
-
|
|
79
|
-
for (const rule of rules.value) {
|
|
80
|
-
for (const tag of rule.requiredTags) {
|
|
81
|
-
tags[tag] = (tags[tag] || 0) + 1;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
for (const tag of rule.excludedTags) {
|
|
85
|
-
tags[tag] = (tags[tag] || 0) + 1;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return tags;
|
|
90
|
-
});
|
|
91
|
-
|
|
92
81
|
const sortedRules = computed(() => {
|
|
93
82
|
if (!rules.value) {
|
|
94
83
|
return null;
|
|
@@ -96,7 +85,7 @@
|
|
|
96
85
|
|
|
97
86
|
let sorted = rules.value.slice();
|
|
98
87
|
|
|
99
|
-
sorted = tagsStates.value.filterByTags(sorted, (rule) => rule.
|
|
88
|
+
sorted = tagsStates.value.filterByTags(sorted, (rule) => rule.tags);
|
|
100
89
|
|
|
101
90
|
const orderProperties = e.RulesOrderProperties.get(globalSettings.rulesOrder);
|
|
102
91
|
|
|
@@ -113,14 +102,14 @@
|
|
|
113
102
|
|
|
114
103
|
sorted = sorted.sort((a: t.Rule, b: t.Rule) => {
|
|
115
104
|
if (globalSettings.rulesOrder === e.RulesOrder.Tags) {
|
|
116
|
-
return utils.compareLexicographically(a.
|
|
105
|
+
return utils.compareLexicographically(a.tags, b.tags);
|
|
117
106
|
}
|
|
118
107
|
|
|
119
108
|
const valueA = _.get(a, orderField, null);
|
|
120
109
|
const valueB = _.get(b, orderField, null);
|
|
121
110
|
|
|
122
111
|
if (valueA === null && valueB === null) {
|
|
123
|
-
return utils.compareLexicographically(a.
|
|
112
|
+
return utils.compareLexicographically(a.tags, b.tags);
|
|
124
113
|
}
|
|
125
114
|
|
|
126
115
|
if (valueA === null) {
|
|
@@ -139,9 +128,17 @@
|
|
|
139
128
|
return -1 * direction;
|
|
140
129
|
}
|
|
141
130
|
|
|
142
|
-
return utils.compareLexicographically(a.
|
|
131
|
+
return utils.compareLexicographically(a.tags, b.tags);
|
|
143
132
|
});
|
|
144
133
|
|
|
145
134
|
return sorted;
|
|
146
135
|
});
|
|
136
|
+
|
|
137
|
+
const rulesNumber = computed(() => {
|
|
138
|
+
return sortedRules.value ? sortedRules.value.length : 0;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const tagsCount = computed(() => {
|
|
142
|
+
return utils.countTags(sortedRules.value);
|
|
143
|
+
});
|
|
147
144
|
</script>
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
</template>
|
|
121
121
|
|
|
122
122
|
<script lang="ts" setup>
|
|
123
|
-
import {computed, ref, onUnmounted, watch} from "vue";
|
|
123
|
+
import {computed, ref, onUnmounted, watch, provide} from "vue";
|
|
124
124
|
import {computedAsync} from "@vueuse/core";
|
|
125
125
|
import * as api from "@/logic/api";
|
|
126
126
|
import * as t from "@/logic/types";
|
|
@@ -130,6 +130,8 @@
|
|
|
130
130
|
|
|
131
131
|
const globalSettings = useGlobalSettingsStore();
|
|
132
132
|
|
|
133
|
+
provide("eventsViewName", "settings");
|
|
134
|
+
|
|
133
135
|
globalSettings.mainPanelMode = e.MainPanelMode.Settings;
|
|
134
136
|
|
|
135
137
|
const tokensCostData = computedAsync(async () => {
|