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.
- package/.eslintrc.cjs +15 -0
- package/.prettierrc.json +13 -0
- package/.vscode/extensions.json +3 -0
- package/README.md +52 -0
- package/env.d.ts +1 -0
- package/index.html +13 -0
- package/package.json +50 -0
- package/public/favicon.ico +0 -0
- package/src/App.vue +33 -0
- package/src/components/ConfigFlag.vue +22 -0
- package/src/components/ConfigSelector.vue +25 -0
- package/src/components/DiscoveryForm.vue +81 -0
- package/src/components/EntriesList.vue +51 -0
- package/src/components/EntryForList.vue +156 -0
- package/src/components/EntryInfo.vue +23 -0
- package/src/components/FeedForList.vue +115 -0
- package/src/components/FeedInfo.vue +35 -0
- package/src/components/FeedsCollections.vue +53 -0
- package/src/components/FeedsList.vue +27 -0
- package/src/components/FfunGithubButtons.vue +22 -0
- package/src/components/FfunTag.vue +95 -0
- package/src/components/OPMLUpload.vue +46 -0
- package/src/components/OpenaiTokensUsage.vue +61 -0
- package/src/components/RuleConstructor.vue +56 -0
- package/src/components/RuleScoreUpdater.vue +33 -0
- package/src/components/RulesList.vue +52 -0
- package/src/components/SimplePagination.vue +81 -0
- package/src/components/SupertokensLogin.vue +118 -0
- package/src/components/TagsFilter.vue +130 -0
- package/src/components/TagsFilterElement.vue +89 -0
- package/src/components/TagsList.vue +125 -0
- package/src/components/UserSetting.vue +129 -0
- package/src/inputs/Marker.vue +70 -0
- package/src/inputs/ScoreSelector.vue +38 -0
- package/src/layouts/SidePanelLayout.vue +231 -0
- package/src/layouts/WideLayout.vue +44 -0
- package/src/logic/api.ts +253 -0
- package/src/logic/constants.ts +8 -0
- package/src/logic/enums.ts +92 -0
- package/src/logic/settings.ts +37 -0
- package/src/logic/timer.ts +25 -0
- package/src/logic/types.ts +371 -0
- package/src/logic/utils.ts +39 -0
- package/src/main.ts +145 -0
- package/src/router/index.ts +61 -0
- package/src/stores/entries.ts +217 -0
- package/src/stores/globalSettings.ts +74 -0
- package/src/stores/globalState.ts +23 -0
- package/src/stores/supertokens.ts +144 -0
- package/src/stores/tags.ts +54 -0
- package/src/values/DateTime.vue +27 -0
- package/src/values/FeedId.vue +22 -0
- package/src/values/Score.vue +42 -0
- package/src/values/URL.vue +25 -0
- package/src/views/AuthView.vue +66 -0
- package/src/views/CollectionsView.vue +23 -0
- package/src/views/DiscoveryView.vue +26 -0
- package/src/views/FeedsView.vue +124 -0
- package/src/views/MainView.vue +67 -0
- package/src/views/NewsView.vue +96 -0
- package/src/views/RulesView.vue +33 -0
- package/src/views/SettingsView.vue +81 -0
- package/tsconfig.app.json +12 -0
- package/tsconfig.json +14 -0
- package/tsconfig.node.json +8 -0
- package/tsconfig.vitest.json +9 -0
- package/vite.config.ts +26 -0
- 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
|
+
}
|
package/.prettierrc.json
ADDED
|
@@ -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
|
+
}
|
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
|
+
>▼</a
|
|
25
|
+
>
|
|
26
|
+
<a
|
|
27
|
+
href="#"
|
|
28
|
+
style="text-decoration: none"
|
|
29
|
+
v-if="showBody"
|
|
30
|
+
@click.prevent="showBody = false"
|
|
31
|
+
>▲</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>
|