feeds-fun 0.1.1 → 0.3.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/README.md CHANGED
@@ -6,3 +6,5 @@
6
6
  - Then filter and sort news how you want.
7
7
 
8
8
  Service is up and running at https://feeds.fun
9
+
10
+ Documentation can be found in the repository: https://github.com/Tiendil/feeds.fun
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feeds-fun",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "author": "Aliaksei Yaletski (Tiendil) <a.eletsky@gmail.com> (https://tiendil.org/)",
5
5
  "description": "Frontend for the Feeds Fun — web-based news reader",
6
6
  "keywords": [
@@ -41,7 +41,9 @@
41
41
 
42
42
  <template v-if="showTags">
43
43
  <br />
44
- <tags-list :tags="entry.tags" />
44
+ <tags-list
45
+ :tags="entry.tags"
46
+ :contributions="entry.scoreContributions" />
45
47
  </template>
46
48
 
47
49
  <div
@@ -77,7 +77,8 @@
77
77
  .tag {
78
78
  display: inline-block;
79
79
  cursor: pointer;
80
- padding: 0.25rem;
80
+ padding: 0.1rem;
81
+ margin-right: 0.2rem;
81
82
  white-space: nowrap;
82
83
  }
83
84
 
@@ -92,4 +93,12 @@
92
93
  .tag.excluded {
93
94
  background-color: #ffcccc;
94
95
  }
96
+
97
+ .tag.positive {
98
+ color: darkgreen;
99
+ }
100
+
101
+ .tag.negative {
102
+ color: darkred;
103
+ }
95
104
  </style>
@@ -7,7 +7,9 @@
7
7
  </tr>
8
8
  </thead>
9
9
  <tbody>
10
- <tr v-for="rule in rules">
10
+ <tr
11
+ v-for="rule in rules"
12
+ :key="rule.id">
11
13
  <td>
12
14
  <rule-score-updater
13
15
  :score="rule.score"
@@ -13,6 +13,11 @@
13
13
  @tag:deselected="onTagDeselected" />
14
14
  </ul>
15
15
 
16
+ <input
17
+ type="text"
18
+ placeholder="Input part of tag..."
19
+ v-model="tagNameFilter" />
20
+
16
21
  <ul
17
22
  v-if="displayedTags.length > 0"
18
23
  style="list-style: none; padding: 0; margin: 0">
@@ -39,6 +44,9 @@
39
44
 
40
45
  <script lang="ts" setup>
41
46
  import {computed, ref} from "vue";
47
+ import {useTagsStore} from "@/stores/tags";
48
+
49
+ const tagsStore = useTagsStore();
42
50
 
43
51
  const selectedTags = ref<{[key: string]: boolean}>({});
44
52
 
@@ -48,6 +56,8 @@
48
56
 
49
57
  const showEntries = ref(showFromStart.value);
50
58
 
59
+ const tagNameFilter = ref("");
60
+
51
61
  function tagComparator(a: string, b: string) {
52
62
  const aCount = properties.tags[a];
53
63
  const bCount = properties.tags[b];
@@ -101,6 +111,16 @@
101
111
  return selectedTags.value[tag] !== true;
102
112
  });
103
113
 
114
+ values = values.filter((tag) => {
115
+ const tagInfo = tagsStore.tags[tag];
116
+
117
+ if (tagInfo === undefined || tagInfo.name === null) {
118
+ return tag.includes(tagNameFilter.value);
119
+ }
120
+
121
+ return tagInfo.name.includes(tagNameFilter.value);
122
+ });
123
+
104
124
  values.sort(tagComparator);
105
125
 
106
126
  values = values.slice(0, showEntries.value);
@@ -9,7 +9,7 @@
9
9
  v-for="tag of displayedTags"
10
10
  :key="tag"
11
11
  :uid="tag"
12
- :mode="!!selectedTags[tag] ? 'selected' : null"
12
+ :mode="tagMode(tag)"
13
13
  :count="entriesStore.reportTagsCount[tag]"
14
14
  count-mode="tooltip"
15
15
  @tag:clicked="onTagClicked" />
@@ -41,7 +41,7 @@
41
41
 
42
42
  const selectedTags = ref<{[key: string]: boolean}>({});
43
43
 
44
- const properties = defineProps<{tags: string[]}>();
44
+ const properties = defineProps<{tags: string[]; contributions: {[key: string]: number}}>();
45
45
 
46
46
  const tagsNumber = computed(() => {
47
47
  return properties.tags.length;
@@ -55,6 +55,32 @@
55
55
  return preparedTags.value.slice(0, showLimit.value);
56
56
  });
57
57
 
58
+ function tagMode(tag: string) {
59
+ if (!!selectedTags.value[tag]) {
60
+ return "selected";
61
+ }
62
+
63
+ // return null;
64
+
65
+ if (!properties.contributions) {
66
+ return null;
67
+ }
68
+
69
+ if (!(tag in properties.contributions)) {
70
+ return null;
71
+ }
72
+
73
+ if (properties.contributions[tag] == 0) {
74
+ return null;
75
+ }
76
+
77
+ if (properties.contributions[tag] > 0) {
78
+ return "positive";
79
+ }
80
+
81
+ return "negative";
82
+ }
83
+
58
84
  const preparedTags = computed(() => {
59
85
  const values = [];
60
86
 
@@ -63,6 +89,17 @@
63
89
  }
64
90
 
65
91
  values.sort((a, b) => {
92
+ const aContributions = Math.abs(properties.contributions[a] || 0);
93
+ const bContributions = Math.abs(properties.contributions[b] || 0);
94
+
95
+ if (aContributions > bContributions) {
96
+ return -1;
97
+ }
98
+
99
+ if (aContributions < bContributions) {
100
+ return 1;
101
+ }
102
+
66
103
  const aCount = entriesStore.reportTagsCount[a];
67
104
  const bCount = entriesStore.reportTagsCount[b];
68
105
 
@@ -90,3 +90,17 @@ export const FeedsOrderProperties = new Map<FeedsOrder, {text: string; orderFiel
90
90
  [FeedsOrder.Loaded, {text: "loaded", orderField: "loadedAt", orderDirection: "desc"}],
91
91
  [FeedsOrder.Linked, {text: "added", orderField: "linkedAt", orderDirection: "desc"}]
92
92
  ]);
93
+
94
+ export enum RulesOrder {
95
+ Tags = "tags",
96
+ Score = "score",
97
+ Created = "created",
98
+ Updated = "updated"
99
+ }
100
+
101
+ export const RulesOrderProperties = new Map<RulesOrder, {text: string; orderField: string; orderDirection: string}>([
102
+ [RulesOrder.Tags, {text: "tags", orderField: "tags", orderDirection: "asc"}],
103
+ [RulesOrder.Score, {text: "score", orderField: "score", orderDirection: "desc"}],
104
+ [RulesOrder.Created, {text: "created", orderField: "createdAt", orderDirection: "desc"}],
105
+ [RulesOrder.Updated, {text: "updated", orderField: "updatedAt", orderDirection: "desc"}]
106
+ ]);
@@ -114,6 +114,7 @@ export class Entry {
114
114
  readonly tags: string[];
115
115
  readonly markers: e.Marker[];
116
116
  readonly score: number;
117
+ readonly scoreContributions: {[key: string]: number};
117
118
  readonly scoreToZero: number;
118
119
  readonly publishedAt: Date;
119
120
  readonly catalogedAt: Date;
@@ -127,6 +128,7 @@ export class Entry {
127
128
  tags,
128
129
  markers,
129
130
  score,
131
+ scoreContributions,
130
132
  publishedAt,
131
133
  catalogedAt,
132
134
  body
@@ -138,6 +140,7 @@ export class Entry {
138
140
  tags: string[];
139
141
  markers: e.Marker[];
140
142
  score: number;
143
+ scoreContributions: {[key: string]: number};
141
144
  publishedAt: Date;
142
145
  catalogedAt: Date;
143
146
  body: string | null;
@@ -149,6 +152,7 @@ export class Entry {
149
152
  this.tags = tags;
150
153
  this.markers = markers;
151
154
  this.score = score;
155
+ this.scoreContributions = scoreContributions;
152
156
  this.publishedAt = publishedAt;
153
157
  this.catalogedAt = catalogedAt;
154
158
  this.body = body;
@@ -181,6 +185,7 @@ export function entryFromJSON({
181
185
  tags,
182
186
  markers,
183
187
  score,
188
+ scoreContributions,
184
189
  publishedAt,
185
190
  catalogedAt,
186
191
  body
@@ -192,6 +197,7 @@ export function entryFromJSON({
192
197
  tags: string[];
193
198
  markers: string[];
194
199
  score: number;
200
+ scoreContributions: {[key: string]: number};
195
201
  publishedAt: string;
196
202
  catalogedAt: string;
197
203
  body: string | null;
@@ -210,6 +216,7 @@ export function entryFromJSON({
210
216
  throw new Error(`Unknown marker: ${m}`);
211
217
  }),
212
218
  score: score,
219
+ scoreContributions: scoreContributions,
213
220
  publishedAt: new Date(publishedAt),
214
221
  catalogedAt: new Date(catalogedAt),
215
222
  body: body
@@ -221,24 +228,30 @@ export type Rule = {
221
228
  readonly tags: string[];
222
229
  readonly score: number;
223
230
  readonly createdAt: Date;
231
+ readonly updatedAt: Date;
224
232
  };
225
233
 
226
234
  export function ruleFromJSON({
227
235
  id,
228
236
  tags,
229
237
  score,
230
- createdAt
238
+ createdAt,
239
+ updatedAt
231
240
  }: {
232
241
  id: string;
233
242
  tags: string[];
234
243
  score: number;
235
244
  createdAt: string;
245
+ updatedAt: string;
236
246
  }): Rule {
247
+ tags = tags.sort();
248
+
237
249
  return {
238
250
  id: toRuleId(id),
239
251
  tags: tags,
240
252
  score: score,
241
- createdAt: new Date(createdAt)
253
+ createdAt: new Date(createdAt),
254
+ updatedAt: new Date(updatedAt)
242
255
  };
243
256
  }
244
257
 
@@ -311,7 +324,7 @@ export function tagInfoFromJSON({
311
324
  }
312
325
 
313
326
  export function noInfoTag(uid: string): TagInfo {
314
- return {uid, name: null, link: null, categories: []};
327
+ return {uid, name: uid, link: null, categories: []};
315
328
  }
316
329
 
317
330
  export type UserSetting = {
@@ -37,3 +37,23 @@ export function timeSince(date: Date) {
37
37
 
38
38
  return `${yearsPast}y`;
39
39
  }
40
+
41
+ export function compareLexicographically(a: string[], b: string[]) {
42
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
43
+ const comparison = a[i].localeCompare(b[i]);
44
+
45
+ if (comparison !== 0) {
46
+ return comparison;
47
+ }
48
+ }
49
+
50
+ if (a.length > b.length) {
51
+ return 1;
52
+ }
53
+
54
+ if (a.length < b.length) {
55
+ return -1;
56
+ }
57
+
58
+ return 0;
59
+ }
@@ -26,6 +26,9 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
26
26
  const feedsOrder = ref(e.FeedsOrder.Title);
27
27
  const failedFeedsFirst = ref(false);
28
28
 
29
+ // Rules
30
+ const rulesOrder = ref(e.RulesOrder.Tags);
31
+
29
32
  watch(mainPanelMode, (newValue, oldValue) => {
30
33
  router.push({name: mainPanelMode.value, params: {}});
31
34
  });
@@ -69,6 +72,7 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
69
72
  userSettings,
70
73
  info,
71
74
  feedsOrder,
72
- failedFeedsFirst
75
+ failedFeedsFirst,
76
+ rulesOrder
73
77
  };
74
78
  });
@@ -36,7 +36,7 @@
36
36
  .score {
37
37
  display: inline-block;
38
38
  cursor: pointer;
39
- padding: 0.25rem;
39
+ padding: 0.1rem;
40
40
  background-color: #c1c1ff;
41
41
  }
42
42
  </style>
@@ -1,13 +1,20 @@
1
1
  <template>
2
- <side-panel-layout :reload-button="false">
2
+ <side-panel-layout :reload-button="true">
3
3
  <template #main-header>
4
4
  Rules
5
5
  <span v-if="rules">[{{ rules.length }}]</span>
6
6
  </template>
7
7
 
8
+ <template #side-menu-item-2>
9
+ Sorted by
10
+ <config-selector
11
+ :values="e.RulesOrderProperties"
12
+ v-model:property="globalSettings.rulesOrder" />
13
+ </template>
14
+
8
15
  <rules-list
9
16
  v-if="rules"
10
- :rules="rules" />
17
+ :rules="sortedRules" />
11
18
  </side-panel-layout>
12
19
  </template>
13
20
 
@@ -15,8 +22,10 @@
15
22
  import {computed, ref, onUnmounted, watch} from "vue";
16
23
  import {computedAsync} from "@vueuse/core";
17
24
  import {useGlobalSettingsStore} from "@/stores/globalSettings";
25
+ import _ from "lodash";
26
+ import * as utils from "@/logic/utils";
18
27
  import * as api from "@/logic/api";
19
- import * as t from "@/logic/types";
28
+ import type * as t from "@/logic/types";
20
29
  import * as e from "@/logic/enums";
21
30
 
22
31
  const globalSettings = useGlobalSettingsStore();
@@ -28,6 +37,60 @@
28
37
  globalSettings.dataVersion;
29
38
  return await api.getRules();
30
39
  }, null);
40
+
41
+ const sortedRules = computed(() => {
42
+ if (!rules.value) {
43
+ return null;
44
+ }
45
+
46
+ const orderProperties = e.RulesOrderProperties.get(globalSettings.rulesOrder);
47
+
48
+ if (!orderProperties) {
49
+ throw new Error(`Invalid order properties: ${globalSettings.rulesOrder}`);
50
+ }
51
+
52
+ const orderField = orderProperties.orderField;
53
+ const direction = {asc: -1, desc: 1}[orderProperties.orderDirection];
54
+
55
+ if (direction === undefined) {
56
+ throw new Error(`Invalid order direction: ${orderProperties.orderDirection}`);
57
+ }
58
+
59
+ let sorted = rules.value.slice();
60
+
61
+ sorted = sorted.sort((a: t.Rule, b: t.Rule) => {
62
+ if (globalSettings.rulesOrder === e.RulesOrder.Tags) {
63
+ return utils.compareLexicographically(a.tags, b.tags);
64
+ }
65
+
66
+ const valueA = _.get(a, orderField, null);
67
+ const valueB = _.get(b, orderField, null);
68
+
69
+ if (valueA === null && valueB === null) {
70
+ return utils.compareLexicographically(a.tags, b.tags);
71
+ }
72
+
73
+ if (valueA === null) {
74
+ return 1 * direction;
75
+ }
76
+
77
+ if (valueB === null) {
78
+ return -1 * direction;
79
+ }
80
+
81
+ if (valueA < valueB) {
82
+ return 1 * direction;
83
+ }
84
+
85
+ if (valueA > valueB) {
86
+ return -1 * direction;
87
+ }
88
+
89
+ return utils.compareLexicographically(a.tags, b.tags);
90
+ });
91
+
92
+ return sorted;
93
+ });
31
94
  </script>
32
95
 
33
96
  <style></style>