feeds-fun 1.25.1 → 1.26.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/src/components/EntryForList.vue +24 -3
- package/src/components/FeedForList.vue +1 -0
- package/src/components/body_list/EntryBody.vue +70 -17
- package/src/components/body_list/EntryCover.vue +75 -0
- package/src/components/body_list/Reference.vue +55 -0
- package/src/components/body_list/References.vue +16 -0
- package/src/components/main/IntegrationsTable.vue +154 -0
- package/src/components/main/ShowMoreButton.vue +17 -0
- package/src/integrations/YouTube.vue +29 -0
- package/src/logic/api.ts +13 -0
- package/src/logic/enums.ts +32 -0
- package/src/logic/iframeSanitizer.ts +267 -0
- package/src/logic/settings.ts +1 -0
- package/src/logic/tests/iframeSanitizer.test.ts +111 -0
- package/src/logic/tests/utils.test.ts +109 -0
- package/src/logic/types.ts +163 -47
- package/src/logic/utils.ts +31 -1
- package/src/main.ts +12 -0
- package/src/stores/entries.ts +9 -1
- package/src/stores/integrations.ts +14 -0
- package/src/values/Icon.vue +18 -0
- package/src/views/FeedsView.vue +10 -0
- package/src/views/MainView.vue +8 -6
- package/src/views/RulesView.vue +8 -2
package/package.json
CHANGED
|
@@ -37,9 +37,8 @@
|
|
|
37
37
|
:href="entry.url"
|
|
38
38
|
target="_blank"
|
|
39
39
|
:class="[{'font-bold': !isRead}, 'flex-grow', 'min-w-fit', 'line-clamp-1', 'pr-4', 'mb-0']"
|
|
40
|
-
@click="onTitleClick"
|
|
41
|
-
|
|
42
|
-
</a>
|
|
40
|
+
@click="onTitleClick"
|
|
41
|
+
v-html="purifiedTitle" />
|
|
43
42
|
|
|
44
43
|
<entry-tags-list
|
|
45
44
|
class="mt-0 pt-0"
|
|
@@ -62,6 +61,7 @@
|
|
|
62
61
|
:title="purifiedTitle"
|
|
63
62
|
:loading="entry.body === null"
|
|
64
63
|
:text="purifiedBody"
|
|
64
|
+
:references="references"
|
|
65
65
|
@body-title-clicked="newsLinkOpenedEvent" />
|
|
66
66
|
</template>
|
|
67
67
|
|
|
@@ -136,6 +136,27 @@
|
|
|
136
136
|
return utils.purifyBody({raw: entry.value.body, default_: "No description"});
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
const references = computed(() => {
|
|
140
|
+
const references = (entry.value.references ?? []).slice();
|
|
141
|
+
|
|
142
|
+
references.sort((left, right) => {
|
|
143
|
+
const leftProperties = e.ReferenceKindProperties.get(left.kind);
|
|
144
|
+
const rightProperties = e.ReferenceKindProperties.get(right.kind);
|
|
145
|
+
|
|
146
|
+
if (leftProperties === undefined) {
|
|
147
|
+
throw new Error(`Unknown reference kind: ${left.kind}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (rightProperties === undefined) {
|
|
151
|
+
throw new Error(`Unknown reference kind: ${right.kind}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return leftProperties.priority - rightProperties.priority;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return references;
|
|
158
|
+
});
|
|
159
|
+
|
|
139
160
|
function newsLinkOpenedEvent() {
|
|
140
161
|
asserts.defined(eventsView);
|
|
141
162
|
events.newsLinkOpened({entryId: entry.value.id, view: eventsView});
|
|
@@ -1,43 +1,96 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="flex my-1">
|
|
3
|
-
<div
|
|
3
|
+
<div
|
|
4
|
+
ref="bodyCard"
|
|
5
|
+
class="max-w-3xl flex-1 bg-white border rounded p-4">
|
|
4
6
|
<h2
|
|
5
7
|
v-if="url"
|
|
6
|
-
class="mt-0"
|
|
8
|
+
class="mt-0 mb-0"
|
|
7
9
|
><a
|
|
8
10
|
:href="url"
|
|
9
11
|
target="_blank"
|
|
10
12
|
@click="emit('body-title-clicked')"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
v-html="title" />
|
|
14
|
+
</h2>
|
|
15
|
+
|
|
16
|
+
<body-list-references
|
|
17
|
+
v-if="references.length > 0"
|
|
18
|
+
:references="references" />
|
|
19
|
+
|
|
20
|
+
<p
|
|
21
|
+
v-if="loading"
|
|
22
|
+
class="mt-4"
|
|
23
|
+
>loading…</p
|
|
13
24
|
>
|
|
14
|
-
|
|
25
|
+
|
|
15
26
|
<div
|
|
16
|
-
v-if="text"
|
|
17
|
-
class="
|
|
18
|
-
|
|
27
|
+
v-else-if="coverReference !== null || text"
|
|
28
|
+
class="mt-4">
|
|
29
|
+
<body-list-entry-cover
|
|
30
|
+
v-if="coverReference !== null"
|
|
31
|
+
:reference="coverReference"
|
|
32
|
+
:container-width="bodyCardWidth" />
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
v-if="text"
|
|
36
|
+
class="ffun-entry-body prose max-w-none"
|
|
37
|
+
v-html="text" />
|
|
38
|
+
|
|
39
|
+
<div
|
|
40
|
+
v-if="hasImageCover"
|
|
41
|
+
class="clear-both" />
|
|
42
|
+
</div>
|
|
19
43
|
</div>
|
|
20
44
|
</div>
|
|
21
45
|
</template>
|
|
22
46
|
|
|
23
47
|
<script lang="ts" setup>
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
26
|
-
import type * as t from "@/logic/types";
|
|
27
|
-
import * as events from "@/logic/events";
|
|
48
|
+
import {computed, useTemplateRef} from "vue";
|
|
49
|
+
import {useElementSize} from "@vueuse/core";
|
|
28
50
|
import * as e from "@/logic/enums";
|
|
29
|
-
import
|
|
30
|
-
import DOMPurify from "dompurify";
|
|
31
|
-
import {useEntriesStore} from "@/stores/entries";
|
|
32
|
-
|
|
33
|
-
const entriesStore = useEntriesStore();
|
|
51
|
+
import type * as t from "@/logic/types";
|
|
34
52
|
|
|
35
53
|
const properties = defineProps<{
|
|
36
54
|
url: string | null;
|
|
37
55
|
title: string | null;
|
|
38
56
|
loading: boolean;
|
|
39
57
|
text: string | null;
|
|
58
|
+
references: t.Reference[];
|
|
40
59
|
}>();
|
|
41
60
|
|
|
42
61
|
const emit = defineEmits(["body-title-clicked"]);
|
|
62
|
+
const bodyCard = useTemplateRef("bodyCard");
|
|
63
|
+
const {width: bodyCardWidth} = useElementSize(bodyCard);
|
|
64
|
+
|
|
65
|
+
const coverReference = computed(() => {
|
|
66
|
+
for (const reference of properties.references) {
|
|
67
|
+
if (reference.kind !== e.ReferenceKind.Video) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (reference.youtubeId() !== null) {
|
|
72
|
+
return reference;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const reference of properties.references) {
|
|
77
|
+
if (reference.kind === e.ReferenceKind.Image) {
|
|
78
|
+
return reference;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const hasImageCover = computed(() => {
|
|
86
|
+
return coverReference.value?.kind === e.ReferenceKind.Image;
|
|
87
|
+
});
|
|
43
88
|
</script>
|
|
89
|
+
|
|
90
|
+
<style scoped>
|
|
91
|
+
.ffun-entry-body :deep(iframe) {
|
|
92
|
+
@apply block w-full max-w-full;
|
|
93
|
+
aspect-ratio: 16 / 9;
|
|
94
|
+
height: auto;
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="youtubeVideoId !== null">
|
|
3
|
+
<integrations-you-tube
|
|
4
|
+
:video-id="youtubeVideoId"
|
|
5
|
+
:title="reference.title ?? 'YouTube video'" />
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<img
|
|
9
|
+
v-else-if="isImage"
|
|
10
|
+
ref="imageElement"
|
|
11
|
+
:class="imageClass"
|
|
12
|
+
:src="reference.url"
|
|
13
|
+
:style="imageStyle"
|
|
14
|
+
:alt="reference.title ?? 'Entry cover'" />
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script lang="ts" setup>
|
|
18
|
+
import {computed, useTemplateRef} from "vue";
|
|
19
|
+
import {useElementSize} from "@vueuse/core";
|
|
20
|
+
import * as e from "@/logic/enums";
|
|
21
|
+
import type * as t from "@/logic/types";
|
|
22
|
+
|
|
23
|
+
const properties = defineProps<{
|
|
24
|
+
reference: t.Reference;
|
|
25
|
+
containerWidth?: number;
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
const imageElement = useTemplateRef("imageElement");
|
|
29
|
+
const {width: imageWidth} = useElementSize(imageElement);
|
|
30
|
+
|
|
31
|
+
const youtubeVideoId = computed(() => {
|
|
32
|
+
if (properties.reference.kind !== e.ReferenceKind.Video) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return properties.reference.youtubeId();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const isImage = computed(() => properties.reference.kind === e.ReferenceKind.Image);
|
|
40
|
+
const shouldWrapImage = computed(() => {
|
|
41
|
+
if (!isImage.value) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (properties.containerWidth === undefined || properties.containerWidth <= 0) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (imageWidth.value <= 0) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return imageWidth.value <= properties.containerWidth / 2;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const imageClass = computed(() => {
|
|
57
|
+
if (shouldWrapImage.value) {
|
|
58
|
+
return "float-left mb-2 mr-4 block max-h-[32rem] max-w-full rounded border border-slate-300 object-cover";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return "mx-auto block max-h-[32rem] max-w-full rounded border border-slate-300 object-cover";
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const imageStyle = computed(() => {
|
|
65
|
+
if (properties.reference.kind !== e.ReferenceKind.Image) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (properties.reference.width === null || properties.reference.width <= 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {maxWidth: `${properties.reference.width}px`};
|
|
74
|
+
});
|
|
75
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<a
|
|
3
|
+
:href="reference.url"
|
|
4
|
+
target="_blank"
|
|
5
|
+
rel="noopener noreferrer"
|
|
6
|
+
:title="kindTitle"
|
|
7
|
+
class="block max-w-full break-words rounded border bg-white px-1 py-1 text-sm font-medium leading-tight text-gray-900 hover:bg-gray-50">
|
|
8
|
+
<template v-if="title !== null">
|
|
9
|
+
<span v-html="title" />
|
|
10
|
+
</template>
|
|
11
|
+
<icon
|
|
12
|
+
v-else
|
|
13
|
+
:icon="kindIcon"
|
|
14
|
+
size="small" />
|
|
15
|
+
</a>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script lang="ts" setup>
|
|
19
|
+
import {computed} from "vue";
|
|
20
|
+
import type * as t from "@/logic/types";
|
|
21
|
+
import * as e from "@/logic/enums";
|
|
22
|
+
import * as utils from "@/logic/utils";
|
|
23
|
+
|
|
24
|
+
const properties = defineProps<{
|
|
25
|
+
reference: t.Reference;
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
const title = computed(() => {
|
|
29
|
+
const value = utils.purifyTitle({raw: properties.reference.title, default_: ""});
|
|
30
|
+
|
|
31
|
+
if (value.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return value;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const kindProperties = computed(() => {
|
|
39
|
+
const value = e.ReferenceKindProperties.get(properties.reference.kind);
|
|
40
|
+
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
throw new Error(`Unknown reference kind: ${properties.reference.kind}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return value;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const kindTitle = computed(() => {
|
|
49
|
+
return kindProperties.value.title;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const kindIcon = computed(() => {
|
|
53
|
+
return kindProperties.value.icon;
|
|
54
|
+
});
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-wrap gap-2">
|
|
3
|
+
<body-list-reference
|
|
4
|
+
v-for="reference in references"
|
|
5
|
+
:key="reference.url"
|
|
6
|
+
:reference="reference" />
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script lang="ts" setup>
|
|
11
|
+
import type * as t from "@/logic/types";
|
|
12
|
+
|
|
13
|
+
defineProps<{
|
|
14
|
+
references: t.Reference[];
|
|
15
|
+
}>();
|
|
16
|
+
</script>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-if="hasRows"
|
|
4
|
+
class="mx-2">
|
|
5
|
+
<div class="overflow-hidden rounded-xl border bg-slate-100">
|
|
6
|
+
<div class="overflow-x-auto">
|
|
7
|
+
<table class="min-w-full table-fixed border-collapse text-left">
|
|
8
|
+
<colgroup>
|
|
9
|
+
<col style="width: 25%" />
|
|
10
|
+
<col style="width: 25%" />
|
|
11
|
+
<col style="width: 25%" />
|
|
12
|
+
<col style="width: 25%" />
|
|
13
|
+
</colgroup>
|
|
14
|
+
|
|
15
|
+
<thead>
|
|
16
|
+
<tr class="bg-blue-100 text-slate-900">
|
|
17
|
+
<th class="head-cell">Source</th>
|
|
18
|
+
<th class="head-cell text-center">
|
|
19
|
+
<span class="head-label">
|
|
20
|
+
Feed discovery
|
|
21
|
+
<icon
|
|
22
|
+
class="head-info-icon"
|
|
23
|
+
icon="info-square-filled"
|
|
24
|
+
size="large"
|
|
25
|
+
title="We discover feeds that are not explicitly specified by their sites and therefore are not visible to other feed readers." />
|
|
26
|
+
</span>
|
|
27
|
+
</th>
|
|
28
|
+
<th class="head-cell text-center">
|
|
29
|
+
<span class="head-label">
|
|
30
|
+
Post cleanup
|
|
31
|
+
<icon
|
|
32
|
+
class="head-info-icon"
|
|
33
|
+
icon="info-square-filled"
|
|
34
|
+
size="large"
|
|
35
|
+
title="We postprocess the content of posts to remove unnecessary markup and make them look better and easier to read." />
|
|
36
|
+
</span>
|
|
37
|
+
</th>
|
|
38
|
+
<th class="head-cell text-center">
|
|
39
|
+
<span class="head-label">
|
|
40
|
+
Extra tags
|
|
41
|
+
<icon
|
|
42
|
+
class="head-info-icon"
|
|
43
|
+
icon="info-square-filled"
|
|
44
|
+
size="large"
|
|
45
|
+
title="We add extra tags to news according to their unique properties in the source." />
|
|
46
|
+
</span>
|
|
47
|
+
</th>
|
|
48
|
+
</tr>
|
|
49
|
+
</thead>
|
|
50
|
+
|
|
51
|
+
<tbody>
|
|
52
|
+
<tr
|
|
53
|
+
v-for="row in visibleRows"
|
|
54
|
+
:key="row.name"
|
|
55
|
+
class="border-t border-slate-300 odd:bg-slate-50 even:bg-slate-100">
|
|
56
|
+
<td class="cell cell-source">
|
|
57
|
+
{{ row.name }}
|
|
58
|
+
</td>
|
|
59
|
+
|
|
60
|
+
<td class="cell cell-icon">
|
|
61
|
+
<div class="icon-badge">
|
|
62
|
+
<icon
|
|
63
|
+
:icon="row.discovery ? 'check' : 'x'"
|
|
64
|
+
:class="row.discovery ? 'text-blue-700' : 'text-slate-400'" />
|
|
65
|
+
</div>
|
|
66
|
+
</td>
|
|
67
|
+
|
|
68
|
+
<td class="cell cell-icon">
|
|
69
|
+
<div class="icon-badge">
|
|
70
|
+
<icon
|
|
71
|
+
:icon="row.postprocessing ? 'check' : 'x'"
|
|
72
|
+
:class="row.postprocessing ? 'text-blue-700' : 'text-slate-400'" />
|
|
73
|
+
</div>
|
|
74
|
+
</td>
|
|
75
|
+
|
|
76
|
+
<td class="cell cell-extra"> coming soon </td>
|
|
77
|
+
</tr>
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div
|
|
84
|
+
v-if="showToggleButton"
|
|
85
|
+
class="mt-4 text-center">
|
|
86
|
+
<main-show-more-button v-model:expanded="showAllRows" />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script lang="ts" setup>
|
|
92
|
+
import {computed, ref} from "vue";
|
|
93
|
+
import {useIntegrationsStore} from "@/stores/integrations";
|
|
94
|
+
|
|
95
|
+
const integrations = useIntegrationsStore();
|
|
96
|
+
|
|
97
|
+
const alwaysVisibleRowsCount = 3;
|
|
98
|
+
|
|
99
|
+
const showAllRows = ref(false);
|
|
100
|
+
|
|
101
|
+
const rows = computed(() => {
|
|
102
|
+
return [...integrations.integrations].sort((a, b) => a.name.localeCompare(b.name));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const visibleRows = computed(() => {
|
|
106
|
+
if (showAllRows.value) {
|
|
107
|
+
return rows.value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return rows.value.slice(0, alwaysVisibleRowsCount);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const showToggleButton = computed(() => {
|
|
114
|
+
return rows.value.length > alwaysVisibleRowsCount;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const hasRows = computed(() => {
|
|
118
|
+
return rows.value.length > 0;
|
|
119
|
+
});
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<style scoped>
|
|
123
|
+
.head-cell {
|
|
124
|
+
@apply px-2 py-3 text-base font-medium md:px-4 md:text-lg;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.head-label {
|
|
128
|
+
@apply flex flex-col items-center justify-center gap-0.5 text-center md:flex-row md:gap-1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.head-info-icon {
|
|
132
|
+
@apply text-slate-500 transition-colors cursor-pointer hover:text-black;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.cell {
|
|
136
|
+
@apply px-2 py-3 md:px-4;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.cell-source {
|
|
140
|
+
@apply text-base font-medium text-slate-900 md:text-lg;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.cell-icon {
|
|
144
|
+
@apply text-center;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.cell-extra {
|
|
148
|
+
@apply text-center text-sm font-medium uppercase tracking-wide text-slate-600 md:text-base;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.icon-badge {
|
|
152
|
+
@apply inline-flex h-8 w-8 items-center justify-center rounded-full border border-slate-300 bg-white md:h-9 md:w-9;
|
|
153
|
+
}
|
|
154
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
class="ffun-main-button short"
|
|
4
|
+
@click="emit('update:expanded', !properties.expanded)">
|
|
5
|
+
{{ properties.expanded ? "Show less" : "Show more" }}
|
|
6
|
+
</button>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script lang="ts" setup>
|
|
10
|
+
const properties = defineProps<{
|
|
11
|
+
expanded: boolean;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{
|
|
15
|
+
"update:expanded": [value: boolean];
|
|
16
|
+
}>();
|
|
17
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full overflow-hidden rounded border border-slate-300">
|
|
3
|
+
<div class="aspect-video">
|
|
4
|
+
<iframe
|
|
5
|
+
class="h-full w-full"
|
|
6
|
+
:src="embedUrl"
|
|
7
|
+
:title="title"
|
|
8
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
9
|
+
allowfullscreen
|
|
10
|
+
referrerpolicy="strict-origin-when-cross-origin" />
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts" setup>
|
|
16
|
+
import {computed} from "vue";
|
|
17
|
+
|
|
18
|
+
const properties = withDefaults(
|
|
19
|
+
defineProps<{
|
|
20
|
+
videoId: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
}>(),
|
|
23
|
+
{
|
|
24
|
+
title: "YouTube video"
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const embedUrl = computed(() => `https://www.youtube-nocookie.com/embed/${properties.videoId}`);
|
|
29
|
+
</script>
|
package/src/logic/api.ts
CHANGED
|
@@ -232,6 +232,19 @@ export async function getCollectionFeeds({collectionId}: {collectionId: t.Collec
|
|
|
232
232
|
return feeds;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
export async function getIntegrations() {
|
|
236
|
+
const response = await postPublic({url: "/get-integrations", data: {}});
|
|
237
|
+
|
|
238
|
+
const integrations = [];
|
|
239
|
+
|
|
240
|
+
for (let rawIntegration of response.integrations) {
|
|
241
|
+
const integration = t.integrationInfoFromJSON(rawIntegration);
|
|
242
|
+
integrations.push(integration);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return integrations;
|
|
246
|
+
}
|
|
247
|
+
|
|
235
248
|
export async function getTagsInfo({uids}: {uids: string[]}) {
|
|
236
249
|
const response = await postPublic({url: "/get-tags-info", data: {uids: uids}});
|
|
237
250
|
|
package/src/logic/enums.ts
CHANGED
|
@@ -155,6 +155,38 @@ export const reverseMarker: {[key: string]: Marker} = {
|
|
|
155
155
|
read: Marker.Read
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
+
/////////////////////
|
|
159
|
+
// Reference kind
|
|
160
|
+
/////////////////////
|
|
161
|
+
|
|
162
|
+
export enum ReferenceKind {
|
|
163
|
+
Unknown = "unknown",
|
|
164
|
+
Author = "author",
|
|
165
|
+
Comments = "comments",
|
|
166
|
+
Page = "page",
|
|
167
|
+
Video = "video",
|
|
168
|
+
Audio = "audio",
|
|
169
|
+
Image = "image",
|
|
170
|
+
Document = "document"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export type ReferenceKindProperty = {
|
|
174
|
+
readonly title: string;
|
|
175
|
+
readonly icon: string;
|
|
176
|
+
readonly priority: number;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const ReferenceKindProperties = new Map<ReferenceKind, ReferenceKindProperty>([
|
|
180
|
+
[ReferenceKind.Unknown, {title: "Unknown", icon: "dots", priority: 80}],
|
|
181
|
+
[ReferenceKind.Author, {title: "Author", icon: "user", priority: 10}],
|
|
182
|
+
[ReferenceKind.Comments, {title: "Comments", icon: "comments", priority: 20}],
|
|
183
|
+
[ReferenceKind.Page, {title: "Page", icon: "world", priority: 70}],
|
|
184
|
+
[ReferenceKind.Video, {title: "Video", icon: "player-play", priority: 30}],
|
|
185
|
+
[ReferenceKind.Audio, {title: "Audio", icon: "volume", priority: 40}],
|
|
186
|
+
[ReferenceKind.Image, {title: "Image", icon: "photo", priority: 50}],
|
|
187
|
+
[ReferenceKind.Document, {title: "Document", icon: "file-text", priority: 60}]
|
|
188
|
+
]);
|
|
189
|
+
|
|
158
190
|
//////////////
|
|
159
191
|
// Feeds order
|
|
160
192
|
//////////////
|