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
@@ -0,0 +1,371 @@
1
+ import * as e from "@/logic/enums";
2
+
3
+ export type FeedId = string & {readonly __brand: unique symbol};
4
+
5
+ export function toFeedId(id: string): FeedId {
6
+ return id as FeedId;
7
+ }
8
+
9
+ export type EntryId = string & {readonly __brand: unique symbol};
10
+
11
+ export function toEntryId(id: string): EntryId {
12
+ return id as EntryId;
13
+ }
14
+
15
+ export type RuleId = string & {readonly __brand: unique symbol};
16
+
17
+ export function toRuleId(id: string): RuleId {
18
+ return id as RuleId;
19
+ }
20
+
21
+ export type FeedsCollectionId = string & {readonly __brand: unique symbol};
22
+
23
+ export function toFeedsCollectionId(id: string): FeedsCollectionId {
24
+ return id as FeedsCollectionId;
25
+ }
26
+
27
+ export type URL = string & {readonly __brand: unique symbol};
28
+
29
+ export function toURL(url: string): URL {
30
+ return url as URL;
31
+ }
32
+
33
+ export class Feed {
34
+ readonly id: FeedId;
35
+ readonly title: string | null;
36
+ readonly description: string | null;
37
+ readonly url: URL;
38
+ readonly state: string;
39
+ readonly lastError: string | null;
40
+ readonly loadedAt: Date | null;
41
+ readonly linkedAt: Date;
42
+ readonly isOk: boolean;
43
+
44
+ constructor({
45
+ id,
46
+ title,
47
+ description,
48
+ url,
49
+ state,
50
+ lastError,
51
+ loadedAt,
52
+ linkedAt,
53
+ isOk
54
+ }: {
55
+ id: FeedId;
56
+ title: string | null;
57
+ description: string | null;
58
+ url: URL;
59
+ state: string;
60
+ lastError: string | null;
61
+ loadedAt: Date | null;
62
+ linkedAt: Date;
63
+ isOk: boolean;
64
+ }) {
65
+ this.id = id;
66
+ this.title = title;
67
+ this.description = description;
68
+ this.url = url;
69
+ this.state = state;
70
+ this.lastError = lastError;
71
+ this.loadedAt = loadedAt;
72
+ this.linkedAt = linkedAt;
73
+ this.isOk = isOk;
74
+ }
75
+ }
76
+
77
+ export function feedFromJSON({
78
+ id,
79
+ title,
80
+ description,
81
+ url,
82
+ state,
83
+ lastError,
84
+ loadedAt,
85
+ linkedAt
86
+ }: {
87
+ id: string;
88
+ title: string;
89
+ description: string;
90
+ url: string;
91
+ state: string;
92
+ lastError: string | null;
93
+ loadedAt: string;
94
+ linkedAt: string;
95
+ }): Feed {
96
+ return {
97
+ id: toFeedId(id),
98
+ title: title !== null ? title : null,
99
+ description: description !== null ? description : null,
100
+ url: toURL(url),
101
+ state: state,
102
+ lastError: lastError,
103
+ loadedAt: loadedAt !== null ? new Date(loadedAt) : null,
104
+ linkedAt: new Date(linkedAt),
105
+ isOk: state === "loaded"
106
+ };
107
+ }
108
+
109
+ export class Entry {
110
+ readonly id: EntryId;
111
+ readonly feedId: FeedId;
112
+ readonly title: string;
113
+ readonly url: URL;
114
+ readonly tags: string[];
115
+ readonly markers: e.Marker[];
116
+ readonly score: number;
117
+ readonly scoreToZero: number;
118
+ readonly publishedAt: Date;
119
+ readonly catalogedAt: Date;
120
+ body: string | null;
121
+
122
+ constructor({
123
+ id,
124
+ feedId,
125
+ title,
126
+ url,
127
+ tags,
128
+ markers,
129
+ score,
130
+ publishedAt,
131
+ catalogedAt,
132
+ body
133
+ }: {
134
+ id: EntryId;
135
+ feedId: FeedId;
136
+ title: string;
137
+ url: URL;
138
+ tags: string[];
139
+ markers: e.Marker[];
140
+ score: number;
141
+ publishedAt: Date;
142
+ catalogedAt: Date;
143
+ body: string | null;
144
+ }) {
145
+ this.id = id;
146
+ this.feedId = feedId;
147
+ this.title = title;
148
+ this.url = url;
149
+ this.tags = tags;
150
+ this.markers = markers;
151
+ this.score = score;
152
+ this.publishedAt = publishedAt;
153
+ this.catalogedAt = catalogedAt;
154
+ this.body = body;
155
+
156
+ this.scoreToZero = -Math.abs(score);
157
+ }
158
+
159
+ setMarker(marker: e.Marker): void {
160
+ if (!this.hasMarker(marker)) {
161
+ this.markers.push(marker);
162
+ }
163
+ }
164
+
165
+ removeMarker(marker: e.Marker): void {
166
+ if (this.hasMarker(marker)) {
167
+ this.markers.splice(this.markers.indexOf(marker), 1);
168
+ }
169
+ }
170
+
171
+ hasMarker(marker: e.Marker): boolean {
172
+ return this.markers.includes(marker);
173
+ }
174
+ }
175
+
176
+ export function entryFromJSON({
177
+ id,
178
+ feedId,
179
+ title,
180
+ url,
181
+ tags,
182
+ markers,
183
+ score,
184
+ publishedAt,
185
+ catalogedAt,
186
+ body
187
+ }: {
188
+ id: string;
189
+ feedId: string;
190
+ title: string;
191
+ url: string;
192
+ tags: string[];
193
+ markers: string[];
194
+ score: number;
195
+ publishedAt: string;
196
+ catalogedAt: string;
197
+ body: string | null;
198
+ }): Entry {
199
+ return new Entry({
200
+ id: toEntryId(id),
201
+ feedId: toFeedId(feedId),
202
+ title,
203
+ url: toURL(url),
204
+ tags: tags,
205
+ markers: markers.map((m: string) => {
206
+ if (m in e.reverseMarker) {
207
+ return e.reverseMarker[m];
208
+ }
209
+
210
+ throw new Error(`Unknown marker: ${m}`);
211
+ }),
212
+ score: score,
213
+ publishedAt: new Date(publishedAt),
214
+ catalogedAt: new Date(catalogedAt),
215
+ body: body
216
+ });
217
+ }
218
+
219
+ export type Rule = {
220
+ readonly id: RuleId;
221
+ readonly tags: string[];
222
+ readonly score: number;
223
+ readonly createdAt: Date;
224
+ };
225
+
226
+ export function ruleFromJSON({
227
+ id,
228
+ tags,
229
+ score,
230
+ createdAt
231
+ }: {
232
+ id: string;
233
+ tags: string[];
234
+ score: number;
235
+ createdAt: string;
236
+ }): Rule {
237
+ return {
238
+ id: toRuleId(id),
239
+ tags: tags,
240
+ score: score,
241
+ createdAt: new Date(createdAt)
242
+ };
243
+ }
244
+
245
+ export type EntryInfo = {
246
+ readonly title: string;
247
+ readonly body: string;
248
+ readonly url: URL;
249
+ readonly publishedAt: Date;
250
+ };
251
+
252
+ export function entryInfoFromJSON({
253
+ title,
254
+ body,
255
+ url,
256
+ publishedAt
257
+ }: {
258
+ title: string;
259
+ body: string;
260
+ url: string;
261
+ publishedAt: string;
262
+ }): EntryInfo {
263
+ return {title, body, url: toURL(url), publishedAt: new Date(publishedAt)};
264
+ }
265
+
266
+ export type FeedInfo = {
267
+ readonly url: URL;
268
+ readonly title: string;
269
+ readonly description: string;
270
+ readonly entries: EntryInfo[];
271
+ };
272
+
273
+ export function feedInfoFromJSON({
274
+ url,
275
+ title,
276
+ description,
277
+ entries
278
+ }: {
279
+ url: string;
280
+ title: string;
281
+ description: string;
282
+ entries: any[];
283
+ }): FeedInfo {
284
+ return {
285
+ url: toURL(url),
286
+ title,
287
+ description,
288
+ entries: entries.map(entryInfoFromJSON)
289
+ };
290
+ }
291
+
292
+ export type TagInfo = {
293
+ readonly uid: string;
294
+ readonly name: string | null;
295
+ readonly link: string | null;
296
+ readonly categories: string[];
297
+ };
298
+
299
+ export function tagInfoFromJSON({
300
+ uid,
301
+ name,
302
+ link,
303
+ categories
304
+ }: {
305
+ uid: string;
306
+ name: string | null;
307
+ link: string | null;
308
+ categories: string[];
309
+ }): TagInfo {
310
+ return {uid, name: name, link: link, categories};
311
+ }
312
+
313
+ export function noInfoTag(uid: string): TagInfo {
314
+ return {uid, name: null, link: null, categories: []};
315
+ }
316
+
317
+ export type UserSetting = {
318
+ readonly kind: string;
319
+ readonly type: string;
320
+ value: string | number | boolean;
321
+ readonly name: string;
322
+ readonly description: string;
323
+ };
324
+
325
+ export function userSettingFromJSON({
326
+ kind,
327
+ type,
328
+ value,
329
+ name,
330
+ description
331
+ }: {
332
+ kind: string;
333
+ type: string;
334
+ value: string | number | boolean;
335
+ name: string;
336
+ description: string;
337
+ }): UserSetting {
338
+ return {kind, type, value, name, description};
339
+ }
340
+
341
+ export class ResourceHistoryRecord {
342
+ readonly intervalStartedAt: Date;
343
+ readonly used: number;
344
+ readonly reserved: number;
345
+
346
+ constructor({intervalStartedAt, used, reserved}: {intervalStartedAt: Date; used: number; reserved: number}) {
347
+ this.intervalStartedAt = intervalStartedAt;
348
+ this.used = used;
349
+ this.reserved = reserved;
350
+ }
351
+
352
+ total(): number {
353
+ return this.used + this.reserved;
354
+ }
355
+ }
356
+
357
+ export function resourceHistoryRecordFromJSON({
358
+ intervalStartedAt,
359
+ used,
360
+ reserved
361
+ }: {
362
+ intervalStartedAt: string;
363
+ used: number;
364
+ reserved: number;
365
+ }): ResourceHistoryRecord {
366
+ return new ResourceHistoryRecord({
367
+ intervalStartedAt: new Date(intervalStartedAt),
368
+ used,
369
+ reserved
370
+ });
371
+ }
@@ -0,0 +1,39 @@
1
+ import _ from "lodash";
2
+
3
+ export function timeSince(date: Date) {
4
+ const now = new Date();
5
+
6
+ const secondsPast = Math.floor((now.getTime() - date.getTime()) / 1000);
7
+
8
+ if (secondsPast < 60) {
9
+ return "<min";
10
+ }
11
+
12
+ const minutesPast = Math.floor(secondsPast / 60);
13
+
14
+ if (minutesPast < 60) {
15
+ return `${minutesPast}min`;
16
+ }
17
+
18
+ const hoursPast = Math.floor(minutesPast / 60);
19
+
20
+ if (hoursPast < 24) {
21
+ return `${hoursPast}h`;
22
+ }
23
+
24
+ const daysPast = Math.floor(hoursPast / 24);
25
+
26
+ if (daysPast < 30) {
27
+ return `${daysPast}d`;
28
+ }
29
+
30
+ const monthsPast = Math.floor(daysPast / 30);
31
+
32
+ if (monthsPast < 12) {
33
+ return `${monthsPast}m`;
34
+ }
35
+
36
+ const yearsPast = Math.floor(monthsPast / 12);
37
+
38
+ return `${yearsPast}y`;
39
+ }
package/src/main.ts ADDED
@@ -0,0 +1,145 @@
1
+ import {createApp} from "vue";
2
+ import {createPinia} from "pinia";
3
+ import * as Sentry from "@sentry/vue";
4
+
5
+ import App from "./App.vue";
6
+ import router from "./router";
7
+
8
+ import FeedsList from "./components/FeedsList.vue";
9
+ import EntriesList from "./components/EntriesList.vue";
10
+ import RulesList from "./components/RulesList.vue";
11
+ import TagsList from "./components/TagsList.vue";
12
+ import ConfigSelector from "./components/ConfigSelector.vue";
13
+ import ConfigFlag from "./components/ConfigFlag.vue";
14
+ import EntryForList from "./components/EntryForList.vue";
15
+ import RuleConstructor from "./components/RuleConstructor.vue";
16
+ import RuleScoreUpdater from "./components/RuleScoreUpdater.vue";
17
+ import TagsFilter from "./components/TagsFilter.vue";
18
+ import TagsFilterElement from "./components/TagsFilterElement.vue";
19
+ import DiscoveryForm from "./components/DiscoveryForm.vue";
20
+ import FeedInfo from "./components/FeedInfo.vue";
21
+ import EntryInfo from "./components/EntryInfo.vue";
22
+ import OpmlUpload from "./components/OPMLUpload.vue";
23
+ import FeedForList from "./components/FeedForList.vue";
24
+ import SupertokensLogin from "./components/SupertokensLogin.vue";
25
+ import FeedsCollections from "./components/FeedsCollections.vue";
26
+ import FfunGithubButtons from "./components/FfunGithubButtons.vue";
27
+ import FfunTag from "./components/FfunTag.vue";
28
+ import SimplePagination from "./components/SimplePagination.vue";
29
+ import UserSetting from "./components/UserSetting.vue";
30
+ import OpenaiTokensUsage from "./components/OpenaiTokensUsage.vue";
31
+
32
+ import ScoreSelector from "./inputs/ScoreSelector.vue";
33
+ import InputMarker from "./inputs/Marker.vue";
34
+
35
+ import ValueUrl from "./values/URL.vue";
36
+ import ValueFeedId from "./values/FeedId.vue";
37
+ import ValueDateTime from "./values/DateTime.vue";
38
+ import ValueScore from "./values/Score.vue";
39
+
40
+ import WideLayout from "./layouts/WideLayout.vue";
41
+ import SidePanelLayout from "./layouts/SidePanelLayout.vue";
42
+
43
+ import {useSupertokens} from "@/stores/supertokens";
44
+
45
+ import VueCountdown from "@chenfengyuan/vue-countdown";
46
+ import GithubButton from "vue-github-button";
47
+
48
+ const app = createApp(App);
49
+
50
+ app.component("FeedsList", FeedsList);
51
+ app.component("EntriesList", EntriesList);
52
+ app.component("RulesList", RulesList);
53
+ app.component("TagsList", TagsList);
54
+ app.component("ConfigSelector", ConfigSelector);
55
+ app.component("ConfigFlag", ConfigFlag);
56
+ app.component("EntryForList", EntryForList);
57
+ app.component("RuleConstructor", RuleConstructor);
58
+ app.component("RuleScoreUpdater", RuleScoreUpdater);
59
+ app.component("TagsFilter", TagsFilter);
60
+ app.component("TagsFilterElement", TagsFilterElement);
61
+ app.component("DiscoveryForm", DiscoveryForm);
62
+ app.component("FeedInfo", FeedInfo);
63
+ app.component("EntryInfo", EntryInfo);
64
+ app.component("OpmlUpload", OpmlUpload);
65
+ app.component("FeedForList", FeedForList);
66
+ app.component("SupertokensLogin", SupertokensLogin);
67
+ app.component("FeedsCollections", FeedsCollections);
68
+ app.component("FfunGithubButtons", FfunGithubButtons);
69
+ app.component("FfunTag", FfunTag);
70
+ app.component("SimplePagination", SimplePagination);
71
+ app.component("UserSetting", UserSetting);
72
+ app.component("OpenaiTokensUsage", OpenaiTokensUsage);
73
+
74
+ app.component("ScoreSelector", ScoreSelector);
75
+ app.component("InputMarker", InputMarker);
76
+
77
+ app.component("ValueUrl", ValueUrl);
78
+ app.component("ValueFeedId", ValueFeedId);
79
+ app.component("ValueDateTime", ValueDateTime);
80
+ app.component("ValueScore", ValueScore);
81
+
82
+ app.component("WideLayout", WideLayout);
83
+ app.component("SidePanelLayout", SidePanelLayout);
84
+
85
+ app.component("vue-countdown", VueCountdown);
86
+ app.component("github-button", GithubButton);
87
+
88
+ app.use(createPinia());
89
+ app.use(router);
90
+
91
+ if (settings.sentryEnable) {
92
+ Sentry.init({
93
+ app,
94
+ dsn: settings.sentryDsn,
95
+ environment: settings.environment,
96
+ sampleRate: settings.sentrySampleRate,
97
+ attachStacktrace: true,
98
+ enableTracing: false,
99
+ integrations: []
100
+ });
101
+ }
102
+
103
+ app.mount("#app");
104
+
105
+ import * as api from "@/logic/api";
106
+ import * as settings from "@/logic/settings";
107
+
108
+ // must be copy of smart_url from backend
109
+ function smartUrl(domain: string, port: number) {
110
+ if (port == 80) {
111
+ return `http://${domain}`;
112
+ }
113
+
114
+ if (port == 443) {
115
+ return `https://${domain}`;
116
+ }
117
+
118
+ return `http://${domain}:${port}`;
119
+ }
120
+
121
+ let supertokens: ReturnType<typeof useSupertokens> | null = null;
122
+
123
+ if (settings.authMode === settings.AuthMode.Supertokens) {
124
+ supertokens = useSupertokens();
125
+
126
+ supertokens.init({
127
+ apiDomain: smartUrl(settings.appDomain, settings.appPort),
128
+ apiBasePath: settings.authSupertokensApiBasePath,
129
+ appName: settings.appName,
130
+ resendAfter: settings.authSupertokensResendAfter
131
+ });
132
+ } else if (settings.authMode === settings.AuthMode.SingleUser) {
133
+ } else {
134
+ throw `Unknown auth mode: ${settings.authMode}`;
135
+ }
136
+
137
+ async function onSessionLost() {
138
+ if (supertokens !== null) {
139
+ await supertokens.logout();
140
+ }
141
+
142
+ router.push({name: "main", params: {}});
143
+ }
144
+
145
+ api.init({onSessionLost: onSessionLost});
@@ -0,0 +1,61 @@
1
+ import {createRouter, createWebHistory} from "vue-router";
2
+ import MainView from "../views/MainView.vue";
3
+ import AuthView from "../views/AuthView.vue";
4
+ import FeedsView from "../views/FeedsView.vue";
5
+ import NewsView from "../views/NewsView.vue";
6
+ import RulesView from "../views/RulesView.vue";
7
+ import DiscoveryView from "../views/DiscoveryView.vue";
8
+ import CollectionsView from "../views/CollectionsView.vue";
9
+ import SettingsView from "../views/SettingsView.vue";
10
+ import * as e from "@/logic/enums";
11
+
12
+ // lazy view loading does not work with router.push function
13
+ // first attempt to router.push into not loaded view, will cause its loading, but will not change components
14
+
15
+ const router = createRouter({
16
+ history: createWebHistory(import.meta.env.BASE_URL),
17
+ routes: [
18
+ {
19
+ path: "/",
20
+ name: "main",
21
+ component: MainView
22
+ },
23
+ {
24
+ path: "/auth",
25
+ name: "auth",
26
+ component: AuthView
27
+ },
28
+ {
29
+ path: "/feeds",
30
+ name: e.MainPanelMode.Feeds,
31
+ component: FeedsView
32
+ },
33
+ {
34
+ path: "/news",
35
+ name: e.MainPanelMode.Entries,
36
+ component: NewsView
37
+ },
38
+ {
39
+ path: "/rules",
40
+ name: e.MainPanelMode.Rules,
41
+ component: RulesView
42
+ },
43
+ {
44
+ path: "/discovery",
45
+ name: e.MainPanelMode.Discovery,
46
+ component: DiscoveryView
47
+ },
48
+ {
49
+ path: "/collections",
50
+ name: e.MainPanelMode.Collections,
51
+ component: CollectionsView
52
+ },
53
+ {
54
+ path: "/settings",
55
+ name: e.MainPanelMode.Settings,
56
+ component: SettingsView
57
+ }
58
+ ]
59
+ });
60
+
61
+ export default router;