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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feeds-fun",
3
- "version": "1.14.2",
3
+ "version": "1.16.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": [
@@ -24,30 +24,44 @@
24
24
 
25
25
  <div v-else-if="foundFeeds === null"></div>
26
26
 
27
- <p
27
+ <div
28
28
  v-else-if="foundFeeds.length === 0"
29
- class="ffun-info-attention"
30
- >No feeds found.</p
31
- >
29
+ class="ffun-info-attention">
30
+ <p
31
+ class="ffun-info-error"
32
+ v-for="message in messages">
33
+ {{ message.message }}
34
+ </p>
35
+
36
+ <p v-if="messages.length === 0"> No feeds found. </p>
37
+ </div>
32
38
 
33
39
  <div
34
40
  v-for="feed in foundFeeds"
35
41
  :key="feed.url">
36
42
  <feed-info :feed="feed" />
37
43
 
38
- <button
39
- class="ffun-form-button"
40
- v-if="!addedFeeds[feed.url]"
41
- :disabled="disableInputs"
42
- @click.prevent="addFeed(feed.url)">
43
- Add
44
- </button>
45
-
46
44
  <p
47
- v-else
48
- class="ffun-info-good"
49
- >Feed added</p
50
- >
45
+ v-if="feed.isLinked"
46
+ class="ffun-info-good">
47
+ You are already subscribed to this feed.
48
+ </p>
49
+
50
+ <template v-else>
51
+ <button
52
+ class="ffun-form-button"
53
+ v-if="!addedFeeds[feed.url]"
54
+ :disabled="disableInputs"
55
+ @click.prevent="addFeed(feed.url)">
56
+ Add
57
+ </button>
58
+
59
+ <p
60
+ v-else
61
+ class="ffun-info-good"
62
+ >Feed added</p
63
+ >
64
+ </template>
51
65
  </div>
52
66
  </div>
53
67
  </template>
@@ -74,17 +88,22 @@
74
88
 
75
89
  const addedFeeds = ref<{[key: string]: boolean}>({});
76
90
 
91
+ let messages = ref<t.ApiMessage[]>([]);
92
+
77
93
  const foundFeeds = computedAsync(async () => {
78
94
  if (searhedUrl.value === "") {
79
95
  return null;
80
96
  }
81
97
 
82
98
  searching.value = true;
99
+ messages.value = [];
83
100
 
84
101
  let feeds: t.FeedInfo[] = [];
85
102
 
86
103
  try {
87
- feeds = await api.discoverFeeds({url: searhedUrl.value});
104
+ const answer = await api.discoverFeeds({url: searhedUrl.value});
105
+ feeds = answer.feeds;
106
+ messages.value = answer.messages;
88
107
  } catch (e) {
89
108
  console.error(e);
90
109
  }
@@ -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 {
@@ -43,6 +43,9 @@
43
43
  import * as api from "@/logic/api";
44
44
  import {computedAsync} from "@vueuse/core";
45
45
  import {useEntriesStore} from "@/stores/entries";
46
+ import {useGlobalSettingsStore} from "@/stores/globalSettings";
47
+
48
+ const globalSettings = useGlobalSettingsStore();
46
49
 
47
50
  const opmlFile = ref<File | null>(null);
48
51
 
@@ -76,6 +79,10 @@
76
79
  try {
77
80
  await api.addOPML({content: content});
78
81
 
82
+ // loading an OPML file is pretty rare and significantly changes the list of feeds
83
+ // => we can force data to be reloaded
84
+ globalSettings.updateDataVersion();
85
+
79
86
  loading.value = false;
80
87
  loaded.value = true;
81
88
  error.value = false;
@@ -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;