diff --git a/src/i18n.js b/src/i18n.js
index 25f6565..5c0eacc 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -29,6 +29,12 @@ export const i18n = createI18n({
"An unknown error has occurred. Please close and reopen the extension. If it still doesn’t work, please contact the support.",
"errors.url.presence": "Enter a URL.",
"errors.url.url": "The URL of the current page is not supported by Flus.",
+ "feeds.autotest.invalid_url":
+ "Unable to test URLs automatically as the tab URL looks invalid.",
+ "feeds.autotest.missing_permissions":
+ "You must authorize the extension to access all websites in the browser settings.",
+ "feeds.autotest.no_results": "No URL corresponding to a feed was found.",
+ "feeds.autotest.submit": "Test URLs automatically",
"feeds.count_detected":
"No feeds detected on this page. | 1 feed detected on this page. | {count} feeds detected on this page.",
"feeds.follow": "Follow",
@@ -41,6 +47,7 @@ export const i18n = createI18n({
"feeds.unfollow": "Unfollow",
"feeds.url_potential_feed": "URL of a potential feed",
"forms.error": "Error:",
+ "forms.or": "or",
"link.count_collections": "No collections | 1 collection | {count} collections",
"link.invalid_protocol":
"This page cannot be handled by Flus (non-supported protocol).",
@@ -56,7 +63,6 @@ export const i18n = createI18n({
"login.errors.server_error":
"The server “{server}” cannot be reached, please check its address.",
"login.intro": "Log in to access Flus.",
- "login.or": "or",
"login.password.label": "Password",
"login.register": "create an account",
"login.server": "Server:",
@@ -112,6 +118,12 @@ export const i18n = createI18n({
"Une erreur inconnue est survenue. Veuillez fermer et réouvrir l’extension. Si cela ne suffit pas, veuillez contacter le support.",
"errors.url.presence": "Saisissez une URL.",
"errors.url.url": "L’URL de la page actuelle n’est pas supportée par Flus.",
+ "feeds.autotest.invalid_url":
+ "Impossible de tester les URL automatiquement car l’URL de l’onglet semble invalide.",
+ "feeds.autotest.missing_permissions":
+ "Vous devez autoriser l’extension à accéder à tous les sites dans les paramètres du navigateur.",
+ "feeds.autotest.no_results": "Aucune URL correspondante à un flux n’a été trouvée.",
+ "feeds.autotest.submit": "Tester des URL automatiquement",
"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",
@@ -124,6 +136,7 @@ export const i18n = createI18n({
"feeds.unfollow": "Ne plus suivre",
"feeds.url_potential_feed": "URL d’un flux potentiel",
"forms.error": "Erreur :",
+ "forms.or": "ou",
"link.count_collections": "Aucune collection | 1 collection | {count} collections",
"link.invalid_protocol":
"Cette page ne peut pas être enregistrée dans Flus (protocole non supporté).",
@@ -139,7 +152,6 @@ export const i18n = createI18n({
"login.errors.server_error":
"Le serveur « {server} » est injoignable, veuillez vérifier son adresse.",
"login.intro": "Identifiez-vous pour accéder à votre veille Flus.",
- "login.or": "ou",
"login.password.label": "Mot de passe",
"login.register": "créer un compte",
"login.server": "Serveur :",
diff --git a/src/manifest.json b/src/manifest.json
index bd6645e..b9bf731 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -18,6 +18,8 @@
"permissions": ["tabs", "storage"],
+ "host_permissions": ["http://*/*", "https://*/*"],
+
"action": {
"default_icon": "icons/icon-32.png",
"default_title": "Flus",
diff --git a/src/screens/FeedsScreen.vue b/src/screens/FeedsScreen.vue
index c10bbcc..0281954 100644
--- a/src/screens/FeedsScreen.vue
+++ b/src/screens/FeedsScreen.vue
@@ -46,6 +46,8 @@
+
+
{{ t("feeds.search_tip") }}
@@ -65,22 +67,37 @@
{{ form.error('url') }}
-
+
+
+
+
+
-
-
+
+
+ {{ t('forms.or') }}
+
+
+
+
+
+
+ {{ autotestResult }}
+
@@ -138,6 +155,7 @@ const alert = ref({
const feeds = ref([]);
const urlToTest = ref("");
+const autotestResult = ref("");
function feedUrl(feed) {
return `${store.auth.server}/collections/${feed.id}`;
@@ -155,6 +173,8 @@ async function getCurrentTab() {
}
function refreshForUrl(url) {
+ form.startRequest();
+
api.search(url)
.then((data) => {
// This array is used to deduplicate feeds with the same name.
@@ -171,6 +191,7 @@ function refreshForUrl(url) {
}
});
ready.value = true;
+ form.finishRequest();
})
.catch((error) => {
if (error instanceof http.HttpError) {
@@ -183,6 +204,7 @@ function refreshForUrl(url) {
}
ready.value = true;
+ form.finishRequest();
});
}
@@ -233,5 +255,97 @@ function testUrl() {
refreshForUrl(urlToTest.value);
}
+async function testCommonFeedsPatterns() {
+ // Try not to add too many patterns as they generate several HTTP requests.
+ const commonFeedsPatterns = [
+ "/feed",
+ "/feed.xml",
+ "/rss.xml",
+ "/atom.xml",
+ "/index.xml",
+ "/rss",
+ "/rss/",
+ "/rss/feed.xml",
+ ];
+
+ const tabUrl = (await getCurrentTab()).url;
+ const parsedUrl = URL.parse(tabUrl);
+
+ if (!parsedUrl) {
+ autotestResult.value = t("feeds.autotest.invalid_url");
+ return;
+ }
+
+ if (!(await hasPermissionsForAutotest())) {
+ const result = await requestPermissionsForAutotest();
+ if (!result) {
+ autotestResult.value = t("feeds.autotest.missing_permissions");
+ return;
+ }
+ }
+
+ form.startRequest();
+
+ const baseUrl = parsedUrl.origin;
+ let foundFeed = false;
+
+ for (const urlPattern of commonFeedsPatterns) {
+ const testedFeedUrl = baseUrl + urlPattern;
+
+ urlToTest.value = testedFeedUrl;
+
+ const response = await fetch(testedFeedUrl);
+
+ if (!response.ok) {
+ continue;
+ }
+
+ const content = await response.text();
+ foundFeed = looksLikeFeedContent(content);
+
+ if (foundFeed) {
+ break;
+ }
+ }
+
+ form.finishRequest();
+
+ if (foundFeed) {
+ testUrl();
+ } else {
+ urlToTest.value = "";
+ autotestResult.value = t("feeds.autotest.no_results");
+ }
+}
+
+function looksLikeFeedContent(content) {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(content, "application/xml");
+
+ const rootElement = doc.documentElement ? doc.documentElement.tagName : null;
+
+ return rootElement === "rss" || rootElement === "feed" || rootElement === "rdf";
+}
+
+async function hasPermissionsForAutotest() {
+ const origins = ["http://*/*", "https://*/*"];
+
+ return await browser.permissions.contains({ origins });
+}
+
+async function requestPermissionsForAutotest() {
+ const origins = ["http://*/*", "https://*/*"];
+
+ try {
+ return await browser.permissions.request({ origins });
+ } catch {
+ // On Firefox, requesting the permission fails with the error:
+ // "permissions.request may only be called from a user input handler".
+ // See https://stackoverflow.com/q/47723297 for a bit more context. I
+ // wasn't able to make it works though.
+ return false;
+ }
+}
+
onMounted(refreshForCurrentTab);
diff --git a/src/screens/LinkScreen.vue b/src/screens/LinkScreen.vue
index 8422e5d..2eeba14 100644
--- a/src/screens/LinkScreen.vue
+++ b/src/screens/LinkScreen.vue
@@ -221,5 +221,7 @@ onMounted(refreshCurrentTabUrl);
browser.tabs.onUpdated.addListener(refreshCurrentTabUrl);
browser.tabs.onActivated.addListener(refreshCurrentTabUrl);
-browser.windows.onFocusChanged.addListener(refreshCurrentTabUrl);
+if (browser.windows) {
+ browser.windows.onFocusChanged.addListener(refreshCurrentTabUrl);
+}
diff --git a/src/screens/LoginScreen.vue b/src/screens/LoginScreen.vue
index 4d65411..c078092 100644
--- a/src/screens/LoginScreen.vue
+++ b/src/screens/LoginScreen.vue
@@ -106,7 +106,7 @@