feeds-fun 1.17.0 → 1.18.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 +1 -1
- package/public/news-filtering-example.png +0 -0
- package/src/components/EntriesList.vue +11 -0
- package/src/components/EntryForList.vue +17 -6
- package/src/components/RuleForList.vue +2 -2
- package/src/components/RulesList.vue +18 -9
- package/src/components/TokensCost.vue +0 -2
- package/src/components/collections/DetailedItem.vue +9 -0
- package/src/components/collections/PublicIntro.vue +63 -0
- package/src/components/collections/PublicSelector.vue +43 -0
- package/src/components/main/Block.vue +1 -1
- package/src/components/main/Item.vue +1 -1
- package/src/components/notifications/LoadedOldNews.vue +47 -0
- package/src/components/page_header/ExternalLinks.vue +11 -6
- package/src/components/tags/EntryTag.vue +4 -3
- package/src/components/tags/FilterTag.vue +6 -3
- package/src/components/tags/RuleTag.vue +4 -3
- package/src/components/tags/TagsFilter.vue +17 -5
- package/src/css/panels.css +5 -5
- package/src/layouts/SidePanelLayout.vue +41 -22
- package/src/layouts/WideLayout.vue +0 -4
- package/src/logic/api.ts +23 -0
- package/src/logic/enums.ts +28 -12
- package/src/logic/events.ts +36 -8
- package/src/logic/tagsFilterState.ts +50 -1
- package/src/logic/types.ts +15 -2
- package/src/logic/utils.ts +104 -1
- package/src/main.ts +6 -0
- package/src/router/index.ts +16 -3
- package/src/stores/collections.ts +17 -1
- package/src/stores/entries.ts +154 -19
- package/src/stores/globalSettings.ts +6 -7
- package/src/views/AuthView.vue +3 -1
- package/src/views/CollectionsView.vue +13 -2
- package/src/views/DiscoveryView.vue +3 -1
- package/src/views/FeedsView.vue +3 -1
- package/src/views/MainView.vue +21 -2
- package/src/views/NewsView.vue +32 -98
- package/src/views/PublicCollectionView.vue +237 -0
- package/src/views/RulesView.vue +26 -29
- package/src/views/SettingsView.vue +3 -1
|
@@ -49,29 +49,41 @@
|
|
|
49
49
|
<div class="ffun-body-panel">
|
|
50
50
|
<div class="ffun-page-header">
|
|
51
51
|
<div class="ffun-page-header-left-block">
|
|
52
|
-
<
|
|
53
|
-
v-
|
|
54
|
-
:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
52
|
+
<a
|
|
53
|
+
v-if="homeButton"
|
|
54
|
+
:href="router.resolve({name: 'main', params: {}}).href"
|
|
55
|
+
class="ffun-page-header-link"
|
|
56
|
+
>Home</a
|
|
57
|
+
>
|
|
58
|
+
|
|
59
|
+
<template v-if="globalState.isLoggedIn">
|
|
60
|
+
<template
|
|
61
|
+
v-for="[mode, props] of e.MainPanelModeProperties"
|
|
62
|
+
:key="mode">
|
|
63
|
+
<template v-if="props.showInMenu">
|
|
64
|
+
<a
|
|
65
|
+
v-if="globalSettings.mainPanelMode !== mode"
|
|
66
|
+
:href="router.resolve({name: mode, params: {}}).href"
|
|
67
|
+
class="ffun-page-header-link"
|
|
68
|
+
@click.prevent="router.push({name: mode, params: {}})">
|
|
69
|
+
{{ props.text }}
|
|
70
|
+
</a>
|
|
71
|
+
|
|
72
|
+
<span
|
|
73
|
+
class="ffun-page-header-link-disabled"
|
|
74
|
+
v-else
|
|
75
|
+
>{{ props.text }}</span
|
|
76
|
+
>
|
|
77
|
+
</template>
|
|
78
|
+
</template>
|
|
68
79
|
</template>
|
|
69
80
|
|
|
70
|
-
<page-header-external-links :show-api="
|
|
81
|
+
<page-header-external-links :show-api="globalState.isLoggedIn" />
|
|
71
82
|
</div>
|
|
72
83
|
|
|
73
84
|
<div class="ffun-page-header-right-block">
|
|
74
85
|
<a
|
|
86
|
+
v-if="globalState.isLoggedIn"
|
|
75
87
|
href="#"
|
|
76
88
|
class="ffun-page-header-link"
|
|
77
89
|
@click.prevent="logout()"
|
|
@@ -110,10 +122,14 @@
|
|
|
110
122
|
const router = useRouter();
|
|
111
123
|
const slots = useSlots();
|
|
112
124
|
|
|
113
|
-
const properties = withDefaults(
|
|
114
|
-
reloadButton
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
const properties = withDefaults(
|
|
126
|
+
defineProps<{reloadButton?: boolean; loginRequired?: boolean; homeButton?: boolean}>(),
|
|
127
|
+
{
|
|
128
|
+
reloadButton: true,
|
|
129
|
+
loginRequired: true,
|
|
130
|
+
homeButton: false
|
|
131
|
+
}
|
|
132
|
+
);
|
|
117
133
|
|
|
118
134
|
async function logout() {
|
|
119
135
|
if (settings.authMode === settings.AuthMode.SingleUser) {
|
|
@@ -122,7 +138,10 @@
|
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
await supertokens.logout();
|
|
125
|
-
|
|
141
|
+
|
|
142
|
+
if (properties.loginRequired) {
|
|
143
|
+
router.push({name: "main", params: {}});
|
|
144
|
+
}
|
|
126
145
|
}
|
|
127
146
|
|
|
128
147
|
const hasSideFooter = computed(() => {
|
package/src/logic/api.ts
CHANGED
|
@@ -7,6 +7,7 @@ const ENTRY_POINT = "/api";
|
|
|
7
7
|
|
|
8
8
|
const API_GET_FEEDS = `${ENTRY_POINT}/get-feeds`;
|
|
9
9
|
const API_GET_LAST_ENTRIES = `${ENTRY_POINT}/get-last-entries`;
|
|
10
|
+
const API_GET_LAST_COLLECTION_ENTRIES = `${ENTRY_POINT}/get-last-collection-entries`;
|
|
10
11
|
const API_GET_ENTRIES_BY_IDS = `${ENTRY_POINT}/get-entries-by-ids`;
|
|
11
12
|
const API_CREATE_OR_UPDATE_RULE = `${ENTRY_POINT}/create-or-update-rule`;
|
|
12
13
|
|
|
@@ -83,6 +84,28 @@ export async function getLastEntries({period}: {period: number}) {
|
|
|
83
84
|
return entries;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
export async function getLastCollectionEntries({
|
|
88
|
+
period,
|
|
89
|
+
collectionSlug
|
|
90
|
+
}: {
|
|
91
|
+
period: number;
|
|
92
|
+
collectionSlug: t.CollectionSlug | null;
|
|
93
|
+
}) {
|
|
94
|
+
const response = await post({
|
|
95
|
+
url: API_GET_LAST_COLLECTION_ENTRIES,
|
|
96
|
+
data: {period: period, collectionSlug: collectionSlug}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const entries = [];
|
|
100
|
+
|
|
101
|
+
for (let rawEntry of response.entries) {
|
|
102
|
+
const entry = t.entryFromJSON(rawEntry, response.tagsMapping);
|
|
103
|
+
entries.push(entry);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
export async function getEntriesByIds({ids}: {ids: t.EntryId[]}) {
|
|
87
110
|
const response = await post({
|
|
88
111
|
url: API_GET_ENTRIES_BY_IDS,
|
package/src/logic/enums.ts
CHANGED
|
@@ -10,16 +10,23 @@ export enum MainPanelMode {
|
|
|
10
10
|
Rules = "rules",
|
|
11
11
|
Discovery = "discovery",
|
|
12
12
|
Collections = "collections",
|
|
13
|
+
PublicCollection = "public-collection",
|
|
13
14
|
Settings = "settings"
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
[MainPanelMode.
|
|
17
|
+
export type MainPanelModeProperty = {
|
|
18
|
+
readonly text: string;
|
|
19
|
+
readonly showInMenu: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const MainPanelModeProperties = new Map<MainPanelMode, MainPanelModeProperty>([
|
|
23
|
+
[MainPanelMode.Entries, {text: "News", showInMenu: true}],
|
|
24
|
+
[MainPanelMode.Feeds, {text: "Feeds", showInMenu: true}],
|
|
25
|
+
[MainPanelMode.Rules, {text: "Rules", showInMenu: true}],
|
|
26
|
+
[MainPanelMode.Discovery, {text: "Discovery", showInMenu: true}],
|
|
27
|
+
[MainPanelMode.Collections, {text: "Collections", showInMenu: true}],
|
|
28
|
+
[MainPanelMode.PublicCollection, {text: "Public Collection", showInMenu: false}],
|
|
29
|
+
[MainPanelMode.Settings, {text: "Settings", showInMenu: true}]
|
|
23
30
|
]);
|
|
24
31
|
|
|
25
32
|
export enum LastEntriesPeriod {
|
|
@@ -38,8 +45,13 @@ export enum LastEntriesPeriod {
|
|
|
38
45
|
AllTime = "alltime"
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
export type LastEntriesPeriodProperty = {
|
|
49
|
+
readonly text: string;
|
|
50
|
+
readonly seconds: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
41
53
|
// Map preserves the order of the keys
|
|
42
|
-
export const LastEntriesPeriodProperties = new Map<LastEntriesPeriod,
|
|
54
|
+
export const LastEntriesPeriodProperties = new Map<LastEntriesPeriod, LastEntriesPeriodProperty>([
|
|
43
55
|
[LastEntriesPeriod.Hour1, {text: "1 hour", seconds: c.hour}],
|
|
44
56
|
[LastEntriesPeriod.Hour3, {text: "3 hours", seconds: 3 * c.hour}],
|
|
45
57
|
[LastEntriesPeriod.Hour6, {text: "6 hours", seconds: 6 * c.hour}],
|
|
@@ -63,10 +75,14 @@ export enum EntriesOrder {
|
|
|
63
75
|
Cataloged = "cataloged"
|
|
64
76
|
}
|
|
65
77
|
|
|
66
|
-
export
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
export type EntriesOrderProperty = {
|
|
79
|
+
readonly text: string;
|
|
80
|
+
readonly orderField: string;
|
|
81
|
+
readonly timeField: string;
|
|
82
|
+
readonly direction: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const EntriesOrderProperties = new Map<EntriesOrder, EntriesOrderProperty>([
|
|
70
86
|
[EntriesOrder.Score, {text: "score", orderField: "score", timeField: "catalogedAt", direction: 1}],
|
|
71
87
|
[EntriesOrder.ScoreToZero, {text: "score ~ 0", orderField: "scoreToZero", timeField: "catalogedAt", direction: 1}],
|
|
72
88
|
[EntriesOrder.ScoreBackward, {text: "score backward", orderField: "score", timeField: "catalogedAt", direction: -1}],
|
package/src/logic/events.ts
CHANGED
|
@@ -1,37 +1,65 @@
|
|
|
1
1
|
import * as api from "@/logic/api";
|
|
2
2
|
import type * as t from "@/logic/types";
|
|
3
|
+
import {inject} from "vue";
|
|
3
4
|
import type {State as TagState} from "@/logic/tagsFilterState";
|
|
4
5
|
|
|
5
|
-
export type
|
|
6
|
+
export type EventsViewName =
|
|
7
|
+
| "news"
|
|
8
|
+
| "rules"
|
|
9
|
+
| "public_collections"
|
|
10
|
+
| "auth"
|
|
11
|
+
| "settings"
|
|
12
|
+
| "discovery"
|
|
13
|
+
| "feeds"
|
|
14
|
+
| "main"
|
|
15
|
+
| "collections";
|
|
16
|
+
export type TagChangeSource = "tags_filter" | "entry_record" | "rule_record";
|
|
6
17
|
|
|
7
|
-
export async function newsLinkOpened({entryId}: {entryId: t.EntryId}) {
|
|
8
|
-
await api.trackEvent({
|
|
18
|
+
export async function newsLinkOpened({entryId, view}: {entryId: t.EntryId; view: EventsViewName}) {
|
|
19
|
+
await api.trackEvent({
|
|
20
|
+
name: "news_link_opened",
|
|
21
|
+
view: view,
|
|
22
|
+
entry_id: entryId
|
|
23
|
+
});
|
|
9
24
|
}
|
|
10
25
|
|
|
11
|
-
export async function newsBodyOpened({entryId}: {entryId: t.EntryId}) {
|
|
12
|
-
await api.trackEvent({
|
|
26
|
+
export async function newsBodyOpened({entryId, view}: {entryId: t.EntryId; view: EventsViewName}) {
|
|
27
|
+
await api.trackEvent({
|
|
28
|
+
name: "news_body_opened",
|
|
29
|
+
view: view,
|
|
30
|
+
entry_id: entryId
|
|
31
|
+
});
|
|
13
32
|
}
|
|
14
33
|
|
|
15
|
-
export async function socialLinkClicked({linkType}: {linkType: string}) {
|
|
16
|
-
await api.trackEvent({
|
|
34
|
+
export async function socialLinkClicked({linkType, view}: {linkType: string; view: EventsViewName}) {
|
|
35
|
+
await api.trackEvent({
|
|
36
|
+
name: "social_link_clicked",
|
|
37
|
+
view: view,
|
|
38
|
+
link_type: linkType
|
|
39
|
+
});
|
|
17
40
|
}
|
|
18
41
|
|
|
19
42
|
export async function tagStateChanged({
|
|
20
43
|
tag,
|
|
21
44
|
fromState,
|
|
22
45
|
toState,
|
|
23
|
-
source
|
|
46
|
+
source,
|
|
47
|
+
view
|
|
24
48
|
}: {
|
|
25
49
|
tag: string;
|
|
26
50
|
fromState: TagState;
|
|
27
51
|
toState: TagState;
|
|
28
52
|
source: TagChangeSource;
|
|
53
|
+
view: EventsViewName;
|
|
29
54
|
}) {
|
|
55
|
+
// const eventsView = inject<events.EventViewName>("eventsViewName");
|
|
56
|
+
|
|
30
57
|
await api.trackEvent({
|
|
31
58
|
name: "tag_filter_state_changed",
|
|
32
59
|
tag: tag,
|
|
33
60
|
from_state: fromState,
|
|
34
61
|
to_state: toState,
|
|
62
|
+
view: view,
|
|
35
63
|
source: source
|
|
36
64
|
});
|
|
37
65
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {ref, computed, reactive} from "vue";
|
|
1
|
+
import {ref, computed, reactive, watch} from "vue";
|
|
2
2
|
import type {ComputedRef} from "vue";
|
|
3
|
+
import _ from "lodash";
|
|
3
4
|
|
|
4
5
|
export type State = "required" | "excluded" | "none";
|
|
5
6
|
|
|
@@ -102,6 +103,34 @@ export class Storage {
|
|
|
102
103
|
return report;
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
tagsForUrl() {
|
|
107
|
+
const selectedTags = Object.keys(this.selectedTags).sort();
|
|
108
|
+
|
|
109
|
+
const tags = [];
|
|
110
|
+
|
|
111
|
+
for (const tag of selectedTags) {
|
|
112
|
+
if (this.requiredTags[tag]) {
|
|
113
|
+
tags.push(tag);
|
|
114
|
+
} else {
|
|
115
|
+
tags.push(`-${tag}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return tags;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setTagsFromUrl(tags: string[]) {
|
|
123
|
+
this.clear();
|
|
124
|
+
|
|
125
|
+
for (const tag of tags) {
|
|
126
|
+
if (tag.startsWith("-")) {
|
|
127
|
+
this.excludedTags[tag.slice(1)] = true;
|
|
128
|
+
} else {
|
|
129
|
+
this.requiredTags[tag] = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
105
134
|
clear() {
|
|
106
135
|
Object.keys(this.requiredTags).forEach((key) => {
|
|
107
136
|
delete this.requiredTags[key];
|
|
@@ -112,3 +141,23 @@ export class Storage {
|
|
|
112
141
|
});
|
|
113
142
|
}
|
|
114
143
|
}
|
|
144
|
+
|
|
145
|
+
// must be called synchoronously from the view
|
|
146
|
+
export function setSyncingTagsWithRoute({tagsStates, route, router}: {tagsStates: Storage; route: any; router: any}) {
|
|
147
|
+
if (!route.params.tags) {
|
|
148
|
+
tagsStates.setTagsFromUrl([]);
|
|
149
|
+
} else {
|
|
150
|
+
tagsStates.setTagsFromUrl(route.params.tags);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
watch(tagsStates, () => {
|
|
154
|
+
const newParams = _.clone(route.params);
|
|
155
|
+
newParams.tags = tagsStates.tagsForUrl();
|
|
156
|
+
|
|
157
|
+
router.push({
|
|
158
|
+
replace: true,
|
|
159
|
+
name: route.name,
|
|
160
|
+
params: newParams
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
package/src/logic/types.ts
CHANGED
|
@@ -24,6 +24,12 @@ export function toCollectionId(id: string): CollectionId {
|
|
|
24
24
|
return id as CollectionId;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export type CollectionSlug = string & {readonly __brand: unique symbol};
|
|
28
|
+
|
|
29
|
+
export function toCollectionSlug(slug: string): CollectionSlug {
|
|
30
|
+
return slug as CollectionSlug;
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
export type URL = string & {readonly __brand: unique symbol};
|
|
28
34
|
|
|
29
35
|
export function toURL(url: string): URL {
|
|
@@ -233,7 +239,7 @@ export type Rule = {
|
|
|
233
239
|
readonly id: RuleId;
|
|
234
240
|
readonly requiredTags: string[];
|
|
235
241
|
readonly excludedTags: string[];
|
|
236
|
-
readonly
|
|
242
|
+
readonly tags: string[];
|
|
237
243
|
readonly score: number;
|
|
238
244
|
readonly createdAt: Date;
|
|
239
245
|
readonly updatedAt: Date;
|
|
@@ -261,7 +267,7 @@ export function ruleFromJSON({
|
|
|
261
267
|
id: toRuleId(id),
|
|
262
268
|
requiredTags: requiredTags,
|
|
263
269
|
excludedTags: excludedTags,
|
|
264
|
-
|
|
270
|
+
tags: requiredTags.concat(excludedTags).sort(),
|
|
265
271
|
score: score,
|
|
266
272
|
createdAt: new Date(createdAt),
|
|
267
273
|
updatedAt: new Date(updatedAt)
|
|
@@ -411,6 +417,7 @@ export function resourceHistoryRecordFromJSON({
|
|
|
411
417
|
|
|
412
418
|
export class Collection {
|
|
413
419
|
readonly id: CollectionId;
|
|
420
|
+
readonly slug: CollectionSlug;
|
|
414
421
|
readonly guiOrder: number;
|
|
415
422
|
readonly name: string;
|
|
416
423
|
readonly description: string;
|
|
@@ -419,6 +426,7 @@ export class Collection {
|
|
|
419
426
|
|
|
420
427
|
constructor({
|
|
421
428
|
id,
|
|
429
|
+
slug,
|
|
422
430
|
guiOrder,
|
|
423
431
|
name,
|
|
424
432
|
description,
|
|
@@ -426,6 +434,7 @@ export class Collection {
|
|
|
426
434
|
showOnMain
|
|
427
435
|
}: {
|
|
428
436
|
id: CollectionId;
|
|
437
|
+
slug: CollectionSlug;
|
|
429
438
|
guiOrder: number;
|
|
430
439
|
name: string;
|
|
431
440
|
description: string;
|
|
@@ -433,6 +442,7 @@ export class Collection {
|
|
|
433
442
|
showOnMain: boolean;
|
|
434
443
|
}) {
|
|
435
444
|
this.id = id;
|
|
445
|
+
this.slug = slug;
|
|
436
446
|
this.guiOrder = guiOrder;
|
|
437
447
|
this.name = name;
|
|
438
448
|
this.description = description;
|
|
@@ -443,6 +453,7 @@ export class Collection {
|
|
|
443
453
|
|
|
444
454
|
export function collectionFromJSON({
|
|
445
455
|
id,
|
|
456
|
+
slug,
|
|
446
457
|
guiOrder,
|
|
447
458
|
name,
|
|
448
459
|
description,
|
|
@@ -450,6 +461,7 @@ export function collectionFromJSON({
|
|
|
450
461
|
showOnMain
|
|
451
462
|
}: {
|
|
452
463
|
id: string;
|
|
464
|
+
slug: string;
|
|
453
465
|
guiOrder: number;
|
|
454
466
|
name: string;
|
|
455
467
|
description: string;
|
|
@@ -458,6 +470,7 @@ export function collectionFromJSON({
|
|
|
458
470
|
}): Collection {
|
|
459
471
|
return {
|
|
460
472
|
id: toCollectionId(id),
|
|
473
|
+
slug: toCollectionSlug(slug),
|
|
461
474
|
guiOrder: guiOrder,
|
|
462
475
|
name: name,
|
|
463
476
|
description: description,
|
package/src/logic/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import _ from "lodash";
|
|
2
|
+
import type * as t from "@/logic/types";
|
|
2
3
|
import DOMPurify from "dompurify";
|
|
3
4
|
|
|
4
5
|
export function timeSince(date: Date) {
|
|
@@ -64,7 +65,6 @@ export function faviconForUrl(url: string): string | null {
|
|
|
64
65
|
const parsedUrl = new URL(url);
|
|
65
66
|
return `${parsedUrl.protocol}//${parsedUrl.host}/favicon.ico`;
|
|
66
67
|
} catch (error) {
|
|
67
|
-
console.error("Invalid URL:", error);
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -96,3 +96,106 @@ export function purifyBody({raw, default_}: {raw: string | null; default_: strin
|
|
|
96
96
|
|
|
97
97
|
return body;
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
export function chooseTagByUsage({
|
|
101
|
+
tagsCount,
|
|
102
|
+
border,
|
|
103
|
+
exclude
|
|
104
|
+
}: {
|
|
105
|
+
tagsCount: {[key: string]: number};
|
|
106
|
+
border: number;
|
|
107
|
+
exclude: string[];
|
|
108
|
+
}) {
|
|
109
|
+
if (Object.keys(tagsCount).length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!exclude) {
|
|
114
|
+
exclude = [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const tags = _.toPairs(tagsCount).sort((a, b) => {
|
|
118
|
+
if (a[1] === b[1]) {
|
|
119
|
+
return a[0].localeCompare(b[0]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return b[1] - a[1];
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < tags.length; i++) {
|
|
126
|
+
if (exclude.includes(tags[i][0])) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (tags[i][1] < border) {
|
|
131
|
+
return tags[i][0];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return tags[tags.length - 1][0];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function countTags(entries: t.Entry[] | t.Rule[] | null) {
|
|
139
|
+
if (!entries) {
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const tagsCount: {[key: string]: number} = {};
|
|
144
|
+
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
for (const tag of entry.tags) {
|
|
147
|
+
if (tag in tagsCount) {
|
|
148
|
+
tagsCount[tag] += 1;
|
|
149
|
+
} else {
|
|
150
|
+
tagsCount[tag] = 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return tagsCount;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function sortIdsList<ID extends string = string>({
|
|
159
|
+
ids,
|
|
160
|
+
storage,
|
|
161
|
+
field,
|
|
162
|
+
direction
|
|
163
|
+
}: {
|
|
164
|
+
ids: ID[];
|
|
165
|
+
storage: {[key: string]: any};
|
|
166
|
+
field: string;
|
|
167
|
+
direction: number;
|
|
168
|
+
}) {
|
|
169
|
+
// Pre-map to avoid repeated lookups in the comparator
|
|
170
|
+
// required for the cases when storage is reactive
|
|
171
|
+
const mapped = ids.map((id) => {
|
|
172
|
+
// @ts-ignore
|
|
173
|
+
return {id, value: storage[id][field]};
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
mapped.sort((a: {id: ID; value: any}, b: {id: ID; value: any}) => {
|
|
177
|
+
if (a.value === null && b.value === null) {
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (a.value === null) {
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (b.value === null) {
|
|
186
|
+
return -1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (a.value < b.value) {
|
|
190
|
+
return direction;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (a.value > b.value) {
|
|
194
|
+
return -direction;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return 0;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return mapped.map((x) => x.id);
|
|
201
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -38,6 +38,7 @@ import PageHeaderExternalLinks from "./components/page_header/ExternalLinks.vue"
|
|
|
38
38
|
import NotificationsApiKey from "./components/notifications/ApiKey.vue";
|
|
39
39
|
import NotificationsCreateRuleHelp from "./components/notifications/CreateRuleHelp.vue";
|
|
40
40
|
import Notifications from "./components/notifications/Block.vue";
|
|
41
|
+
import NotificationsLoadedOldNews from "./components/notifications/LoadedOldNews.vue";
|
|
41
42
|
|
|
42
43
|
import CollectionsNotification from "./components/collections/Notification.vue";
|
|
43
44
|
import CollectionsWarning from "./components/collections/Warning.vue";
|
|
@@ -46,6 +47,8 @@ import CollectionsBlockItem from "./components/collections/BlockItem.vue";
|
|
|
46
47
|
import CollectionsDetailedItem from "./components/collections/DetailedItem.vue";
|
|
47
48
|
import CollectionsSubscribingProgress from "./components/collections/SubscribingProgress.vue";
|
|
48
49
|
import CollectionsFeedItem from "./components/collections/FeedItem.vue";
|
|
50
|
+
import CollectionsPublicSelector from "./components/collections/PublicSelector.vue";
|
|
51
|
+
import CollectionsPublicIntro from "./components/collections/PublicIntro.vue";
|
|
49
52
|
|
|
50
53
|
import ScoreSelector from "./inputs/ScoreSelector.vue";
|
|
51
54
|
|
|
@@ -105,6 +108,7 @@ app.component("PageHeaderExternalLinks", PageHeaderExternalLinks);
|
|
|
105
108
|
app.component("NotificationsApiKey", NotificationsApiKey);
|
|
106
109
|
app.component("NotificationsCreateRuleHelp", NotificationsCreateRuleHelp);
|
|
107
110
|
app.component("Notifications", Notifications);
|
|
111
|
+
app.component("NotificationsLoadedOldNews", NotificationsLoadedOldNews);
|
|
108
112
|
|
|
109
113
|
app.component("CollectionsNotification", CollectionsNotification);
|
|
110
114
|
app.component("CollectionsWarning", CollectionsWarning);
|
|
@@ -113,6 +117,8 @@ app.component("CollectionsBlockItem", CollectionsBlockItem);
|
|
|
113
117
|
app.component("CollectionsDetailedItem", CollectionsDetailedItem);
|
|
114
118
|
app.component("CollectionsSubscribingProgress", CollectionsSubscribingProgress);
|
|
115
119
|
app.component("CollectionsFeedItem", CollectionsFeedItem);
|
|
120
|
+
app.component("CollectionsPublicSelector", CollectionsPublicSelector);
|
|
121
|
+
app.component("CollectionsPublicIntro", CollectionsPublicIntro);
|
|
116
122
|
|
|
117
123
|
app.component("ScoreSelector", ScoreSelector);
|
|
118
124
|
|
package/src/router/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import RulesView from "../views/RulesView.vue";
|
|
|
7
7
|
import DiscoveryView from "../views/DiscoveryView.vue";
|
|
8
8
|
import CollectionsView from "../views/CollectionsView.vue";
|
|
9
9
|
import SettingsView from "../views/SettingsView.vue";
|
|
10
|
+
import PublicCollectionView from "../views/PublicCollectionView.vue";
|
|
10
11
|
import * as e from "@/logic/enums";
|
|
11
12
|
|
|
12
13
|
// lazy view loading does not work with router.push function
|
|
@@ -31,12 +32,12 @@ const router = createRouter({
|
|
|
31
32
|
component: FeedsView
|
|
32
33
|
},
|
|
33
34
|
{
|
|
34
|
-
path: "/news",
|
|
35
|
+
path: "/news/:tags*",
|
|
35
36
|
name: e.MainPanelMode.Entries,
|
|
36
37
|
component: NewsView
|
|
37
38
|
},
|
|
38
39
|
{
|
|
39
|
-
path: "/rules",
|
|
40
|
+
path: "/rules/:tags*",
|
|
40
41
|
name: e.MainPanelMode.Rules,
|
|
41
42
|
component: RulesView
|
|
42
43
|
},
|
|
@@ -54,8 +55,20 @@ const router = createRouter({
|
|
|
54
55
|
path: "/settings",
|
|
55
56
|
name: e.MainPanelMode.Settings,
|
|
56
57
|
component: SettingsView
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
path: "/show/:collectionSlug/:tags*",
|
|
61
|
+
name: "public-collection",
|
|
62
|
+
component: PublicCollectionView
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
path: "/:pathMatch(.*)*",
|
|
66
|
+
redirect: "/"
|
|
57
67
|
}
|
|
58
|
-
]
|
|
68
|
+
],
|
|
69
|
+
scrollBehavior(to, from, savedPosition) {
|
|
70
|
+
return {top: 0};
|
|
71
|
+
}
|
|
59
72
|
});
|
|
60
73
|
|
|
61
74
|
export default router;
|
|
@@ -33,6 +33,10 @@ export const useCollectionsStore = defineStore("collectionsStore", () => {
|
|
|
33
33
|
return order;
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
const collectionsOrdered = computed(() => {
|
|
37
|
+
return collectionsOrder.value.map((id) => collections.value[id]);
|
|
38
|
+
});
|
|
39
|
+
|
|
36
40
|
async function getFeeds({collectionId}: {collectionId: t.CollectionId}) {
|
|
37
41
|
if (collectionId in feeds.value) {
|
|
38
42
|
return feeds.value[collectionId];
|
|
@@ -45,9 +49,21 @@ export const useCollectionsStore = defineStore("collectionsStore", () => {
|
|
|
45
49
|
return feeds.value[collectionId];
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
function getCollectionBySlug({slug}: {slug: t.CollectionSlug}) {
|
|
53
|
+
for (const collection of Object.values(collections.value)) {
|
|
54
|
+
if (collection.slug === slug) {
|
|
55
|
+
return collection;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
return {
|
|
49
63
|
collections,
|
|
50
64
|
collectionsOrder,
|
|
51
|
-
|
|
65
|
+
collectionsOrdered,
|
|
66
|
+
getFeeds,
|
|
67
|
+
getCollectionBySlug
|
|
52
68
|
};
|
|
53
69
|
});
|