Skip to content

Commit aed2e30

Browse files
feat: Allow to follow the feeds of the current page
1 parent 3bb55b9 commit aed2e30

File tree

8 files changed

+264
-6
lines changed

8 files changed

+264
-6
lines changed

src/api.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ function logout() {
2727
}
2828

2929
function search(url) {
30-
return http.post("/search", { url }).then((data) => {
31-
return data.links[0];
32-
});
30+
return http.post("/search", { url });
3331
}
3432

3533
function markLinkAsRead(link) {
@@ -44,6 +42,14 @@ function collections() {
4442
return http.get("/collections");
4543
}
4644

45+
function follow(collection) {
46+
return http.post(`/collections/${collection.id}/follow`);
47+
}
48+
49+
function unfollow(collection) {
50+
return http.delete(`/collections/${collection.id}/follow`);
51+
}
52+
4753
function addCollectionToLink(link, collection) {
4854
return http.put(`/links/${link.id}/collections/${collection.id}`);
4955
}
@@ -66,6 +72,8 @@ export default {
6672
authenticate,
6773
logout,
6874
collections,
75+
follow,
76+
unfollow,
6977
addCollectionToLink,
7078
removeCollectionFromLink,
7179
search,

src/i18n.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export const i18n = createI18n({
2525
"errors.unknown":
2626
"An unknown error has occurred. Please close and reopen the extension. If it still doesn’t work, please contact the support.",
2727
"errors.url.url": "The URL of the current page is not supported by Flus.",
28+
"feeds.count_detected":
29+
"No feeds detected on this page. | 1 feed detected on this page. | {count} feeds detected on this page.",
30+
"feeds.follow": "Follow",
31+
"feeds.loading": "Loading the feeds…",
32+
"feeds.open_in_flus": "Open in Flus",
33+
"feeds.title": "Web feeds",
34+
"feeds.unfollow": "Unfollow",
2835
"forms.error": "Error:",
2936
"link.count_collections": "No collections | 1 collection | {count} collections",
3037
"link.invalid_protocol":
@@ -49,6 +56,7 @@ export const i18n = createI18n({
4956
"login.title": "Login",
5057
"menu.back": "Back",
5158
"menu.close": "Close the menu",
59+
"menu.feeds": "List of Web feeds",
5260
"menu.logout": "Log out",
5361
"menu.open": "Open the menu",
5462
"menu.open_flus": "Open Flus",
@@ -90,6 +98,13 @@ export const i18n = createI18n({
9098
"errors.unknown":
9199
"Une erreur inconnue est survenue. Veuillez fermer et réouvrir l’extension. Si cela ne suffit pas, veuillez contacter le support.",
92100
"errors.url.url": "L’URL de la page actuelle n’est pas supportée par Flus.",
101+
"feeds.count_detected":
102+
"Aucun flux détecté sur cette page. | 1 flux détecté sur cette page. | {count} flux détectés sur cette page.",
103+
"feeds.follow": "Suivre",
104+
"feeds.loading": "Chargement des flux…",
105+
"feeds.open_in_flus": "Ouvrir dans Flus",
106+
"feeds.title": "Flux Web",
107+
"feeds.unfollow": "Ne plus suivre",
93108
"forms.error": "Erreur :",
94109
"link.count_collections": "Aucune collection | 1 collection | {count} collections",
95110
"link.invalid_protocol":
@@ -114,6 +129,7 @@ export const i18n = createI18n({
114129
"login.title": "Connexion",
115130
"menu.back": "Retour",
116131
"menu.close": "Fermer le menu",
132+
"menu.feeds": "Liste des flux Web",
117133
"menu.logout": "Se déconnecter",
118134
"menu.open": "Ouvrir le menu",
119135
"menu.open_flus": "Ouvrir Flus",

src/models/feed.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// This file is part of Flus Browser
2+
// SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
export default class {
5+
id = "";
6+
name = "";
7+
url = "";
8+
isFollowed = false;
9+
10+
init(fetchedFeed) {
11+
this.id = fetchedFeed.id;
12+
this.name = fetchedFeed.name;
13+
this.url = fetchedFeed.url;
14+
this.isFollowed = fetchedFeed.is_followed;
15+
}
16+
17+
follow() {
18+
this.isFollowed = true;
19+
}
20+
21+
unfollow() {
22+
this.isFollowed = false;
23+
}
24+
}

src/router.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { ref, computed } from "vue";
55

66
import { store } from "./store.js";
77

8+
import FeedsScreen from "./screens/FeedsScreen.vue";
89
import LinkScreen from "./screens/LinkScreen.vue";
910
import SettingsScreen from "./screens/SettingsScreen.vue";
1011

1112
const routes = {
1213
"/": LinkScreen,
14+
"/feeds": FeedsScreen,
1315
"/settings": SettingsScreen,
1416
};
1517

src/screens/FeedsScreen.vue

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<!-- This file is part of Flus Browser
2+
-- SPDX-License-Identifier: AGPL-3.0-or-later
3+
-->
4+
5+
<template>
6+
<Screen :title="title" header>
7+
<div v-if="ready && alert.type === ''" class="flow">
8+
<p class="text--center">
9+
{{ t("feeds.count_detected", feeds.length) }}
10+
</p>
11+
12+
<ul v-if="feeds.length > 0" class="list--nostyle list--padded list--separated">
13+
<li v-for="feed in feeds" class="cols cols--center cols--always cols--gap-smaller">
14+
<span class="col--extend">
15+
{{ feed.name }}
16+
</span>
17+
18+
<div class="cols cols--always cols--center cols--gap-small">
19+
<button
20+
v-if="!feed.isFollowed"
21+
@click.prevent="() => follow(feed)"
22+
>
23+
{{ t("feeds.follow") }}
24+
</button>
25+
26+
<button
27+
v-else
28+
@click.prevent="() => unfollow(feed)"
29+
>
30+
{{ t("feeds.unfollow") }}
31+
</button>
32+
33+
<a
34+
class="button button--icon"
35+
:href="feedUrl(feed)"
36+
:title="t('feeds.open_in_flus')"
37+
target="_blank"
38+
>
39+
<Icon name="pop-out" />
40+
<span class="sr-only">
41+
{{ t("feeds.open_in_flus") }}
42+
</span>
43+
</a>
44+
</div>
45+
</li>
46+
</ul>
47+
</div>
48+
49+
<div v-else-if="ready && alert.type == 'info'">
50+
<p class="panel panel--rounded panel--caribbean text--bold" role="alert">
51+
<Icon name="info" />
52+
53+
{{ alert.message }}
54+
</p>
55+
</div>
56+
57+
<div v-else-if="ready && alert.type == 'error'">
58+
<p class="panel panel--rounded panel--danger text--bold" role="alert">
59+
<Icon name="error" />
60+
61+
{{ t("forms.error") }}
62+
{{ alert.message }}
63+
</p>
64+
</div>
65+
66+
<div v-else-if="!ready" class="flow text--center">
67+
<div class="spinner"></div>
68+
69+
<p>
70+
{{ t("feeds.loading") }}
71+
</p>
72+
</div>
73+
</Screen>
74+
</template>
75+
76+
<script setup>
77+
import { ref, reactive, onMounted } from "vue";
78+
import { useI18n } from "vue-i18n";
79+
import browser from "webextension-polyfill";
80+
81+
import { store } from "../store.js";
82+
import api from "../api.js";
83+
import http from "../http.js";
84+
import Feed from "../models/feed.js";
85+
86+
const { t, locale } = useI18n();
87+
locale.value = store.locale;
88+
89+
const title = t("feeds.title");
90+
91+
const ready = ref(false);
92+
const alert = ref({
93+
type: "",
94+
message: "",
95+
});
96+
97+
const feeds = ref([]);
98+
99+
function feedUrl(feed) {
100+
return `${store.auth.server}/collections/${feed.id}`;
101+
}
102+
103+
async function getCurrentTab() {
104+
return await browser.tabs
105+
.query({
106+
active: true,
107+
currentWindow: true,
108+
})
109+
.then((tabs) => {
110+
return tabs[0];
111+
});
112+
}
113+
114+
async function refreshForCurrentTab() {
115+
const url = (await getCurrentTab()).url;
116+
117+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
118+
alert.value = {
119+
type: "info",
120+
message: t("link.invalid_protocol"),
121+
};
122+
123+
ready.value = true;
124+
125+
return;
126+
}
127+
128+
api.search(url)
129+
.then((data) => {
130+
// This array is used to deduplicate feeds with the same name.
131+
// This usually happens when a site declares both Atom and RSS
132+
// feeds. In our case, it's generally disturbing for the user to
133+
// have several feeds with the same name.
134+
const feedsNames = [];
135+
data.feeds.forEach((searchedFeed) => {
136+
const feed = reactive(new Feed());
137+
feed.init(searchedFeed);
138+
if (!feedsNames.includes(feed.name)) {
139+
feeds.value = [...feeds.value, feed];
140+
feedsNames.push(feed.name);
141+
}
142+
});
143+
ready.value = true;
144+
})
145+
.catch((error) => {
146+
if (error instanceof http.HttpError) {
147+
alert.value = {
148+
type: "error",
149+
message: error.errors.url.map((error) => {
150+
return t(`errors.url.${error.code}`);
151+
}),
152+
};
153+
} else {
154+
alert.value = {
155+
type: "error",
156+
message: t("errors.unknown"),
157+
};
158+
}
159+
160+
ready.value = true;
161+
});
162+
}
163+
164+
async function follow(feed) {
165+
api.follow(feed)
166+
.then(() => {
167+
feed.follow();
168+
})
169+
.catch(() => {
170+
alert.value = {
171+
type: "error",
172+
message: t("errors.unknown"),
173+
};
174+
});
175+
}
176+
177+
async function unfollow(feed) {
178+
api.unfollow(feed)
179+
.then(() => {
180+
feed.unfollow();
181+
})
182+
.catch(() => {
183+
alert.value = {
184+
type: "error",
185+
message: t("errors.unknown"),
186+
};
187+
});
188+
}
189+
190+
onMounted(refreshForCurrentTab);
191+
</script>

src/screens/LinkScreen.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ async function refreshForCurrentTab() {
151151
}
152152
153153
api.search(url)
154-
.then((fetchedLink) => {
155-
link.init(fetchedLink);
154+
.then((data) => {
155+
link.init(data.links[0]);
156156
ready.value = true;
157157
})
158158
.catch((error) => {

src/screens/Screen.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,20 @@
1515
</a>
1616
</div>
1717

18-
<div>
18+
<div class="cols cols--always cols--gap-smaller">
19+
<a
20+
v-if="currentPath != '/feeds'"
21+
href="#/feeds"
22+
class="button button--icon button--ghost"
23+
:title="t('menu.feeds')"
24+
>
25+
<Icon name="feed" />
26+
27+
<span class="sr-only">
28+
{{ t("menu.feeds") }}
29+
</span>
30+
</a>
31+
1932
<button
2033
v-if="store.menuOpened"
2134
class="button button--icon button--ghost"

src/stylesheets/application.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ body {
6161
margin-bottom: 1.5rem;
6262
}
6363

64+
.list--separated > * + * {
65+
border-top: 1px solid var(--color-grey-line);
66+
}
67+
6468
.tag {
6569
display: inline-block;
6670
margin-right: var(--space-smaller);

0 commit comments

Comments
 (0)