feeds-fun 0.0.4

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.
Files changed (68) hide show
  1. package/.eslintrc.cjs +15 -0
  2. package/.prettierrc.json +13 -0
  3. package/.vscode/extensions.json +3 -0
  4. package/README.md +52 -0
  5. package/env.d.ts +1 -0
  6. package/index.html +13 -0
  7. package/package.json +50 -0
  8. package/public/favicon.ico +0 -0
  9. package/src/App.vue +33 -0
  10. package/src/components/ConfigFlag.vue +22 -0
  11. package/src/components/ConfigSelector.vue +25 -0
  12. package/src/components/DiscoveryForm.vue +81 -0
  13. package/src/components/EntriesList.vue +51 -0
  14. package/src/components/EntryForList.vue +156 -0
  15. package/src/components/EntryInfo.vue +23 -0
  16. package/src/components/FeedForList.vue +115 -0
  17. package/src/components/FeedInfo.vue +35 -0
  18. package/src/components/FeedsCollections.vue +53 -0
  19. package/src/components/FeedsList.vue +27 -0
  20. package/src/components/FfunGithubButtons.vue +22 -0
  21. package/src/components/FfunTag.vue +95 -0
  22. package/src/components/OPMLUpload.vue +46 -0
  23. package/src/components/OpenaiTokensUsage.vue +61 -0
  24. package/src/components/RuleConstructor.vue +56 -0
  25. package/src/components/RuleScoreUpdater.vue +33 -0
  26. package/src/components/RulesList.vue +52 -0
  27. package/src/components/SimplePagination.vue +81 -0
  28. package/src/components/SupertokensLogin.vue +118 -0
  29. package/src/components/TagsFilter.vue +130 -0
  30. package/src/components/TagsFilterElement.vue +89 -0
  31. package/src/components/TagsList.vue +125 -0
  32. package/src/components/UserSetting.vue +129 -0
  33. package/src/inputs/Marker.vue +70 -0
  34. package/src/inputs/ScoreSelector.vue +38 -0
  35. package/src/layouts/SidePanelLayout.vue +231 -0
  36. package/src/layouts/WideLayout.vue +44 -0
  37. package/src/logic/api.ts +253 -0
  38. package/src/logic/constants.ts +8 -0
  39. package/src/logic/enums.ts +92 -0
  40. package/src/logic/settings.ts +37 -0
  41. package/src/logic/timer.ts +25 -0
  42. package/src/logic/types.ts +371 -0
  43. package/src/logic/utils.ts +39 -0
  44. package/src/main.ts +145 -0
  45. package/src/router/index.ts +61 -0
  46. package/src/stores/entries.ts +217 -0
  47. package/src/stores/globalSettings.ts +74 -0
  48. package/src/stores/globalState.ts +23 -0
  49. package/src/stores/supertokens.ts +144 -0
  50. package/src/stores/tags.ts +54 -0
  51. package/src/values/DateTime.vue +27 -0
  52. package/src/values/FeedId.vue +22 -0
  53. package/src/values/Score.vue +42 -0
  54. package/src/values/URL.vue +25 -0
  55. package/src/views/AuthView.vue +66 -0
  56. package/src/views/CollectionsView.vue +23 -0
  57. package/src/views/DiscoveryView.vue +26 -0
  58. package/src/views/FeedsView.vue +124 -0
  59. package/src/views/MainView.vue +67 -0
  60. package/src/views/NewsView.vue +96 -0
  61. package/src/views/RulesView.vue +33 -0
  62. package/src/views/SettingsView.vue +81 -0
  63. package/tsconfig.app.json +12 -0
  64. package/tsconfig.json +14 -0
  65. package/tsconfig.node.json +8 -0
  66. package/tsconfig.vitest.json +9 -0
  67. package/vite.config.ts +26 -0
  68. package/vitest.config.ts +15 -0
