feeds-fun 1.17.1 → 1.18.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/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/side_pannel/CollapseButton.vue +56 -0
- 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/css/side_panel_layout.css +4 -0
- package/src/layouts/SidePanelLayout.vue +51 -26
- 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 +56 -8
- package/src/logic/tagsFilterState.ts +61 -1
- package/src/logic/types.ts +15 -2
- package/src/logic/utils.ts +104 -1
- package/src/main.ts +10 -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 +11 -8
- 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 +18 -1
- package/src/views/NewsView.vue +36 -97
- package/src/views/PublicCollectionView.vue +237 -0
- package/src/views/RulesView.vue +31 -29
- package/src/views/SettingsView.vue +3 -1
package/src/css/panels.css
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.ffun-x-info {
|
|
27
|
-
@apply ffun-x-panel border p-4 rounded-md;
|
|
27
|
+
@apply ffun-x-panel border p-4 rounded-md my-2;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.ffun-info-settings {
|
|
@@ -32,18 +32,18 @@
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
.ffun-info-common {
|
|
35
|
-
@apply ffun-x-info bg-blue-50 border-blue-200 text-blue-
|
|
35
|
+
@apply ffun-x-info bg-blue-50 border-blue-200 text-blue-900;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
.ffun-info-good {
|
|
39
|
-
@apply ffun-x-info bg-green-50 border-green-200 text-green-
|
|
39
|
+
@apply ffun-x-info bg-green-50 border-green-200 text-green-900;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
.ffun-info-bad {
|
|
43
|
-
@apply ffun-x-info bg-red-50 border-red-200 text-red-
|
|
43
|
+
@apply ffun-x-info bg-red-50 border-red-200 text-red-900;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
.ffun-info-waiting {
|
|
47
|
-
@apply ffun-x-info bg-yellow-50 border-yellow-200 text-yellow-
|
|
47
|
+
@apply ffun-x-info bg-yellow-50 border-yellow-200 text-yellow-900;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="ffun-side-panel-layout">
|
|
3
|
-
<div
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<div
|
|
4
|
+
v-if="globalSettings.showSidebar"
|
|
5
|
+
class="ffun-side-panel">
|
|
6
|
+
<div class="ffun-page-header pr-0 mr-0 flex min-w-full">
|
|
7
|
+
<div class="ffun-page-header-title grow">
|
|
6
8
|
<slot name="main-header"></slot>
|
|
7
9
|
</div>
|
|
10
|
+
|
|
11
|
+
<side-panel-collapse-button />
|
|
8
12
|
</div>
|
|
9
13
|
|
|
10
14
|
<hr />
|
|
@@ -49,29 +53,43 @@
|
|
|
49
53
|
<div class="ffun-body-panel">
|
|
50
54
|
<div class="ffun-page-header">
|
|
51
55
|
<div class="ffun-page-header-left-block">
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
v-
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
<side-panel-collapse-button v-if="!globalSettings.showSidebar" />
|
|
57
|
+
|
|
58
|
+
<a
|
|
59
|
+
v-if="homeButton"
|
|
60
|
+
:href="router.resolve({name: 'main', params: {}}).href"
|
|
61
|
+
class="ffun-page-header-link"
|
|
62
|
+
>Home</a
|
|
63
|
+
>
|
|
64
|
+
|
|
65
|
+
<template v-if="globalState.isLoggedIn">
|
|
66
|
+
<template
|
|
67
|
+
v-for="[mode, props] of e.MainPanelModeProperties"
|
|
68
|
+
:key="mode">
|
|
69
|
+
<template v-if="props.showInMenu">
|
|
70
|
+
<a
|
|
71
|
+
v-if="globalSettings.mainPanelMode !== mode"
|
|
72
|
+
:href="router.resolve({name: mode, params: {}}).href"
|
|
73
|
+
class="ffun-page-header-link"
|
|
74
|
+
@click.prevent="router.push({name: mode, params: {}})">
|
|
75
|
+
{{ props.text }}
|
|
76
|
+
</a>
|
|
77
|
+
|
|
78
|
+
<span
|
|
79
|
+
class="ffun-page-header-link-disabled"
|
|
80
|
+
v-else
|
|
81
|
+
>{{ props.text }}</span
|
|
82
|
+
>
|
|
83
|
+
</template>
|
|
84
|
+
</template>
|
|
68
85
|
</template>
|
|
69
86
|
|
|
70
|
-
<page-header-external-links :show-api="
|
|
87
|
+
<page-header-external-links :show-api="globalState.isLoggedIn" />
|
|
71
88
|
</div>
|
|
72
89
|
|
|
73
90
|
<div class="ffun-page-header-right-block">
|
|
74
91
|
<a
|
|
92
|
+
v-if="globalState.isLoggedIn"
|
|
75
93
|
href="#"
|
|
76
94
|
class="ffun-page-header-link"
|
|
77
95
|
@click.prevent="logout()"
|
|
@@ -80,7 +98,7 @@
|
|
|
80
98
|
</div>
|
|
81
99
|
</div>
|
|
82
100
|
|
|
83
|
-
<hr class="my-2 border-slate-400" />
|
|
101
|
+
<hr class="mx-4 my-2 border-slate-400" />
|
|
84
102
|
|
|
85
103
|
<main class="mb-4 px-4 min-h-screen">
|
|
86
104
|
<slot></slot>
|
|
@@ -110,10 +128,14 @@
|
|
|
110
128
|
const router = useRouter();
|
|
111
129
|
const slots = useSlots();
|
|
112
130
|
|
|
113
|
-
const properties = withDefaults(
|
|
114
|
-
reloadButton
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
const properties = withDefaults(
|
|
132
|
+
defineProps<{reloadButton?: boolean; loginRequired?: boolean; homeButton?: boolean}>(),
|
|
133
|
+
{
|
|
134
|
+
reloadButton: true,
|
|
135
|
+
loginRequired: true,
|
|
136
|
+
homeButton: false
|
|
137
|
+
}
|
|
138
|
+
);
|
|
117
139
|
|
|
118
140
|
async function logout() {
|
|
119
141
|
if (settings.authMode === settings.AuthMode.SingleUser) {
|
|
@@ -122,7 +144,10 @@
|
|
|
122
144
|
}
|
|
123
145
|
|
|
124
146
|
await supertokens.logout();
|
|
125
|
-
|
|
147
|
+
|
|
148
|
+
if (properties.loginRequired) {
|
|
149
|
+
router.push({name: "main", params: {}});
|
|
150
|
+
}
|
|
126
151
|
}
|
|
127
152
|
|
|
128
153
|
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,85 @@
|
|
|
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
|
|
8
|
-
|
|
18
|
+
export type SidebarVisibilityChangeEvent = "hide" | "show";
|
|
19
|
+
export type SidebarVisibilityChangeSource = "top_sidebar_button";
|
|
20
|
+
|
|
21
|
+
export async function newsLinkOpened({entryId, view}: {entryId: t.EntryId; view: EventsViewName}) {
|
|
22
|
+
await api.trackEvent({
|
|
23
|
+
name: "news_link_opened",
|
|
24
|
+
view: view,
|
|
25
|
+
entry_id: entryId
|
|
26
|
+
});
|
|
9
27
|
}
|
|
10
28
|
|
|
11
|
-
export async function newsBodyOpened({entryId}: {entryId: t.EntryId}) {
|
|
12
|
-
await api.trackEvent({
|
|
29
|
+
export async function newsBodyOpened({entryId, view}: {entryId: t.EntryId; view: EventsViewName}) {
|
|
30
|
+
await api.trackEvent({
|
|
31
|
+
name: "news_body_opened",
|
|
32
|
+
view: view,
|
|
33
|
+
entry_id: entryId
|
|
34
|
+
});
|
|
13
35
|
}
|
|
14
36
|
|
|
15
|
-
export async function socialLinkClicked({linkType}: {linkType: string}) {
|
|
16
|
-
await api.trackEvent({
|
|
37
|
+
export async function socialLinkClicked({linkType, view}: {linkType: string; view: EventsViewName}) {
|
|
38
|
+
await api.trackEvent({
|
|
39
|
+
name: "social_link_clicked",
|
|
40
|
+
view: view,
|
|
41
|
+
link_type: linkType
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function sidebarStateChanged({
|
|
46
|
+
subEvent,
|
|
47
|
+
view,
|
|
48
|
+
source
|
|
49
|
+
}: {
|
|
50
|
+
subEvent: SidebarVisibilityChangeEvent;
|
|
51
|
+
view: EventsViewName;
|
|
52
|
+
source: SidebarVisibilityChangeSource;
|
|
53
|
+
}) {
|
|
54
|
+
await api.trackEvent({
|
|
55
|
+
name: "sidebar_state_changed",
|
|
56
|
+
view: view,
|
|
57
|
+
sub_event: subEvent,
|
|
58
|
+
source: source
|
|
59
|
+
});
|
|
17
60
|
}
|
|
18
61
|
|
|
19
62
|
export async function tagStateChanged({
|
|
20
63
|
tag,
|
|
21
64
|
fromState,
|
|
22
65
|
toState,
|
|
23
|
-
source
|
|
66
|
+
source,
|
|
67
|
+
view
|
|
24
68
|
}: {
|
|
25
69
|
tag: string;
|
|
26
70
|
fromState: TagState;
|
|
27
71
|
toState: TagState;
|
|
28
72
|
source: TagChangeSource;
|
|
73
|
+
view: EventsViewName;
|
|
29
74
|
}) {
|
|
75
|
+
// const eventsView = inject<events.EventViewName>("eventsViewName");
|
|
76
|
+
|
|
30
77
|
await api.trackEvent({
|
|
31
78
|
name: "tag_filter_state_changed",
|
|
32
79
|
tag: tag,
|
|
33
80
|
from_state: fromState,
|
|
34
81
|
to_state: toState,
|
|
82
|
+
view: view,
|
|
35
83
|
source: source
|
|
36
84
|
});
|
|
37
85
|
}
|
|
@@ -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,34 @@ 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
|
+
}
|
|
164
|
+
|
|
165
|
+
// must be called synchoronously from the view
|
|
166
|
+
export function setSyncingTagsSidebarPoint({tagsStates, globalSettings}: {tagsStates: Storage; globalSettings: any}) {
|
|
167
|
+
watch(
|
|
168
|
+
tagsStates,
|
|
169
|
+
() => {
|
|
170
|
+
globalSettings.showSidebarPoint = tagsStates.hasSelectedTags;
|
|
171
|
+
},
|
|
172
|
+
{immediate: true}
|
|
173
|
+
);
|
|
174
|
+
}
|
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
|
+
}
|