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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feeds-fun",
3
- "version": "1.15.0",
3
+ "version": "1.16.1",
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": [
@@ -9,9 +9,7 @@
9
9
  <entry-for-list
10
10
  :entryId="entryId"
11
11
  :time-field="timeField"
12
- :show-tags="showTags"
13
- :tags-count="tagsCount"
14
- @entry:bodyVisibilityChanged="onBodyVisibilityChanged" />
12
+ :tags-count="tagsCount" />
15
13
  </li>
16
14
  </ul>
17
15
 
@@ -35,14 +33,11 @@
35
33
  const properties = defineProps<{
36
34
  entriesIds: Array<t.EntryId>;
37
35
  timeField: string;
38
- showTags: boolean;
39
36
  showFromStart: number;
40
37
  showPerPage: number;
41
38
  tagsCount: {[key: string]: number};
42
39
  }>();
43
40
 
44
- const emit = defineEmits(["entry:bodyVisibilityChanged"]);
45
-
46
41
  const showEntries = ref(properties.showFromStart);
47
42
 
48
43
  const entriesToShow = computed(() => {
@@ -51,14 +46,14 @@
51
46
  }
52
47
  return properties.entriesIds.slice(0, showEntries.value);
53
48
  });
54
-
55
- function onBodyVisibilityChanged({entryId, visible}: {entryId: t.EntryId; visible: boolean}) {
56
- emit("entry:bodyVisibilityChanged", {entryId, visible});
57
- }
58
49
  </script>
59
50
 
60
51
  <style scoped>
52
+ .entry-block {
53
+ }
54
+
61
55
  .entry-block:not(:last-child) {
62
56
  border-bottom-width: 1px;
57
+ @apply py-1;
63
58
  }
64
59
  </style>
@@ -1,5 +1,7 @@
1
1
  <template>
2
- <div class="flex">
2
+ <div
3
+ ref="entryTop"
4
+ class="flex text-lg">
3
5
  <div class="flex-shrink-0 w-8 text-right pr-1">
4
6
  <value-score
5
7
  :value="entry.score"
@@ -9,7 +11,7 @@
9
11
  <div class="flex-shrink-0 w-8 text-right pr-1">
10
12
  <favicon-element
11
13
  :url="entry.url"
12
- class="w-4 h-4 align-text-bottom mx-1 inline" />
14
+ class="w-5 h-5 align-text-bottom mx-1 inline" />
13
15
  </div>
14
16
 
15
17
  <div class="flex-shrink-0 text-right">
@@ -25,16 +27,17 @@
25
27
  <a
26
28
  :href="entry.url"
27
29
  target="_blank"
28
- :class="[{'font-bold': isRead}, 'flex-grow', 'min-w-fit', 'line-clamp-1', 'pr-4', 'mb-0']"
30
+ :class="[{'font-bold': !isRead}, 'flex-grow', 'min-w-fit', 'line-clamp-1', 'pr-4', 'mb-0']"
29
31
  @click="onTitleClick">
30
32
  {{ purifiedTitle }}
31
33
  </a>
32
34
 
33
35
  <tags-list
34
- v-if="showTags"
35
36
  class="mt-0 pt-0"
36
37
  :tags="entry.tags"
37
38
  :tags-count="tagsCount"
39
+ :show-all="showBody"
40
+ @request-to-show-all="entriesStore.displayEntry({entryId: entry.id})"
38
41
  :contributions="entry.scoreContributions" />
39
42
  </div>
40
43
 
@@ -70,7 +73,7 @@
70
73
 
71
74
  <script lang="ts" setup>
72
75
  import _ from "lodash";
73
- import {computed, ref} from "vue";
76
+ import {computed, ref, useTemplateRef} from "vue";
74
77
  import type * as t from "@/logic/types";
75
78
  import * as events from "@/logic/events";
76
79
  import * as e from "@/logic/enums";
@@ -80,15 +83,14 @@
80
83
 
81
84
  const entriesStore = useEntriesStore();
82
85
 
86
+ const topElement = useTemplateRef("entryTop");
87
+
83
88
  const properties = defineProps<{
84
89
  entryId: t.EntryId;
85
90
  timeField: string;
86
- showTags: boolean;
87
91
  tagsCount: {[key: string]: number};
88
92
  }>();