package/.eslintrc.cjs ADDED
@@ -0,0 +1,15 @@
1
+ /* eslint-env node */
2
+ require('@rushstack/eslint-patch/modern-module-resolution')
3
+
4
+ module.exports = {
5
+ root: true,
6
+ 'extends': [
7
+ 'plugin:vue/vue3-essential',
8
+ 'eslint:recommended',
9
+ '@vue/eslint-config-typescript',
10
+ '@vue/eslint-config-prettier/skip-formatting'
11
+ ],
12
+ parserOptions: {
13
+ ecmaVersion: 'latest'
14
+ }
15
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/prettierrc",
3
+ "semi": true,
4
+ "tabWidth": 2,
5
+ "singleQuote": false,
6
+ "printWidth": 119,
7
+ "trailingComma": "none",
8
+ "bracketSpacing": false,
9
+ "bracketSameLine": true,
10
+ "htmlWhitespaceSensitivity": "strict",
11
+ "vueIndentScriptAndStyle": true,
12
+ "singleAttributePerLine": true
13
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3
+ }
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # site
2
+
3
+ This template should help get you started developing with Vue 3 in Vite.
4
+
5
+ ## Recommended IDE Setup
6
+
7
+ [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
8
+
9
+ ## Type Support for `.vue` Imports in TS
10
+
11
+ TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
12
+
13
+ If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
14
+
15
+ 1. Disable the built-in TypeScript Extension
16
+ 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
17
+ 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
18
+ 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
19
+
20
+ ## Customize configuration
21
+
22
+ See [Vite Configuration Reference](https://vitejs.dev/config/).
23
+
24
+ ## Project Setup
25
+
26
+ ```sh
27
+ npm install
28
+ ```
29
+
30
+ ### Compile and Hot-Reload for Development
31
+
32
+ ```sh
33
+ npm run dev
34
+ ```
35
+
36
+ ### Type-Check, Compile and Minify for Production
37
+
38
+ ```sh
39
+ npm run build
40
+ ```
41
+
42
+ ### Run Unit Tests with [Vitest](https://vitest.dev/)
43
+
44
+ ```sh
45
+ npm run test:unit
46
+ ```
47
+
48
+ ### Lint with [ESLint](https://eslint.org/)
49
+
50
+ ```sh
51
+ npm run lint
52
+ ```
package/env.d.ts ADDED
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="icon" href="/favicon.ico">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Feeds Fun</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "feeds-fun",
3
+ "version": "0.0.4",
4
+ "private": false,
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "run-p type-check build-only",
8
+ "preview": "vite preview",
9
+ "test:unit": "vitest",
10
+ "build-only": "vite build",
11
+ "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
12
+ "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
13
+ "format": "prettier --write src/",
14
+ "format-check": "prettier --check src/"
15
+ },
16
+ "dependencies": {
17
+ "@chenfengyuan/vue-countdown": "^2.1.1",
18
+ "@sentry/vue": "^7.54.0",
19
+ "@vueuse/core": "^9.13.0",
20
+ "axios": "^1.3.4",
21
+ "dompurify": "^3.0.1",
22
+ "pinia": "^2.0.32",
23
+ "set-interval-async": "^3.0.3",
24
+ "supertokens-web-js": "^0.5.0",
25
+ "vue": "^3.2.47",
26
+ "vue-github-button": "^3.1.0",
27
+ "vue-router": "^4.1.6"
28
+ },
29
+ "devDependencies": {
30
+ "@rushstack/eslint-patch": "^1.2.0",
31
+ "@types/dompurify": "^3.0.2",
32
+ "@types/jsdom": "^21.1.0",
33
+ "@types/lodash": "^4.14.196",
34
+ "@types/node": "^18.14.2",
35
+ "@vitejs/plugin-vue": "^4.0.0",
36
+ "@vue/eslint-config-prettier": "^7.1.0",
37
+ "@vue/eslint-config-typescript": "^11.0.2",
38
+ "@vue/test-utils": "^2.3.0",
39
+ "@vue/tsconfig": "^0.1.3",
40
+ "eslint": "^8.34.0",
41
+ "eslint-plugin-vue": "^9.9.0",
42
+ "jsdom": "^21.1.0",
43
+ "npm-run-all": "^4.1.5",
44
+ "prettier": "^2.8.4",
45
+ "typescript": "~4.8.4",
46
+ "vite": "^4.1.4",
47
+ "vitest": "^0.29.1",
48
+ "vue-tsc": "^1.2.0"
49
+ }
50
+ }
Binary file
package/src/App.vue ADDED
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <router-view />
3
+ </template>
4
+
5
+ <script setup lang="ts"></script>
6
+
7
+ <style scoped>
8
+ .container {
9
+ display: flex;
10
+ }
11
+
12
+ .nav-panel {
13
+ width: 10rem;
14
+ flex-shrink: 0;
15
+ background-color: #f0f0f0;
16
+ padding: 1rem;
17
+ }
18
+
19
+ .main-content {
20
+ flex-grow: 1;
21
+ padding: 1rem;
22
+ }
23
+
24
+ .config-menu {
25
+ list-style-type: none;
26
+ margin: 0;
27
+ padding: 0;
28
+ }
29
+
30
+ .config-menu-item {
31
+ margin-bottom: 1rem;
32
+ }
33
+ </style>
@@ -0,0 +1,22 @@
1
+ <template>
2
+ <a
3
+ v-if="flag"
4
+ href="#"
5
+ @click="emit('update:flag', false)"
6
+ >{{ onText }}</a
7
+ >
8
+ <a
9
+ v-if="!flag"
10
+ href="#"
11
+ @click="emit('update:flag', true)"
12
+ >{{ offText }}</a
13
+ >
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ defineProps<{flag: boolean; onText: string; offText: string}>();
18
+
19
+ const emit = defineEmits(["update:flag"]);
20
+ </script>
21
+
22
+ <style scoped></style>
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <select @change="updateProperty($event)">
3
+ <option
4
+ v-for="[value, props] of values"
5
+ :value="value"
6
+ :selected="value === property">
7
+ {{ props.text }}
8
+ </option>
9
+ </select>
10
+ </template>
11
+
12
+ <script lang="ts" setup>
13
+ import * as e from "@/logic/enums";
14
+
15
+ const properties = defineProps<{values: any; property: string}>();
16
+
17
+ const emit = defineEmits(["update:property"]);
18
+
19
+ function updateProperty(event: Event) {
20
+ const target = event.target as HTMLSelectElement;
21
+ emit("update:property", target.value);
22
+ }
23
+ </script>
24
+
25
+ <style></style>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <div>
3
+ <input
4
+ type="text"
5
+ v-model="search"
6
+ :disabled="loading"
7
+ placeholder="Search for feeds" />
8
+
9
+ <button
10
+ :disabled="loading"
11
+ @click.prevent="searhedUrl = search">
12
+ Search
13
+ </button>
14
+
15
+ <hr />
16
+
17
+ <div v-if="!loading && foundFeeds.length === 0">No feeds found</div>
18
+
19
+ <div v-else-if="loading">Searching for feeds…</div>
20
+
21
+ <div v-else>
22
+ <div
23
+ v-for="feed in foundFeeds"
24
+ :key="feed.url">
25
+ <feed-info :feed="feed" />
26
+
27
+ <button
28
+ v-if="!addedFeeds[feed.url]"
29
+ @click.prevent="addFeed(feed.url)">
30
+ Add
31
+ </button>
32
+ <p v-else>Feed added</p>
33
+ <hr />
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <script lang="ts" setup>
40
+ import {computed, ref} from "vue";
41
+ import type * as t from "@/logic/types";
42
+ import * as e from "@/logic/enums";
43
+ import * as api from "@/logic/api";
44
+ import {computedAsync} from "@vueuse/core";
45
+ import {useEntriesStore} from "@/stores/entries";
46
+
47
+ const search = ref("");
48
+ const loading = ref(false);
49
+
50
+ const searhedUrl = ref("");
51
+
52
+ const addedFeeds = ref<{[key: string]: boolean}>({});
53
+
54
+ const foundFeeds = computedAsync(async () => {
55
+ if (searhedUrl.value === "") {
56
+ return [];
57
+ }
58
+
59
+ loading.value = true;
60
+
61
+ let feeds: t.FeedInfo[] = [];
62
+
63
+ try {
64
+ feeds = await api.discoverFeeds({url: searhedUrl.value});
65
+ } catch (e) {
66
+ console.error(e);
67
+ }
68
+
69
+ loading.value = false;
70
+
71
+ return feeds;
72
+ }, []);
73
+
74
+ async function addFeed(url: string) {
75
+ addedFeeds.value[url] = true;
76
+
77
+ await api.addFeed({url: url});
78
+ }
79
+ </script>
80
+
81
+ <style scoped></style>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <div>
3
+ <template v-if="entriesToShow.length > 0">
4
+ <ul style="list-style-type: none; margin: 0; padding: 0">
5
+ <li
6
+ v-for="entryId in entriesToShow"
7
+ :key="entryId"
8
+ style="margin-bottom: 0.25rem">
9
+ <entry-for-list
10
+ :entryId="entryId"
11
+ :time-field="timeField"
12
+ :show-tags="showTags" />
13
+ </li>
14
+ </ul>
15
+
16
+ <hr />
17
+
18
+ <simple-pagination
19
+ :showFromStart="showFromStart"
20
+ :showPerPage="showPerPage"
21
+ :total="entriesIds.length"
22
+ :counterOnNewLine="false"
23
+ v-model:showEntries="showEntries" />
24
+ </template>
25
+ </div>
26
+ </template>
27
+
28
+ <script lang="ts" setup>
29
+ import {computed, ref} from "vue";
30
+ import type * as t from "@/logic/types";
31
+ import {computedAsync} from "@vueuse/core";
32
+
33
+ const properties = defineProps<{
34
+ entriesIds: Array<t.EntryId>;
35
+ timeField: string;
36
+ showTags: boolean;
37
+ showFromStart: number;
38
+ showPerPage: number;
39
+ }>();
40
+
41
+ const showEntries = ref(properties.showFromStart);
42
+
43
+ const entriesToShow = computed(() => {
44
+ if (properties.entriesIds == null) {
45
+ return [];
46
+ }
47
+ return properties.entriesIds.slice(0, showEntries.value);
48
+ });
49
+ </script>
50
+
51
+ <style></style>
@@ -0,0 +1,156 @@
1
+ <template>
2
+ <div class="container">
3
+ <div style="flex-shrink: 0; width: 2rem; text-align: right; padding-right: 0.25rem">
4
+ <value-score
5
+ :value="entry.score"
6
+ :entry-id="entry.id"
7
+ class="entity-for-list-score" />
8
+ </div>
9
+
10
+ <div style="flex-grow: 1">
11
+ <input-marker
12
+ :marker="e.Marker.Read"
13
+ :entry-id="entryId"
14
+ on-text="read"
15
+ off-text="new!" />
16
+
17
+ ·
18
+
19
+ <a
20
+ href="#"
21
+ style="text-decoration: none"
22
+ v-if="!showBody"
23
+ @click.prevent="displayBody()"
24
+ >&#9660;</a
25
+ >
26
+ <a
27
+ href="#"
28
+ style="text-decoration: none"
29
+ v-if="showBody"
30
+ @click.prevent="showBody = false"
31
+ >&#9650;</a
32
+ >
33
+
34
+ <a
35
+ :href="entry.url"
36
+ target="_blank"
37
+ @click="onTitleClick()"
38
+ rel="noopener noreferrer">
39
+ {{ purifiedTitle }}
40
+ </a>
41
+
42
+ <template v-if="showTags">
43
+ <br />
44
+ <tags-list :tags="entry.tags" />
45
+ </template>
46
+
47
+ <div
48
+ v-if="showBody"
49
+ style="display: flex; justify-content: center">
50
+ <div style="max-width: 50rem">
51
+ <h2>{{ purifiedTitle }}</h2>
52
+ <p v-if="entry.body === null">loading...</p>
53
+ <div
54
+ v-if="entry.body !== null"
55
+ v-html="purifiedBody" />
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <div style="flex-shrink: 0; width: 1rem; left-padding: 0.25rem">
61
+ <value-date-time
62
+ :value="timeFor"
63
+ :reversed="true" />
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <script lang="ts" setup>
69
+ import _ from "lodash";
70
+ import {computed, ref} from "vue";
71
+ import type * as t from "@/logic/types";
72
+ import * as e from "@/logic/enums";
73
+ import {computedAsync} from "@vueuse/core";
74
+ import DOMPurify from "dompurify";
75
+ import {useEntriesStore} from "@/stores/entries";
76
+
77
+ const entriesStore = useEntriesStore();
78
+
79
+ const properties = defineProps<{
80
+ entryId: t.EntryId;
81
+ timeField: string;
82
+ showTags: boolean;
83
+ }>();
84
+
85
+ const entry = computed(() => {
86
+ if (properties.entryId in entriesStore.entries) {
87
+ return entriesStore.entries[properties.entryId];
88
+ }
89
+
90
+ throw new Error(`Unknown entry: ${properties.entryId}`);
91
+ });
92
+
93
+ const showBody = ref(false);
94
+
95
+ const timeFor = computed(() => {
96
+ if (entry.value === null) {
97
+ return null;
98
+ }
99
+
100
+ return _.get(entry.value, properties.timeField, null);
101
+ });
102
+
103
+ function displayBody() {
104
+ showBody.value = true;
105
+
106
+ if (entry.value === null) {
107
+ throw new Error("entry is null");
108
+ }
109
+
110
+ entriesStore.requestFullEntry({entryId: entry.value.id});
111
+ }
112
+
113
+ const purifiedTitle = computed(() => {
114
+ if (entry.value === null) {
115
+ return "";
116
+ }
117
+
118
+ // TODO: remove emojis?
119
+ let title = DOMPurify.sanitize(entry.value.title, {ALLOWED_TAGS: []});
120
+
121
+ if (title.length === 0) {
122
+ title = "No title";
123
+ }
124
+
125
+ return title;
126
+ });
127
+
128
+ const purifiedBody = computed(() => {
129
+ if (entry.value === null) {
130
+ return "";
131
+ }
132
+
133
+ if (entry.value.body === null) {
134
+ return "";
135
+ }
136
+ return DOMPurify.sanitize(entry.value.body);
137
+ });
138
+
139
+ async function onTitleClick() {
140
+ await entriesStore.setMarker({
141
+ entryId: properties.entryId,
142
+ marker: e.Marker.Read
143
+ });
144
+ }
145
+ </script>
146
+
147
+ <style scoped>
148
+ .container {
149
+ display: flex;
150
+ }
151
+
152
+ .container :deep(img) {
153
+ max-width: 100%;
154
+ height: auto;
155
+ }
156
+ </style>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <div>
3
+ <a
4
+ :href="entry.url"
5
+ target="_blank"
6
+ rel="noopener noreferrer">
7
+ {{ entry.title }}
8
+ </a>
9
+ </div>
10
+ </template>
11
+
12
+ <script lang="ts" setup>
13
+ import {computed, ref} from "vue";
14
+ import type * as t from "@/logic/types";
15
+ import * as e from "@/logic/enums";
16
+ import * as api from "@/logic/api";
17
+ import {computedAsync} from "@vueuse/core";
18
+ import {useEntriesStore} from "@/stores/entries";
19
+
20
+ const props = defineProps<{entry: t.EntryInfo}>();
21
+ </script>
22
+
23
+ <style scoped></style>
@@ -0,0 +1,115 @@
1
+ <template>
2
+ <div
3
+ v-if="feed !== null"
4
+ class="container">
5
+ <div style="flex-shrink: 0; width: 4rem; left-padding: 0.25rem">
6
+ <a
7
+ href="#"
8
+ @click.prevent="unsubscribe()">
9
+ remove
10
+ </a>
11
+ </div>
12
+
13
+ <div
14
+ style="flex-shrink: 0; width: 3rem; left-padding: 0.25rem; cursor: default"
15
+ title="Time of last load">
16
+ <value-date-time
17
+ :value="feed.loadedAt"
18
+ :reversed="true" />
19
+ </div>
20
+
21
+ <div
22
+ style="flex-shrink: 0; width: 3rem; left-padding: 0.25rem; cursor: default"
23
+ title="When was added">
24
+ <value-date-time
25
+ :value="feed.linkedAt"
26
+ :reversed="true" />
27
+ </div>
28
+
29
+ <div style="flex-shrink: 0; width: 2rem; text-align: right; padding-right: 0.25rem">
30
+ <span
31
+ v-if="feed.isOk"
32
+ title="everything is ok"
33
+ class="state-ok"
34
+ >ok</span
35
+ >
36
+ <span
37
+ v-else
38
+ :title="feed.lastError || 'unknown error'"
39
+ class="state-error"
40
+ >⚠</span
41
+ >
42
+ </div>
43
+
44
+ <div style="flex-grow: 1">
45
+ <value-url
46
+ :value="feed.url"
47
+ :text="purifiedTitle" />
48
+ <template v-if="globalSettings.showFeedsDescriptions">
49
+ <br />
50
+ <div v-html="purifiedDescription" />
51
+ </template>
52
+ </div>
53
+ </div>
54
+ </template>
55
+
56
+ <script lang="ts" setup>
57
+ import {computed, ref} from "vue";
58
+ import type * as t from "@/logic/types";
59
+ import * as e from "@/logic/enums";
60
+ import * as api from "@/logic/api";
61
+ import {computedAsync} from "@vueuse/core";
62
+ import DOMPurify from "dompurify";
63
+ import {useGlobalSettingsStore} from "@/stores/globalSettings";
64
+
65
+ const globalSettings = useGlobalSettingsStore();
66
+
67
+ const properties = defineProps<{feed: t.Feed}>();
68
+
69
+ const purifiedTitle = computed(() => {
70
+ if (properties.feed.title === null) {
71
+ return "";
72
+ }
73
+
74
+ let title = DOMPurify.sanitize(properties.feed.title, {ALLOWED_TAGS: []});
75
+
76
+ if (title.length === 0) {
77
+ return null;
78
+ }
79
+
80
+ return title;
81
+ });
82
+
83
+ const purifiedDescription = computed(() => {
84
+ if (properties.feed.description === null) {
85
+ return "";
86
+ }
87
+ return DOMPurify.sanitize(properties.feed.description);
88
+ });
89
+
90
+ async function unsubscribe() {
91
+ await api.unsubscribe({feedId: properties.feed.id});
92
+ globalSettings.updateDataVersion();
93
+ }
94
+ </script>
95
+
96
+ <style scoped>
97
+ .container {
98
+ display: flex;
99
+ }
100
+
101
+ .container :deep(img) {
102
+ max-width: 100%;
103
+ height: auto;
104
+ }
105
+
106
+ .state-ok {
107
+ color: green;
108
+ cursor: default;
109
+ }
110
+
111
+ .state-error {
112
+ color: red;
113
+ cursor: default;
114
+ }
115
+ </style>