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.
@@ -231,7 +231,9 @@ export function entryFromJSON(
231
231
 
232
232
  export type Rule = {
233
233
  readonly id: RuleId;
234
- readonly tags: string[];
234
+ readonly requiredTags: string[];
235
+ readonly excludedTags: string[];
236
+ readonly allTags: string[];
235
237
  readonly score: number;
236
238
  readonly createdAt: Date;
237
239
  readonly updatedAt: Date;
@@ -239,22 +241,27 @@ export type Rule = {
239
241
 
240
242
  export function ruleFromJSON({
241
243
  id,
242
- tags,
244
+ requiredTags,
245
+ excludedTags,
243
246
  score,
244
247
  createdAt,
245
248
  updatedAt
246
249
  }: {
247
250
  id: string;
248
- tags: string[];
251
+ requiredTags: string[];
252
+ excludedTags: string[];
249
253
  score: number;
250
254
  createdAt: string;
251
255
  updatedAt: string;
252
256
  }): Rule {
253
- tags = tags.sort();
257
+ requiredTags = requiredTags.sort();
258
+ excludedTags = excludedTags.sort();
254
259
 
255
260
  return {
256
261
  id: toRuleId(id),
257
- tags: tags,
262
+ requiredTags: requiredTags,
263
+ excludedTags: excludedTags,
264
+ allTags: requiredTags.concat(excludedTags),
258
265
  score: score,
259
266
  createdAt: new Date(createdAt),
260
267
  updatedAt: new Date(updatedAt)
@@ -287,24 +294,28 @@ export type FeedInfo = {
287
294
  readonly title: string;
288
295
  readonly description: string;
289
296
  readonly entries: EntryInfo[];
297
+ readonly isLinked: boolean;
290
298
  };
291
299
 
292
300
  export function feedInfoFromJSON({
293
301
  url,
294
302
  title,
295
303
  description,
296
- entries
304
+ entries,
305
+ isLinked
297
306
  }: {
298
307
  url: string;
299
308
  title: string;
300
309
  description: string;
301
310
  entries: any[];
311
+ isLinked: boolean;
302
312
  }): FeedInfo {
303
313
  return {
304
314
  url: toURL(url),
305
315
  title,
306
316
  description,
307
- entries: entries.map(entryInfoFromJSON)
317
+ entries: entries.map(entryInfoFromJSON),
318
+ isLinked
308
319
  };
309
320
  }
310
321
 
@@ -478,3 +489,19 @@ export function collectionFeedInfoFromJSON({
478
489
  id: toFeedId(id)
479
490
  });
480
491
  }
492
+
493
+ export class ApiMessage {
494
+ readonly type: string;
495
+ readonly code: string;
496
+ readonly message: string;
497
+
498
+ constructor({type, code, message}: {type: string; code: string; message: string}) {
499
+ this.type = type;
500
+ this.code = code;
501
+ this.message = message;
502
+ }
503
+ }
504
+
505
+ export function apiMessageFromJSON({type, code, message}: {type: string; code: string; message: string}): ApiMessage {
506
+ return new ApiMessage({type, code, message});
507
+ }
@@ -8,12 +8,14 @@ import * as api from "@/logic/api";
8
8
  import {Timer} from "@/logic/timer";
9
9
  import {computedAsync} from "@vueuse/core";
10
10
  import {useGlobalSettingsStore} from "@/stores/globalSettings";
11
+ import * as events from "@/logic/events";
11
12
 
12
13
  export const useEntriesStore = defineStore("entriesStore", () => {
13
14
  const globalSettings = useGlobalSettingsStore();
14
15
 
15
16
  const entries = ref<{[key: t.EntryId]: t.Entry}>({});
16
17
  const requestedEntries = ref<{[key: t.EntryId]: boolean}>({});
18
+ const displayedEntryId = ref<t.EntryId | null>(null);
17
19
 
18
20
  function registerEntry(entry: t.Entry) {
19
21
  if (entry.id in entries.value) {
@@ -95,11 +97,35 @@ export const useEntriesStore = defineStore("entriesStore", () => {
95
97
  }
96
98
  }
97
99
 
100
+ async function displayEntry({entryId}: {entryId: t.EntryId}) {
101
+ displayedEntryId.value = entryId;
102
+
103
+ requestFullEntry({entryId: entryId});
104
+
105
+ if (!entries.value[entryId].hasMarker(e.Marker.Read)) {
106
+ await setMarker({
107
+ entryId: entryId,
108
+ marker: e.Marker.Read
109
+ });
110
+ }
111
+
112
+ await events.newsBodyOpened({entryId: entryId});
113
+ }
114
+
115
+ function hideEntry({entryId}: {entryId: t.EntryId}) {
116
+ if (displayedEntryId.value === entryId) {
117
+ displayedEntryId.value = null;
118
+ }
119
+ }
120
+
98
121
  return {
99
122
  entries,
100
123
  requestFullEntry,
101
124
  setMarker,
102
125
  removeMarker,
103
- loadedEntriesReport
126
+ loadedEntriesReport,
127
+ displayedEntryId,
128
+ displayEntry,
129
+ hideEntry
104
130
  };
105
131
  });
@@ -18,7 +18,6 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
18
18
  // Entries
19
19
  const lastEntriesPeriod = ref(e.LastEntriesPeriod.Day3);
20
20
  const entriesOrder = ref(e.EntriesOrder.Score);
21
- const showEntriesTags = ref(true);
22
21
  const showRead = ref(true);
23
22
 
24
23
  // Feeds
@@ -64,7 +63,6 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
64
63
  mainPanelMode,
65
64
  lastEntriesPeriod,
66
65
  entriesOrder,
67
- showEntriesTags,
68
66
  showRead,
69
67
  dataVersion,
70
68
  updateDataVersion,
@@ -30,7 +30,7 @@
30
30
  for (const rule of rules) {
31
31
  const tags = [];
32
32
 
33
- for (const tagId of rule.tags) {
33
+ for (const tagId of rule.requiredTags) {
34
34
  const tagInfo = tagsStore.tags[tagId];
35
35
  if (tagInfo) {
36
36
  tags.push(tagInfo.name);
@@ -39,7 +39,16 @@
39
39
  }
40
40
  }
41
41
 
42
- strings.push(rule.score.toString().padStart(2, " ") + " — " + tags.join(", "));
42
+ for (const tagId of rule.excludedTags) {
43
+ const tagInfo = tagsStore.tags[tagId];
44
+ if (tagInfo) {
45
+ tags.push("NOT " + tagInfo.name);
46
+ } else {
47
+ tags.push("NOT " + tagId);
48
+ }
49
+ }
50
+
51
+ strings.push(rule.score.toString().padStart(2, " ") + " — " + tags.join(" AND "));
43
52
  }
44
53
 
45
54
  alert(strings.join("\n"));
@@ -10,16 +10,26 @@
10
10
 
11
11
  <h2>Lood feeds from an OPML file</h2>
12
12
 
13
+ <div class="ffun-info-good">
14
+ <p>
15
+ <a
16
+ href="https://en.wikipedia.org/wiki/OPML"
17
+ target="_blank"
18
+ >OPML</a
19
+ >
20
+ is a widely-used format for transferring news feed lists between platforms.
21
+ </p>
22
+
23
+ <p
24
+ >Export your feeds from your old reader in OPML format and import them into our reader to seamlessly
25
+ transition!</p
26
+ >
27
+ </div>
28
+
13
29
  <opml-upload />
14
30
 
15
31
  <h2>Search for a feed</h2>
16
32
 
17
- <div class="ffun-info-attention">
18
- <p> The discovery feature is experimental and might not work on all websites. </p>
19
-
20
- <p> If we can’t find a feed for a site, try finding the feed's URL manually, then enter it in the form. </p>
21
- </div>
22
-
23
33
  <discovery-form />
24
34
  </side-panel-layout>
25
35
  </template>
@@ -25,6 +25,15 @@
25
25
  off-text="last" />
26
26
  </template>
27
27
 
28
+ <template #side-menu-item-4>
29
+ <a
30
+ class="ffun-form-button p-1 my-1 block w-full text-center"
31
+ href="/api/get-opml"
32
+ target="_blank"
33
+ >Download OPML</a
34
+ >
35
+ </template>
36
+
28
37
  <template #main-header>
29
38
  Feeds
30
39
  <span v-if="sortedFeeds"> [{{ sortedFeeds.length }}] </span>
@@ -15,15 +15,6 @@
15
15
  </template>
16
16
 
17
17
  <template #side-menu-item-3>
18
- Show tags:
19
- <config-flag
20
- style="min-width: 2.5rem"
21
- v-model:flag="globalSettings.showEntriesTags"
22
- on-text="no"
23
- off-text="yes" />
24
- </template>
25
-
26
- <template #side-menu-item-4>
27
18
  Show read:
28
19
  <config-flag
29
20
  style="min-width: 2.5rem"
@@ -35,7 +26,7 @@
35
26
  <template #side-footer>
36
27
  <tags-filter
37
28
  :tags="tagsCount"
38
- @tag:stateChanged="onTagStateChanged" />
29
+ :show-create-rule="true" />
39
30
  </template>
40
31
 
41
32
  <template #main-header>
@@ -55,16 +46,14 @@
55
46
  <entries-list
56
47
  :entriesIds="entriesReport"
57
48
  :time-field="timeField"
58
- :show-tags="globalSettings.showEntriesTags"
59
49
  :tags-count="tagsCount"
60
50
  :showFromStart="25"
61
- :showPerPage="25"
62
- @entry:bodyVisibilityChanged="onBodyVisibilityChanged" />
51
+ :showPerPage="25" />
63
52
  </side-panel-layout>
64
53
  </template>
65
54
 
66
55
  <script lang="ts" setup>
67
- import {computed, ref, onUnmounted, watch} from "vue";
56
+ import {computed, ref, onUnmounted, watch, provide} from "vue";
68
57
  import {computedAsync} from "@vueuse/core";
69
58
  import * as api from "@/logic/api";
70
59
  import * as tagsFilterState from "@/logic/tagsFilterState";
@@ -79,67 +68,86 @@
79
68
 
80
69
  const tagsStates = ref<tagsFilterState.Storage>(new tagsFilterState.Storage());
81
70
 
71
+ provide("tagsStates", tagsStates);
72
+
82
73
  globalSettings.mainPanelMode = e.MainPanelMode.Entries;
83
74
 
84
75
  globalSettings.updateDataVersion();
85
76
 
86
77
  const entriesWithOpenedBody = ref<{[key: t.EntryId]: boolean}>({});
87
78
 
88
- const entriesReport = computed(() => {
79
+ const _sortedEntries = computed(() => {
89
80
  if (entriesStore.loadedEntriesReport === null) {
90
81
  return [];
91
82
  }
92
83
 
93
- let report = entriesStore.loadedEntriesReport.slice();
84
+ const orderProperties = e.EntriesOrderProperties.get(globalSettings.entriesOrder);
94
85
 
95
- if (!globalSettings.showRead) {
96
- report = report.filter((entryId) => {
97
- if (entriesWithOpenedBody.value[entryId]) {
98
- // always show read entries with open body
99
- // otherwise, they will hide right after opening it
100
- return true;
101
- }
102
- return !entriesStore.entries[entryId].hasMarker(e.Marker.Read);
103
- });
86
+ if (orderProperties === undefined) {
87
+ throw new Error(`Unknown order ${globalSettings.entriesOrder}`);
104
88
  }
105
89
 
106
- report = tagsStates.value.filterByTags(report, (entryId) => entriesStore.entries[entryId].tags);
107
-
108
- report = report.sort((a: t.EntryId, b: t.EntryId) => {
109
- const orderProperties = e.EntriesOrderProperties.get(globalSettings.entriesOrder);
90
+ const field = orderProperties.orderField;
91
+ const direction = orderProperties.direction;
110
92
 
111
- if (orderProperties === undefined) {
112
- throw new Error(`Unknown order ${globalSettings.entriesOrder}`);
113
- }
93
+ // let report = entriesStore.loadedEntriesReport.slice();
114
94
 
115
- const field = orderProperties.orderField;
116
-
117
- const valueA = _.get(entriesStore.entries[a], field, null);
118
- const valueB = _.get(entriesStore.entries[b], field, null);
95
+ // Pre-map to avoid repeated lookups in the comparator
96
+ const mapped = entriesStore.loadedEntriesReport.map((entryId) => {
97
+ // @ts-ignore
98
+ return {entryId, value: entriesStore.entries[entryId][field]};
99
+ });
119
100
 
120
- if (valueA === null && valueB === null) {
101
+ mapped.sort((a: {entryId: t.EntryId; value: any}, b: {entryId: t.EntryId; value: any}) => {
102
+ if (a.value === null && b.value === null) {
121
103
  return 0;
122
104
  }
123
105
 
124
- if (valueA === null) {
106
+ if (a.value === null) {
125
107
  return 1;
126
108
  }
127
109
 
128
- if (valueB === null) {
110
+ if (b.value === null) {
129
111
  return -1;
130
112
  }
131
113
 
132
- if (valueA < valueB) {
133
- return orderProperties.direction;
114
+ if (a.value < b.value) {
115
+ return direction;
134
116
  }
135
117
 
136
- if (valueA > valueB) {
137
- return -orderProperties.direction;
118
+ if (a.value > b.value) {
119
+ return -direction;
138
120
  }
139
121
 
140
122
  return 0;
141
123
  });
142
124
 
125
+ const report = mapped.map((x) => x.entryId);
126
+
127
+ return report;
128
+ });
129
+
130
+ const _visibleEntries = computed(() => {
131
+ let report = _sortedEntries.value.slice();
132
+
133
+ if (!globalSettings.showRead) {
134
+ report = report.filter((entryId) => {
135
+ if (entriesStore.displayedEntryId == entryId) {
136
+ // always show read entries with open body
137
+ // otherwise, they will hide right after opening it
138
+ return true;
139
+ }
140
+ return !entriesStore.entries[entryId].hasMarker(e.Marker.Read);
141
+ });
142
+ }
143
+
144
+ return report;
145
+ });
146
+
147
+ const entriesReport = computed(() => {
148
+ let report = _visibleEntries.value.slice();
149
+
150
+ report = tagsStates.value.filterByTags(report, (entryId) => entriesStore.entries[entryId].tags);
143
151
  return report;
144
152
  });
145
153
 
@@ -193,14 +201,6 @@
193
201
 
194
202
  return orderProperties.timeField;
195
203
  });
196
-
197
- function onTagStateChanged({tag, state}: {tag: string; state: tagsFilterState.State}) {
198
- tagsStates.value.onTagStateChanged({tag, state});
199
- }
200
-
201
- function onBodyVisibilityChanged({entryId, visible}: {entryId: t.EntryId; visible: boolean}) {
202
- entriesWithOpenedBody.value[entryId] = visible;
203
- }
204
204
  </script>
205
205
 
206
206
  <style></style>
@@ -13,11 +13,21 @@
13
13
  </template>
14
14
 
15
15
  <template #side-footer>
16
- <tags-filter
17
- :tags="tags"
18
- @tag:stateChanged="onTagStateChanged" />
16
+ <tags-filter :tags="tagsCount" />
19
17
  </template>
20
18
 
19
+ <div class="ffun-info-good">
20
+ <p
21
+ >You can create new rules on the
22
+ <a
23
+ href="#"
24
+ @click.prevent="goToNews()"
25
+ >news</a
26
+ >
27
+ tab.</p
28
+ >
29
+ </div>
30
+
21
31
  <rules-list
22
32
  v-if="rules"
23
33
  :rules="sortedRules" />
@@ -25,7 +35,8 @@
25
35
  </template>
26
36
 
27
37
  <script lang="ts" setup>
28
- import {computed, ref, onUnmounted, watch} from "vue";
38
+ import {computed, ref, onUnmounted, watch, provide} from "vue";
39
+ import {useRouter} from "vue-router";
29
40
  import {computedAsync} from "@vueuse/core";
30
41
  import {useGlobalSettingsStore} from "@/stores/globalSettings";
31
42
  import _ from "lodash";
@@ -34,19 +45,28 @@
34
45
  import type * as t from "@/logic/types";
35
46
  import * as e from "@/logic/enums";
36
47
  import * as tagsFilterState from "@/logic/tagsFilterState";
48
+
49
+ const router = useRouter();
50
+
37
51
  const tagsStates = ref<tagsFilterState.Storage>(new tagsFilterState.Storage());
38
52
 
53
+ provide("tagsStates", tagsStates);
54
+
39
55
  const globalSettings = useGlobalSettingsStore();
40
56
 
41
57
  globalSettings.mainPanelMode = e.MainPanelMode.Rules;
42
58
 
59
+ function goToNews() {
60
+ router.push({name: e.MainPanelMode.Entries, params: {}});
61
+ }
62
+
43
63
  const rules = computedAsync(async () => {
44
64
  // force refresh
45
65
  globalSettings.dataVersion;
46
66
  return await api.getRules();
47
67
  }, null);
48
68
 
49
- const tags = computed(() => {
69
+ const tagsCount = computed(() => {
50
70
  if (!rules.value) {
51
71
  return {};
52
72
  }
@@ -54,7 +74,11 @@
54
74
  const tags: {[key: string]: number} = {};
55
75
 
56
76
  for (const rule of rules.value) {
57
- for (const tag of rule.tags) {
77
+ for (const tag of rule.requiredTags) {
78
+ tags[tag] = (tags[tag] || 0) + 1;
79
+ }
80
+
81
+ for (const tag of rule.excludedTags) {
58
82
  tags[tag] = (tags[tag] || 0) + 1;
59
83
  }
60
84
  }
@@ -69,7 +93,7 @@
69
93
 
70
94
  let sorted = rules.value.slice();
71
95
 
72
- sorted = tagsStates.value.filterByTags(sorted, (rule) => rule.tags);
96
+ sorted = tagsStates.value.filterByTags(sorted, (rule) => rule.requiredTags.concat(rule.excludedTags));
73
97
 
74
98
  const orderProperties = e.RulesOrderProperties.get(globalSettings.rulesOrder);
75
99
 
@@ -86,14 +110,14 @@
86
110
 
87
111
  sorted = sorted.sort((a: t.Rule, b: t.Rule) => {
88
112
  if (globalSettings.rulesOrder === e.RulesOrder.Tags) {
89
- return utils.compareLexicographically(a.tags, b.tags);
113
+ return utils.compareLexicographically(a.allTags, b.allTags);
90
114
  }
91
115
 
92
116
  const valueA = _.get(a, orderField, null);
93
117
  const valueB = _.get(b, orderField, null);
94
118
 
95
119
  if (valueA === null && valueB === null) {
96
- return utils.compareLexicographically(a.tags, b.tags);
120
+ return utils.compareLexicographically(a.allTags, b.allTags);
97
121
  }
98
122
 
99
123
  if (valueA === null) {
@@ -112,13 +136,9 @@
112
136
  return -1 * direction;
113
137
  }
114
138
 
115
- return utils.compareLexicographically(a.tags, b.tags);
139
+ return utils.compareLexicographically(a.allTags, b.allTags);
116
140
  });
117
141
 
118
142
  return sorted;
119
143
  });
120
-
121
- function onTagStateChanged({tag, state}: {tag: string; state: tagsFilterState.State}) {
122
- tagsStates.value.onTagStateChanged({tag, state});
123
- }
124
144
  </script>