89
93
 
90
- const emit = defineEmits(["entry:bodyVisibilityChanged"]);
91
-
92
94
  const entry = computed(() => {
93
95
  if (properties.entryId in entriesStore.entries) {
94
96
  return entriesStore.entries[properties.entryId];
@@ -98,10 +100,12 @@
98
100
  });
99
101
 
100
102
  const isRead = computed(() => {
101
- return !entriesStore.entries[entry.value.id].hasMarker(e.Marker.Read);
103
+ return entriesStore.entries[entry.value.id].hasMarker(e.Marker.Read);
102
104
  });
103
105
 
104
- const showBody = ref(false);
106
+ const showBody = computed(() => {
107
+ return entry.value.id == entriesStore.displayedEntryId;
108
+ });
105
109
 
106
110
  const timeFor = computed(() => {
107
111
  if (entry.value === null) {
@@ -111,25 +115,6 @@
111
115
  return _.get(entry.value, properties.timeField, null);
112
116
  });
113
117
 
114
- async function displayBody() {
115
- showBody.value = true;
116
-
117
- emit("entry:bodyVisibilityChanged", {entryId: properties.entryId, visible: true});
118
-
119
- if (entry.value === null) {
120
- throw new Error("entry is null");
121
- }
122
-
123
- entriesStore.requestFullEntry({entryId: entry.value.id});
124
-
125
- await events.newsBodyOpened({entryId: entry.value.id});
126
- }
127
-
128
- function hideBody() {
129
- showBody.value = false;
130
- emit("entry:bodyVisibilityChanged", {entryId: properties.entryId, visible: false});
131
- }
132
-
133
118
  const purifiedTitle = computed(() => {
134
119
  if (entry.value === null) {
135
120
  return "";
@@ -166,20 +151,16 @@
166
151
  event.stopPropagation();
167
152
 
168
153
  if (showBody.value) {
169
- hideBody();
154
+ entriesStore.hideEntry({entryId: entry.value.id});
170
155
  } else {
171
- displayBody();
156
+ await entriesStore.displayEntry({entryId: entry.value.id});
157
+
158
+ if (topElement.value) {
159
+ topElement.value.scrollIntoView({behavior: "instant"});
160
+ }
172
161
  }
173
162
  } else {
174
163
  await newsLinkOpenedEvent();
175
164
  }
176
-
177
- // TODO: is it will be too slow?
178
- if (showBody.value) {
179
- await entriesStore.setMarker({
180
- entryId: properties.entryId,
181
- marker: e.Marker.Read
182
- });
183
- }
184
165
  }
185
166
  </script>
@@ -1,35 +1,52 @@
1
1
  <template>
2
- <div
3
- :class="classes"
4
- :title="tooltip"
5
- @click.prevent="onClick()">
6
- <span v-if="countMode == 'prefix'">[{{ count }}]</span>
7
-
8
- {{ tagInfo.name }}
9
-
2
+ <div class="inline-block">
10
3
  <a
11
- v-if="tagInfo.link"
12
- :href="tagInfo.link"
13
- target="_blank"
14
- @click.stop=""
15
- rel="noopener noreferrer">
16
- &#8599;
17
- </a>
4
+ href="#"
5
+ v-if="showSwitch"
6
+ class="pr-1"
7
+ @click.prevent="onRevers()"
8
+ >⇄</a
9
+ >
10
+ <div
11
+ :class="classes"
12
+ :title="tooltip"
13
+ @click.prevent="onClick()">
14
+ <span v-if="countMode == 'prefix'">[{{ count }}]</span>
15
+
16
+ {{ tagInfo.name }}
17
+
18
+ <a
19
+ v-if="tagInfo.link"
20
+ :href="tagInfo.link"
21
+ target="_blank"
22
+ @click.stop=""
23
+ rel="noopener noreferrer">
24
+ &#8599;
25
+ </a>
26
+ </div>
18
27
  </div>
19
28
  </template>
20
29
 
21
30
  <script lang="ts" setup>
22
31
  import * as t from "@/logic/types";
23
- import {computed, ref} from "vue";
32
+ import {computed, ref, inject} from "vue";
33
+ import type {Ref} from "vue";
24
34
  import {useTagsStore} from "@/stores/tags";
35
+ import * as tagsFilterState from "@/logic/tagsFilterState";
36
+ import * as asserts from "@/logic/asserts";
25
37
 
26
38
  const tagsStore = useTagsStore();
27
39
 
40
+ const tagsStates = inject<Ref<tagsFilterState.Storage>>("tagsStates");
41
+
42
+ asserts.defined(tagsStates);
43
+
28
44
  const properties = defineProps<{
29
45
  uid: string;
30
46
  count?: number | null;
31
47
  countMode?: string | null;
32
- mode?: string | null;
48
+ secondaryMode?: string | null;
49
+ showSwitch?: boolean | null;
33
50
  }>();
34
51
 
35
52
  const tagInfo = computed(() => {
@@ -44,27 +61,39 @@
44
61
  return t.noInfoTag(properties.uid);
45
62
  });
46
63
 
47
- const emit = defineEmits(["tag:clicked"]);
48
-
49
64
  const classes = computed(() => {
50
65
  const result: {[key: string]: boolean} = {
51
66
  tag: true
52
67
  };
53
68
 
54
- if (properties.mode) {
55
- result[properties.mode] = true;
69
+ if (tagsStates.value.requiredTags[properties.uid]) {
70
+ result["required"] = true;
71
+ }
72
+
73
+ if (tagsStates.value.excludedTags[properties.uid]) {
74
+ result["excluded"] = true;
75
+ }
76
+
77
+ if (properties.secondaryMode) {
78
+ result[properties.secondaryMode] = true;
56
79
  }
57
80
 
58
81
  return result;
59
82
  });
60
83
 
61
84
  function onClick() {
62
- emit("tag:clicked", properties.uid);
85
+ asserts.defined(tagsStates);
86
+ tagsStates.value.onTagClicked({tag: properties.uid});
87
+ }
88
+
89
+ function onRevers() {
90
+ asserts.defined(tagsStates);
91
+ tagsStates.value.onTagReversed({tag: properties.uid});
63
92
  }
64
93
 
65
94
  const tooltip = computed(() => {
66
95
  if (properties.countMode == "tooltip" && properties.count) {
67
- return `articles with the tag: ${properties.count} (${properties.uid})`;
96
+ return `articles with this tag: ${properties.count}`;
68
97
  }
69
98
  return "";
70
99
  });
@@ -72,19 +101,15 @@
72
101
 
73
102
  <style scoped>
74
103
  .tag {
75
- @apply inline-block cursor-pointer p-0 mr-2 whitespace-nowrap;
76
- }
77
-
78
- .tag.selected {
79
- @apply font-bold text-purple-700;
104
+ @apply inline-block cursor-pointer p-0 mr-2 whitespace-nowrap hover:bg-green-100 px-1 hover:rounded-lg;
80
105
  }
81
106
 
82
107
  .tag.required {
83
- @apply text-green-700;
108
+ @apply text-green-700 font-bold;
84
109
  }
85
110
 
86
111
  .tag.excluded {
87
- @apply text-red-700;
112
+ @apply text-red-700 font-bold;
88
113
  }
89
114
 
90
115
  .tag.positive {
@@ -1,42 +1,73 @@
1
1
  <template>
2
- <div class="">
3
- <score-selector
4
- class="inline-block mr-2"
5
- v-model="currentScore" />
6
-
7
- <a
8
- class="ffun-form-button"
9
- href="#"
10
- v-if="canCreateRule"
11
- @click.prevent="createOrUpdateRule()"
12
- >create rule</a
13
- >
14
-
15
- <p class="ffun-info-attention"> The news list will be updated right after you create a rule. </p>
2
+ <div>
3
+ <div
4
+ v-if="tagsStates.hasSelectedTags"
5
+ class="flex items-center">
6
+ <div class="flex-none">
7
+ <score-selector
8
+ class="inline-block mr-2 my-auto"
9
+ v-model="currentScore" />
10
+ </div>
11
+
12
+ <a
13
+ class="ffun-form-button p-1 my-1 block text-center inline-block flex-grow"
14
+ href="#"
15
+ @click.prevent="createOrUpdateRule()"
16
+ >Create Rule</a
17
+ >
18
+ </div>
19
+
20
+ <p
21
+ class="ffun-info-good"
22
+ v-else>
23
+ <template v-if="showSuccess"> Rule created. </template>
24
+ <template v-else> Select tags to create a rule. </template>
25
+ </p>
16
26
  </div>
17
27
  </template>
18
28
 
19
29
  <script lang="ts" setup>
20
- import {computed, ref} from "vue";
21
- import {useGlobalSettingsStore} from "@/stores/globalSettings";
30
+ import {computed, ref, inject, watch} from "vue";
31
+ import type {Ref} from "vue";
32
+ import {useTagsStore} from "@/stores/tags";
33
+ import type * as tagsFilterState from "@/logic/tagsFilterState";
34
+ import * as asserts from "@/logic/asserts";
22
35
  import * as api from "@/logic/api";
23
- const properties = defineProps<{tags: string[]}>();
36
+ import {useGlobalSettingsStore} from "@/stores/globalSettings";
37
+
38
+ const tagsStore = useTagsStore();
24
39
 
25
40
  const globalSettings = useGlobalSettingsStore();
26
41
 
27
- const emit = defineEmits(["rule-constructor:created"]);
42
+ const currentScore = ref(1);
28
43
 
29
- // fibonacci numbers
30
- const scores = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610];
44
+ const showSuccess = ref(false);
31
45
 
32
- const currentScore = ref(1);
46
+ const tagsStates = inject<Ref<tagsFilterState.Storage>>("tagsStates");
47
+ asserts.defined(tagsStates);
33
48
 
34
- const canCreateRule = computed(() => {
35
- return properties.tags.length > 0;
36
- });
49
+ watch(
50
+ () => tagsStates.value.hasSelectedTags,
51
+ () => {
52
+ // This condition is needed to prevent immediate reset of the success message
53
+ // right after the rule is created in createOrUpdateRule
54
+ if (tagsStates.value.hasSelectedTags) {
55
+ showSuccess.value = false;
56
+ }
57
+ }
58
+ );
37
59
 
38
60
  async function createOrUpdateRule() {
39
- await api.createOrUpdateRule({tags: properties.tags, score: currentScore.value});
61
+ asserts.defined(tagsStates);
62
+ await api.createOrUpdateRule({
63
+ requiredTags: Object.keys(tagsStates.value.requiredTags),
64
+ excludedTags: Object.keys(tagsStates.value.excludedTags),
65
+ score: currentScore.value
66
+ });
67
+
68
+ tagsStates.value.clear();
69
+
70
+ showSuccess.value = true;
40
71
 
41
72
  // this line leads to the reloading of news and any other data
42
73
  // not an elegant solution, but it works with the current API implementation
@@ -44,7 +75,5 @@
44
75
  // - without reloading
45
76
  // - maybe, without reordering too
46
77
  globalSettings.updateDataVersion();
47
-
48
- emit("rule-constructor:created");
49
78
  }
50
79
  </script>
@@ -19,9 +19,19 @@
19
19
 
20
20
  <div class="flex-grow">
21
21
  <template
22
- v-for="tag of rule.tags"
22
+ v-for="tag of rule.requiredTags"
23
23
  :key="tag">
24
- <ffun-tag :uid="tag" />&nbsp;
24
+ <ffun-tag
25
+ :uid="tag"
26
+ secondary-mode="positive" />&nbsp;
27
+ </template>
28
+
29
+ <template
30
+ v-for="tag of rule.excludedTags"
31
+ :key="tag">
32
+ <ffun-tag
33
+ :uid="tag"
34
+ secondary-mode="negative" />&nbsp;
25
35
  </template>
26
36
  </div>
27
37
  </div>
@@ -67,7 +77,8 @@
67
77
  await api.updateRule({
68
78
  id: properties.rule.id,
69
79
  score: newScore,
70
- tags: properties.rule.tags
80
+ requiredTags: properties.rule.requiredTags,
81
+ excludedTags: properties.rule.excludedTags
71
82
  });
72
83
 
73
84
  scoreChanged.value = true;
@@ -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>