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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {computed, ref, watch} from "vue";
|
|
2
|
+
import {useRouter} from "vue-router";
|
|
3
|
+
import {defineStore} from "pinia";
|
|
4
|
+
import _ from "lodash";
|
|
5
|
+
import * as t from "@/logic/types";
|
|
6
|
+
import * as e from "@/logic/enums";
|
|
7
|
+
import * as api from "@/logic/api";
|
|
8
|
+
import {Timer} from "@/logic/timer";
|
|
9
|
+
import {computedAsync} from "@vueuse/core";
|
|
10
|
+
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
11
|
+
|
|
12
|
+
export const useEntriesStore = defineStore("entriesStore", () => {
|
|
13
|
+
const globalSettings = useGlobalSettingsStore();
|
|
14
|
+
|
|
15
|
+
const entries = ref<{[key: t.EntryId]: t.Entry}>({});
|
|
16
|
+
const requestedEntries = ref<{[key: t.EntryId]: boolean}>({});
|
|
17
|
+
|
|
18
|
+
const requiredTags = ref<{[key: string]: boolean}>({});
|
|
19
|
+
const excludedTags = ref<{[key: string]: boolean}>({});
|
|
20
|
+
|
|
21
|
+
const firstTimeEntriesLoading = ref(true);
|
|
22
|
+
|
|
23
|
+
function registerEntry(entry: t.Entry) {
|
|
24
|
+
if (entry.id in entries.value) {
|
|
25
|
+
if (entry.body === null && entries.value[entry.id].body !== null) {
|
|
26
|
+
entry.body = entries.value[entry.id].body;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
entries.value[entry.id] = entry;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const loadedEntriesReport = computedAsync(async () => {
|
|
34
|
+
const periodProperties = e.LastEntriesPeriodProperties.get(globalSettings.lastEntriesPeriod);
|
|
35
|
+
|
|
36
|
+
if (periodProperties === undefined) {
|
|
37
|
+
throw new Error(`Unknown period ${globalSettings.lastEntriesPeriod}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const period = periodProperties.seconds;
|
|
41
|
+
|
|
42
|
+
// force refresh
|
|
43
|
+
globalSettings.dataVersion;
|
|
44
|
+
|
|
45
|
+
const loadedEntries = await api.getLastEntries({
|
|
46
|
+
period: period
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const report = [];
|
|
50
|
+
|
|
51
|
+
for (const entry of loadedEntries) {
|
|
52
|
+
registerEntry(entry);
|
|
53
|
+
report.push(entry.id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
firstTimeEntriesLoading.value = false;
|
|
57
|
+
|
|
58
|
+
return report;
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const entriesReport = computedAsync(async () => {
|
|
62
|
+
let report = loadedEntriesReport.value.slice();
|
|
63
|
+
|
|
64
|
+
if (!globalSettings.showRead) {
|
|
65
|
+
report = report.filter((entryId) => {
|
|
66
|
+
return !entries.value[entryId].hasMarker(e.Marker.Read);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
report = report.filter((entryId) => {
|
|
71
|
+
for (const tag of entries.value[entryId].tags) {
|
|
72
|
+
if (excludedTags.value[tag]) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
report = report.filter((entryId) => {
|
|
80
|
+
for (const tag of Object.keys(requiredTags.value)) {
|
|
81
|
+
if (requiredTags.value[tag] && !entries.value[entryId].tags.includes(tag)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
report = report.sort((a: t.EntryId, b: t.EntryId) => {
|
|
89
|
+
const orderProperties = e.EntriesOrderProperties.get(globalSettings.entriesOrder);
|
|
90
|
+
|
|
91
|
+
if (orderProperties === undefined) {
|
|
92
|
+
throw new Error(`Unknown order ${globalSettings.entriesOrder}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const field = orderProperties.orderField;
|
|
96
|
+
|
|
97
|
+
const valueA = _.get(entries.value[a], field, null);
|
|
98
|
+
const valueB = _.get(entries.value[b], field, null);
|
|
99
|
+
|
|
100
|
+
if (valueA === null && valueB === null) {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (valueA === null) {
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (valueB === null) {
|
|
109
|
+
return -1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (valueA < valueB) {
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (valueA > valueB) {
|
|
117
|
+
return -1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return 0;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return report;
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
const reportTagsCount = computed(() => {
|
|
127
|
+
const tagsCount: {[key: string]: number} = {};
|
|
128
|
+
|
|
129
|
+
for (const entryId of entriesReport.value) {
|
|
130
|
+
const entry = entries.value[entryId];
|
|
131
|
+
|
|
132
|
+
for (const tag of entry.tags) {
|
|
133
|
+
if (tag in tagsCount) {
|
|
134
|
+
tagsCount[tag] += 1;
|
|
135
|
+
} else {
|
|
136
|
+
tagsCount[tag] = 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return tagsCount;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
function requestFullEntry({entryId}: {entryId: t.EntryId}) {
|
|
145
|
+
if (entryId in entries.value && entries.value[entryId].body !== null) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
requestedEntries.value[entryId] = true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function loadFullEntries() {
|
|
153
|
+
const ids: t.EntryId[] = Object.keys(requestedEntries.value).map((key) => t.toEntryId(key));
|
|
154
|
+
|
|
155
|
+
if (ids.length === 0) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const entries = await api.getEntriesByIds({ids: ids});
|
|
160
|
+
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
registerEntry(entry);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
requestedEntries.value = {};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const requestedEntriesTimer = new Timer(loadFullEntries, 1000);
|
|
169
|
+
|
|
170
|
+
requestedEntriesTimer.start();
|
|
171
|
+
|
|
172
|
+
async function setMarker({entryId, marker}: {entryId: t.EntryId; marker: e.Marker}) {
|
|
173
|
+
await api.setMarker({entryId: entryId, marker: marker});
|
|
174
|
+
|
|
175
|
+
if (entryId in entries.value) {
|
|
176
|
+
entries.value[entryId].setMarker(marker);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function removeMarker({entryId, marker}: {entryId: t.EntryId; marker: e.Marker}) {
|
|
181
|
+
await api.removeMarker({entryId: entryId, marker: marker});
|
|
182
|
+
|
|
183
|
+
if (entryId in entries.value) {
|
|
184
|
+
entries.value[entryId].removeMarker(marker);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function requireTag({tag}: {tag: string}) {
|
|
189
|
+
requiredTags.value[tag] = true;
|
|
190
|
+
excludedTags.value[tag] = false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function excludeTag({tag}: {tag: string}) {
|
|
194
|
+
excludedTags.value[tag] = true;
|
|
195
|
+
requiredTags.value[tag] = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function resetTag({tag}: {tag: string}) {
|
|
199
|
+
excludedTags.value[tag] = false;
|
|
200
|
+
requiredTags.value[tag] = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
entries,
|
|
205
|
+
entriesReport,
|
|
206
|
+
reportTagsCount,
|
|
207
|
+
requestFullEntry,
|
|
208
|
+
setMarker,
|
|
209
|
+
removeMarker,
|
|
210
|
+
requireTag,
|
|
211
|
+
excludeTag,
|
|
212
|
+
resetTag,
|
|
213
|
+
requiredTags,
|
|
214
|
+
excludedTags,
|
|
215
|
+
firstTimeEntriesLoading
|
|
216
|
+
};
|
|
217
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {ref, watch} from "vue";
|
|
2
|
+
import {useRouter} from "vue-router";
|
|
3
|
+
import {defineStore} from "pinia";
|
|
4
|
+
import {computedAsync} from "@vueuse/core";
|
|
5
|
+
import {useGlobalState} from "@/stores/globalState";
|
|
6
|
+
|
|
7
|
+
import * as e from "@/logic/enums";
|
|
8
|
+
import * as api from "@/logic/api";
|
|
9
|
+
|
|
10
|
+
export const useGlobalSettingsStore = defineStore("globalSettings", () => {
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
const globalState = useGlobalState();
|
|
13
|
+
|
|
14
|
+
// General
|
|
15
|
+
const mainPanelMode = ref(e.MainPanelMode.Entries);
|
|
16
|
+
const dataVersion = ref(0);
|
|
17
|
+
|
|
18
|
+
// Entries
|
|
19
|
+
const lastEntriesPeriod = ref(e.LastEntriesPeriod.Day3);
|
|
20
|
+
const entriesOrder = ref(e.EntriesOrder.Score);
|
|
21
|
+
const showEntriesTags = ref(true);
|
|
22
|
+
const showRead = ref(true);
|
|
23
|
+
|
|
24
|
+
// Feeds
|
|
25
|
+
const showFeedsDescriptions = ref(true);
|
|
26
|
+
const feedsOrder = ref(e.FeedsOrder.Title);
|
|
27
|
+
const failedFeedsFirst = ref(false);
|
|
28
|
+
|
|
29
|
+
watch(mainPanelMode, (newValue, oldValue) => {
|
|
30
|
+
router.push({name: mainPanelMode.value, params: {}});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function updateDataVersion() {
|
|
34
|
+
dataVersion.value++;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// backend side settings
|
|
38
|
+
const userSettings = computedAsync(async () => {
|
|
39
|
+
if (!globalState.isLoggedIn) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// force refresh
|
|
44
|
+
dataVersion.value;
|
|
45
|
+
|
|
46
|
+
return await api.getUserSettings();
|
|
47
|
+
}, null);
|
|
48
|
+
|
|
49
|
+
const info = computedAsync(async () => {
|
|
50
|
+
if (!globalState.isLoggedIn) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// force refresh
|
|
55
|
+
dataVersion.value;
|
|
56
|
+
|
|
57
|
+
return await api.getInfo();
|
|
58
|
+
}, null);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
mainPanelMode,
|
|
62
|
+
lastEntriesPeriod,
|
|
63
|
+
entriesOrder,
|
|
64
|
+
showEntriesTags,
|
|
65
|
+
showRead,
|
|
66
|
+
dataVersion,
|
|
67
|
+
updateDataVersion,
|
|
68
|
+
showFeedsDescriptions,
|
|
69
|
+
userSettings,
|
|
70
|
+
info,
|
|
71
|
+
feedsOrder,
|
|
72
|
+
failedFeedsFirst
|
|
73
|
+
};
|
|
74
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {ref, watch, computed} from "vue";
|
|
2
|
+
import {useRouter} from "vue-router";
|
|
3
|
+
import {defineStore} from "pinia";
|
|
4
|
+
import {useSupertokens} from "@/stores/supertokens";
|
|
5
|
+
import * as settings from "@/logic/settings";
|
|
6
|
+
|
|
7
|
+
import * as e from "@/logic/enums";
|
|
8
|
+
|
|
9
|
+
export const useGlobalState = defineStore("globalState", () => {
|
|
10
|
+
const supertokens = useSupertokens();
|
|
11
|
+
|
|
12
|
+
const isLoggedIn = computed(() => {
|
|
13
|
+
if (settings.authMode === settings.AuthMode.SingleUser) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return supertokens.isLoggedIn;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
isLoggedIn
|
|
22
|
+
};
|
|
23
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {ref, watch} from "vue";
|
|
2
|
+
import {useRouter} from "vue-router";
|
|
3
|
+
import {defineStore} from "pinia";
|
|
4
|
+
import {Timer} from "@/logic/timer";
|
|
5
|
+
import SuperTokens from "supertokens-web-js";
|
|
6
|
+
import Session from "supertokens-web-js/recipe/session";
|
|
7
|
+
import Passwordless from "supertokens-web-js/recipe/passwordless";
|
|
8
|
+
import * as passwordless from "supertokens-web-js/recipe/passwordless";
|
|
9
|
+
import {computedAsync} from "@vueuse/core";
|
|
10
|
+
|
|
11
|
+
export const useSupertokens = defineStore("supertokens", () => {
|
|
12
|
+
const isLoggedIn = ref<boolean | null>(null);
|
|
13
|
+
|
|
14
|
+
async function refresh() {
|
|
15
|
+
isLoggedIn.value = await Session.doesSessionExist();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const refresher = new Timer(refresh, 1000);
|
|
19
|
+
|
|
20
|
+
const allowResendAfter = ref(60 * 1000);
|
|
21
|
+
|
|
22
|
+
function init({
|
|
23
|
+
apiDomain,
|
|
24
|
+
apiBasePath,
|
|
25
|
+
appName,
|
|
26
|
+
resendAfter
|
|
27
|
+
}: {
|
|
28
|
+
apiDomain: string;
|
|
29
|
+
apiBasePath: string;
|
|
30
|
+
appName: string;
|
|
31
|
+
resendAfter: number;
|
|
32
|
+
}) {
|
|
33
|
+
allowResendAfter.value = resendAfter;
|
|
34
|
+
|
|
35
|
+
SuperTokens.init({
|
|
36
|
+
// enableDebugLogs: true,
|
|
37
|
+
appInfo: {
|
|
38
|
+
apiDomain: apiDomain,
|
|
39
|
+
apiBasePath: apiBasePath,
|
|
40
|
+
appName: appName
|
|
41
|
+
},
|
|
42
|
+
recipeList: [Session.init(), Passwordless.init()]
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
refresher.start();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function processError(err: any) {
|
|
49
|
+
await refresh();
|
|
50
|
+
|
|
51
|
+
if (err.isSuperTokensGeneralError === true) {
|
|
52
|
+
window.alert(err.message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function sendMagicLink(email: string) {
|
|
57
|
+
try {
|
|
58
|
+
let response = await passwordless.createCode({
|
|
59
|
+
email
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return response.status === "OK";
|
|
63
|
+
} catch (err: any) {
|
|
64
|
+
await processError(err);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function logout() {
|
|
70
|
+
await Session.signOut();
|
|
71
|
+
|
|
72
|
+
await passwordless.clearLoginAttemptInfo();
|
|
73
|
+
|
|
74
|
+
await refresh();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function resendMagicLink() {
|
|
78
|
+
try {
|
|
79
|
+
let response = await passwordless.resendCode();
|
|
80
|
+
|
|
81
|
+
if (response.status === "OK") {
|
|
82
|
+
return true;
|
|
83
|
+
} else if (response.status === "RESTART_FLOW_ERROR") {
|
|
84
|
+
// this can happen if the user has already successfully logged in into
|
|
85
|
+
// another device whilst also trying to login to this one.
|
|
86
|
+
return false;
|
|
87
|
+
} else {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
} catch (err: any) {
|
|
91
|
+
await processError(err);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function hasInitialMagicLinkBeenSent() {
|
|
97
|
+
return (await passwordless.getLoginAttemptInfo()) !== undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function handleMagicLinkClicked({
|
|
101
|
+
onSignUp,
|
|
102
|
+
onSignIn,
|
|
103
|
+
onSignFailed
|
|
104
|
+
}: {
|
|
105
|
+
onSignUp: () => void;
|
|
106
|
+
onSignIn: () => void;
|
|
107
|
+
onSignFailed: () => void;
|
|
108
|
+
}) {
|
|
109
|
+
try {
|
|
110
|
+
let response = await passwordless.consumeCode();
|
|
111
|
+
|
|
112
|
+
await refresh();
|
|
113
|
+
|
|
114
|
+
if (response.status === "OK") {
|
|
115
|
+
if (response.createdNewUser) {
|
|
116
|
+
await onSignUp();
|
|
117
|
+
} else {
|
|
118
|
+
await onSignIn();
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
await onSignFailed();
|
|
122
|
+
}
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
await processError(err);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function clearLoginAttempt() {
|
|
129
|
+
await passwordless.clearLoginAttemptInfo();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
allowResendAfter,
|
|
134
|
+
|
|
135
|
+
init,
|
|
136
|
+
sendMagicLink,
|
|
137
|
+
resendMagicLink,
|
|
138
|
+
isLoggedIn,
|
|
139
|
+
logout,
|
|
140
|
+
hasInitialMagicLinkBeenSent,
|
|
141
|
+
handleMagicLinkClicked,
|
|
142
|
+
clearLoginAttempt
|
|
143
|
+
};
|
|
144
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {computed, ref, watch} from "vue";
|
|
2
|
+
import {useRouter} from "vue-router";
|
|
3
|
+
import {defineStore} from "pinia";
|
|
4
|
+
|
|
5
|
+
import type * as t from "@/logic/types";
|
|
6
|
+
import * as e from "@/logic/enums";
|
|
7
|
+
import * as api from "@/logic/api";
|
|
8
|
+
import {Timer} from "@/logic/timer";
|
|
9
|
+
import {computedAsync} from "@vueuse/core";
|
|
10
|
+
|
|
11
|
+
export const useTagsStore = defineStore("tagsStore", () => {
|
|
12
|
+
const tags = ref<{[key: string]: t.TagInfo}>({});
|
|
13
|
+
const requestedTags = ref<{[key: string]: boolean}>({});
|
|
14
|
+
|
|
15
|
+
const firstTimeTagsLoading = ref(true);
|
|
16
|
+
|
|
17
|
+
function registerTag(tag: t.TagInfo) {
|
|
18
|
+
tags.value[tag.uid] = tag;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requestTagInfo({tagUid}: {tagUid: string}) {
|
|
22
|
+
if (tagUid in tags.value) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
requestedTags.value[tagUid] = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function loadTagsInfo() {
|
|
30
|
+
const uids = Object.keys(requestedTags.value);
|
|
31
|
+
|
|
32
|
+
if (uids.length === 0) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tagsInfo = await api.getTagsInfo({uids: uids});
|
|
37
|
+
|
|
38
|
+
for (const uid in tagsInfo) {
|
|
39
|
+
const tag = tagsInfo[uid];
|
|
40
|
+
registerTag(tag);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
requestedTags.value = {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const requestedTagsTimer = new Timer(loadTagsInfo, 1000);
|
|
47
|
+
|
|
48
|
+
requestedTagsTimer.start();
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
tags,
|
|
52
|
+
requestTagInfo
|
|
53
|
+
};
|
|
54
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
{{ text }}
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script lang="ts" setup>
|
|
6
|
+
import {computed} from "vue";
|
|
7
|
+
import * as u from "@/logic/utils";
|
|
8
|
+
|
|
9
|
+
const properties = defineProps<{
|
|
10
|
+
value: Date | null;
|
|
11
|
+
reversed?: boolean;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const text = computed(() => {
|
|
15
|
+
if (properties.value === null) {
|
|
16
|
+
return "—";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (properties.reversed) {
|
|
20
|
+
return u.timeSince(properties.value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return properties.value.toLocaleString();
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<style></style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
{{ feedId }}
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script lang="ts" setup>
|
|
6
|
+
import {computed} from "vue";
|
|
7
|
+
import type * as t from "@/logic/types";
|
|
8
|
+
|
|
9
|
+
const properties = defineProps<{value: t.FeedId}>();
|
|
10
|
+
|
|
11
|
+
const feedId = computed(() => {
|
|
12
|
+
if (properties.value == null) {
|
|
13
|
+
return `—`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const id = properties.value;
|
|
17
|
+
|
|
18
|
+
return id.slice(0, 3) + "***" + id.slice(-3);
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<style></style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="score"
|
|
4
|
+
@click.prevent="onClick()">
|
|
5
|
+
{{ value }}
|
|
6
|
+
</div>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script lang="ts" setup>
|
|
10
|
+
import * as api from "@/logic/api";
|
|
11
|
+
import type * as t from "@/logic/types";
|
|
12
|
+
|
|
13
|
+
const properties = defineProps<{value: number; entryId: t.EntryId}>();
|
|
14
|
+
|
|
15
|
+
async function onClick() {
|
|
16
|
+
const rules = await api.getScoreDetails({entryId: properties.entryId});
|
|
17
|
+
|
|
18
|
+
if (rules.length === 0) {
|
|
19
|
+
alert("No rules for this news");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
rules.sort((a, b) => b.score - a.score);
|
|
24
|
+
|
|
25
|
+
const strings = [];
|
|
26
|
+
|
|
27
|
+
for (const rule of rules) {
|
|
28
|
+
strings.push(rule.score.toString().padStart(2, " ") + " — " + rule.tags.join(", "));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
alert(strings.join("\n"));
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<style scoped>
|
|
36
|
+
.score {
|
|
37
|
+
display: inline-block;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
padding: 0.25rem;
|
|
40
|
+
background-color: #c1c1ff;
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<a
|
|
3
|
+
:href="value"
|
|
4
|
+
target="_blank"
|
|
5
|
+
rel="noopener noreferrer"
|
|
6
|
+
>{{ renderedText }}</a
|
|
7
|
+
>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script lang="ts" setup>
|
|
11
|
+
import {computed} from "vue";
|
|
12
|
+
import type * as t from "@/logic/types";
|
|
13
|
+
|
|
14
|
+
const properties = defineProps<{value: t.URL; text?: string | null}>();
|
|
15
|
+
|
|
16
|
+
const renderedText = computed(() => {
|
|
17
|
+
if (properties.text) {
|
|
18
|
+
return properties.text;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return properties.value;
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<style></style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<wide-layout>
|
|
3
|
+
<template #header> Feeds Fun </template>
|
|
4
|
+
|
|
5
|
+
<div v-if="!linkProcessed">Checking login status...</div>
|
|
6
|
+
|
|
7
|
+
<div v-else-if="globalState.isLoggedIn">
|
|
8
|
+
<button @click="goToWorkspace()">Go To Feeds</button>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div v-else>
|
|
12
|
+
<supertokens-login />
|
|
13
|
+
</div>
|
|
14
|
+
</wide-layout>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script lang="ts" setup>
|
|
18
|
+
import {useRouter} from "vue-router";
|
|
19
|
+
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
20
|
+
import {useSupertokens} from "@/stores/supertokens";
|
|
21
|
+
import {useGlobalState} from "@/stores/globalState";
|
|
22
|
+
import {computedAsync} from "@vueuse/core";
|
|
23
|
+
import {computed, ref, onBeforeMount} from "vue";
|
|
24
|
+
import * as settings from "@/logic/settings";
|
|
25
|
+
|
|
26
|
+
const globalSettings = useGlobalSettingsStore();
|
|
27
|
+
const globalState = useGlobalState();
|
|
28
|
+
|
|
29
|
+
const supertokens = useSupertokens();
|
|
30
|
+
|
|
31
|
+
const router = useRouter();
|
|
32
|
+
|
|
33
|
+
const linkProcessed = ref(false);
|
|
34
|
+
|
|
35
|
+
function goToWorkspace() {
|
|
36
|
+
router.push({name: globalSettings.mainPanelMode, params: {}});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onBeforeMount(async () => {
|
|
40
|
+
if (globalState.isLoggedIn) {
|
|
41
|
+
goToWorkspace();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function onSignIn() {
|
|
46
|
+
goToWorkspace();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function onSignFailed() {
|
|
50
|
+
await supertokens.clearLoginAttempt();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (await supertokens.hasInitialMagicLinkBeenSent()) {
|
|
54
|
+
await supertokens.handleMagicLinkClicked({
|
|
55
|
+
onSignUp: onSignIn,
|
|
56
|
+
onSignIn: onSignIn,
|
|
57
|
+
onSignFailed: onSignFailed
|
|
58
|
+
});
|
|
59
|
+
linkProcessed.value = true;
|
|
60
|
+
} else {
|
|
61
|
+
linkProcessed.value = true;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<style></style>
|