From edacb6d4172ea346d5326d7010b14fe047c4ff17 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 24 Nov 2025 16:57:54 +0100 Subject: [PATCH 1/2] fix: Fix issues on Firefox mobile --- src/screens/LinkScreen.vue | 4 +++- src/screens/MenuScreen.vue | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) 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/MenuScreen.vue b/src/screens/MenuScreen.vue index 6773e07..d079369 100644 --- a/src/screens/MenuScreen.vue +++ b/src/screens/MenuScreen.vue @@ -67,7 +67,9 @@ async function openShortcuts() { const shouldShowShortcuts = ref(false); onMounted(async () => { - shouldShowShortcuts.value = - (await browserUtils.isChrome()) || browser.commands.openShortcutSettings != null; + const isChrome = await browserUtils.isChrome(); + const hasShortcutCommand = browser.commands && browser.commands.openShortcutSettings != null; + + shouldShowShortcuts.value = isChrome || hasShortcutCommand; }); From d6db32e20b0ddb1a19158e1fdd0d6ce1e1bc8618 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 24 Nov 2025 16:58:59 +0100 Subject: [PATCH 2/2] feat: Allow to test common feeds URLs patterns --- src/i18n.js | 16 +++- src/manifest.json | 2 + src/screens/FeedsScreen.vue | 142 ++++++++++++++++++++++++++++++++---- src/screens/LoginScreen.vue | 2 +- 4 files changed, 145 insertions(+), 17 deletions(-) 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/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 @@

- {{ t("login.or") }} + {{ t("forms.or") }} {{ t("login.register") }}