feeds-fun 1.11.1 → 1.12.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 +1 -1
- package/src/components/DiscoveryForm.vue +5 -2
- package/src/components/FeedForList.vue +19 -7
- package/src/components/{OpenaiTokensUsage.vue → TokensCost.vue} +19 -9
- package/src/components/UserSettingForNotification.vue +1 -1
- package/src/components/collections/Block.vue +97 -0
- package/src/components/collections/BlockItem.vue +45 -0
- package/src/components/collections/DetailedItem.vue +115 -0
- package/src/components/collections/FeedItem.vue +62 -0
- package/src/components/collections/Notification.vue +11 -0
- package/src/components/collections/SubscribingProgress.vue +41 -0
- package/src/components/collections/Warning.vue +70 -0
- package/src/components/notifications/ApiKey.vue +42 -0
- package/src/components/notifications/Block.vue +61 -0
- package/src/components/{NotificationCreateRuleHelp.vue → notifications/CreateRuleHelp.vue} +2 -2
- package/src/layouts/SidePanelLayout.vue +0 -2
- package/src/logic/api.ts +31 -7
- package/src/logic/types.ts +108 -11
- package/src/main.ts +26 -12
- package/src/stores/collections.ts +53 -0
- package/src/stores/feeds.ts +48 -0
- package/src/style.css +17 -9
- package/src/views/CollectionsView.vue +21 -4
- package/src/views/DiscoveryView.vue +6 -0
- package/src/views/FeedsView.vue +11 -12
- package/src/views/MainView.vue +7 -0
- package/src/views/NewsView.vue +3 -2
- package/src/views/SettingsView.vue +29 -21
- package/src/components/FeedsCollections.vue +0 -102
- package/src/components/NotificationCollections.vue +0 -7
- package/src/components/NotificationOpenaiApiKey.vue +0 -28
- package/src/components/Notifications.vue +0 -43
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<notifications-api-key v-if="showAPIKeyNotification" />
|
|
3
|
+
<collections-notification v-if="showCollectionsNotification" />
|
|
4
|
+
<notifications-create-rule-help v-if="showCreateRuleHelpNotification" />
|
|
5
|
+
<collections-warning v-if="showCollectionsWarning" />
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script lang="ts" setup>
|
|
9
|
+
import {computed, ref, onUnmounted, watch} from "vue";
|
|
10
|
+
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
11
|
+
import {useCollectionsStore} from "@/stores/collections";
|
|
12
|
+
|
|
13
|
+
const properties = defineProps<{
|
|
14
|
+
apiKey: boolean;
|
|
15
|
+
createRuleHelp: boolean;
|
|
16
|
+
collectionsNotification_: boolean;
|
|
17
|
+
collectionsWarning_: boolean;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const collections = useCollectionsStore();
|
|
21
|
+
const globalSettings = useGlobalSettingsStore();
|
|
22
|
+
|
|
23
|
+
const showApiKeyMessage = computed(() => {
|
|
24
|
+
return (
|
|
25
|
+
globalSettings.userSettings &&
|
|
26
|
+
!globalSettings.userSettings.openai_api_key.value &&
|
|
27
|
+
!globalSettings.userSettings.gemini_api_key.value &&
|
|
28
|
+
!globalSettings.userSettings.hide_message_about_setting_up_key.value
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const showCollectionsNotification = computed(() => {
|
|
33
|
+
return (
|
|
34
|
+
properties.collectionsNotification_ &&
|
|
35
|
+
globalSettings.userSettings &&
|
|
36
|
+
!globalSettings.userSettings.hide_message_about_adding_collections.value
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const showCreateRuleHelpNotification = computed(() => {
|
|
41
|
+
return !showCollectionsNotification.value && properties.createRuleHelp;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const showAPIKeyNotification = computed(() => {
|
|
45
|
+
return (
|
|
46
|
+
!showCollectionsNotification.value &&
|
|
47
|
+
!showCreateRuleHelpNotification.value &&
|
|
48
|
+
properties.apiKey &&
|
|
49
|
+
showApiKeyMessage.value
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const showCollectionsWarning = computed(() => {
|
|
54
|
+
return (
|
|
55
|
+
properties.collectionsWarning_ &&
|
|
56
|
+
!showCollectionsNotification.value &&
|
|
57
|
+
globalSettings.userSettings &&
|
|
58
|
+
!globalSettings.userSettings.hide_message_check_your_feed_urls.value
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
</script>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="ffun-info-attention">
|
|
3
|
-
<p> Make your first rule to experience the
|
|
3
|
+
<p> Make your first rule to experience the full power of the Feeds Fun! </p>
|
|
4
4
|
|
|
5
5
|
<ul class="list-decimal list-inside">
|
|
6
|
-
<li>Click
|
|
6
|
+
<li>Click any tag under a news item.</li>
|
|
7
7
|
<li>Select more tags if needed.</li>
|
|
8
8
|
<li>Set a score for the rule.</li>
|
|
9
9
|
<li>Click "Create Rule".</li>
|
|
@@ -149,13 +149,11 @@
|
|
|
149
149
|
import {useRouter, RouterLink, RouterView} from "vue-router";
|
|
150
150
|
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
151
151
|
import {useGlobalState} from "@/stores/globalState";
|
|
152
|
-
import {useEntriesStore} from "@/stores/entries";
|
|
153
152
|
import {useSupertokens} from "@/stores/supertokens";
|
|
154
153
|
import * as e from "@/logic/enums";
|
|
155
154
|
import * as settings from "@/logic/settings";
|
|
156
155
|
|
|
157
156
|
const globalSettings = useGlobalSettingsStore();
|
|
158
|
-
const entriesStore = useEntriesStore();
|
|
159
157
|
const supertokens = useSupertokens();
|
|
160
158
|
const globalState = useGlobalState();
|
|
161
159
|
|
package/src/logic/api.ts
CHANGED
|
@@ -20,8 +20,9 @@ const API_DISCOVER_FEEDS = `${ENTRY_POINT}/discover-feeds`;
|
|
|
20
20
|
const API_ADD_FEED = `${ENTRY_POINT}/add-feed`;
|
|
21
21
|
const API_ADD_OPML = `${ENTRY_POINT}/add-opml`;
|
|
22
22
|
const API_UNSUBSCRIBE = `${ENTRY_POINT}/unsubscribe`;
|
|
23
|
-
const
|
|
24
|
-
const
|
|
23
|
+
const API_GET_COLLECTIONS = `${ENTRY_POINT}/get-collections`;
|
|
24
|
+
const API_GET_COLLECTION_FEEDS = `${ENTRY_POINT}/get-collection-feeds`;
|
|
25
|
+
const API_SUBSCRIBE_TO_COLLECTIONS = `${ENTRY_POINT}/subscribe-to-collections`;
|
|
25
26
|
const API_GET_TAGS_INFO = `${ENTRY_POINT}/get-tags-info`;
|
|
26
27
|
const API_GET_USER_SETTINGS = `${ENTRY_POINT}/get-user-settings`;
|
|
27
28
|
const API_SET_USER_SETTING = `${ENTRY_POINT}/set-user-setting`;
|
|
@@ -186,15 +187,38 @@ export async function unsubscribe({feedId}: {feedId: t.FeedId}) {
|
|
|
186
187
|
await post({url: API_UNSUBSCRIBE, data: {feedId: feedId}});
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
export async function
|
|
190
|
-
const response = await post({url:
|
|
190
|
+
export async function getCollections() {
|
|
191
|
+
const response = await post({url: API_GET_COLLECTIONS, data: {}});
|
|
191
192
|
|
|
192
|
-
|
|
193
|
+
const collections = [];
|
|
194
|
+
|
|
195
|
+
for (let rawCollection of response.collections) {
|
|
196
|
+
const collection = t.collectionFromJSON(rawCollection);
|
|
197
|
+
collections.push(collection);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return collections;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function getCollectionFeeds({collectionId}: {collectionId: t.CollectionId}) {
|
|
204
|
+
const response = await post({
|
|
205
|
+
url: API_GET_COLLECTION_FEEDS,
|
|
206
|
+
data: {collectionId: collectionId}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const feeds = [];
|
|
210
|
+
|
|
211
|
+
for (let rawFeed of response.feeds) {
|
|
212
|
+
const feed = t.collectionFeedInfoFromJSON(rawFeed);
|
|
213
|
+
feeds.push(feed);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return feeds;
|
|
193
217
|
}
|
|
194
218
|
|
|
195
|
-
export async function
|
|
219
|
+
export async function subscribeToCollections({collectionsIds}: {collectionsIds: t.CollectionId[]}) {
|
|
196
220
|
await post({
|
|
197
|
-
url:
|
|
221
|
+
url: API_SUBSCRIBE_TO_COLLECTIONS,
|
|
198
222
|
data: {collections: collectionsIds}
|
|
199
223
|
});
|
|
200
224
|
}
|
package/src/logic/types.ts
CHANGED
|
@@ -18,10 +18,10 @@ export function toRuleId(id: string): RuleId {
|
|
|
18
18
|
return id as RuleId;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export type
|
|
21
|
+
export type CollectionId = string & {readonly __brand: unique symbol};
|
|
22
22
|
|
|
23
|
-
export function
|
|
24
|
-
return id as
|
|
23
|
+
export function toCollectionId(id: string): CollectionId {
|
|
24
|
+
return id as CollectionId;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export type URL = string & {readonly __brand: unique symbol};
|
|
@@ -40,6 +40,7 @@ export class Feed {
|
|
|
40
40
|
readonly loadedAt: Date | null;
|
|
41
41
|
readonly linkedAt: Date;
|
|
42
42
|
readonly isOk: boolean;
|
|
43
|
+
readonly collectionIds: CollectionId[];
|
|
43
44
|
|
|
44
45
|
constructor({
|
|
45
46
|
id,
|
|
@@ -50,7 +51,8 @@ export class Feed {
|
|
|
50
51
|
lastError,
|
|
51
52
|
loadedAt,
|
|
52
53
|
linkedAt,
|
|
53
|
-
isOk
|
|
54
|
+
isOk,
|
|
55
|
+
collectionIds
|
|
54
56
|
}: {
|
|
55
57
|
id: FeedId;
|
|
56
58
|
title: string | null;
|
|
@@ -61,6 +63,7 @@ export class Feed {
|
|
|
61
63
|
loadedAt: Date | null;
|
|
62
64
|
linkedAt: Date;
|
|
63
65
|
isOk: boolean;
|
|
66
|
+
collectionIds: CollectionId[];
|
|
64
67
|
}) {
|
|
65
68
|
this.id = id;
|
|
66
69
|
this.title = title;
|
|
@@ -71,6 +74,7 @@ export class Feed {
|
|
|
71
74
|
this.loadedAt = loadedAt;
|
|
72
75
|
this.linkedAt = linkedAt;
|
|
73
76
|
this.isOk = isOk;
|
|
77
|
+
this.collectionIds = collectionIds;
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -82,7 +86,8 @@ export function feedFromJSON({
|
|
|
82
86
|
state,
|
|
83
87
|
lastError,
|
|
84
88
|
loadedAt,
|
|
85
|
-
linkedAt
|
|
89
|
+
linkedAt,
|
|
90
|
+
collectionIds
|
|
86
91
|
}: {
|
|
87
92
|
id: string;
|
|
88
93
|
title: string;
|
|
@@ -92,6 +97,7 @@ export function feedFromJSON({
|
|
|
92
97
|
lastError: string | null;
|
|
93
98
|
loadedAt: string;
|
|
94
99
|
linkedAt: string;
|
|
100
|
+
collectionIds: string[];
|
|
95
101
|
}): Feed {
|
|
96
102
|
return {
|
|
97
103
|
id: toFeedId(id),
|
|
@@ -102,7 +108,8 @@ export function feedFromJSON({
|
|
|
102
108
|
lastError: lastError,
|
|
103
109
|
loadedAt: loadedAt !== null ? new Date(loadedAt) : null,
|
|
104
110
|
linkedAt: new Date(linkedAt),
|
|
105
|
-
isOk: state === "loaded"
|
|
111
|
+
isOk: state === "loaded",
|
|
112
|
+
collectionIds: collectionIds.map(toCollectionId)
|
|
106
113
|
};
|
|
107
114
|
}
|
|
108
115
|
|
|
@@ -348,7 +355,13 @@ export function userSettingFromJSON({
|
|
|
348
355
|
name: string;
|
|
349
356
|
description: string;
|
|
350
357
|
}): UserSetting {
|
|
351
|
-
return {
|
|
358
|
+
return {
|
|
359
|
+
kind,
|
|
360
|
+
type,
|
|
361
|
+
value: type === "decimal" ? parseFloat(value as string) : value,
|
|
362
|
+
name,
|
|
363
|
+
description
|
|
364
|
+
};
|
|
352
365
|
}
|
|
353
366
|
|
|
354
367
|
export class ResourceHistoryRecord {
|
|
@@ -373,12 +386,96 @@ export function resourceHistoryRecordFromJSON({
|
|
|
373
386
|
reserved
|
|
374
387
|
}: {
|
|
375
388
|
intervalStartedAt: string;
|
|
376
|
-
used: number;
|
|
377
|
-
reserved: number;
|
|
389
|
+
used: number | string;
|
|
390
|
+
reserved: number | string;
|
|
378
391
|
}): ResourceHistoryRecord {
|
|
379
392
|
return new ResourceHistoryRecord({
|
|
380
393
|
intervalStartedAt: new Date(intervalStartedAt),
|
|
381
|
-
|
|
382
|
-
|
|
394
|
+
// TODO: refactor to use kind of Decimals and to respect input types
|
|
395
|
+
used: parseFloat(used as string),
|
|
396
|
+
reserved: parseFloat(reserved as string)
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export class Collection {
|
|
401
|
+
readonly id: CollectionId;
|
|
402
|
+
readonly guiOrder: number;
|
|
403
|
+
readonly name: string;
|
|
404
|
+
readonly description: string;
|
|
405
|
+
readonly feedsNumber: number;
|
|
406
|
+
|
|
407
|
+
constructor({
|
|
408
|
+
id,
|
|
409
|
+
guiOrder,
|
|
410
|
+
name,
|
|
411
|
+
description,
|
|
412
|
+
feedsNumber
|
|
413
|
+
}: {
|
|
414
|
+
id: CollectionId;
|
|
415
|
+
guiOrder: number;
|
|
416
|
+
name: string;
|
|
417
|
+
description: string;
|
|
418
|
+
feedsNumber: number;
|
|
419
|
+
}) {
|
|
420
|
+
this.id = id;
|
|
421
|
+
this.guiOrder = guiOrder;
|
|
422
|
+
this.name = name;
|
|
423
|
+
this.description = description;
|
|
424
|
+
this.feedsNumber = feedsNumber;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function collectionFromJSON({
|
|
429
|
+
id,
|
|
430
|
+
guiOrder,
|
|
431
|
+
name,
|
|
432
|
+
description,
|
|
433
|
+
feedsNumber
|
|
434
|
+
}: {
|
|
435
|
+
id: string;
|
|
436
|
+
guiOrder: number;
|
|
437
|
+
name: string;
|
|
438
|
+
description: string;
|
|
439
|
+
feedsNumber: number;
|
|
440
|
+
}): Collection {
|
|
441
|
+
return {
|
|
442
|
+
id: toCollectionId(id),
|
|
443
|
+
guiOrder: guiOrder,
|
|
444
|
+
name: name,
|
|
445
|
+
description: description,
|
|
446
|
+
feedsNumber: feedsNumber
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export class CollectionFeedInfo {
|
|
451
|
+
readonly url: URL;
|
|
452
|
+
readonly title: string;
|
|
453
|
+
readonly description: string;
|
|
454
|
+
readonly id: FeedId;
|
|
455
|
+
|
|
456
|
+
constructor({url, title, description, id}: {url: URL; title: string; description: string; id: FeedId}) {
|
|
457
|
+
this.url = url;
|
|
458
|
+
this.title = title;
|
|
459
|
+
this.description = description;
|
|
460
|
+
this.id = id;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function collectionFeedInfoFromJSON({
|
|
465
|
+
url,
|
|
466
|
+
title,
|
|
467
|
+
description,
|
|
468
|
+
id
|
|
469
|
+
}: {
|
|
470
|
+
url: string;
|
|
471
|
+
title: string;
|
|
472
|
+
description: string;
|
|
473
|
+
id: string;
|
|
474
|
+
}): CollectionFeedInfo {
|
|
475
|
+
return new CollectionFeedInfo({
|
|
476
|
+
url: toURL(url),
|
|
477
|
+
title: title,
|
|
478
|
+
description: description,
|
|
479
|
+
id: toFeedId(id)
|
|
383
480
|
});
|
|
384
481
|
}
|
package/src/main.ts
CHANGED
|
@@ -20,19 +20,26 @@ import FeedInfo from "./components/FeedInfo.vue";
|
|
|
20
20
|
import OpmlUpload from "./components/OPMLUpload.vue";
|
|
21
21
|
import FeedForList from "./components/FeedForList.vue";
|
|
22
22
|
import SupertokensLogin from "./components/SupertokensLogin.vue";
|
|
23
|
-
import FeedsCollections from "./components/FeedsCollections.vue";
|
|
24
23
|
import FfunTag from "./components/FfunTag.vue";
|
|
25
24
|
import SimplePagination from "./components/SimplePagination.vue";
|
|
26
25
|
import UserSetting from "./components/UserSetting.vue";
|
|
27
|
-
import
|
|
26
|
+
import TokensCost from "./components/TokensCost.vue";
|
|
28
27
|
import FaviconElement from "./components/FaviconElement.vue";
|
|
29
|
-
import NotificationCollections from "./components/NotificationCollections.vue";
|
|
30
|
-
import NotificationOpenaiApiKey from "./components/NotificationOpenaiApiKey.vue";
|
|
31
|
-
import NotificationCreateRuleHelp from "./components/NotificationCreateRuleHelp.vue";
|
|
32
|
-
import Notifications from "./components/Notifications.vue";
|
|
33
28
|
import RuleForList from "./components/RuleForList.vue";
|
|
34
29
|
import UserSettingForNotification from "./components/UserSettingForNotification.vue";
|
|
35
30
|
|
|
31
|
+
import NotificationsApiKey from "./components/notifications/ApiKey.vue";
|
|
32
|
+
import NotificationsCreateRuleHelp from "./components/notifications/CreateRuleHelp.vue";
|
|
33
|
+
import Notifications from "./components/notifications/Block.vue";
|
|
34
|
+
|
|
35
|
+
import CollectionsNotification from "./components/collections/Notification.vue";
|
|
36
|
+
import CollectionsWarning from "./components/collections/Warning.vue";
|
|
37
|
+
import CollectionsBlock from "./components/collections/Block.vue";
|
|
38
|
+
import CollectionsBlockItem from "./components/collections/BlockItem.vue";
|
|
39
|
+
import CollectionsDetailedItem from "./components/collections/DetailedItem.vue";
|
|
40
|
+
import CollectionsSubscribingProgress from "./components/collections/SubscribingProgress.vue";
|
|
41
|
+
import CollectionsFeedItem from "./components/collections/FeedItem.vue";
|
|
42
|
+
|
|
36
43
|
import ScoreSelector from "./inputs/ScoreSelector.vue";
|
|
37
44
|
import InputMarker from "./inputs/Marker.vue";
|
|
38
45
|
|
|
@@ -64,19 +71,26 @@ app.component("FeedInfo", FeedInfo);
|
|
|
64
71
|
app.component("OpmlUpload", OpmlUpload);
|
|
65
72
|
app.component("FeedForList", FeedForList);
|
|
66
73
|
app.component("SupertokensLogin", SupertokensLogin);
|
|
67
|
-
app.component("FeedsCollections", FeedsCollections);
|
|
68
74
|
app.component("FfunTag", FfunTag);
|
|
69
75
|
app.component("SimplePagination", SimplePagination);
|
|
70
76
|
app.component("UserSetting", UserSetting);
|
|
71
|
-
app.component("
|
|
77
|
+
app.component("TokensCost", TokensCost);
|
|
72
78
|
app.component("FaviconElement", FaviconElement);
|
|
73
|
-
app.component("NotificationCollections", NotificationCollections);
|
|
74
|
-
app.component("NotificationOpenaiApiKey", NotificationOpenaiApiKey);
|
|
75
|
-
app.component("NotificationCreateRuleHelp", NotificationCreateRuleHelp);
|
|
76
|
-
app.component("Notifications", Notifications);
|
|
77
79
|
app.component("RuleForList", RuleForList);
|
|
78
80
|
app.component("UserSettingForNotification", UserSettingForNotification);
|
|
79
81
|
|
|
82
|
+
app.component("NotificationsApiKey", NotificationsApiKey);
|
|
83
|
+
app.component("NotificationsCreateRuleHelp", NotificationsCreateRuleHelp);
|
|
84
|
+
app.component("Notifications", Notifications);
|
|
85
|
+
|
|
86
|
+
app.component("CollectionsNotification", CollectionsNotification);
|
|
87
|
+
app.component("CollectionsWarning", CollectionsWarning);
|
|
88
|
+
app.component("CollectionsBlock", CollectionsBlock);
|
|
89
|
+
app.component("CollectionsBlockItem", CollectionsBlockItem);
|
|
90
|
+
app.component("CollectionsDetailedItem", CollectionsDetailedItem);
|
|
91
|
+
app.component("CollectionsSubscribingProgress", CollectionsSubscribingProgress);
|
|
92
|
+
app.component("CollectionsFeedItem", CollectionsFeedItem);
|
|
93
|
+
|
|
80
94
|
app.component("ScoreSelector", ScoreSelector);
|
|
81
95
|
app.component("InputMarker", InputMarker);
|
|
82
96
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {computed, ref, watch} from "vue";
|
|
2
|
+
import {useRouter} from "vue-router";
|
|
3
|
+
import {defineStore} from "pinia";
|
|
4
|
+
|
|
5
|
+
import type * as t from "@/logic/types";
|
|
6
|
+
import * as e from "@/logic/enums";
|
|
7
|
+
import * as api from "@/logic/api";
|
|
8
|
+
import {Timer} from "@/logic/timer";
|
|
9
|
+
import {computedAsync} from "@vueuse/core";
|
|
10
|
+
|
|
11
|
+
export const useCollectionsStore = defineStore("collectionsStore", () => {
|
|
12
|
+
const feeds = ref<{[id: t.CollectionId]: t.CollectionFeedInfo[]}>({});
|
|
13
|
+
|
|
14
|
+
const collections = computedAsync(async () => {
|
|
15
|
+
const collectionsList = await api.getCollections();
|
|
16
|
+
|
|
17
|
+
const collections: {[key: t.CollectionId]: t.Collection} = {};
|
|
18
|
+
|
|
19
|
+
for (const collection of collectionsList) {
|
|
20
|
+
collections[collection.id] = collection;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return collections;
|
|
24
|
+
}, {});
|
|
25
|
+
|
|
26
|
+
const collectionsOrder = computed(() => {
|
|
27
|
+
const order = Object.keys(collections.value) as t.CollectionId[];
|
|
28
|
+
|
|
29
|
+
order.sort((a, b) => {
|
|
30
|
+
return collections.value[a].guiOrder - collections.value[b].guiOrder;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return order;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function getFeeds({collectionId}: {collectionId: t.CollectionId}) {
|
|
37
|
+
if (collectionId in feeds.value) {
|
|
38
|
+
return feeds.value[collectionId];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const feedsList = await api.getCollectionFeeds({collectionId: collectionId});
|
|
42
|
+
|
|
43
|
+
feeds.value[collectionId] = feedsList;
|
|
44
|
+
|
|
45
|
+
return feeds.value[collectionId];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
collections,
|
|
50
|
+
collectionsOrder,
|
|
51
|
+
getFeeds
|
|
52
|
+
};
|
|
53
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {computed, ref, watch} from "vue";
|
|
2
|
+
import {useRouter} from "vue-router";
|
|
3
|
+
import {defineStore} from "pinia";
|
|
4
|
+
|
|
5
|
+
import type * as t from "@/logic/types";
|
|
6
|
+
import * as e from "@/logic/enums";
|
|
7
|
+
import * as api from "@/logic/api";
|
|
8
|
+
import {Timer} from "@/logic/timer";
|
|
9
|
+
import {computedAsync} from "@vueuse/core";
|
|
10
|
+
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
11
|
+
|
|
12
|
+
export const useFeedsStore = defineStore("feedsStore", () => {
|
|
13
|
+
const globalSettings = useGlobalSettingsStore();
|
|
14
|
+
|
|
15
|
+
const feeds = computedAsync(async () => {
|
|
16
|
+
// force refresh
|
|
17
|
+
globalSettings.dataVersion;
|
|
18
|
+
|
|
19
|
+
const feedsList = await api.getFeeds();
|
|
20
|
+
|
|
21
|
+
const feedsDict: {[key: t.FeedId]: t.Feed} = {};
|
|
22
|
+
|
|
23
|
+
for (const feed of feedsList) {
|
|
24
|
+
feedsDict[feed.id] = feed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return feedsDict;
|
|
28
|
+
}, {});
|
|
29
|
+
|
|
30
|
+
async function unsubscribe(feedId: t.FeedId) {
|
|
31
|
+
await api.unsubscribe({feedId: feedId});
|
|
32
|
+
globalSettings.updateDataVersion();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function subscribe(url: t.URL) {
|
|
36
|
+
await api.addFeed({
|
|
37
|
+
url: url
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
globalSettings.updateDataVersion();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
feeds,
|
|
45
|
+
unsubscribe,
|
|
46
|
+
subscribe
|
|
47
|
+
};
|
|
48
|
+
});
|
package/src/style.css
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
@apply text-2xl font-semibold leading-relaxed my-3;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
h3 {
|
|
21
|
+
@apply text-xl font-semibold leading-relaxed my-2;
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
hr {
|
|
21
25
|
@apply border-slate-400 my-2;
|
|
22
26
|
}
|
|
@@ -32,24 +36,32 @@
|
|
|
32
36
|
@apply text-blue-600 hover:text-blue-800 cursor-pointer;
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
.ffun-normalized-text ul {
|
|
40
|
+
@apply list-disc list-inside;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.ffun-normalized-text code {
|
|
44
|
+
@apply bg-slate-800 px-1 rounded bg-opacity-10 text-sm;
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
.ffun-info-common {
|
|
36
|
-
@apply border-2 p-2 my-1 rounded;
|
|
48
|
+
@apply ffun-normalized-text border-2 p-2 my-1 rounded border-slate-200;
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
.ffun-info-good {
|
|
40
|
-
@apply ffun-info-common
|
|
52
|
+
@apply ffun-info-common bg-green-50;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
.ffun-info-bad {
|
|
44
|
-
@apply ffun-info-common
|
|
56
|
+
@apply ffun-info-common bg-red-50;
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
.ffun-info-warning {
|
|
48
|
-
@apply ffun-info-common
|
|
60
|
+
@apply ffun-info-common bg-orange-50;
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
.ffun-info-attention {
|
|
52
|
-
@apply ffun-info-common
|
|
64
|
+
@apply ffun-info-common bg-purple-50;
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
.ffun-header-link {
|
|
@@ -87,8 +99,4 @@
|
|
|
87
99
|
.ffun-checkbox {
|
|
88
100
|
@apply ffun-input-common h-4 w-4 align-middle;
|
|
89
101
|
}
|
|
90
|
-
|
|
91
|
-
.ffun-normalized-text ul {
|
|
92
|
-
@apply list-disc list-inside;
|
|
93
|
-
}
|
|
94
102
|
}
|
|
@@ -2,9 +2,20 @@
|
|
|
2
2
|
<side-panel-layout :reload-button="false">
|
|
3
3
|
<template #main-header> Collections </template>
|
|
4
4
|
|
|
5
|
-
<
|
|
5
|
+
<div class="ffun-info-attention">
|
|
6
|
+
<p
|
|
7
|
+
>We've prepared some ready-to-use thematic collections just for you. All the feeds in them are fully tagged
|
|
8
|
+
free of charge.</p
|
|
9
|
+
>
|
|
10
|
+
<p>Subscribe to some and enjoy the full power of Feeds Fun!</p>
|
|
11
|
+
</div>
|
|
6
12
|
|
|
7
|
-
<
|
|
13
|
+
<div
|
|
14
|
+
v-for="collectionId in collections.collectionsOrder"
|
|
15
|
+
:key="collectionId"
|
|
16
|
+
class="collection-block pb-4">
|
|
17
|
+
<collections-detailed-item :collectionId="collectionId" />
|
|
18
|
+
</div>
|
|
8
19
|
</side-panel-layout>
|
|
9
20
|
</template>
|
|
10
21
|
|
|
@@ -15,11 +26,17 @@
|
|
|
15
26
|
import * as t from "@/logic/types";
|
|
16
27
|
import * as e from "@/logic/enums";
|
|
17
28
|
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
18
|
-
import {
|
|
29
|
+
import {useCollectionsStore} from "@/stores/collections";
|
|
19
30
|
|
|
20
31
|
const globalSettings = useGlobalSettingsStore();
|
|
21
32
|
|
|
33
|
+
const collections = useCollectionsStore();
|
|
34
|
+
|
|
22
35
|
globalSettings.mainPanelMode = e.MainPanelMode.Collections;
|
|
23
36
|
</script>
|
|
24
37
|
|
|
25
|
-
<style
|
|
38
|
+
<style scoped>
|
|
39
|
+
.collection-block:not(:last-child) {
|
|
40
|
+
border-bottom-width: 1px;
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
<side-panel-layout :reload-button="false">
|
|
3
3
|
<template #main-header> Discovery </template>
|
|
4
4
|
|
|
5
|
+
<notifications
|
|
6
|
+
:create-rule-help="false"
|
|
7
|
+
:api-key="false"
|
|
8
|
+
:collections-notification_="false"
|
|
9
|
+
:collections-warning_="true" />
|
|
10
|
+
|
|
5
11
|
<h2>Lood feeds from an OPML file</h2>
|
|
6
12
|
|
|
7
13
|
<opml-upload />
|
package/src/views/FeedsView.vue
CHANGED
|
@@ -32,8 +32,10 @@
|
|
|
32
32
|
|
|
33
33
|
<notifications
|
|
34
34
|
v-if="sortedFeeds !== null"
|
|
35
|
-
:
|
|
36
|
-
:
|
|
35
|
+
:create-rule-help="false"
|
|
36
|
+
:api-key="false"
|
|
37
|
+
:collections-notification_="sortedFeeds === null || sortedFeeds.length == 0"
|
|
38
|
+
:collections-warning_="true" />
|
|
37
39
|
|
|
38
40
|
<feeds-list
|
|
39
41
|
v-if="sortedFeeds"
|
|
@@ -48,26 +50,23 @@
|
|
|
48
50
|
import {computed, ref, onUnmounted, watch} from "vue";
|
|
49
51
|
import {computedAsync} from "@vueuse/core";
|
|
50
52
|
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
53
|
+
import {useFeedsStore} from "@/stores/feeds";
|
|
51
54
|
import * as api from "@/logic/api";
|
|
52
55
|
import type * as t from "@/logic/types";
|
|
53
56
|
import * as e from "@/logic/enums";
|
|
54
57
|
|
|
55
58
|
const globalSettings = useGlobalSettingsStore();
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
const feedsStore = useFeedsStore();
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
// force refresh
|
|
61
|
-
globalSettings.dataVersion;
|
|
62
|
-
return await api.getFeeds();
|
|
63
|
-
}, null);
|
|
62
|
+
globalSettings.mainPanelMode = e.MainPanelMode.Feeds;
|
|
64
63
|
|
|
65
64
|
const sortedFeeds = computed(() => {
|
|
66
|
-
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
65
|
+
let sorted = Object.values(feedsStore.feeds);
|
|
69
66
|
|
|
70
|
-
|
|
67
|
+
if (sorted.length === 0) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
71
70
|
|
|
72
71
|
const orderProperties = e.FeedsOrderProperties.get(globalSettings.feedsOrder);
|
|
73
72
|
|
package/src/views/MainView.vue
CHANGED
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
<li>Filter and sort news how you want ⇒ read only what you want.</li>
|
|
19
19
|
</ul>
|
|
20
20
|
|
|
21
|
+
<h2>Curated collections of feeds</h2>
|
|
22
|
+
|
|
23
|
+
<ul class="list-disc list-inside text-left">
|
|
24
|
+
<li>We handpick the most interesting feeds into thematic collections.</li>
|
|
25
|
+
<li>Feeds in the collections are tagged for free.</li>
|
|
26
|
+
</ul>
|
|
27
|
+
|
|
21
28
|
<h2>You are in control</h2>
|
|
22
29
|
|
|
23
30
|
<ul class="list-disc list-inside text-left">
|