Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ It is designed as a simple, yet complete tool for organising the links you gathe

This repository contains the code of the browser extension.
The extension is the perfect companion for the web application as it allows you to easily mark links as read, save them for later, or add them to collections, as well as add notes.
It is also able to let you follow Web feeds.

For the code of the Web application, see [flusio/Flus](https://github.com/flusio/Flus).

Expand Down
14 changes: 11 additions & 3 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ function logout() {
}

function search(url) {
return http.post("/search", { url }).then((data) => {
return data.links[0];
});
return http.post("/search", { url });
}

function markLinkAsRead(link) {
Expand All @@ -44,6 +42,14 @@ function collections() {
return http.get("/collections");
}

function follow(collection) {
return http.post(`/collections/${collection.id}/follow`);
}

function unfollow(collection) {
return http.delete(`/collections/${collection.id}/follow`);
}

function addCollectionToLink(link, collection) {
return http.put(`/links/${link.id}/collections/${collection.id}`);
}
Expand All @@ -66,6 +72,8 @@ export default {
authenticate,
logout,
collections,
follow,
unfollow,
addCollectionToLink,
removeCollectionFromLink,
search,
Expand Down
16 changes: 16 additions & 0 deletions src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export const i18n = createI18n({
"errors.unknown":
"An unknown error has occurred. Please close and reopen the extension. If it still doesn’t work, please contact the support.",
"errors.url.url": "The URL of the current page is not supported by Flus.",
"feeds.count_detected":
"No feeds detected on this page. | 1 feed detected on this page. | {count} feeds detected on this page.",
"feeds.follow": "Follow",
"feeds.loading": "Loading the feeds…",
"feeds.open_in_flus": "Open in Flus",
"feeds.title": "List of Web feeds",
"feeds.unfollow": "Unfollow",
"forms.error": "Error:",
"link.count_collections": "No collections | 1 collection | {count} collections",
"link.invalid_protocol":
Expand All @@ -49,6 +56,7 @@ export const i18n = createI18n({
"login.title": "Login",
"menu.back": "Back",
"menu.close": "Close the menu",
"menu.feeds": "List of Web feeds",
"menu.logout": "Log out",
"menu.open": "Open the menu",
"menu.open_flus": "Open Flus",
Expand Down Expand Up @@ -90,6 +98,13 @@ export const i18n = createI18n({
"errors.unknown":
"Une erreur inconnue est survenue. Veuillez fermer et réouvrir l’extension. Si cela ne suffit pas, veuillez contacter le support.",
"errors.url.url": "L’URL de la page actuelle n’est pas supportée par Flus.",
"feeds.count_detected":
"Aucun flux détecté sur cette page. | 1 flux détecté sur cette page. | {count} flux détectés sur cette page.",
"feeds.follow": "Suivre",
"feeds.loading": "Chargement des flux…",
"feeds.open_in_flus": "Ouvrir dans Flus",
"feeds.title": "Liste des flux Web",
"feeds.unfollow": "Ne plus suivre",
"forms.error": "Erreur :",
"link.count_collections": "Aucune collection | 1 collection | {count} collections",
"link.invalid_protocol":
Expand All @@ -114,6 +129,7 @@ export const i18n = createI18n({
"login.title": "Connexion",
"menu.back": "Retour",
"menu.close": "Fermer le menu",
"menu.feeds": "Liste des flux Web",
"menu.logout": "Se déconnecter",
"menu.open": "Ouvrir le menu",
"menu.open_flus": "Ouvrir Flus",
Expand Down
24 changes: 24 additions & 0 deletions src/models/feed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// This file is part of Flus Browser
// SPDX-License-Identifier: AGPL-3.0-or-later

export default class {
id = "";
name = "";
url = "";
isFollowed = false;

init(fetchedFeed) {
this.id = fetchedFeed.id;
this.name = fetchedFeed.name;
this.url = fetchedFeed.url;
this.isFollowed = fetchedFeed.is_followed;
}

follow() {
this.isFollowed = true;
}

unfollow() {
this.isFollowed = false;
}
}
2 changes: 2 additions & 0 deletions src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { ref, computed } from "vue";

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

import FeedsScreen from "./screens/FeedsScreen.vue";
import LinkScreen from "./screens/LinkScreen.vue";
import SettingsScreen from "./screens/SettingsScreen.vue";

const routes = {
"/": LinkScreen,
"/feeds": FeedsScreen,
"/settings": SettingsScreen,
};

Expand Down
191 changes: 191 additions & 0 deletions src/screens/FeedsScreen.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<!-- This file is part of Flus Browser
-- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<Screen :title="title" header>
<div v-if="ready && alert.type === ''" class="flow">
<p class="text--center">
{{ t("feeds.count_detected", feeds.length) }}
</p>

<ul v-if="feeds.length > 0" class="list--nostyle list--padded list--separated">
<li v-for="feed in feeds" class="cols cols--center cols--always cols--gap-smaller">
<span class="col--extend">
{{ feed.name }}
</span>

<div class="cols cols--always cols--center cols--gap-small">
<button
v-if="!feed.isFollowed"
@click.prevent="() => follow(feed)"
>
{{ t("feeds.follow") }}
</button>

<button
v-else
@click.prevent="() => unfollow(feed)"
>
{{ t("feeds.unfollow") }}
</button>

<a
class="button button--icon"
:href="feedUrl(feed)"
:title="t('feeds.open_in_flus')"
target="_blank"
>
<Icon name="pop-out" />
<span class="sr-only">
{{ t("feeds.open_in_flus") }}
</span>
</a>
</div>
</li>
</ul>
</div>

<div v-else-if="ready && alert.type == 'info'">
<p class="panel panel--rounded panel--caribbean text--bold" role="alert">
<Icon name="info" />

{{ alert.message }}
</p>
</div>

<div v-else-if="ready && alert.type == 'error'">
<p class="panel panel--rounded panel--danger text--bold" role="alert">
<Icon name="error" />

{{ t("forms.error") }}
{{ alert.message }}
</p>
</div>

<div v-else-if="!ready" class="flow text--center">
<div class="spinner"></div>

<p>
{{ t("feeds.loading") }}
</p>
</div>
</Screen>
</template>

<script setup>
import { ref, reactive, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import browser from "webextension-polyfill";

import { store } from "../store.js";
import api from "../api.js";
import http from "../http.js";
import Feed from "../models/feed.js";

const { t, locale } = useI18n();
locale.value = store.locale;

const title = t("feeds.title");

const ready = ref(false);
const alert = ref({
type: "",
message: "",
});

const feeds = ref([]);

function feedUrl(feed) {
return `${store.auth.server}/collections/${feed.id}`;
}

async function getCurrentTab() {
return await browser.tabs
.query({
active: true,
currentWindow: true,
})
.then((tabs) => {
return tabs[0];
});
}

async function refreshForCurrentTab() {
const url = (await getCurrentTab()).url;

if (!url.startsWith("http://") && !url.startsWith("https://")) {
alert.value = {
type: "info",
message: t("link.invalid_protocol"),
};

ready.value = true;

return;
}

api.search(url)
.then((data) => {
// This array is used to deduplicate feeds with the same name.
// This usually happens when a site declares both Atom and RSS
// feeds. In our case, it's generally disturbing for the user to
// have several feeds with the same name.
const feedsNames = [];
data.feeds.forEach((searchedFeed) => {
const feed = reactive(new Feed());
feed.init(searchedFeed);
if (!feedsNames.includes(feed.name)) {
feeds.value = [...feeds.value, feed];
feedsNames.push(feed.name);
}
});
ready.value = true;
})
.catch((error) => {
if (error instanceof http.HttpError) {
alert.value = {
type: "error",
message: error.errors.url.map((error) => {
return t(`errors.url.${error.code}`);
}),
};
} else {
alert.value = {
type: "error",
message: t("errors.unknown"),
};
}

ready.value = true;
});
}

async function follow(feed) {
api.follow(feed)
.then(() => {
feed.follow();
})
.catch(() => {
alert.value = {
type: "error",
message: t("errors.unknown"),
};
});
}

async function unfollow(feed) {
api.unfollow(feed)
.then(() => {
feed.unfollow();
})
.catch(() => {
alert.value = {
type: "error",
message: t("errors.unknown"),
};
});
}

onMounted(refreshForCurrentTab);
</script>
4 changes: 2 additions & 2 deletions src/screens/LinkScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ async function refreshForCurrentTab() {
}

api.search(url)
.then((fetchedLink) => {
link.init(fetchedLink);
.then((data) => {
link.init(data.links[0]);
ready.value = true;
})
.catch((error) => {
Expand Down
15 changes: 14 additions & 1 deletion src/screens/Screen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,20 @@
</a>
</div>

<div>
<div class="cols cols--always cols--gap-smaller">
<a
v-if="currentPath != '/feeds'"
href="#/feeds"
class="button button--icon button--ghost"
:title="t('menu.feeds')"
>
<Icon name="feed" />

<span class="sr-only">
{{ t("menu.feeds") }}
</span>
</a>

<button
v-if="store.menuOpened"
class="button button--icon button--ghost"
Expand Down
4 changes: 4 additions & 0 deletions src/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ body {
margin-bottom: 1.5rem;
}

.list--separated > * + * {
border-top: 1px solid var(--color-grey-line);
}

.tag {
display: inline-block;
margin-right: var(--space-smaller);
Expand Down