diff --git a/README.md b/README.md index d75f366..55b5622 100644 --- a/README.md +++ b/README.md @@ -17,21 +17,34 @@ replié est simplement plus court, il est donc marqué lu plus tôt. ## Installation (Docker) -Le dépôt est directement le dossier de l'extension. Dans le volume monté sur -`/var/www/FreshRSS/extensions` : +Le dépôt est directement le dossier de l'extension. L'image officielle +`freshrss/freshrss` ne contient pas `git` : on clone **depuis l'hôte** dans le +volume monté sur `/var/www/FreshRSS/extensions`. + +**Volume monté depuis un dossier de l'hôte** (`./extensions:/var/www/FreshRSS/extensions`) : ```sh -docker exec -it sh -c \ - 'cd /var/www/FreshRSS/extensions && git clone https://git.siteop.biz/vincentb99/freshrss-read-more.git' +git clone https://git.siteop.biz/vincentb99/freshrss-read-more.git ./extensions/freshrss-read-more ``` -(ou cloner depuis l'hôte dans le dossier du volume). Puis dans FreshRSS : -*Configuration → Extensions → Read More → activer*. +**Volume Docker nommé** (ex. `freshrss_extensions`) : + +```sh +docker run --rm -v freshrss_extensions:/ext alpine/git \ + clone https://git.siteop.biz/vincentb99/freshrss-read-more.git /ext/freshrss-read-more +``` + +Les fichiers doivent être lisibles par le serveur web du conteneur (c'est le cas par +défaut d'un clone). Puis dans FreshRSS : *Configuration → Extensions → Read More → activer*. ### Mise à jour +Même principe, depuis l'hôte : + ```sh -cd /var/www/FreshRSS/extensions/freshrss-read-more && git pull +git -C ./extensions/freshrss-read-more pull +# ou, volume nommé : +docker run --rm -v freshrss_extensions:/ext alpine/git -C /ext/freshrss-read-more pull ``` puis recharger la page dans le navigateur. diff --git a/docs/superpowers/specs/2026-11-06-read-more-design.md b/docs/superpowers/specs/2026-11-06-read-more-design.md index 28e89ac..4bd58d0 100644 --- a/docs/superpowers/specs/2026-11-06-read-more-design.md +++ b/docs/superpowers/specs/2026-11-06-read-more-design.md @@ -68,7 +68,7 @@ ci-dessous valent donc pour 1.30.4. - **Vue courante côté JS** — `context.current_view === 'reader'`. - **Chargement de nouveaux articles** — événement `freshrss:load-more` dispatché sur `document.body` (bubbles) à chaque lot chargé. -- **Passage de config au JS** — hook `Minz_HookType::JsVars` ; les données retournées +- **Passage de config au JS** — hook `Minz_HookType::JsVars` (clé `read_more.collapsed_height`, `read_more.label`) ; les données retournées apparaissent dans `context.extensions`. - **Découverte des extensions** — tout dossier de `extensions/` (`THIRDPARTY_EXTENSIONS_PATH`) contenant un `metadata.json` valide ; pas de préfixe de nom imposé. En Docker : @@ -99,7 +99,7 @@ Extension de type `user` (activable par utilisateur), sans base de données ni b metadata.json name "Read More", entrypoint "ReadMore", type "user" extension.php class ReadMoreExtension extends Minz_Extension init(): registerTranslates(), appendScript/appendStyle (static/) - registerHook(JsVars) -> ['read_more' => ['height' => …, libellés]] + registerHook(JsVars) -> ['read_more' => ['collapsed_height' => …, 'label' => …]] handleConfigureAction(): valide et enregistre `collapsed_height` (int, bornes raisonnables) configure.phtml formulaire : hauteur repliée (px) i18n/fr/ext.php, i18n/en/ext.php libellés (« Lire plus », config) diff --git a/static/read-more.js b/static/read-more.js index 40caf9d..13b0d37 100644 --- a/static/read-more.js +++ b/static/read-more.js @@ -42,6 +42,11 @@ if (state !== undefined && state !== STATE.short) { return; } + // Re-evaluating a short entry: never shrink one that starts above the viewport, + // the page would jump up and unseen articles could be marked read on next scroll. + if (state === STATE.short && flux.getBoundingClientRect().top < 0) { + return; + } const text = flux.querySelector('.text'); if (!text) { return; @@ -71,7 +76,8 @@ }; const doc = win.document; processEntries(doc, opts); - doc.body.addEventListener('freshrss:load-more', () => processEntries(doc, opts)); + // Listen on document: async scripts may run before exists; the event bubbles. + doc.addEventListener('freshrss:load-more', () => processEntries(doc, opts)); // Late-loading images can make a "short" article long enough to collapse. doc.addEventListener('load', function (ev) { const target = ev.target; diff --git a/tests/unit/read-more.test.js b/tests/unit/read-more.test.js index bfc7752..5624f1b 100644 --- a/tests/unit/read-more.test.js +++ b/tests/unit/read-more.test.js @@ -143,3 +143,40 @@ test('init handles load-more', () => { assert.equal(document.querySelectorAll('.read-more-btn').length, 2); assert.equal(document.getElementById('flux_2').dataset.readMore, 'collapsed'); }); + +test('init works before body is parsed', () => { + const { window, document, ReadMore } = setup([]); + document.documentElement.removeChild(document.body); + setContext(window, 'reader'); + assert.doesNotThrow(() => ReadMore.init(window, measure)); + const body = document.createElement('body'); + body.innerHTML = entry(1, 1000); + document.documentElement.appendChild(body); + body.dispatchEvent(new window.Event('freshrss:load-more', { bubbles: true })); + assert.equal(document.getElementById('flux_1').dataset.readMore, 'collapsed'); +}); + +test('late image load does not collapse entry already above viewport top', () => { + const { window, document, ReadMore } = setup([entry(1, 300)]); + setContext(window, 'reader'); + ReadMore.init(window, measure); + const flux = document.getElementById('flux_1'); + flux.getBoundingClientRect = () => ({ top: -100, bottom: 200 }); + const text = flux.querySelector('.text'); + text.dataset.h = '1000'; + text.querySelector('img').dispatchEvent(new window.Event('load')); + assert.ok(!text.classList.contains('read-more-collapsed')); + assert.equal(flux.querySelectorAll('.read-more-btn').length, 0); +}); + +test('load-more does not collapse short entry already above viewport top', () => { + const { window, document, ReadMore } = setup([entry(1, 300)]); + setContext(window, 'reader'); + ReadMore.init(window, measure); + const flux = document.getElementById('flux_1'); + flux.getBoundingClientRect = () => ({ top: -100, bottom: 200 }); + flux.querySelector('.text').dataset.h = '1000'; + document.body.dispatchEvent(new window.Event('freshrss:load-more', { bubbles: true })); + assert.equal(flux.dataset.readMore, 'short'); + assert.equal(flux.querySelectorAll('.read-more-btn').length, 0); +});