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.
@@ -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
- count-mode="no"
20
- :mode="tagStates[tag]"
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 selectedTags = ref<{[key: string]: boolean}>({});
63
+ const globalSettings = useGlobalSettingsStore();
66
64
 
67
- const tagStates = ref<{[key: string]: tagsFilterState.State}>({});
65
+ const currentScore = ref(1);
68
66
 
69
- const emit = defineEmits(["tag:stateChanged"]);
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 tagComparator(a: string, b: string) {
80
- const aCount = properties.tags[a];
81
- const bCount = properties.tags[b];
78
+ function createTagComparator() {
79
+ const counts = properties.tags;
82
80
 
83
- if (aCount > bCount) {
84
- return -1;
85
- }
81
+ function tagComparator(a: string, b: string) {
82
+ const aCount = counts[a];
83
+ const bCount = counts[b];
86
84
 
87
- if (aCount < bCount) {
88
- return 1;
89
- }
85
+ if (aCount > bCount) {
86
+ return -1;
87
+ }
90
88
 
91
- if (a > b) {
92
- return 1;
93
- }
89
+ if (aCount < bCount) {
90
+ return 1;
91
+ }
94
92
 
95
- if (a < b) {
96
- return -1;
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 0;
104
+ return tagComparator;
100
105
  }
101
106
 
102
107
  const displayedSelectedTags = computed(() => {
103
- let values = Object.keys(selectedTags.value);
108
+ let values = Object.keys(tagsStates.value.selectedTags);
104
109
 
105
- values = values.filter((tag) => {
106
- return selectedTags.value[tag] === true;
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(selectedTags.value).length;
122
+ return Object.keys(properties.tags).length + Object.keys(tagsStates.value.selectedTags).length;
119
123
  });
120
124
 
121
- const displayedTags = computed(() => {
125
+ const _sortedTags = computed(() => {
122
126
  let values = Object.keys(properties.tags);
123
127
 
124
- if (values.length === 0) {
125
- return [];
126
- }
128
+ const comparator = createTagComparator();
127
129
 
128
- values = values.filter((tag) => {
129
- return selectedTags.value[tag] !== true;
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
- function onTagClicked(tag: string) {
150
- const state = tagStates.value[tag] || "none";
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-sm">
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
- count-mode="tooltip"
11
- @tag:clicked="onTagClicked" />
9
+ :secondary-mode="tagMode(tag)"
10
+ count-mode="tooltip" />
12
11
 
13
12
  <a
14
- class="ffun-normal-link"
13
+ class=""
14
+ title="Click on the news title to open it and see all tags"
15
15
  href="#"
16
- v-if="canShowAll"
17
- @click.prevent="showAll = true"
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 showAll = ref(false);
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.value) {
43
+ if (properties.showAll) {
57
44
  return preparedTags.value;
58
45
  }
59
46
 
60
- return preparedTags.value.slice(0, showLimit.value);
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
- <p> Make your first rule to experience the full power of the Feeds Fun! </p>
4
-
5
- <ul class="list-decimal list-inside">
6
- <li>Click any tag under a news item.</li>
7
- <li>Select more tags if needed.</li>
8
- <li>Set a score for the rule.</li>
9
- <li>Click "Create Rule".</li>
10
- <li>See how the news gets organized based on your preferences!</li>
11
- </ul>
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, 377, 610, -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>
@@ -35,8 +35,8 @@
35
35
  class="ffun-form-button p-1 my-1 block w-full text-center"
36
36
  v-if="reloadButton"
37
37
  href="#"
38
- @click="globalSettings.dataVersion += 1"
39
- >Reload</a
38
+ @click="globalSettings.updateDataVersion()"
39
+ >Refresh</a
40
40
  >
41
41
 
42
42
  <hr v-if="hasSideFooter" />
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({tags, score}: {tags: string[]; score: number}) {
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: {tags: tags, score: score}
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({id, tags, score}: {id: t.RuleId; tags: string[]; score: number}) {
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, tags: tags, score: score}
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
- return feeds;
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}) {
@@ -0,0 +1,5 @@
1
+ export function defined<T>(value: T | null | undefined, name?: string): asserts value is T {
2
+ if (value === null || value === undefined) {
3
+ throw new Error(`${name ?? "Value"} is null or undefined`);
4
+ }
5
+ }
@@ -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] = false;
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] = false;
37
+ if (this.requiredTags[tag]) {
38
+ delete this.requiredTags[tag];
39
+ }
23
40
  } else if (state === "none") {
24
- this.excludedTags[tag] = false;
25
- this.requiredTags[tag] = false;
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
- report = report.filter((entity) => {
44
- for (const tag of Object.keys(this.requiredTags)) {
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
  }