feeds-fun 1.15.0 → 1.16.1

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.
@@ -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>
@@ -36,7 +36,7 @@
36
36
  v-if="reloadButton"
37
37
  href="#"
38
38
  @click="globalSettings.updateDataVersion()"
39
- >Reload</a
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
  }
@@ -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
  }
@@ -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)
@@ -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"));
@@ -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>