discovery-media-player 0.1.142 → 0.1.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/HOST-CONTRACT.md +62 -1
- package/package.json +1 -1
- package/server/browser.generated.js +1 -1
- package/server/page-audience.js +13 -6
- package/server/page-visionneuse.js +423 -17
- package/server/presentations.js +51 -3
- package/server/routes-direct.js +1 -1
- package/server/schema.js +23 -0
- package/supabase/init.sql +99 -1
- package/supabase/migrations/0021-empreintes-de-voix.sql +27 -0
- package/supabase/migrations/0024-rotation-en-direct.sql +34 -0
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -9,7 +9,7 @@ What a host application may call, what it must implement, and what will not chan
|
|
|
9
9
|
version bump. If you are integrating the player, this page and [`API.md`](https://github.com/Juli1artha/discovery-media-player/blob/main/docs/API.md) are the two you
|
|
10
10
|
need.
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Six rules
|
|
13
13
|
|
|
14
14
|
1. **One source of truth.** Fix the player in the player's repository, never in a host — not even
|
|
15
15
|
in a host that once contained it. A fix written host-side is a copy, and copies drift.
|
|
@@ -23,6 +23,15 @@ need.
|
|
|
23
23
|
the context and often the fix. Releasing stays with the maintainer because publishing a version
|
|
24
24
|
decides deploy order. A local workaround is fine when you are blocked, on two conditions: report
|
|
25
25
|
it the same day, and remove it when the release lands.
|
|
26
|
+
6. **Describe what you do — including what you think is trivial.** Not *"report deviations"*: you
|
|
27
|
+
cannot know what one is, because that would mean knowing this page better than we do. ⚠️ The
|
|
28
|
+
over-specified sentence in [the `tts-cache` section](#four-things-that-will-bite) stood for
|
|
29
|
+
weeks, and it took a host mentioning its own naming **as a curiosity** for anyone to look. Its
|
|
30
|
+
own account of it, on 27/08: *"je ne l'ai décrite que parce que je citais `preview-fr-v2` comme
|
|
31
|
+
une curiosité, sans savoir que c'était un écart. Si j'avais su que votre page l'interdisait, je
|
|
32
|
+
me serais probablement conformé."* Complying would have orphaned **908 objects, permanently**.
|
|
33
|
+
A rule that asks you to spot the deviation cannot work; a rule that asks you to describe your
|
|
34
|
+
integration asks for nothing you do not already have. Boring descriptions are the useful ones.
|
|
26
35
|
|
|
27
36
|
## Identity card
|
|
28
37
|
|
|
@@ -99,6 +108,58 @@ different question when you want a real answer:
|
|
|
99
108
|
GET /api/doc?contract=1&schema=1
|
|
100
109
|
```
|
|
101
110
|
|
|
111
|
+
⚠️ **And if that is what you poll, the lazy verdicts are ones you will never see — which is the
|
|
112
|
+
whole point, and worth knowing before you read a warning aimed at somebody else.** `&schema=1`
|
|
113
|
+
probes every expectation *before answering*, then keeps the answers for the life of the process. So
|
|
114
|
+
its verdict is `complet`, or `indetermine` when the witness row itself does not answer — **never**
|
|
115
|
+
`non-sonde`, and `partiel` only in the window where two of them race. A plain `?contract=1` on that
|
|
116
|
+
same process reports `complet` afterwards too, because the probing already happened.
|
|
117
|
+
|
|
118
|
+
The consequence is a split an integrating host measured on 28/08, having read `complet` for a week
|
|
119
|
+
from a daily cron and put it down to luck: **the readers who need the "do not mistake this for a
|
|
120
|
+
regression" warning and the readers who ever meet it are disjoint.** Poll with `&schema=1` and you
|
|
121
|
+
cannot observe the degradation the warning describes; read `?contract=1` alone and you meet it
|
|
122
|
+
without having asked for a probe. Neither is a defect. But if you are checking that warning against
|
|
123
|
+
what you see, and you poll with `&schema=1`, **your instance is not the one it is about**.
|
|
124
|
+
|
|
125
|
+
⚠️ **`complet` means complete *for the code you are running*, never complete for the repository —
|
|
126
|
+
and `connues` is there so you do not have to take that on trust.** The expectation list is compiled
|
|
127
|
+
into the player. A migration published after your version does not appear in it, is never probed,
|
|
128
|
+
and can therefore never turn `manquant` red. So a host who applies the schema **before** upgrading
|
|
129
|
+
the code — the safe order, the one we recommend — is exactly the host this green does not inform.
|
|
130
|
+
|
|
131
|
+
The STUDIO host measured it on both sides on 30/08: on `0.1.142` they applied migration 0024, re-read
|
|
132
|
+
the card, and got `attendues: 9 · sondees: 9 · complet · manquant: []` — word for word what they had
|
|
133
|
+
read *before* applying it. They could not watch 0024 leave a list it had never entered.
|
|
134
|
+
|
|
135
|
+
No version of this server can know a migration that postdates it. What it can do, and now does, is
|
|
136
|
+
say what it knows:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
"schema": {
|
|
140
|
+
"couvre": "colonnes-conditionnelles",
|
|
141
|
+
"attendues": 10, "sondees": 10, "verdict": "complet", "manquant": [],
|
|
142
|
+
"connues": ["supabase/migrations/0001-destinataire-atteste.sql", "…", "supabase/migrations/0024-rotation-en-direct.sql"]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Read it this way, in this order:
|
|
147
|
+
|
|
148
|
+
1. **Is the migration you care about in `connues`?** If not, this card cannot speak about it, whatever
|
|
149
|
+
`verdict` says. Upgrade the player, or check the database directly (below).
|
|
150
|
+
2. **Only then** does `verdict` answer for it. `manquant` names its files in the same strings as
|
|
151
|
+
`connues`, so membership is a plain comparison and not an inference about our release history.
|
|
152
|
+
|
|
153
|
+
⚠️ **To check a migration this player does not know about, ask the database, not the card.** The
|
|
154
|
+
query does not depend on which version you are running, which is the only case where you need an
|
|
155
|
+
answer:
|
|
156
|
+
|
|
157
|
+
```sql
|
|
158
|
+
select column_name, data_type, is_nullable, column_default
|
|
159
|
+
from information_schema.columns
|
|
160
|
+
where table_name = 'doc_presentations' and column_name = 'view_rotation';
|
|
161
|
+
```
|
|
162
|
+
|
|
102
163
|
The three `presence*` fields report what the host has **observed**, not what it is configured to do:
|
|
103
164
|
|
|
104
165
|
| field | meaning |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.143",
|
|
4
4
|
"description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pdf-viewer",
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
// Injecté par api/doc.js dans un <script nonce=…> de la visionneuse. Le code navigateur du
|
|
7
7
|
// player vit en modules TypeScript testés sous player/src/ — c'est ici qu'il atterrit.
|
|
8
8
|
"use strict";
|
|
9
|
-
module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var C=Object.defineProperty,We=Object.defineProperties,Ve=Object.getOwnPropertyDescriptor,ze=Object.getOwnPropertyDescriptors,Fe=Object.getOwnPropertyNames,ve=Object.getOwnPropertySymbols;var we=Object.prototype.hasOwnProperty,Be=Object.prototype.propertyIsEnumerable;var Me=(e,n,t)=>n in e?C(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,K=(e,n)=>{for(var t in n||(n={}))we.call(n,t)&&Me(e,t,n[t]);if(ve)for(var t of ve(n))Be.call(n,t)&&Me(e,t,n[t]);return e},xe=(e,n)=>We(e,ze(n));var M=(e,n)=>{for(var t in n)C(e,t,{get:n[t],enumerable:!0})},je=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Fe(n))!we.call(e,o)&&o!==t&&C(e,o,{get:()=>n[o],enumerable:!(r=Ve(n,o))||r.enumerable});return e};var Ge=e=>je(C({},\"__esModule\",{value:!0}),e);var Kn={};M(Kn,{bridge:()=>N,cadence:()=>L,chat:()=>U,live:()=>H,presentation:()=>D,presentationState:()=>V,tracking:()=>$,viewer:()=>W});var N={};M(N,{onHostMessage:()=>tn,onPlayerMessage:()=>nn,parseHostMessage:()=>Te,parsePlayerMessage:()=>Ee,sendToHost:()=>Xe,sendToPlayer:()=>Je,toWire:()=>Z});var Y=\"3dd-doc-\",Ke=/^[A-Za-z0-9_-]{1,64}$/,Ye=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ze=[\"embed-denied\"],qe=[\"present-invite\",\"present-handover\",\"present-switch\"],Qe=[\"handover-done\"];function Z(e){let n={type:Y+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function _e(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(Y))return null;let o=r.slice(Y.length);if(n.includes(o))return{type:o};if(Ze.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let s=e.slug;return typeof s!=\"string\"||!Ke.test(s)?null:{type:o,slug:s}}function Ee(e){return _e(e,Ye,qe)}function Te(e){return _e(e,Qe,[])}function Xe(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(Z(e),\"*\")}catch(t){}}function Je(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(Z(n),\"*\")}catch(r){}}function en(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function nn(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!en(t,s.source))return;let a=Ee(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function tn(e,n){var s;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(s=t.parent)!=null?s:null,o=a=>{if(!r||r===t||a.source!==r)return;let l=Te(a.data);l&&e(l)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var $={};M($,{createTracker:()=>pn});var L={};M(L,{PRESENTER_ACTIONS_PER_HOUR:()=>X,PRESENT_CACHE_MS:()=>an,PRESENT_QUOTA_MARGIN:()=>Ne,PRESENT_QUOTA_PER_HOUR:()=>un,PRESENT_READS_PER_HOUR:()=>Ie,PRESENT_READ_BURST:()=>Ae,PRESENT_READ_COALESCE_MS:()=>Ce,PRESENT_RESYNC_MS:()=>Re,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>sn,PRESENT_WRITE_TIMEOUT_MS:()=>J,READERS_PER_EGRESS:()=>O,RESYNC_READS_PER_HOUR:()=>Pe,SESSION_IDLE_MS:()=>Q,SESSION_INTERVAL_MS:()=>q,SESSION_QUOTA_PER_HOUR:()=>rn,SESSION_WRITES_PER_HOUR:()=>Se,VIEW_EVENTS_PER_READER_HOUR:()=>ke,VIEW_QUOTA_PER_HOUR:()=>on});var q=3e4,Se=Math.ceil(120),O=25,rn=Se*O,ke=720,on=ke*O,Q=18e4,Re=25e3,Pe=Math.ceil(36e5/Re),X=720,Ie=Pe+X,sn=X,Ae=20,J=1e4,Ce=400,an=Ce,Ne=4,un=(Ie+Ae)*O*Ne;var ln=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function cn(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function dn(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;return o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):(n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0}),!0)}catch(r){return!1}}}function pn(e={}){var be,ye;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",s=(be=e.idleMs)!=null?be:Q,a=(ye=e.sessionEveryMs)!=null?ye:3e4,l=e.slug||\"\",p=e.internal||null,g=e.sessionId||cn(),f=e.send||dn(o,n),c={},u=0,b=0,d=0,w=0,v=null,T=r(),S=!1,z=!1,I=[],De=()=>!!l||!!p,ce=i=>{if(!De())return!1;try{return f(i)!==!1}catch(m){return!1}},F=()=>!!t&&t.visibilityState===\"visible\"&&!S,de=i=>{let m=r()-i,y=Math.max(0,T-i)+s;return Math.max(0,Math.min(m,y))},B=()=>{v!=null&&u>0&&(c[u]=(c[u]||0)+de(v)/1e3),v=F()?r():null},pe=()=>{B(),v=null},j=()=>{v==null&&F()&&(v=r())},G=()=>{let i=0;for(let m in c)i+=c[m];return v!=null&&(i+=de(v)/1e3),Math.round(i)},fe=()=>{let i={};for(let m in c)i[m]=Math.round(c[m]);return i},k=()=>{T=r(),S&&(S=!1,j())},me=(i,m)=>{p||ce({slug:l,event:i,page:m||b,maxPage:d,seconds:G(),sessionId:g})},ge=\"\",A=()=>{B();let i=G(),m=`${i}|${d}|${w}`;if(m===ge)return;let y={event:\"session\",sessionId:g,numPages:w,maxPage:d,totalSeconds:i,pagesTime:fe()};p?(y.internal=!0,y.docId=p.docId,y.email=p.email,y.name=p.name,p.it&&(y.it=p.it)):y.slug=l,ce(y)&&(ge=m)},R=(i,m,y,he)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(m,y,he),I.push(()=>{i.removeEventListener(m,y,he)}))};return{sessionId:g,totalSeconds:G,maxPage:()=>d,pageTimes:fe,start(){if(z)return;z=!0,me(\"open\",1),u=1,T=r(),v=F()?r():null,R(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(pe(),A()):(k(),j())}),R(n,\"focus\",()=>{k(),j()});for(let y of ln)R(n,y,k,{passive:!0});R(e.scrollElement,\"scroll\",k,{passive:!0}),R(n,\"beforeunload\",A);let i=n.setInterval(()=>{!S&&r()-T>s&&(S=!0,pe())},5e3),m=n.setInterval(A,a);I.push(()=>{n.clearInterval(i),n.clearInterval(m)})},setPage(i){!i||i===b||(T=r(),B(),u=i,b=i,i>d&&(d=i,me(\"page\",i)))},setPageCount(i){w=i||0},noteActivity:k,flush:A,stop(){for(;I.length;){let i=I.pop();try{i==null||i()}catch(m){}}z=!1}}}var H={};M(H,{STORAGE_KEYS:()=>x,attendeeKey:()=>bn,authorToken:()=>yn,avatarHtml:()=>ee,canModerate:()=>oe,createBudget:()=>En,createFileEcritures:()=>Tn,createScheduler:()=>xn,escapeHtml:()=>h,fetchBorne:()=>_n,flattenPresence:()=>fn,formatMessageBody:()=>ne,initials:()=>Oe,isMentioned:()=>vn,isMine:()=>_,presenceId:()=>Sn,reactorId:()=>re,referenceAuteur:()=>hn,shouldNotify:()=>Mn,unreadLabel:()=>wn});var x={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function h(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function Oe(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function ee(e,n){return e?`<img src=\"${h(e)}\" alt=\"\">`:h(Oe(n))}function ne(e){let n=h(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function fn(e){let n=new Map;for(let t in e||{})for(let r of(e||{})[t]||[]){let o=String(r.uid||r.email||r.name||\"\").toLowerCase()||`_${t}_${n.size}`;(!n.get(o)||r.role===\"presenter\")&&n.set(o,r)}return[...n.values()]}function mn(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function gn(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function bn(e,n=\"anon\",t=te){try{if(!e)return`anon-${n}`;let r=e.getItem(x.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(x.attendeeKey,o),o}catch(r){return`anon-${n}`}}function te(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function yn(e,n=te){let t=mn(e,x.authorToken);if(t)return t;let r=n();return gn(e,x.authorToken,r),r}async function hn(e,n){let t=n!==void 0?n:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null;if(!t||!e)return\"\";try{let r=async o=>{let s=await t.digest(\"SHA-256\",new TextEncoder().encode(o));return Array.from(new Uint8Array(s),a=>a.toString(16).padStart(2,\"0\")).join(\"\")};return(await r(\"ref:\"+await r(e))).slice(0,16)}catch(r){return\"\"}}function re(e){return String(e&&e.ref||\"\")}function _(e,n){return!e||!n||!e.author_ref||!n.ref?!1:e.author_ref===n.ref}function oe(e){return!!(e&&e.role===\"presenter\")}function vn(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!_(e,n):!1}function Mn(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||_(n,t)?!1:o}function wn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function xn(e,n={}){var b;let t=(b=n.minMs)!=null?b:300,r=n.now||(()=>Date.now()),o=n.setTimer||((d,w)=>setTimeout(d,w)),s=n.clearTimer||(d=>clearTimeout(d)),a=!1,l=!1,p=-1/0,g=null,f=!1;function c(){if(f||a){l=!0;return}let d=t-(r()-p);if(d>0){u(d);return}a=!0,l=!1,p=r();let w=!1;e(()=>{w||(w=!0,a=!1,l&&!f&&c())})}function u(d){f||g!==null||(g=o(()=>{g=null,c()},Math.max(0,d)))}return{signaler(){f||c()},maintenant(){f||(p=-1/0,c())},arreter(){f=!0,g!==null&&(s(g),g=null)}}}function _n(e,n,t=J,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let s=r.setTimer||((u,b)=>setTimeout(u,b)),a=r.clearTimer||(u=>clearTimeout(u)),l=null;try{l=new AbortController}catch(u){}let p=l?xe(K({},n||{}),{signal:l.signal}):K({},n||{}),g=null,f=new Promise((u,b)=>{g=s(()=>{try{l==null||l.abort()}catch(d){}b(new Error(\"delai\"))},t)}),c=()=>{try{a(g)}catch(u){}};return Promise.race([o(e,p),f]).then(u=>(c(),u),u=>{throw c(),u})}function En(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let s=n();return r=Math.min(t,r+(s-o)*e.parHeure/36e5),o=s,r<1?!1:(r-=1,!0)},restants:()=>r}}function Tn(e={}){var g;let n=(g=e.minMs)!=null?g:0,t=e.now||(()=>Date.now()),r=e.setTimer||((f,c)=>setTimeout(f,c)),o=new Map,s=!1,a=-1/0,l=!1;function p(){if(s||o.size===0)return;let f=n-(t()-a);if(f>0){if(l)return;l=!0,r(()=>{l=!1,p()},f);return}let c=o.keys().next().value,u=o.get(c);o.delete(c),s=!0,a=t(),Promise.resolve().then(u.tache).then(b=>u.regler(b),()=>u.regler(null)).then(()=>{s=!1,p()})}return{poser(f,c){return new Promise(u=>{let b=o.get(f);o.set(f,{tache:c,regler:d=>{b&&b.regler(d),u(d)}}),p()})},enAttente:()=>o.size}}function Sn(e,n=te){try{if(e){let t=e.getItem(x.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(x.presenceId,r),r}}catch(t){}return`p-${n()}`}var U={};M(U,{messageClassName:()=>Rn,renderActions:()=>He,renderAttachment:()=>$e,renderMessage:()=>kn,renderReactions:()=>Le});function Le(e,n){let t=e&&e.reactions||{},r=re(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let l=a.indexOf(r)>=0;o+=`<button class=\"re-chip${l?\" on\":\"\"}\" data-e=\"${h(s)}\">${h(s)} ${a.length}</button>`}return o}function $e(e){if(!e||!e.url)return\"\";let n=h(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=h(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function He(e,n){let t=_(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||oe(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function kn(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${h(e.reply_name||\"\")}</b> ${h(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${ne(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${ee(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${h(e.author_name||\"Invit\\xE9\")}</b>${s}</div>`+t+o+$e(e.attachment)+`<div class=cm-re>${Le(e,n.me)}</div></span><span class=cm-act>${He(e,n)}</span>`}function Rn(e,n,t){return\"cm\"+(_(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var D={};M(D,{DEFAULT_CENTER:()=>ae,DEFAULT_ZOOM:()=>ue,MAP_TYPES:()=>P,cycleMapType:()=>Pn,initialMapContent:()=>An,mapTypeLabel:()=>In,sanitizeContent:()=>le});var P=[\"roadmap\",\"satellite\",\"hybrid\"],ae=[46.6,2.5],ue=6;function E(e){let n=Number(e);return Number.isFinite(n)?n:null}function se(e){if(!Array.isArray(e))return null;let n=E(e[0]),t=E(e[1]);return n!=null&&t!=null?[n,t]:null}var ie=(e,n,t)=>Math.max(n,Math.min(t,e));function le(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=se(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:E(r.heading)||0,pitch:ie(E(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ie(E(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:se(n.center)||[...ae],zoom:ie(Math.trunc(E(n.zoom)||ue),1,21),marker:se(n.marker),mapType:P.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Pn(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function In(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function An(){return{kind:\"map\",center:[...ae],zoom:ue,marker:null,mapType:null,label:null}}var W={};M(W,{CURRENT_PAGE_MARGIN:()=>Hn,DEFAULT_ASPECT:()=>On,MAX_ZOOM:()=>Nn,MIN_PAGE_WIDTH:()=>Ln,MIN_ZOOM:()=>Cn,PRERENDER_MARGIN:()=>$n,arrowState:()=>Wn,averageColor:()=>Fn,clampPage:()=>Ue,clampZoom:()=>Un,fitWidth:()=>zn,isImageDocument:()=>Bn,progressPercent:()=>Dn});var Cn=.5,Nn=3,On=1.35,Ln=280,$n=\"500px 0px\",Hn=\"-48% 0px -48% 0px\";function Un(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Ue(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function Dn(e,n){return n?Math.round(Ue(e,n)/n*100):0}function Wn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var Vn=28;function zn(e){var a;let n=(a=e.zoom)!=null?a:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=e.aspect||1.35,s=((e.containerHeight||600)-Vn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Fn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let s=0;s<e.length;s+=4)t+=e[s],r+=e[s+1],o+=e[s+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Bn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var V={};M(V,{presentationTransition:()=>jn,switchDocUrl:()=>Gn});function jn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=le(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function Gn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Ge(Kn);})();", SOURCE_HASH: "483d392a214f6bf8" };
|
|
9
|
+
module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var O=Object.defineProperty,Be=Object.defineProperties,je=Object.getOwnPropertyDescriptor,Ge=Object.getOwnPropertyDescriptors,Ke=Object.getOwnPropertyNames,Me=Object.getOwnPropertySymbols;var _e=Object.prototype.hasOwnProperty,Ye=Object.prototype.propertyIsEnumerable;var we=(e,n,t)=>n in e?O(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,Z=(e,n)=>{for(var t in n||(n={}))_e.call(n,t)&&we(e,t,n[t]);if(Me)for(var t of Me(n))Ye.call(n,t)&&we(e,t,n[t]);return e},Ee=(e,n)=>Be(e,Ge(n));var M=(e,n)=>{for(var t in n)O(e,t,{get:n[t],enumerable:!0})},Ze=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of Ke(n))!_e.call(e,o)&&o!==t&&O(e,o,{get:()=>n[o],enumerable:!(r=je(n,o))||r.enumerable});return e};var qe=e=>Ze(O({},\"__esModule\",{value:!0}),e);var Jn={};M(Jn,{bridge:()=>L,cadence:()=>H,chat:()=>W,live:()=>D,presentation:()=>V,presentationState:()=>F,tracking:()=>U,viewer:()=>z});var L={};M(L,{onHostMessage:()=>an,onPlayerMessage:()=>sn,parseHostMessage:()=>ke,parsePlayerMessage:()=>Se,sendToHost:()=>tn,sendToPlayer:()=>rn,toWire:()=>Q});var q=\"3dd-doc-\",Qe=/^[A-Za-z0-9_-]{1,64}$/,Xe=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Je=[\"embed-denied\"],en=[\"present-invite\",\"present-handover\",\"present-switch\"],nn=[\"handover-done\"];function Q(e){let n={type:q+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function Te(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(q))return null;let o=r.slice(q.length);if(n.includes(o))return{type:o};if(Je.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let s=e.slug;return typeof s!=\"string\"||!Qe.test(s)?null:{type:o,slug:s}}function Se(e){return Te(e,Xe,en)}function ke(e){return Te(e,nn,[])}function tn(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(Q(e),\"*\")}catch(t){}}function rn(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(Q(n),\"*\")}catch(r){}}function on(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function sn(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!on(t,s.source))return;let a=Se(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function an(e,n){var s;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(s=t.parent)!=null?s:null,o=a=>{if(!r||r===t||a.source!==r)return;let u=ke(a.data);u&&e(u)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var U={};M(U,{createTracker:()=>bn});var H={};M(H,{PRESENTER_ACTIONS_PER_HOUR:()=>ee,PRESENT_CACHE_MS:()=>dn,PRESENT_QUOTA_MARGIN:()=>Le,PRESENT_QUOTA_PER_HOUR:()=>pn,PRESENT_READS_PER_HOUR:()=>Ce,PRESENT_READ_BURST:()=>Ne,PRESENT_READ_COALESCE_MS:()=>Oe,PRESENT_RESYNC_MS:()=>Ie,PRESENT_SIGNAL_BUDGET_PER_HOUR:()=>cn,PRESENT_WRITE_TIMEOUT_MS:()=>ne,READERS_PER_EGRESS:()=>$,RESYNC_READS_PER_HOUR:()=>Pe,SESSION_IDLE_MS:()=>J,SESSION_INTERVAL_MS:()=>X,SESSION_QUOTA_PER_HOUR:()=>un,SESSION_WRITES_PER_HOUR:()=>Re,VIEW_EVENTS_PER_READER_HOUR:()=>Ae,VIEW_QUOTA_PER_HOUR:()=>ln});var X=3e4,Re=Math.ceil(120),$=25,un=Re*$,Ae=720,ln=Ae*$,J=18e4,Ie=25e3,Pe=Math.ceil(36e5/Ie),ee=720,Ce=Pe+ee,cn=ee,Ne=20,ne=1e4,Oe=400,dn=Oe,Le=4,pn=(Ce+Ne)*$*Le;var mn=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function fn(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function gn(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;return o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):(n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0}),!0)}catch(r){return!1}}}function bn(e={}){var ye,xe;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",s=(ye=e.idleMs)!=null?ye:J,a=(xe=e.sessionEveryMs)!=null?xe:3e4,u=e.slug||\"\",p=e.internal||null,d=e.sessionId||fn(),m=e.send||gn(o,n),c={},l=0,b=0,f=0,w=0,v=null,S=r(),k=!1,B=!1,C=[],Fe=()=>!!u||!!p,pe=i=>{if(!Fe())return!1;try{return m(i)!==!1}catch(g){return!1}},j=()=>!!t&&t.visibilityState===\"visible\"&&!k,me=i=>{let g=r()-i,y=Math.max(0,S-i)+s;return Math.max(0,Math.min(g,y))},G=()=>{v!=null&&l>0&&(c[l]=(c[l]||0)+me(v)/1e3),v=j()?r():null},fe=()=>{G(),v=null},K=()=>{v==null&&j()&&(v=r())},Y=()=>{let i=0;for(let g in c)i+=c[g];return v!=null&&(i+=me(v)/1e3),Math.round(i)},ge=()=>{let i={};for(let g in c)i[g]=Math.round(c[g]);return i},R=()=>{S=r(),k&&(k=!1,K())},be=(i,g)=>{p||pe({slug:u,event:i,page:g||b,maxPage:f,seconds:Y(),sessionId:d})},he=\"\",N=()=>{G();let i=Y(),g=`${i}|${f}|${w}`;if(g===he)return;let y={event:\"session\",sessionId:d,numPages:w,maxPage:f,totalSeconds:i,pagesTime:ge()};p?(y.internal=!0,y.docId=p.docId,y.email=p.email,y.name=p.name,p.it&&(y.it=p.it)):y.slug=u,pe(y)&&(he=g)},A=(i,g,y,ve)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(g,y,ve),C.push(()=>{i.removeEventListener(g,y,ve)}))};return{sessionId:d,totalSeconds:Y,maxPage:()=>f,pageTimes:ge,start(){if(B)return;B=!0,be(\"open\",1),l=1,S=r(),v=j()?r():null,A(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(fe(),N()):(R(),K())}),A(n,\"focus\",()=>{R(),K()});for(let y of mn)A(n,y,R,{passive:!0});A(e.scrollElement,\"scroll\",R,{passive:!0}),A(n,\"beforeunload\",N);let i=n.setInterval(()=>{!k&&r()-S>s&&(k=!0,fe())},5e3),g=n.setInterval(N,a);C.push(()=>{n.clearInterval(i),n.clearInterval(g)})},setPage(i){!i||i===b||(S=r(),G(),l=i,b=i,i>f&&(f=i,be(\"page\",i)))},setPageCount(i){w=i||0},noteActivity:R,flush:N,stop(){for(;C.length;){let i=C.pop();try{i==null||i()}catch(g){}}B=!1}}}var D={};M(D,{STORAGE_KEYS:()=>_,attendeeKey:()=>vn,authorToken:()=>Mn,avatarHtml:()=>te,canModerate:()=>ie,createBudget:()=>Rn,createFileEcritures:()=>An,createScheduler:()=>Sn,escapeHtml:()=>x,fetchBorne:()=>kn,flattenPresence:()=>hn,formatMessageBody:()=>re,initials:()=>$e,isMentioned:()=>_n,isMine:()=>E,presenceId:()=>In,reactorId:()=>se,referenceAuteur:()=>wn,shouldNotify:()=>En,unreadLabel:()=>Tn});var _={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\",presenceId:\"3dd-present-uid\"};function x(e){return String(e==null?\"\":e).replace(/&/g,\"&\").replace(/</g,\"<\").replace(/>/g,\">\").replace(/\"/g,\""\")}function $e(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function te(e,n){return e?`<img src=\"${x(e)}\" alt=\"\">`:x($e(n))}function re(e){let n=x(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function hn(e){let n=new Map;for(let t in e||{})for(let r of(e||{})[t]||[]){let o=String(r.uid||r.email||r.name||\"\").toLowerCase()||`_${t}_${n.size}`;(!n.get(o)||r.role===\"presenter\")&&n.set(o,r)}return[...n.values()]}function yn(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function xn(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function vn(e,n=\"anon\",t=oe){try{if(!e)return`anon-${n}`;let r=e.getItem(_.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(_.attendeeKey,o),o}catch(r){return`anon-${n}`}}function oe(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function Mn(e,n=oe){let t=yn(e,_.authorToken);if(t)return t;let r=n();return xn(e,_.authorToken,r),r}async function wn(e,n){let t=n!==void 0?n:typeof globalThis!=\"undefined\"&&globalThis.crypto?globalThis.crypto.subtle:null;if(!t||!e)return\"\";try{let r=async o=>{let s=await t.digest(\"SHA-256\",new TextEncoder().encode(o));return Array.from(new Uint8Array(s),a=>a.toString(16).padStart(2,\"0\")).join(\"\")};return(await r(\"ref:\"+await r(e))).slice(0,16)}catch(r){return\"\"}}function se(e){return String(e&&e.ref||\"\")}function E(e,n){return!e||!n||!e.author_ref||!n.ref?!1:e.author_ref===n.ref}function ie(e){return!!(e&&e.role===\"presenter\")}function _n(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!E(e,n):!1}function En(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||E(n,t)?!1:o}function Tn(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function Sn(e,n={}){var b;let t=(b=n.minMs)!=null?b:300,r=n.now||(()=>Date.now()),o=n.setTimer||((f,w)=>setTimeout(f,w)),s=n.clearTimer||(f=>clearTimeout(f)),a=!1,u=!1,p=-1/0,d=null,m=!1;function c(){if(m||a){u=!0;return}let f=t-(r()-p);if(f>0){l(f);return}a=!0,u=!1,p=r();let w=!1;e(()=>{w||(w=!0,a=!1,u&&!m&&c())})}function l(f){m||d!==null||(d=o(()=>{d=null,c()},Math.max(0,f)))}return{signaler(){m||c()},maintenant(){m||(p=-1/0,c())},arreter(){m=!0,d!==null&&(s(d),d=null)}}}function kn(e,n,t=ne,r={}){let o=r.fetch||(typeof fetch==\"function\"?fetch:void 0);if(!o)return Promise.reject(new Error(\"fetch indisponible\"));let s=r.setTimer||((l,b)=>setTimeout(l,b)),a=r.clearTimer||(l=>clearTimeout(l)),u=null;try{u=new AbortController}catch(l){}let p=u?Ee(Z({},n||{}),{signal:u.signal}):Z({},n||{}),d=null,m=new Promise((l,b)=>{d=s(()=>{try{u==null||u.abort()}catch(f){}b(new Error(\"delai\"))},t)}),c=()=>{try{a(d)}catch(l){}};return Promise.race([o(e,p),m]).then(l=>(c(),l),l=>{throw c(),l})}function Rn(e){let n=e.now||(()=>Date.now()),t=Math.max(1,e.rafale),r=t,o=n();return{prendre(){let s=n();return r=Math.min(t,r+(s-o)*e.parHeure/36e5),o=s,r<1?!1:(r-=1,!0)},restants:()=>r}}function An(e={}){var d;let n=(d=e.minMs)!=null?d:0,t=e.now||(()=>Date.now()),r=e.setTimer||((m,c)=>setTimeout(m,c)),o=new Map,s=!1,a=-1/0,u=!1;function p(){if(s||o.size===0)return;let m=n-(t()-a);if(m>0){if(u)return;u=!0,r(()=>{u=!1,p()},m);return}let c=o.keys().next().value,l=o.get(c);o.delete(c),s=!0,a=t(),Promise.resolve().then(l.tache).then(b=>l.regler(b),()=>l.regler(null)).then(()=>{s=!1,p()})}return{poser(m,c){return new Promise(l=>{let b=o.get(m);o.set(m,{tache:c,regler:f=>{b&&b.regler(f),l(f)}}),p()})},enAttente:()=>o.size}}function In(e,n=oe){try{if(e){let t=e.getItem(_.presenceId);if(t)return t;let r=`p-${n()}`;return e.setItem(_.presenceId,r),r}}catch(t){}return`p-${n()}`}var W={};M(W,{messageClassName:()=>Cn,renderActions:()=>De,renderAttachment:()=>Ue,renderMessage:()=>Pn,renderReactions:()=>He});function He(e,n){let t=e&&e.reactions||{},r=se(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let u=a.indexOf(r)>=0;o+=`<button class=\"re-chip${u?\" on\":\"\"}\" data-e=\"${x(s)}\">${x(s)} ${a.length}</button>`}return o}function Ue(e){if(!e||!e.url)return\"\";let n=x(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=x(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function De(e,n){let t=E(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||ie(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function Pn(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${x(e.reply_name||\"\")}</b> ${x(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${re(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${te(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${x(e.author_name||\"Invit\\xE9\")}</b>${s}</div>`+t+o+Ue(e.attachment)+`<div class=cm-re>${He(e,n.me)}</div></span><span class=cm-act>${De(e,n)}</span>`}function Cn(e,n,t){return\"cm\"+(E(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var V={};M(V,{DEFAULT_CENTER:()=>le,DEFAULT_ZOOM:()=>ce,MAP_TYPES:()=>I,cycleMapType:()=>Nn,initialMapContent:()=>Ln,mapTypeLabel:()=>On,sanitizeContent:()=>de});var I=[\"roadmap\",\"satellite\",\"hybrid\"],le=[46.6,2.5],ce=6;function T(e){let n=Number(e);return Number.isFinite(n)?n:null}function ae(e){if(!Array.isArray(e))return null;let n=T(e[0]),t=T(e[1]);return n!=null&&t!=null?[n,t]:null}var ue=(e,n,t)=>Math.max(n,Math.min(t,e));function de(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=ae(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:T(r.heading)||0,pitch:ue(T(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:ue(T(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:ae(n.center)||[...le],zoom:ue(Math.trunc(T(n.zoom)||ce),1,21),marker:ae(n.marker),mapType:I.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function Nn(e){let n=I.indexOf(e);return I[(n+1)%I.length]}function On(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function Ln(){return{kind:\"map\",center:[...le],zoom:ce,marker:null,mapType:null,label:null}}var z={};M(z,{CURRENT_PAGE_MARGIN:()=>Vn,DEFAULT_ASPECT:()=>Un,MAX_ZOOM:()=>Hn,MIN_PAGE_WIDTH:()=>Dn,MIN_ZOOM:()=>$n,PRERENDER_MARGIN:()=>Wn,ROTATIONS:()=>Zn,ancrageApresZoom:()=>qn,arrowState:()=>Bn,aspectApresRotation:()=>ze,averageColor:()=>Kn,clampPage:()=>Ve,clampZoom:()=>zn,fitWidth:()=>Gn,isImageDocument:()=>Yn,progressPercent:()=>Fn,rotationEffective:()=>P});var $n=.5,Hn=3,Un=1.35,Dn=280,Wn=\"500px 0px\",Vn=\"-48% 0px -48% 0px\";function zn(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Ve(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function Fn(e,n){return n?Math.round(Ve(e,n)/n*100):0}function Bn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var jn=28;function Gn(e){var a;let n=(a=e.zoom)!=null?a:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=ze(e.aspect,e.rotation),s=((e.containerHeight||600)-jn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function Kn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let s=0;s<e.length;s+=4)t+=e[s],r+=e[s+1],o+=e[s+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Yn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var Zn=[0,90,180,270],h=(e,n=0)=>typeof e==\"number\"&&Number.isFinite(e)?e:n;function P(e,n){return(Math.round((h(e)+h(n))/90)%4+4)%4*90}function ze(e,n){let t=h(e,0)>0?e:1.35,r=P(0,n);return r===90||r===270?1/t:t}function We(e,n,t,r,o,s,a){let u=Math.min(Math.max(0,o),Math.max(0,r)),p=u+Math.max(0,r-u)*a,d=Math.min(Math.max(0,s),Math.max(0,r)),m=r-d>0?(p-d)/(r-d):1,c=Math.max(0,(t-r)/2),l=n+e-c,b=d+Math.max(0,l-d)*m-n;return Math.min(Math.max(0,b),Math.max(0,p-t))}function qn(e){let n=h(e.zoomAvant,0),t=h(e.zoomApres,0),r={x:h(e.defilement&&e.defilement.x),y:h(e.defilement&&e.defilement.y)};if(n<=0||t<=0)return r;let o=t/n,s=e.fixe||{largeur:0,hauteur:0},a=e.fixeDebut||{largeur:0,hauteur:0};return{x:We(r.x,h(e.point&&e.point.x),h(e.cadre&&e.cadre.largeur),h(e.contenu&&e.contenu.largeur),h(s.largeur),h(a.largeur),o),y:We(r.y,h(e.point&&e.point.y),h(e.cadre&&e.cadre.hauteur),h(e.contenu&&e.contenu.hauteur),h(s.hauteur),h(a.hauteur),o)}}var F={};M(F,{presentationTransition:()=>Qn,switchDocUrl:()=>Xn});function Qn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=de(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}],o=P(0,e.view_rotation);return o!==P(0,n.rotation)&&r.push({kind:\"rotate\",rotation:o}),e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function Xn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return qe(Jn);})();", SOURCE_HASH: "720cf1b3ec849d63" };
|
package/server/page-audience.js
CHANGED
|
@@ -20,7 +20,7 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
20
20
|
const presenter = esc(pres.presenter_name || "");
|
|
21
21
|
const logo = esc(logoUrl || "");
|
|
22
22
|
const fileUrl = `/api/doc?present=${encodeURIComponent(pres.slug)}&file=1`;
|
|
23
|
-
const cfg = jsonPourScript({ fileUrl, docUrl: pres.file_url, pdfjs: PDFJS, pdfjsWorker: PDFJS_WORKER, slug: pres.slug, fileName: pres.file_name || "", page: pres.current_page || 1, active: pres.active !== false, content: pres.content || null, supaUrl, supaKey, title: pres.doc_title || pres.file_name || "Document" });
|
|
23
|
+
const cfg = jsonPourScript({ fileUrl, docUrl: pres.file_url, pdfjs: PDFJS, pdfjsWorker: PDFJS_WORKER, slug: pres.slug, fileName: pres.file_name || "", page: pres.current_page || 1, rotation: pres.view_rotation || 0, active: pres.active !== false, content: pres.content || null, supaUrl, supaKey, title: pres.doc_title || pres.file_name || "Document" });
|
|
24
24
|
return `<!doctype html><html lang=fr><head><meta charset=utf-8>
|
|
25
25
|
<meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=3">
|
|
26
26
|
<meta name=robots content="noindex,nofollow">
|
|
@@ -95,7 +95,7 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
95
95
|
<script nonce="${nonce}">
|
|
96
96
|
(function(){
|
|
97
97
|
var CFG=${cfg};
|
|
98
|
-
var PDF=null, total=0, cur=CFG.page||1, ready=false;
|
|
98
|
+
var PDF=null, total=0, cur=CFG.page||1, rot=CFG.rotation||0, ready=false;
|
|
99
99
|
// ⚠️ DEUX GÉNÉRATIONS, PARCE QU'IL Y A DEUX COURSES DISTINCTES (P1 audit externe).
|
|
100
100
|
//
|
|
101
101
|
// docGen : le présentateur change de document pendant qu'un chargement est en cours. Le
|
|
@@ -137,9 +137,13 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
137
137
|
// arrivait : ce résultat n'est plus celui qu'on attend. On ne le pose pas.
|
|
138
138
|
if(monDoc!==docGen || monRendu!==renderGen) return;
|
|
139
139
|
var availW=Math.max(120, stage.clientWidth-40), availH=Math.max(120, stage.clientHeight-40);
|
|
140
|
-
|
|
140
|
+
// ⚠️ ON COMPOSE, ON N'ÉCRASE PAS — même piège que dans la visionneuse. getViewport prend par
|
|
141
|
+
// défaut la rotation du fichier ; passer une valeur absolue coucherait un document numérisé qui
|
|
142
|
+
// était droit. La composition vit dans src/viewer.ts, où elle est éprouvée.
|
|
143
|
+
var rotEff=Player.viewer.rotationEffective(page.rotate,rot);
|
|
144
|
+
var v1=page.getViewport({scale:1,rotation:rotEff});
|
|
141
145
|
var scale=Math.min(availW/v1.width, availH/v1.height); // fit entier → une page à l'écran
|
|
142
|
-
var dpr=window.devicePixelRatio||1, vp=page.getViewport({scale:scale});
|
|
146
|
+
var dpr=window.devicePixelRatio||1, vp=page.getViewport({scale:scale,rotation:rotEff});
|
|
143
147
|
var cv=document.createElement('canvas'); cv.width=Math.floor(vp.width*dpr); cv.height=Math.floor(vp.height*dpr);
|
|
144
148
|
cv.setAttribute('role','img'); cv.setAttribute('aria-label','Page '+n+(total>1?' sur '+total:''));
|
|
145
149
|
cv.style.width=vp.width+'px'; cv.style.height=vp.height+'px';
|
|
@@ -282,10 +286,10 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
282
286
|
if(!row) return;
|
|
283
287
|
// La table et la diffusion portent la même vérité : sans cette garde, chaque changement de
|
|
284
288
|
// page serait rendu deux fois (scintillement), et chaque carte ré-ouverte inutilement.
|
|
285
|
-
var sig=''; try{ sig=JSON.stringify([row.active,row.current_page,row.file_url,row.content]); }catch(e){ sig=String(Math.random()); }
|
|
289
|
+
var sig=''; try{ sig=JSON.stringify([row.active,row.current_page,row.file_url,row.content,row.view_rotation]); }catch(e){ sig=String(Math.random()); }
|
|
286
290
|
if(sig===_etatVu) return;
|
|
287
291
|
_etatVu=sig;
|
|
288
|
-
var actions=Player.presentationState.presentationTransition(row,{docUrl:CFG.docUrl});
|
|
292
|
+
var actions=Player.presentationState.presentationTransition(row,{docUrl:CFG.docUrl,rotation:rot});
|
|
289
293
|
for(var i=0;i<actions.length;i++){ var a=actions[i];
|
|
290
294
|
if(a.kind==='ended'){ ended(); return; }
|
|
291
295
|
if(a.kind==='show-map'){ if(window.Map3DD){ if(a.content.kind==='streetview') Map3DD.enterSV(a.content,false); else Map3DD.enter(a.content,false); } return; }
|
|
@@ -293,6 +297,9 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
293
297
|
// switchDoc pose LUI-MÊME docUrl/fileName/titre : les poser ici, en partie, était
|
|
294
298
|
// exactement ce qui laissait fileName en arrière.
|
|
295
299
|
if(a.kind==='switch-doc'){ switchDoc(row); return; }
|
|
300
|
+
// ⚠️ AVANT la page, et c'est l'ordre que presentationTransition garantit : tourner après avoir
|
|
301
|
+
// rendu ferait rendre deux fois, la première fois pour rien.
|
|
302
|
+
if(a.kind==='rotate'){ rot=a.rotation; renderGen++; show(cur); }
|
|
296
303
|
if(a.kind==='show-page'){ show(a.page); }
|
|
297
304
|
}
|
|
298
305
|
}
|
|
@@ -134,7 +134,7 @@ function viewerHtml(share, nonce, logoUrl, pitch) {
|
|
|
134
134
|
cherche une sortie. Sans bordure : c'est une sortie, pas une action de plus. Elle
|
|
135
135
|
reste visible sous 520px, où le titre s'efface (.bar>b{display:none}). */
|
|
136
136
|
.barx{border-color:transparent;margin-right:-4px;flex:none}
|
|
137
|
-
.scroll{flex:1;overflow:auto;position:relative}
|
|
137
|
+
.scroll{flex:1;overflow:auto;position:relative;touch-action:pan-x pan-y} /* ⚠️ pan-x pan-y, PAS auto : le navigateur garde le défilement et NOUS rendons le pincement. Sans cette ligne le geste à deux doigts est happé par le zoom de page avant qu'aucun événement de pointeur ne nous parvienne. Le zoom navigateur reste disponible SUR LE RESTE de l'interface — le retirer partout serait une régression d'accessibilité pour qui grossit le chrome, pas le document. */
|
|
138
138
|
#pages{display:flex;flex-direction:column;align-items:center;gap:16px;width:max-content;min-width:100%;margin:0 auto;padding:22px 14px}
|
|
139
139
|
.page{position:relative;background:#fff;box-shadow:0 6px 22px #0006;border-radius:3px}
|
|
140
140
|
.page canvas{display:block;border-radius:3px}
|
|
@@ -216,7 +216,7 @@ ${LEGAL_CSS}
|
|
|
216
216
|
.barmenu button{display:block;width:100%;text-align:left;border:0;background:transparent;font:inherit;font-size:13.5px;color:#1c1c1c;padding:9px 12px;border-radius:8px;cursor:pointer}
|
|
217
217
|
.barmenu button:hover{background:#f2efe9}
|
|
218
218
|
@media (max-width:860px){
|
|
219
|
-
.bar .zoom,#fs,#presentBtn,#shareBtn,#dlBtn{display:none}
|
|
219
|
+
.bar .zoom,#rotL,#rotR,#vignBtn,#fs,#presentBtn,#shareBtn,#dlBtn{display:none}
|
|
220
220
|
.barMore{display:inline-flex}
|
|
221
221
|
}
|
|
222
222
|
@media (max-width:520px){ .bar>b{display:none} }
|
|
@@ -224,6 +224,22 @@ ${LEGAL_CSS}
|
|
|
224
224
|
→ .lrow/.lmain sans flex, la fenêtre scrollait à la place du conteneur et le rendu paresseux mourait. */
|
|
225
225
|
.lrow{flex:1;display:flex;min-height:0;position:relative}
|
|
226
226
|
.lmain{flex:1;min-width:0;display:flex;flex-direction:column;position:relative}
|
|
227
|
+
/* PANNEAU DE VIGNETTES. Largeur POSÉE D'UN COUP, sans transition : la reconstruction du
|
|
228
|
+
document mesure scrollEl.clientWidth juste après la bascule, et une largeur en cours
|
|
229
|
+
d'animation lui ferait calculer des pages à une taille qui n'existe qu'un dixième de
|
|
230
|
+
seconde. Le confort d'un glissement coûterait une mesure fausse. */
|
|
231
|
+
.vign{flex:0 0 auto;width:0;overflow:hidden;background:#0000004d;border-right:1px solid #ffffff14}
|
|
232
|
+
body.vign-on .vign{width:168px}
|
|
233
|
+
.vign-in{width:168px;height:100%;overflow-y:auto;overflow-x:hidden;padding:12px 0 24px;display:flex;flex-direction:column;align-items:center;gap:10px;scrollbar-width:thin}
|
|
234
|
+
.vg{position:relative;width:132px;flex:0 0 auto;padding:0;border:2px solid transparent;border-radius:4px;background:#ffffff10;cursor:pointer;overflow:hidden}
|
|
235
|
+
.vg canvas{display:block;width:100%;height:100%}
|
|
236
|
+
.vg:hover{border-color:#ffffff4d}
|
|
237
|
+
.vg.on{border-color:#fff;box-shadow:0 0 0 1px #0008}
|
|
238
|
+
.vg-n{position:absolute;right:3px;bottom:3px;padding:0 5px;border-radius:3px;background:#000a;color:#fff;font-size:11px;line-height:16px;pointer-events:none}
|
|
239
|
+
.vg:focus-visible{outline:2px solid #fff;outline-offset:1px}
|
|
240
|
+
/* Sous le point de rupture de la barre : TIROIR qui recouvre, pas colonne qui pousse — une
|
|
241
|
+
colonne de 168 px sur un écran de 380 px ne laisserait pas de document à lire. */
|
|
242
|
+
@media(max-width:860px){ body.vign-on .vign{position:absolute;left:0;top:0;bottom:0;z-index:7;box-shadow:0 0 24px #000a} }
|
|
227
243
|
${preview ? LIVE_CSS : ""}
|
|
228
244
|
${preview ? MAP_CSS : ""}
|
|
229
245
|
${botOn ? BOT_CSS : ""}
|
|
@@ -232,6 +248,9 @@ ${LEGAL_CSS}
|
|
|
232
248
|
<div class=bar>${embed ? '<button class="ic barx" id=embedCloseBtn title=Fermer aria-label=Fermer><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>' : ""}<b>${title}</b>${share.is_test ? '<span class=testchip>\ud83c\udfad Répétition — session de test</span>' : ""}<span class=sp></span><span class=pg id=pg></span>
|
|
233
249
|
${preview ? LIVE_BAR : ""}
|
|
234
250
|
<div class=zoom><button id=zout title="Dézoomer">−</button><span id=zlbl>100%</span><button id=zin title="Zoomer">+</button></div>
|
|
251
|
+
<button class=ic id=rotL title="Pivoter à gauche" aria-label="Pivoter le document à gauche"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8h11a5 5 0 0 1 5 5v1M3 8l4-4M3 8l4 4"/></svg></button>
|
|
252
|
+
<button class=ic id=rotR title="Pivoter à droite" aria-label="Pivoter le document à droite"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8H10a5 5 0 0 0-5 5v1M21 8l-4-4M21 8l-4 4"/></svg></button>
|
|
253
|
+
<button class=ic id=vignBtn title="Vignettes" aria-label="Afficher les vignettes" aria-pressed=false><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="6" height="7" rx="1"/><rect x="3" y="13" width="6" height="7" rx="1"/><path d="M13 6h8M13 12h8M13 18h8"/></svg></button>
|
|
235
254
|
<button class=ic id=fs title="Plein écran"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg></button>
|
|
236
255
|
${preview ? '<button class=dl id=presentBtn title="Présenter en direct (lien public, page synchronisée)">Présenter</button>' : ''}
|
|
237
256
|
<button class="dl primary" id=shareBtn>Partager</button>
|
|
@@ -239,6 +258,9 @@ ${LEGAL_CSS}
|
|
|
239
258
|
<div class=barmenu id=barMenu>
|
|
240
259
|
<button data-t=zin>Zoom avant</button>
|
|
241
260
|
<button data-t=zout>Zoom arrière</button>
|
|
261
|
+
<button data-t=rotL>Pivoter à gauche</button>
|
|
262
|
+
<button data-t=rotR>Pivoter à droite</button>
|
|
263
|
+
<button data-t=vignBtn>Vignettes</button>
|
|
242
264
|
<button data-t=fs>Plein écran</button>
|
|
243
265
|
${preview ? '<button data-t=presentBtn>Présenter en direct</button>' : ''}
|
|
244
266
|
<button data-t=shareBtn>Partager</button>
|
|
@@ -270,6 +292,7 @@ ${LEGAL_CSS}
|
|
|
270
292
|
</div>
|
|
271
293
|
</div>
|
|
272
294
|
<div class=lrow>
|
|
295
|
+
<div class=vign id=vign aria-hidden=true><div class=vign-in id=vignIn role=list aria-label="Vignettes des pages"></div></div>
|
|
273
296
|
<div class=lmain>
|
|
274
297
|
<div class=scroll id=scroll tabindex=0 role=region aria-label="Document">
|
|
275
298
|
<div id=pages></div>
|
|
@@ -296,7 +319,7 @@ ${LEGAL_CSS}
|
|
|
296
319
|
// renvoyé par le stockage). Une image = une page, sans pdf.js.
|
|
297
320
|
var IS_IMG=Player.viewer.isImageDocument(CFG.fileName,CFG.fileUrl);
|
|
298
321
|
var imgSrc='';
|
|
299
|
-
var scrollEl=document.getElementById('scroll'), pagesEl=document.getElementById('pages');
|
|
322
|
+
var scrollEl=document.getElementById('scroll'), pagesEl=document.getElementById('pages'), vignIn=document.getElementById('vignIn');
|
|
300
323
|
// Suivi de lecture (temps réel à l'écran par page, page la plus loin atteinte, session) :
|
|
301
324
|
// player/src/tracking.ts — typé et testé, y compris la séparation prospect / aperçu interne.
|
|
302
325
|
var T=Player.tracking.createTracker({ slug:CFG.slug||'', internal:CFG.internal||null, scrollElement:scrollEl });
|
|
@@ -309,8 +332,9 @@ ${LEGAL_CSS}
|
|
|
309
332
|
vsplitTint:vsplitTint,
|
|
310
333
|
get cur(){return cur;},get numPages(){return numPages;},get pdfDoc(){return pdfDoc;},
|
|
311
334
|
get onePage(){return onePage;},
|
|
335
|
+
get reportEnAttente(){return tRestaurer!==null;},
|
|
312
336
|
get soloOffered(){return soloOffered;},set soloOffered(v){soloOffered=!!v;}};
|
|
313
|
-
var pdfDoc=null, zoom=1, firstAspect=1.35, rendered={}, io=null, ioCur=null;
|
|
337
|
+
var pdfDoc=null, zoom=1, rot=0, firstAspect=1.35, rendered={}, io=null, ioCur=null;
|
|
314
338
|
// ATTENTION : LE LECTEUR GARDAIT TOUTES LES PAGES RENDUES (P1 audit externe).
|
|
315
339
|
//
|
|
316
340
|
// rendered[n] n etait jamais evince, les canvas restaient dans le DOM, et un build() de refit
|
|
@@ -332,7 +356,7 @@ ${LEGAL_CSS}
|
|
|
332
356
|
var DPR_MAX=2; // au-dela, le gain visuel est nul et le cout quadratique
|
|
333
357
|
var PIXELS_MAX=4e6; // plafond par canvas : ~4 M pixels = ~16 Mo de tampon
|
|
334
358
|
var onePage=false, soloOffered=false; // mode « une seule page » (présentation guidée) + offre de découverte solo
|
|
335
|
-
function setCur(p){ if(!p||p===cur)return; cur=p; try{ evincerLoin(p); }catch(e){} T.setPage(p); try{ if(window.__soloEnd)window.__soloEnd(p); }catch(e){} var pg=document.getElementById('pg'); if(pg&&numPages)pg.textContent='Page '+p+' / '+numPages; if(PRES) pushPage(); }
|
|
359
|
+
function setCur(p){ if(!p||p===cur)return; cur=p; try{ evincerLoin(p); }catch(e){} T.setPage(p); try{ vignMarquer(p); }catch(e){} try{ if(window.__soloEnd)window.__soloEnd(p); }catch(e){} var pg=document.getElementById('pg'); if(pg&&numPages)pg.textContent='Page '+p+' / '+numPages; if(PRES) pushPage(); }
|
|
336
360
|
// Bouton « Partager » : re-partage tracé (forward) → crée un lien ENFANT et propose de l'envoyer par email.
|
|
337
361
|
function wireShare(){
|
|
338
362
|
var pop=document.getElementById('pop'), btn=document.getElementById('shareBtn'), err=document.getElementById('shErr');
|
|
@@ -432,12 +456,16 @@ ${LEGAL_CSS}
|
|
|
432
456
|
// ⚠️ L'ANTI-REBOND DE 250 ms A DISPARU, ET C'EST VOULU. Il repoussait l'échéance à chaque appel —
|
|
433
457
|
// un défilement continu pouvait donc ne JAMAIS écrire. La file regroupe par genre sans jamais
|
|
434
458
|
// repousser : la dernière page demandée part au prochain tour, et elle part.
|
|
459
|
+
// ⚠️ LA ROTATION VOYAGE AVEC LA PAGE, PAR LE MEME APPEL. Elle aurait pu avoir son action a elle ;
|
|
460
|
+
// elle aurait alors eu son propre rang d ecriture, et deux ecritures concurrentes auraient pu
|
|
461
|
+
// s inverser — l audience recevant une rotation posterieure a la page qui la precede. Un seul
|
|
462
|
+
// message porte les deux, donc un seul ordre, et le controle de rang existant les couvre tous deux.
|
|
435
463
|
function pushPage(){ if(!PRES)return;
|
|
436
464
|
var f=fileEcritures(); var page=cur||1;
|
|
437
465
|
if(!f) return envoyerPage(page);
|
|
438
466
|
return f.poser('page',function(){ return envoyerPage(cur||page); }); }
|
|
439
467
|
function envoyerPage(page){ if(!PRES)return Promise.resolve();
|
|
440
|
-
return Player.live.fetchBorne('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-page',slug:PRES.slug,control:PRES.control,page:page,seq:prochainRang()})})
|
|
468
|
+
return Player.live.fetchBorne('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'present-page',slug:PRES.slug,control:PRES.control,page:page,rotation:rot,seq:prochainRang()})})
|
|
441
469
|
.then(function(r){ if(r&&r.ok)diffuserEtat(); })
|
|
442
470
|
.catch(function(){}); }
|
|
443
471
|
// ⚠️ TROIS GESTES, ET L'ORDRE ÉTAIT LE PIRE DES SIX. On signalait la fin, puis on COUPAIT LE
|
|
@@ -543,6 +571,22 @@ ${LEGAL_CSS}
|
|
|
543
571
|
if(!d||!d.ok||!d.control){ if(d&&d.status===403){ Player.bridge.sendToHost({type:'present-denied'}); } return; }
|
|
544
572
|
PRES={slug:slug,control:d.control}; saveCtl(slug,d.control); _seq=0;
|
|
545
573
|
showBar(slug); liveConnect(slug,d.control); startHb();
|
|
574
|
+
// ⚠️ A LA REPRISE, LA PRESENTATION L EMPORTE SUR L ETAT LOCAL — pour l orientation comme
|
|
575
|
+
// pour la page, alors qu on ne rendait que la page. Un rechargement remet la vue a son
|
|
576
|
+
// etat d origine (rot vaut 0), c est voulu ; mais l audience, elle, continuait d afficher
|
|
577
|
+
// la view_rotation de la base. Le presentateur voyait donc son document droit et son
|
|
578
|
+
// audience le voyait couche, sans que rien ne le dise a personne — et il n avait aucune
|
|
579
|
+
// raison de toucher le bouton, puisque de son cote tout allait bien.
|
|
580
|
+
//
|
|
581
|
+
// ⚠️ ON ADOPTE, ON NE POUSSE PAS. La base porte deja cette valeur : la renvoyer serait une
|
|
582
|
+
// ecriture pour rien, et pushPage() emporte AUSSI la page — or cur vaut 1 tant que le
|
|
583
|
+
// document se charge, donc pousser ici ramenerait toute l audience a la page 1. C est la
|
|
584
|
+
// raison pour laquelle la reprise ne pousse rien, et elle vaut encore.
|
|
585
|
+
//
|
|
586
|
+
// Au DEMARRAGE c est l inverse, et c est coherent : startPresent pousse la rotation
|
|
587
|
+
// courante du presentateur, parce qu il n y a encore rien a reprendre.
|
|
588
|
+
var rotPres=Player.viewer.rotationEffective(0,d.rotation);
|
|
589
|
+
if(rotPres!==rot){ rot=rotPres; if(pdfDoc||IS_IMG){ build(); if(vignOuvert) vignConstruire(); } }
|
|
546
590
|
var target=Math.max(1,d.page||1);
|
|
547
591
|
var tries=0; (function jump(){ var el=(document.getElementById('pages')||document).querySelector('.page[data-p="'+target+'"]'); if(el){ el.scrollIntoView({block:'start'}); } else if(tries++<40){ setTimeout(jump,150); } })();
|
|
548
592
|
}).catch(function(){});
|
|
@@ -618,9 +662,167 @@ ${LEGAL_CSS}
|
|
|
618
662
|
T.start();
|
|
619
663
|
document.getElementById('zin').addEventListener('click',function(){ setZoom(zoom+0.2); });
|
|
620
664
|
document.getElementById('zout').addEventListener('click',function(){ setZoom(zoom-0.2); });
|
|
665
|
+
document.getElementById('rotL').addEventListener('click',function(){ tournerDe(-1); });
|
|
666
|
+
document.getElementById('rotR').addEventListener('click',function(){ tournerDe(1); });
|
|
667
|
+
document.getElementById('vignBtn').addEventListener('click',basculerVignettes);
|
|
668
|
+
if(vignIn){
|
|
669
|
+
var desarmer=function(){ vignSuivi=false; };
|
|
670
|
+
vignIn.addEventListener('wheel',desarmer,{passive:true});
|
|
671
|
+
vignIn.addEventListener('pointerdown',desarmer,{passive:true});
|
|
672
|
+
vignIn.addEventListener('keydown',function(e){ if(e.key==='ArrowDown'||e.key==='ArrowUp') vignSuivi=false; },{passive:true});
|
|
673
|
+
}
|
|
674
|
+
// Un document d une seule page, ou une image : le panneau n aurait rien a montrer.
|
|
675
|
+
if(IS_IMG){ var vb=document.getElementById('vignBtn'); if(vb) vb.style.display='none'; }
|
|
676
|
+
brancherZoomAuGeste();
|
|
621
677
|
render();
|
|
622
678
|
}
|
|
623
|
-
|
|
679
|
+
// ── ZOOM AU GESTE : apercu immediat, reconstruction differee ─────────────────────────────────
|
|
680
|
+
//
|
|
681
|
+
// ⚠️ POURQUOI LE PINCEMENT ZOOMAIT L ECRAN ENTIER : PERSONNE N ECOUTAIT. Sur trackpad, un pincement
|
|
682
|
+
// n est pas un evenement tactile — le navigateur l envoie comme un wheel portant ctrlKey. Aucun
|
|
683
|
+
// ecouteur ici ne le lisait, donc le navigateur appliquait son defaut, que la balise viewport
|
|
684
|
+
// autorise explicitement. Ce n etait pas un reglage manquant, c etait un silence.
|
|
685
|
+
//
|
|
686
|
+
// ⚠️ ET LE DEFAUT ETAIT PLUS LARGE QUE LE CONFORT : sous 860 px la barre replie le bloc de zoom.
|
|
687
|
+
// Sur mobile le pincement est donc le SEUL geste qu un lecteur tente — et il deformait toute
|
|
688
|
+
// l interface. Ce bloc ne rajoute pas une commodite, il repare le seul zoom jamais essaye.
|
|
689
|
+
//
|
|
690
|
+
// ⚠️ EN DEUX TEMPS, PARCE QU UN SEUL NE TIENT PAS. build() vide le conteneur, annule les rendus en
|
|
691
|
+
// vol et recree toutes les pages : juste pour un clic, ruineux pour un pincement qui produit des
|
|
692
|
+
// dizaines d evenements par seconde. Pendant le geste on ne pose qu une transformation CSS ; la
|
|
693
|
+
// reconstruction arrive UNE fois, quand les doigts s arretent.
|
|
694
|
+
//
|
|
695
|
+
// Note de forme : ce script vit dans un litteral de gabarit — pas d accent grave ici.
|
|
696
|
+
var geste=null, tGeste=null;
|
|
697
|
+
var DELAI_CONFIRMATION=180; // ms de silence avant de reconstruire
|
|
698
|
+
|
|
699
|
+
function majEtiquetteZoom(){ var l=document.getElementById('zlbl'); if(l)l.textContent=Math.round(zoom*100)+'%'; }
|
|
700
|
+
|
|
701
|
+
// L ancre est prise DANS LE CONTENEUR DES PAGES et une seule fois, au debut du geste : la mesurer a
|
|
702
|
+
// nouveau pendant l apercu la lirait a travers la transformation deja posee.
|
|
703
|
+
function ancreDuGeste(cx,cy){
|
|
704
|
+
var r=pagesEl.getBoundingClientRect();
|
|
705
|
+
return { x:cx-r.left, y:cy-r.top, largeur:r.width||1, cx:cx, cy:cy };
|
|
706
|
+
}
|
|
707
|
+
// ⚠️ ORIGINE EN HAUT AU CENTRE, ET UNE TRANSLATION QUI IMMOBILISE LE POINT VISE. #pages est centre
|
|
708
|
+
// (align-items:center, margin:0 auto) : grandir autour de son centre ferait FUIR le point que le
|
|
709
|
+
// lecteur regarde. La translation qui le fige vaut (ancre - origine) x (1 - facteur).
|
|
710
|
+
function poserApercu(f,ancre){
|
|
711
|
+
var tx=(ancre.x-ancre.largeur/2)*(1-f), ty=ancre.y*(1-f);
|
|
712
|
+
pagesEl.style.transformOrigin='50% 0';
|
|
713
|
+
pagesEl.style.transform='translate('+tx+'px,'+ty+'px) scale('+f+')';
|
|
714
|
+
pagesEl.style.willChange='transform';
|
|
715
|
+
}
|
|
716
|
+
function retirerApercu(){ pagesEl.style.transform=''; pagesEl.style.transformOrigin=''; pagesEl.style.willChange=''; }
|
|
717
|
+
// L apercu s arrete aux memes bornes que la reconstruction, sinon le document continue de grandir
|
|
718
|
+
// sous les doigts puis SAUTE en arriere au relachement.
|
|
719
|
+
function facteurBorne(g){ return Player.viewer.clampZoom(g.depart*g.facteur)/g.depart; }
|
|
720
|
+
|
|
721
|
+
// ⚠️ LA PART FIXE DU CONTENU N EST PAS UN DETAIL. #pages porte 22 px de marge en haut et en bas et
|
|
722
|
+
// 16 px entre chaque page : cent pages font plus de 1500 px qui ne grandissent PAS avec le zoom. Les
|
|
723
|
+
// compter comme proportionnels projette le lecteur trop bas, d autant plus qu il est loin dans le
|
|
724
|
+
// document.
|
|
725
|
+
function partFixeDuContenu(){ return { largeur:28, hauteur:44+16*Math.max(0,numPages-1) }; }
|
|
726
|
+
// ⚠️ ET LA TETE DE CE FIXE SE COMPORTE AUTREMENT : les 22 px de marge HAUTE arrivent avant la
|
|
727
|
+
// premiere page et ne bougent jamais, alors que les espaces entre pages sont proportionnels au
|
|
728
|
+
// nombre de pages au-dessus du point vise. Les confondre coutait 14,5 px de fuite, MESURES dans
|
|
729
|
+
// Chromium sur un document de 40 pages : le point vise glissait d autant plus qu on lisait loin.
|
|
730
|
+
function teteFixeDuContenu(){ return { largeur:14, hauteur:22 }; }
|
|
731
|
+
|
|
732
|
+
// Le point de passage unique : boutons, molette, trackpad et doigts finissent tous ici.
|
|
733
|
+
function confirmerZoom(z,cx,cy){
|
|
734
|
+
clearTimeout(tGeste); tGeste=null; geste=null;
|
|
735
|
+
retirerApercu();
|
|
736
|
+
var avant=zoom, apres=Player.viewer.clampZoom(z);
|
|
737
|
+
if(apres===avant){ majEtiquetteZoom(); return; }
|
|
738
|
+
var sr=scrollEl.getBoundingClientRect();
|
|
739
|
+
var cible=Player.viewer.ancrageApresZoom({
|
|
740
|
+
defilement:{x:scrollEl.scrollLeft,y:scrollEl.scrollTop},
|
|
741
|
+
point:{x:cx-sr.left,y:cy-sr.top},
|
|
742
|
+
cadre:{largeur:scrollEl.clientWidth,hauteur:scrollEl.clientHeight},
|
|
743
|
+
contenu:{largeur:scrollEl.scrollWidth,hauteur:scrollEl.scrollHeight},
|
|
744
|
+
fixe:partFixeDuContenu(), fixeDebut:teteFixeDuContenu(),
|
|
745
|
+
zoomAvant:avant, zoomApres:apres });
|
|
746
|
+
zoom=apres; majEtiquetteZoom();
|
|
747
|
+
// ⚠️ if(pdfDoc) SEUL LAISSAIT LES IMAGES SANS ZOOM. Le commentaire de renderImage annonce que
|
|
748
|
+
// « tout le chrome — loader, zoom, plein ecran… — fonctionne tel quel » : c etait faux pour le
|
|
749
|
+
// zoom, pdfDoc restant nul sur une image. Le defaut passait inapercu tant que le zoom tenait a
|
|
750
|
+
// deux boutons ; intercepter le pincement sans le corriger aurait AVALE le geste sur ces
|
|
751
|
+
// documents, donc fait pire qu avant.
|
|
752
|
+
if(!pdfDoc && !IS_IMG) return;
|
|
753
|
+
build();
|
|
754
|
+
scrollEl.scrollLeft=cible.x; scrollEl.scrollTop=cible.y;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Les boutons visent le centre du cadre. Ils heritent donc de l ancrage, et la derive qu ils avaient
|
|
758
|
+
// deja — les pages changent de largeur, le defilement reste en pixels — disparait.
|
|
759
|
+
function setZoom(z){
|
|
760
|
+
var sr=scrollEl.getBoundingClientRect();
|
|
761
|
+
confirmerZoom(z, sr.left+scrollEl.clientWidth/2, sr.top+scrollEl.clientHeight/2);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function brancherZoomAuGeste(){
|
|
765
|
+
// A. TRACKPAD ET CTRL+MOLETTE. NON PASSIF, et c est la ligne qui compte : wheel est passif par
|
|
766
|
+
// defaut sur les navigateurs recents, et un ecouteur passif voit son preventDefault IGNORE —
|
|
767
|
+
// l ecran zoomerait quand meme, en silence.
|
|
768
|
+
scrollEl.addEventListener('wheel',function(e){
|
|
769
|
+
if(!e.ctrlKey)return;
|
|
770
|
+
e.preventDefault();
|
|
771
|
+
if(!geste) geste={ancre:ancreDuGeste(e.clientX,e.clientY),depart:zoom,facteur:1};
|
|
772
|
+
// Exponentielle plutot que lineaire : le geste devient symetrique — pincer puis ecarter d autant
|
|
773
|
+
// revient exactement au zoom de depart, ce qu une addition ne donne pas.
|
|
774
|
+
geste.facteur*=Math.exp(-e.deltaY/180);
|
|
775
|
+
geste.ancre.cx=e.clientX; geste.ancre.cy=e.clientY;
|
|
776
|
+
poserApercu(facteurBorne(geste),geste.ancre);
|
|
777
|
+
// La molette n a pas de fin declaree : le silence en tient lieu.
|
|
778
|
+
clearTimeout(tGeste);
|
|
779
|
+
tGeste=setTimeout(function(){ if(geste) confirmerZoom(geste.depart*geste.facteur,geste.ancre.cx,geste.ancre.cy); },DELAI_CONFIRMATION);
|
|
780
|
+
},{passive:false});
|
|
781
|
+
|
|
782
|
+
// B. SAFARI. Ses evenements de geste ne sont pas standard mais sont les seuls disponibles la-bas ;
|
|
783
|
+
// sans le preventDefault du gesturestart, Safari zoome la page avant nous.
|
|
784
|
+
if('ongesturestart' in window){
|
|
785
|
+
scrollEl.addEventListener('gesturestart',function(e){ e.preventDefault(); geste={ancre:ancreDuGeste(e.clientX,e.clientY),depart:zoom,facteur:1}; },{passive:false});
|
|
786
|
+
scrollEl.addEventListener('gesturechange',function(e){ e.preventDefault(); if(!geste)return; geste.facteur=e.scale||1; poserApercu(facteurBorne(geste),geste.ancre); },{passive:false});
|
|
787
|
+
scrollEl.addEventListener('gestureend',function(e){ e.preventDefault(); if(!geste)return; confirmerZoom(geste.depart*geste.facteur,geste.ancre.cx,geste.ancre.cy); },{passive:false});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// C. TACTILE A DEUX DOIGTS. On ne prend la main qu a DEUX pointeurs : un seul doigt reste le
|
|
791
|
+
// defilement du navigateur, qui le fait mieux que nous et sans a-coups.
|
|
792
|
+
var doigts=Object.create(null), nDoigts=0, ecart0=0;
|
|
793
|
+
function ecartEtMilieu(){
|
|
794
|
+
var a=null,b=null;
|
|
795
|
+
for(var k in doigts){ if(!a)a=doigts[k]; else { b=doigts[k]; break; } }
|
|
796
|
+
if(!a||!b)return null;
|
|
797
|
+
var dx=a.x-b.x, dy=a.y-b.y;
|
|
798
|
+
return { d:Math.sqrt(dx*dx+dy*dy), cx:(a.x+b.x)/2, cy:(a.y+b.y)/2 };
|
|
799
|
+
}
|
|
800
|
+
scrollEl.addEventListener('pointerdown',function(e){
|
|
801
|
+
if(e.pointerType!=='touch')return;
|
|
802
|
+
if(!doigts[e.pointerId]) nDoigts++;
|
|
803
|
+
doigts[e.pointerId]={x:e.clientX,y:e.clientY};
|
|
804
|
+
if(nDoigts===2){ var m=ecartEtMilieu(); if(m&&m.d>0){ ecart0=m.d; geste={ancre:ancreDuGeste(m.cx,m.cy),depart:zoom,facteur:1}; } }
|
|
805
|
+
},{passive:true});
|
|
806
|
+
scrollEl.addEventListener('pointermove',function(e){
|
|
807
|
+
if(e.pointerType!=='touch'||!doigts[e.pointerId])return;
|
|
808
|
+
doigts[e.pointerId]={x:e.clientX,y:e.clientY};
|
|
809
|
+
if(nDoigts!==2||!geste||!ecart0)return;
|
|
810
|
+
var m=ecartEtMilieu(); if(!m)return;
|
|
811
|
+
e.preventDefault();
|
|
812
|
+
geste.facteur=m.d/ecart0; geste.ancre.cx=m.cx; geste.ancre.cy=m.cy;
|
|
813
|
+
poserApercu(facteurBorne(geste),geste.ancre);
|
|
814
|
+
},{passive:false});
|
|
815
|
+
// ⚠️ ON CONFIRME DES QU IL NE RESTE QU UN DOIGT, pas quand ils sont tous partis : un lecteur qui
|
|
816
|
+
// leve un doigt et garde l autre pose continue a lire, et l apercu resterait fige sur une
|
|
817
|
+
// transformation que rien ne viendrait plus confirmer.
|
|
818
|
+
function doigtLeve(e){
|
|
819
|
+
if(e.pointerType!=='touch'||!doigts[e.pointerId])return;
|
|
820
|
+
delete doigts[e.pointerId]; nDoigts=Math.max(0,nDoigts-1);
|
|
821
|
+
if(nDoigts<2&&geste){ var g=geste; ecart0=0; confirmerZoom(g.depart*g.facteur,g.ancre.cx,g.ancre.cy); }
|
|
822
|
+
}
|
|
823
|
+
scrollEl.addEventListener('pointerup',doigtLeve,{passive:true});
|
|
824
|
+
scrollEl.addEventListener('pointercancel',doigtLeve,{passive:true});
|
|
825
|
+
}
|
|
624
826
|
// ⚠️ UN REFUS ARRÊTE LE LECTEUR, et les deux replis plus doux ont été MESURÉS avant d'être
|
|
625
827
|
// écartés — pas jugés sur leur mine :
|
|
626
828
|
//
|
|
@@ -682,7 +884,13 @@ ${LEGAL_CSS}
|
|
|
682
884
|
function onePageReserve(){ var band=capReserve(); return document.body.classList.contains('botplayer')?(document.body.classList.contains('vsplit')?Math.round(window.innerHeight*0.38)+70:(isLand()?12:260)):(band?band:(document.body.classList.contains('deskaudio')?4:0)); }
|
|
683
885
|
// La géométrie (bornes, respiration, seuil d'abandon) vit dans player/src/viewer.ts ; ici on ne
|
|
684
886
|
// fournit que les MESURES, seules choses que le DOM connaisse.
|
|
685
|
-
|
|
887
|
+
// ⚠️ LA PROPORTION EFFECTIVE GOUVERNE LE SUIVI DE LECTURE, PAS SEULEMENT L AFFICHAGE. Elle fixe la
|
|
888
|
+
// hauteur des gabarits poses AVANT rendu ; ces gabarits fixent la longueur du document ; cette
|
|
889
|
+
// longueur decide de la page que l observateur d intersection appelle « courante » ; et c est cette
|
|
890
|
+
// page-la que le suivi enregistre. Une proportion non tournee a 90 degres fausse donc les
|
|
891
|
+
// statistiques d un partage, en silence et sans rien casser a l ecran.
|
|
892
|
+
function aspectEffectif(){ return Player.viewer.aspectApresRotation(firstAspect,rot); }
|
|
893
|
+
function targetWidth(){ return Player.viewer.fitWidth({containerWidth:baseWidth(),containerHeight:scrollEl.clientHeight,zoom:zoom,onePage:onePage,aspect:firstAspect,rotation:rot,overlap:botOverlap(),reserve:onePageReserve()}); }
|
|
686
894
|
// IMAGE — la visionneuse ne savait lire QUE du PDF (pdf.js). Une axonométrie en .jpg
|
|
687
895
|
// partait donc dans getDocument() et échouait sur « structure invalide ».
|
|
688
896
|
// Elle devient une page unique : tout le chrome — loader, zoom, plein écran, Partager,
|
|
@@ -706,12 +914,18 @@ ${LEGAL_CSS}
|
|
|
706
914
|
task.onProgress=function(p){ if(p&&p.total){ var pct=Math.max(8,Math.min(99,Math.round(p.loaded/p.total*100))); var bar=document.getElementById('lbar'); if(bar)bar.classList.remove('idle'); var f=document.getElementById('lbarFill'); if(f)f.style.width=pct+'%'; var l=document.getElementById('lpct'); if(l)l.textContent=pct+' %'; } };
|
|
707
915
|
task.promise.then(function(pdf){
|
|
708
916
|
pdfDoc=pdf; numPages=pdf.numPages; window.__n=pdf.numPages; T.setPageCount(pdf.numPages);
|
|
917
|
+
// Un document d une seule page : le panneau de vignettes n aurait rien a montrer.
|
|
918
|
+
if(numPages<2){ var vb1=document.getElementById('vignBtn'); if(vb1) vb1.style.display='none'; }
|
|
709
919
|
document.getElementById('pg').textContent='Page 1 / '+pdf.numPages;
|
|
710
920
|
pdf.getPage(1).then(function(p){ var vp=p.getViewport({scale:1}); firstAspect=vp.height/vp.width; build(); try{if(window.PlayerBot)window.PlayerBot.init(VIEWER);}catch(e){} })
|
|
711
921
|
.catch(function(){ build(); try{if(window.PlayerBot)window.PlayerBot.init(VIEWER);}catch(e){} });
|
|
712
922
|
}).catch(function(){ loadError("Impossible d'afficher ce document."); });
|
|
713
923
|
}
|
|
714
924
|
function build(){
|
|
925
|
+
// Un apercu de zoom n a plus de sens des qu on reconstruit : le laisser poserait une
|
|
926
|
+
// transformation orpheline sur des pages neuves. Cas reel : un redimensionnement de
|
|
927
|
+
// fenetre pendant un pincement.
|
|
928
|
+
retirerApercu();
|
|
715
929
|
// Une nouvelle construction invalide tout ce qui etait en vol : sans cela, les rendus lances
|
|
716
930
|
// par la precedente peignent dans des elements DETACHES (pagesEl a ete vide) et le travail est
|
|
717
931
|
// fait deux fois. C est le meme mecanisme de generation que la page audience.
|
|
@@ -727,7 +941,7 @@ ${LEGAL_CSS}
|
|
|
727
941
|
ioCur=new IntersectionObserver(function(es){es.forEach(function(e){ if(e.isIntersecting){ setCur(+e.target.dataset.p); } });},{root:scrollEl,rootMargin:Player.viewer.CURRENT_PAGE_MARGIN,threshold:0});
|
|
728
942
|
pagesEl.innerHTML='';
|
|
729
943
|
var w=Math.round(targetWidth());
|
|
730
|
-
for(var i=1;i<=numPages;i++){ var d=document.createElement('div'); d.className='page ph'; d.dataset.p=i; d.style.width=w+'px'; d.style.height=Math.round(w*
|
|
944
|
+
for(var i=1;i<=numPages;i++){ var d=document.createElement('div'); d.className='page ph'; d.dataset.p=i; d.style.width=w+'px'; d.style.height=Math.round(w*aspectEffectif())+'px'; d.textContent='Page '+i; pagesEl.appendChild(d); io.observe(d); ioCur.observe(d); }
|
|
731
945
|
var _band=capReserve(); var _pb=document.body.classList.contains('botplayer')?(document.body.classList.contains('vsplit')?Math.round(window.innerHeight*0.38)+50:(isLand()?0:240)):(botOverlap()+(_band?_band+12:(document.body.classList.contains('deskaudio')?16:0))); pagesEl.style.paddingBottom = onePage ? (_pb+'px') : ''; // centre la page dans l'espace VISIBLE (au-dessus de la sheet mobile / du bandeau desktop / sous le header en audio seul)
|
|
732
946
|
if(onePage) showPage(cur||1);
|
|
733
947
|
}
|
|
@@ -737,11 +951,184 @@ ${LEGAL_CSS}
|
|
|
737
951
|
var els=pagesEl.querySelectorAll('.page'); for(var i=0;i<els.length;i++){ els[i].classList.toggle('cur',(+els[i].dataset.p)===p); } var el=pagesEl.querySelector('.page[data-p="'+p+'"]'); if(el){ renderPage(p,el); var nx=pagesEl.querySelector('.page[data-p="'+(p+1)+'"]'); if(nx)renderPage(p+1,nx); } setCur(p); syncArrows();
|
|
738
952
|
var pf=document.getElementById('pglineF'); if(pf&&numPages)pf.style.width=Player.viewer.progressPercent(p,numPages)+'%'; } // ligne de progression (mode présentation)
|
|
739
953
|
function enterOnePage(){ if(onePage)return; onePage=true; document.body.classList.add('onepage'); document.body.classList.add('botlock'); if(pdfDoc){ var c=cur||1; build(); showPage(c); } syncArrows(); }
|
|
740
|
-
function exitOnePage(){ if(!onePage)return; onePage=false; soloOffered=false; document.body.classList.remove('onepage'); document.body.classList.remove('botlock'); var c=cur||1; if(pdfDoc){ build();
|
|
954
|
+
function exitOnePage(){ if(!onePage)return; onePage=false; soloOffered=false; document.body.classList.remove('onepage'); document.body.classList.remove('botlock'); var c=cur||1; if(pdfDoc){ build(); restaurerPage(c); } syncArrows(); }
|
|
741
955
|
// Re-fit du document à la largeur réelle (resize fenêtre, ouverture/fermeture du chat ancré, plein écran)
|
|
742
956
|
// en conservant la page courante. Débouncé pour rester fluide.
|
|
743
|
-
|
|
744
|
-
|
|
957
|
+
// ⚠️ UN REPORT DE PAGE N EST VALABLE QUE TANT QUE PERSONNE N A NAVIGUE ENTRE-TEMPS.
|
|
958
|
+
//
|
|
959
|
+
// Une reconstruction ne peut pas restaurer la page tout de suite : les gabarits viennent d etre
|
|
960
|
+
// poses et la geometrie n est pas encore stable. On differe donc de 30 ms. Mais ce report SURVIT a
|
|
961
|
+
// une navigation faite dans l intervalle, et il la DEFAIT — le lecteur qui ouvre le panneau de
|
|
962
|
+
// vignettes puis clique aussitot une vignette est ramene la ou il etait.
|
|
963
|
+
//
|
|
964
|
+
// Trouve par la forge, pas ici : en local les vignettes ne se rendaient pas, le banc attendait donc
|
|
965
|
+
// bien au-dela des 30 ms et la course ne se produisait jamais. Reproduit ensuite en ouvrant le
|
|
966
|
+
// panneau et en cliquant dans le meme instant.
|
|
967
|
+
var tRestaurer=null;
|
|
968
|
+
function restaurerPage(c){
|
|
969
|
+
clearTimeout(tRestaurer);
|
|
970
|
+
tRestaurer=setTimeout(function(){ tRestaurer=null; scrollToPage(c); },30);
|
|
971
|
+
}
|
|
972
|
+
function scrollToPage(p){
|
|
973
|
+
// Toute navigation explicite PERIME le report en attente : c est ce qui ferme la course.
|
|
974
|
+
clearTimeout(tRestaurer); tRestaurer=null;
|
|
975
|
+
var el=pagesEl.querySelector('.page[data-p="'+p+'"]'); if(el) el.scrollIntoView({block:'start'});
|
|
976
|
+
}
|
|
977
|
+
// ⚠️ ET « NAVIGUER » NE SE RESUME PAS A CLIQUER. Le report ne se perimait qu au passage par
|
|
978
|
+
// scrollToPage — donc au clic sur une vignette, une fleche, le sommaire. Le lecteur qui ouvre le
|
|
979
|
+
// panneau puis fait DEFILER LE DOCUMENT LUI-MEME dans les 30 ms ne passe par rien de tout cela :
|
|
980
|
+
// son geste etait defait et il revenait a sa page de depart. Meme course que ci-dessus, autre
|
|
981
|
+
// porte. La forge l a trouvee en rendant les vignettes plus vite qu ici — le banc du clic, lui,
|
|
982
|
+
// passait, parce qu un clic de vignette PASSE par scrollToPage.
|
|
983
|
+
//
|
|
984
|
+
// ⚠️ ON N ECOUTE PAS 'scroll'. La reconstruction en emet elle-meme : annuler le report sur sa
|
|
985
|
+
// propre consequence le rendrait inutile, et le lecteur perdrait sa page a chaque refit — la
|
|
986
|
+
// regression exacte que ce report existe pour empecher. On ecoute donc l ENTREE DU LECTEUR,
|
|
987
|
+
// qu aucune mise en page ne fabrique.
|
|
988
|
+
//
|
|
989
|
+
// L ordre joue en notre faveur : ces evenements arrivent AVANT le clic qui arme un report (le
|
|
990
|
+
// bouton du panneau et les vignettes sont HORS de scrollEl), jamais apres. Annuler ici ne peut
|
|
991
|
+
// donc pas desarmer un report qui n existe pas encore.
|
|
992
|
+
//
|
|
993
|
+
// Ce que ca NE couvre PAS, et c est assume : une touche pressee alors que le document n a pas le
|
|
994
|
+
// focus. Elle ne fait pas defiler le document non plus, donc il n y a rien a perimer.
|
|
995
|
+
['wheel','touchmove','pointerdown','keydown'].forEach(function(nom){
|
|
996
|
+
scrollEl.addEventListener(nom,function(){ clearTimeout(tRestaurer); tRestaurer=null; },{passive:true});
|
|
997
|
+
});
|
|
998
|
+
// ⚠️ PAS window.__refit ICI : il commence par « if(!pdfDoc) return », donc il ne ferait RIEN sur un
|
|
999
|
+
// document image — le meme garde qui laissait les images sans zoom avant le lot 1.
|
|
1000
|
+
// ── PANNEAU DE VIGNETTES ─────────────────────────────────────────────────────────────────────
|
|
1001
|
+
//
|
|
1002
|
+
// ⚠️ LE MOTEUR EXISTAIT DEJA, DANS LE CHAT — et une seule de ses parties ne se transpose pas.
|
|
1003
|
+
// gabarit-live.js fabrique des vignettes de pieces jointes avec un cache borne, une file de
|
|
1004
|
+
// concurrence, un chargement paresseux et des URL objet plutot que des data-URL. Tout cela sert
|
|
1005
|
+
// ici. Ce qui NE se transpose pas est le getDocument suivi d un destroy : ce panneau montre les
|
|
1006
|
+
// pages du document DEJA OUVERT. Le reprendre tel quel telechargerait le fichier une seconde fois
|
|
1007
|
+
// et ferait tourner deux workers sur le meme PDF.
|
|
1008
|
+
//
|
|
1009
|
+
// ⚠️ ET LE CACHE EST BORNE PARCE QU UN DOCUMENT NE L EST PAS. Cinq cents pages font cinq cents
|
|
1010
|
+
// boutons — c est peu — mais cinq cents canvas seraient le defaut que la fenetre glissante ferme
|
|
1011
|
+
// deja pour les pages. On garde les derniers, on rend les autres a l etat de gabarit, et
|
|
1012
|
+
// l observateur les refabrique s ils reviennent a l ecran.
|
|
1013
|
+
var VIGN_LARGEUR=132, VIGN_MAX=48, VIGN_PARALLELE=2;
|
|
1014
|
+
var vignOuvert=false, vignIO=null, vignFile=[], vignActifs=0, vignOrdre=[], vignFaites=Object.create(null), vignEchecs=Object.create(null);
|
|
1015
|
+
// ⚠️ LE SUIVI AUTOMATIQUE SE DESARME DES QUE LE LECTEUR TOUCHE LE PANNEAU. Sans ca, parcourir les
|
|
1016
|
+
// vignettes ramene sans cesse le panneau sur la page en cours et le lecteur se bat contre son
|
|
1017
|
+
// outil. On ecoute les GESTES, pas l evenement scroll : un defilement que NOUS provoquons ne doit
|
|
1018
|
+
// pas compter comme une intention du lecteur.
|
|
1019
|
+
var vignSuivi=true;
|
|
1020
|
+
|
|
1021
|
+
function vignVider(bt){
|
|
1022
|
+
bt.innerHTML='';
|
|
1023
|
+
bt.style.height=Math.round(VIGN_LARGEUR*aspectEffectif())+'px';
|
|
1024
|
+
var s=document.createElement('span'); s.className='vg-n'; s.textContent=bt.dataset.p; bt.appendChild(s);
|
|
1025
|
+
}
|
|
1026
|
+
function vignEvincer(){
|
|
1027
|
+
while(vignOrdre.length>VIGN_MAX){
|
|
1028
|
+
var vieux=vignOrdre.shift();
|
|
1029
|
+
delete vignFaites[vieux];
|
|
1030
|
+
var bt=vignIn.querySelector('.vg[data-p="'+vieux+'"]');
|
|
1031
|
+
if(bt){ vignVider(bt); if(vignIO) vignIO.observe(bt); }
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function vignSuivante(){
|
|
1035
|
+
if(vignActifs>=VIGN_PARALLELE||!vignFile.length)return;
|
|
1036
|
+
var t=vignFile.shift(); vignActifs++;
|
|
1037
|
+
Promise.resolve().then(t).catch(function(){}).then(function(){ vignActifs--; vignSuivante(); });
|
|
1038
|
+
}
|
|
1039
|
+
function vignRendre(bt){
|
|
1040
|
+
var n=+bt.dataset.p;
|
|
1041
|
+
if(!pdfDoc||vignFaites[n])return;
|
|
1042
|
+
vignFaites[n]=1; vignOrdre.push(n); vignEvincer();
|
|
1043
|
+
// ⚠️ UN ECHEC DOIT POUVOIR ETRE RETENTE, ET C EST LE DEFAUT QUE LE CHEMIN PRINCIPAL DOCUMENTE
|
|
1044
|
+
// DEJA : « on ne marque pas RENDUE avant de l avoir fait ». Ici la marque est posee AVANT, pour
|
|
1045
|
+
// dedoublonner les demandes concurrentes ; il faut donc la RETIRER si le rendu echoue, sinon la
|
|
1046
|
+
// vignette reste vide pour toujours et plus rien ne la redemande. Trouve en mesurant : le
|
|
1047
|
+
// navigateur d essai rejetait chaque rendu, et la file avalait le rejet sans un mot.
|
|
1048
|
+
//
|
|
1049
|
+
// ⚠️ ET LA REPRISE EST BORNEE, PARCE QU OBSERVER UN ELEMENT DEJA VISIBLE RAPPELLE LE CALLBACK
|
|
1050
|
+
// TOUT DE SUITE. Re-observer sans compter fabriquerait une BOUCLE INFINIE des que les rendus
|
|
1051
|
+
// echouent en serie — un moteur trop ancien, une memoire pleine — et brulerait le processeur du
|
|
1052
|
+
// lecteur en silence. Mesure sur un navigateur ou chaque rendu echoue : sans borne, la file ne
|
|
1053
|
+
// se vidait jamais. Deux tentatives, puis on laisse la vignette vide plutot que de tourner.
|
|
1054
|
+
var oublier=function(){
|
|
1055
|
+
delete vignFaites[n];
|
|
1056
|
+
var j=vignOrdre.indexOf(n); if(j>=0) vignOrdre.splice(j,1);
|
|
1057
|
+
vignEchecs[n]=(vignEchecs[n]||0)+1;
|
|
1058
|
+
if(vignEchecs[n]<2 && vignIO && bt.isConnected) vignIO.observe(bt);
|
|
1059
|
+
};
|
|
1060
|
+
vignFile.push(function(){
|
|
1061
|
+
return pdfDoc.getPage(n).then(function(page){
|
|
1062
|
+
if(!vignFaites[n])return; // evincee pendant l attente
|
|
1063
|
+
var rotEff=Player.viewer.rotationEffective(page.rotate,rot);
|
|
1064
|
+
var v0=page.getViewport({scale:1,rotation:rotEff});
|
|
1065
|
+
var v=page.getViewport({scale:VIGN_LARGEUR/v0.width,rotation:rotEff});
|
|
1066
|
+
var c=document.createElement('canvas');
|
|
1067
|
+
c.width=Math.ceil(v.width); c.height=Math.ceil(v.height);
|
|
1068
|
+
return page.render({canvasContext:c.getContext('2d'),viewport:v}).promise.then(function(){
|
|
1069
|
+
if(!vignFaites[n])return;
|
|
1070
|
+
bt.style.height=''; bt.innerHTML=''; bt.appendChild(c);
|
|
1071
|
+
var s=document.createElement('span'); s.className='vg-n'; s.textContent=n; bt.appendChild(s);
|
|
1072
|
+
}, oublier);
|
|
1073
|
+
}, oublier);
|
|
1074
|
+
});
|
|
1075
|
+
vignSuivante();
|
|
1076
|
+
}
|
|
1077
|
+
function vignMarquer(p){
|
|
1078
|
+
if(!vignOuvert||!vignIn)return;
|
|
1079
|
+
var els=vignIn.querySelectorAll('.vg');
|
|
1080
|
+
for(var i=0;i<els.length;i++) els[i].classList.toggle('on',(+els[i].dataset.p)===p);
|
|
1081
|
+
if(!vignSuivi)return;
|
|
1082
|
+
var el=vignIn.querySelector('.vg[data-p="'+p+'"]');
|
|
1083
|
+
// On deplace le defilement DU PANNEAU, jamais scrollIntoView : celui-ci remonte la chaine des
|
|
1084
|
+
// ancetres scrollables et emporterait le document avec lui.
|
|
1085
|
+
if(el) vignIn.scrollTop=Math.max(0,el.offsetTop-(vignIn.clientHeight-el.offsetHeight)/2);
|
|
1086
|
+
}
|
|
1087
|
+
function vignConstruire(){
|
|
1088
|
+
if(!vignIn)return;
|
|
1089
|
+
if(vignIO){ vignIO.disconnect(); vignIO=null; }
|
|
1090
|
+
vignIn.innerHTML=''; vignFaites=Object.create(null); vignEchecs=Object.create(null); vignOrdre=[]; vignFile=[];
|
|
1091
|
+
if(!pdfDoc||numPages<2)return;
|
|
1092
|
+
vignIO=new IntersectionObserver(function(es){
|
|
1093
|
+
es.forEach(function(e){ if(e.isIntersecting){ vignIO.unobserve(e.target); vignRendre(e.target); } });
|
|
1094
|
+
},{root:vignIn,rootMargin:'220px 0px'});
|
|
1095
|
+
for(var i=1;i<=numPages;i++){
|
|
1096
|
+
var bt=document.createElement('button');
|
|
1097
|
+
bt.type='button'; bt.className='vg'; bt.dataset.p=i; bt.setAttribute('role','listitem');
|
|
1098
|
+
bt.setAttribute('aria-label','Aller à la page '+i);
|
|
1099
|
+
vignVider(bt);
|
|
1100
|
+
bt.addEventListener('click',function(){
|
|
1101
|
+
var n=+this.dataset.p; vignSuivi=true;
|
|
1102
|
+
if(onePage) showPage(n); else scrollToPage(n);
|
|
1103
|
+
vignMarquer(n);
|
|
1104
|
+
});
|
|
1105
|
+
vignIn.appendChild(bt); vignIO.observe(bt);
|
|
1106
|
+
}
|
|
1107
|
+
vignMarquer(cur||1);
|
|
1108
|
+
}
|
|
1109
|
+
function basculerVignettes(){
|
|
1110
|
+
vignOuvert=!vignOuvert;
|
|
1111
|
+
document.body.classList.toggle('vign-on',vignOuvert);
|
|
1112
|
+
var b=document.getElementById('vignBtn'); if(b) b.setAttribute('aria-pressed',vignOuvert?'true':'false');
|
|
1113
|
+
var v=document.getElementById('vign'); if(v) v.setAttribute('aria-hidden',vignOuvert?'false':'true');
|
|
1114
|
+
if(vignOuvert){ vignSuivi=true; vignConstruire(); }
|
|
1115
|
+
else if(vignIO){ vignIO.disconnect(); vignIO=null; vignIn.innerHTML=''; vignFaites=Object.create(null); vignEchecs=Object.create(null); vignOrdre=[]; vignFile=[]; }
|
|
1116
|
+
// Le cadre a change de largeur : le document se recalcule, en gardant sa page.
|
|
1117
|
+
var c=cur||1;
|
|
1118
|
+
if(pdfDoc||IS_IMG){ build(); if(onePage) showPage(c); else restaurerPage(c); }
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function tournerDe(quarts){
|
|
1122
|
+
rot=Player.viewer.rotationEffective(rot,quarts*90);
|
|
1123
|
+
// En presentation, l audience suit : le meme message que la page, donc le meme ordre.
|
|
1124
|
+
try{ if(PRES) pushPage(); }catch(e){}
|
|
1125
|
+
var c=cur||1;
|
|
1126
|
+
if(!pdfDoc && !IS_IMG) return;
|
|
1127
|
+
build();
|
|
1128
|
+
if(vignOuvert) vignConstruire(); // la proportion des vignettes a change
|
|
1129
|
+
if(onePage) showPage(c); else restaurerPage(c);
|
|
1130
|
+
}
|
|
1131
|
+
window.__refit=function(){ if(!pdfDoc)return; var c=cur||1; build(); restaurerPage(c); };
|
|
745
1132
|
var _rzT; window.addEventListener('resize',function(){ clearTimeout(_rzT); _rzT=setTimeout(function(){ window.__refit(); },160); });
|
|
746
1133
|
// ÉCRAN PARTAGÉ mobile : le fond au-dessus/en-dessous du document prolonge les couleurs de la page
|
|
747
1134
|
// (échantillon des bords haut/bas du canvas) — du header jusqu'à la vidéo, dynamique à chaque page.
|
|
@@ -766,7 +1153,7 @@ ${LEGAL_CSS}
|
|
|
766
1153
|
if(taches[n]){ try{ taches[n].cancel(); }catch(e){} delete taches[n]; }
|
|
767
1154
|
var w=Math.round(targetWidth());
|
|
768
1155
|
el.innerHTML=''; el.className='page ph'; el.textContent='Page '+n;
|
|
769
|
-
el.style.width=w+'px'; el.style.height=Math.round(w*
|
|
1156
|
+
el.style.width=w+'px'; el.style.height=Math.round(w*aspectEffectif())+'px';
|
|
770
1157
|
delete rendered[n]; delete enCours[n];
|
|
771
1158
|
}
|
|
772
1159
|
// ATTENTION : ON BORNE PAR UN BUDGET DE PIXELS, PAS SEULEMENT PAR UN NOMBRE DE PAGES.
|
|
@@ -805,8 +1192,21 @@ ${LEGAL_CSS}
|
|
|
805
1192
|
if(IS_IMG){
|
|
806
1193
|
var w=Math.round(targetWidth());
|
|
807
1194
|
var im=document.createElement('img');
|
|
808
|
-
im.src=imgSrc; im.alt=''; im.style.
|
|
809
|
-
el.
|
|
1195
|
+
im.src=imgSrc; im.alt=''; im.style.display='block';
|
|
1196
|
+
el.classList.remove('ph'); el.textContent=''; el.style.width=w+'px';
|
|
1197
|
+
// ⚠️ UN DOCUMENT IMAGE N A PAS DE VIEWPORT : sa rotation ne peut etre qu une transformation CSS.
|
|
1198
|
+
// Et un element transforme occupe TOUJOURS sa boite d origine — sans echanger les dimensions du
|
|
1199
|
+
// cadre et sortir l image du flux, la mise en page se disloque : la page suivante viendrait se
|
|
1200
|
+
// poser par-dessus.
|
|
1201
|
+
if(rot===90||rot===270){
|
|
1202
|
+
im.style.width=Math.round(w/(firstAspect||1))+'px';
|
|
1203
|
+
im.style.position='absolute'; im.style.left='50%'; im.style.top='50%';
|
|
1204
|
+
im.style.transform='translate(-50%,-50%) rotate('+rot+'deg)';
|
|
1205
|
+
el.style.height=Math.round(w*aspectEffectif())+'px';
|
|
1206
|
+
} else {
|
|
1207
|
+
im.style.width=w+'px'; el.style.height='';
|
|
1208
|
+
if(rot===180) im.style.transform='rotate(180deg)';
|
|
1209
|
+
}
|
|
810
1210
|
el.appendChild(im);
|
|
811
1211
|
im.onload=function(){ if(gen!==renderGen)return; delete enCours[n]; rendered[n]=1; hideLoader(); };
|
|
812
1212
|
im.onerror=function(){ delete enCours[n]; }; // reessayable
|
|
@@ -821,8 +1221,14 @@ ${LEGAL_CSS}
|
|
|
821
1221
|
// ×2 est nul, le cout est quadratique. On plafonne le DPR, puis on plafonne le NOMBRE DE
|
|
822
1222
|
// PIXELS du canvas : deux bornes, parce que le zoom peut faire grandir la page independamment
|
|
823
1223
|
// de la densite de l ecran.
|
|
824
|
-
|
|
825
|
-
|
|
1224
|
+
// ⚠️ ON COMPOSE, ON N ECRASE PAS. La documentation de getViewport dit : « si omise, la rotation
|
|
1225
|
+
// vaut celle de la page ». Passer une valeur ABSOLUE ecraserait donc le /Rotate du fichier — que
|
|
1226
|
+
// portent tres couramment les documents numerises en paysage. Un « remettre a zero » qui passerait
|
|
1227
|
+
// 0 ne redresserait pas un document de travers : il COUCHERAIT un document qui etait droit. La
|
|
1228
|
+
// composition vit dans src/viewer.ts, ou elle est eprouvee.
|
|
1229
|
+
var rotEff=Player.viewer.rotationEffective(page.rotate,rot);
|
|
1230
|
+
var scale=Math.min(5,targetWidth()/page.getViewport({scale:1,rotation:rotEff}).width);
|
|
1231
|
+
var v=page.getViewport({scale:scale,rotation:rotEff});
|
|
826
1232
|
// ATTENTION : LE PLANCHER A 1 CREVAIT LE BUDGET, ET LE BUDGET ETAIT ANNONCE.
|
|
827
1233
|
//
|
|
828
1234
|
// L ancienne formule reduisait le DPR sans jamais le laisser descendre SOUS 1. Or au zoom
|
package/server/presentations.js
CHANGED
|
@@ -7,6 +7,13 @@ const crypto = require("crypto");
|
|
|
7
7
|
// Base de données via le contexte injecté (cf. _player-context.js) — aucune adhérence au studio.
|
|
8
8
|
// ⚠️ Le contexte est REÇU, pas construit. Ce module ne doit pas savoir d'où il vient : c'est ce
|
|
9
9
|
// qui lui permettra de partir dans le dépôt du player sans emporter le studio avec lui.
|
|
10
|
+
// Les quatre orientations qu'une vue peut prendre. ÉNONCÉE UNE FOIS : cette porte est franchie à
|
|
11
|
+
// l'écriture (ce que le navigateur du présentateur envoie) ET à la lecture (ce qu'une base peut
|
|
12
|
+
// porter si un hôte l'a écrite à la main). Deux copies d'une liste blanche, c'est une divergence en
|
|
13
|
+
// attente — et c'est exactement ce qui vient d'arriver entre le serveur et le navigateur sur le
|
|
14
|
+
// TYPE accepté.
|
|
15
|
+
const ROTATIONS = [0, 90, 180, 270];
|
|
16
|
+
|
|
10
17
|
let PLAYER = null;
|
|
11
18
|
function init(ctx) { PLAYER = ctx; _bumpSansDurcissementJusqua = 0; _avertRpcPresence = false; _etatDurcissement = "inconnu"; }
|
|
12
19
|
|
|
@@ -115,7 +122,34 @@ async function reclaimPresentation(slug, email) {
|
|
|
115
122
|
{ control_hash: sha(control), active: true, ...(rangDispo ? { write_seq: 0 } : {}), last_seen: new Date().toISOString(), updated_at: new Date().toISOString() },
|
|
116
123
|
);
|
|
117
124
|
if (!ecrit) return { ok: false, status: 409 };
|
|
118
|
-
|
|
125
|
+
// ⚠️ LA REPRISE RENDAIT LA PAGE ET PAS L'ORIENTATION, ET C'EST UNE DIVERGENCE MUETTE. Le
|
|
126
|
+
// présentateur qui recharge repart avec `rot = 0` — c'est voulu, un rechargement remet la vue à
|
|
127
|
+
// son état d'origine — pendant que l'audience continue d'afficher la `view_rotation` de la base.
|
|
128
|
+
// Il voyait donc son document droit et l'audience le voyait couché, sans que rien ne le dise à
|
|
129
|
+
// personne : il n'a aucune raison de toucher le bouton, de son côté tout va bien.
|
|
130
|
+
//
|
|
131
|
+
// La règle est celle qui valait déjà pour la page, étendue à ce qui manquait : À LA REPRISE, LA
|
|
132
|
+
// PRÉSENTATION L'EMPORTE SUR L'ÉTAT LOCAL. Au DÉMARRAGE c'est l'inverse, et c'est cohérent —
|
|
133
|
+
// `startPresent` pousse la rotation courante du présentateur, parce qu'il n'y a encore rien à
|
|
134
|
+
// reprendre.
|
|
135
|
+
//
|
|
136
|
+
// ⚠️ AUCUN PIÈGE DE MIGRATION ICI, et il vaut la peine de dire pourquoi plutôt que de le
|
|
137
|
+
// supposer : on LIT, `getPresentation` lit `select=*`, donc chez un hôte non migré la colonne est
|
|
138
|
+
// simplement absente — `undefined`, puis 0. Le rejet du PATCH entier sur colonne inconnue ne
|
|
139
|
+
// concerne que l'écriture, et il n'y en a pas.
|
|
140
|
+
//
|
|
141
|
+
// Normalisée comme à l'écriture, par la MÊME liste blanche : une base qu'un hôte a écrite à la
|
|
142
|
+
// main peut porter n'importe quoi, et un viewport oblique casse la couche de texte de toute
|
|
143
|
+
// l'audience.
|
|
144
|
+
// ⚠️ LA VALEUR EST EXTRAITE SUR SA PROPRE LIGNE, ET CE N'EST PAS DU STYLE. Écrit en ternaire —
|
|
145
|
+
// `… ? row.view_rotation : 0` — ce code est indiscernable d'une clé d'objet `view_rotation:` pour
|
|
146
|
+
// la garde `colonneMigreeConditionnelle`, qui refuse toute colonne migrée écrite sans condition.
|
|
147
|
+
// Elle avait raison les deux fois précédentes ; ici elle se trompe, parce qu'on LIT. Mais la
|
|
148
|
+
// rendre assez fine pour distinguer un ternaire d'une clé lui ferait rater de vrais PATCH, et une
|
|
149
|
+
// garde qui sur-approxime est le bon compromis quand le coût de s'y plier est une ligne.
|
|
150
|
+
const brute = row.view_rotation;
|
|
151
|
+
const rot = typeof brute === "number" && ROTATIONS.includes(brute) ? brute : 0;
|
|
152
|
+
return { ok: true, slug, control, page: row.current_page || 1, rotation: rot, fileUrl: row.file_url, fileName: row.file_name, docTitle: row.doc_title, docId: row.doc_id };
|
|
119
153
|
}
|
|
120
154
|
|
|
121
155
|
// Heartbeat présentateur (control requis) : rafraîchit last_seen → distingue une présentation vivante d'une orpheline.
|
|
@@ -296,7 +330,7 @@ async function rangAccepte(row, seq) {
|
|
|
296
330
|
return { controle: true, perime: false, rang };
|
|
297
331
|
}
|
|
298
332
|
|
|
299
|
-
async function setPage(slug, control, page, seq) {
|
|
333
|
+
async function setPage(slug, control, page, seq, rotation) {
|
|
300
334
|
const row = await getPresentation(slug);
|
|
301
335
|
if (!row) return { ok: false, status: 404 };
|
|
302
336
|
if (!tokenMatches(control, row.control_hash)) return { ok: false, status: 403 };
|
|
@@ -305,12 +339,26 @@ async function setPage(slug, control, page, seq) {
|
|
|
305
339
|
// navigateur n'affiche rien — la page qu'il voulait écrire est déjà dépassée par une plus récente.
|
|
306
340
|
if (rang.perime) return { ok: true, perime: true };
|
|
307
341
|
const p = Math.max(1, Math.trunc(Number(page) || 1));
|
|
342
|
+
// ⚠️ UNE LISTE BLANCHE, PAS UNE NORMALISATION. Le navigateur du présentateur ramène déjà sa
|
|
343
|
+
// rotation au quart de tour ; tout ce qui arrive ici hors des quatre valeurs attendues est donc
|
|
344
|
+
// malformé ou hostile, et se ramène à zéro plutôt qu'à « la valeur la plus proche ». Deviner ce
|
|
345
|
+
// qu'un client cassé voulait dire lui donnerait raison.
|
|
346
|
+
//
|
|
347
|
+
// ⚠️ ET LE TYPE COMPTE, PAS SEULEMENT LA VALEUR. `Number("90")` vaut 90 : accepter la chaîne
|
|
348
|
+
// rendrait cette porte PLUS PERMISSIVE que celle du navigateur, où `rotationEffective` exige un
|
|
349
|
+
// nombre. Deux validateurs du même geste qui ne disent pas la même chose, c'est une des deux qui
|
|
350
|
+
// ment — et on ne saurait pas laquelle. Relevé par le banc, pas par la relecture.
|
|
351
|
+
const rot = typeof rotation === "number" && ROTATIONS.includes(rotation) ? rotation : 0;
|
|
352
|
+
// ⚠️ CONDITIONNEL, ET C'EST LA COLONNE QUI L'IMPOSE. PostgREST rejette le PATCH ENTIER si elle
|
|
353
|
+
// manque : la nommer chez un hôte non migré ne perdrait pas la rotation, elle perdrait AUSSI le
|
|
354
|
+
// changement de page. La rotation ne se propage simplement pas là-bas, et rien d'autre ne bouge.
|
|
355
|
+
const rotDispo = await require("./schema").attendue("rotationDirecte");
|
|
308
356
|
// Le jeton DANS la condition : si la présentation a été terminée entre-temps, il a été annulé
|
|
309
357
|
// et cette écriture ne trouve plus sa ligne. Le rang aussi, pour que deux écritures concurrentes
|
|
310
358
|
// ne puissent pas s'inverser — la comparaison et l'écriture deviennent le même geste.
|
|
311
359
|
const ecrit = await ecrireSiEncoreVrai(
|
|
312
360
|
`doc_presentations?slug=eq.${enc(slug)}&control_hash=eq.${sha(control)}${filtreRang(rang)}`,
|
|
313
|
-
{ current_page: p, active: true, last_seen: new Date().toISOString(), updated_at: new Date().toISOString(), ...(rang.controle ? { write_seq: rang.rang } : {}) },
|
|
361
|
+
{ current_page: p, active: true, last_seen: new Date().toISOString(), updated_at: new Date().toISOString(), ...(rang.controle ? { write_seq: rang.rang } : {}), ...(rotDispo ? { view_rotation: rot } : {}) },
|
|
314
362
|
);
|
|
315
363
|
// Refus SILENCIEUX, comme un rang périmé : le navigateur n'a rien à afficher, la page qu'il
|
|
316
364
|
// voulait écrire n'a simplement plus de présentation où aller.
|
package/server/routes-direct.js
CHANGED
|
@@ -39,7 +39,7 @@ async function traiter(req, res, body, _slug) {
|
|
|
39
39
|
return jp(200, { ok: true, slug: out.slug, control: out.control });
|
|
40
40
|
}
|
|
41
41
|
const r = body.action === "present-page"
|
|
42
|
-
? await setPage(String(body.slug || ""), String(body.control || ""), body.page, body.seq)
|
|
42
|
+
? await setPage(String(body.slug || ""), String(body.control || ""), body.page, body.seq, body.rotation)
|
|
43
43
|
: body.action === "present-touch"
|
|
44
44
|
? await touchPresentation(String(body.slug || ""), String(body.control || ""))
|
|
45
45
|
: await endPresentation(String(body.slug || ""), String(body.control || ""));
|
package/server/schema.js
CHANGED
|
@@ -39,6 +39,11 @@ const ATTENDUES = {
|
|
|
39
39
|
migration: "supabase/migrations/0002-ordre-des-ecritures.sql",
|
|
40
40
|
fonction: "refuser une écriture de pilotage doublée en vol",
|
|
41
41
|
},
|
|
42
|
+
rotationDirecte: {
|
|
43
|
+
table: "doc_presentations", colonne: "view_rotation",
|
|
44
|
+
migration: "supabase/migrations/0024-rotation-en-direct.sql",
|
|
45
|
+
fonction: "faire suivre à l'audience l'orientation que le présentateur donne au document",
|
|
46
|
+
},
|
|
42
47
|
envoiUnique: {
|
|
43
48
|
table: "doc_presentation_messages", colonne: "client_key",
|
|
44
49
|
migration: "supabase/migrations/0005-envoi-unique.sql",
|
|
@@ -185,6 +190,24 @@ function etatDuSchema() {
|
|
|
185
190
|
const attendues = Object.keys(ATTENDUES).length;
|
|
186
191
|
return {
|
|
187
192
|
attendues,
|
|
193
|
+
// ⚠️ CE QUE CE CODE SAIT ATTENDRE — ET SANS CETTE LISTE, « complet » EST UN VERT QUI NE PEUT PAS
|
|
194
|
+
// ROUGIR. La session STUDIO l'a mesuré des deux côtés le 30/08 : elle applique la 0024, relit la
|
|
195
|
+
// carte, et lit `attendues: 9 · sondees: 9 · complet · manquant: []` — mot pour mot ce qu'elle
|
|
196
|
+
// lisait AVANT la migration. Elle ne pouvait pas voir la 0024 quitter une liste où elle n'était
|
|
197
|
+
// jamais entrée : leur player est en 0.1.142, et `view_rotation` n'entre dans ATTENDUES qu'avec
|
|
198
|
+
// la version qui apporte la fonction.
|
|
199
|
+
//
|
|
200
|
+
// `complet` a donc toujours voulu dire « complet POUR LE CODE QUE JE SUIS », jamais « complet
|
|
201
|
+
// pour le dépôt » — et rien, ni ici ni dans HOST-CONTRACT.md, ne disait la différence. Un hôte
|
|
202
|
+
// qui applique le schéma AVANT le code (l'ordre sûr, celui que nous recommandons) est
|
|
203
|
+
// exactement celui que ce vert ne renseigne pas.
|
|
204
|
+
//
|
|
205
|
+
// Aucune version de ce serveur ne peut connaître une migration postérieure à elle. Ce qu'elle
|
|
206
|
+
// peut faire, et que la carte ne faisait pas, c'est DIRE CE QU'ELLE CONNAÎT : « 0024 n'est pas
|
|
207
|
+
// dans cette liste » devient une lecture directe, au lieu d'une déduction qui demande de
|
|
208
|
+
// connaître l'historique du dépôt. C'est la seule forme de réponse qui reste vraie quand le
|
|
209
|
+
// lecteur est plus récent que nous.
|
|
210
|
+
connues: [...new Set(Object.values(ATTENDUES).map((a) => a.migration))].sort(),
|
|
188
211
|
sondees: reponses.size,
|
|
189
212
|
// ⚠️ UN MOT, PAS UN TABLEAU VIDE À INTERPRÉTER. `manquant: []` a quatre sens selon ce qu'on
|
|
190
213
|
// sait par ailleurs — rien demandé, tout vérifié, vérifié en partie, base muette — et forcer
|
package/supabase/init.sql
CHANGED
|
@@ -174,6 +174,10 @@ create table if not exists public.doc_presentations (
|
|
|
174
174
|
doc_title text,
|
|
175
175
|
presenter_name text,
|
|
176
176
|
current_page integer not null default 1,
|
|
177
|
+
-- Orientation imposée par le PRÉSENTATEUR (0/90/180/270), distincte du /Rotate que porte le
|
|
178
|
+
-- fichier : le player les COMPOSE. Voir 0024 — elle est normalisée à la RÉCEPTION, cette valeur
|
|
179
|
+
-- pouvant arriver du navigateur du présentateur par la voie broadcast.
|
|
180
|
+
view_rotation integer not null default 0,
|
|
177
181
|
active boolean not null default true,
|
|
178
182
|
created_at timestamptz not null default now(),
|
|
179
183
|
updated_at timestamptz not null default now(),
|
|
@@ -359,7 +363,8 @@ create index if not exists doc_tts_objects_created_idx
|
|
|
359
363
|
on public.doc_tts_objects (created_at);
|
|
360
364
|
-- RLS activée SANS politique : sous RLS, l'absence de politique refuse tout — seul le rôle de
|
|
361
365
|
-- service passe. Même posture que `player_rate_limits`, et pour la même raison : ni un visiteur ni
|
|
362
|
-
-- l'équipe n'ont de raison de lire cette table.
|
|
366
|
+
-- l'équipe n'ont de raison de lire cette table. ⚠️ « Seul le rôle de service » est vrai en effet,
|
|
367
|
+
-- pas en droit : voir le bloc « Accès » plus bas, et l'en-tête de la migration 0021.
|
|
363
368
|
alter table public.doc_tts_objects enable row level security;
|
|
364
369
|
comment on table public.doc_tts_objects is
|
|
365
370
|
'Trace des objets écrits dans le bucket public tts-cache : une empreinte, une date, jamais le '
|
|
@@ -588,6 +593,88 @@ $$;
|
|
|
588
593
|
-- RLS activé SANS politique permissive : seul `service_role` (donc la route du player) passe.
|
|
589
594
|
-- ⚠️ N'ajoutez pas de politique de lecture publique « pour que le direct fonctionne ». C'est
|
|
590
595
|
-- précisément l'état dont l'hôte historique a dû sortir : le suivi passe par broadcast.
|
|
596
|
+
--
|
|
597
|
+
-- ⚠️ ET « SEUL `service_role` PASSE » EST VRAI EN EFFET, PAS EN DROIT. Un hôte l'a relevé le 27/08 :
|
|
598
|
+
-- sur une installation de type Supabase, `anon` possède le droit SELECT sur ces tables, hérité des
|
|
599
|
+
-- privilèges par défaut du schéma `public`. Ce n'est donc pas l'absence de `grant` qui les ferme,
|
|
600
|
+
-- c'est UNIQUEMENT la RLS — et il n'y a rien en dessous. Une politique permissive ajoutée « pour
|
|
601
|
+
-- débloquer un cas » ne retire pas une protection sur deux : elle retire la seule. Un hôte qui veut
|
|
602
|
+
-- la seconde couche l'écrit chez lui (`revoke select … from anon, authenticated`) ; ce dépôt ne la
|
|
603
|
+
-- pose pas à sa place, ces rôles étant ceux de son installation et non de Postgres.
|
|
604
|
+
--
|
|
605
|
+
-- ⚠️ ET CE CONSEIL ÉTAIT INCOMPLET — SA PRÉCONDITION MANQUAIT, ET SANS ELLE IL CASSE. Un `revoke`
|
|
606
|
+
-- global n'est sûr QUE là où aucune politique n'accorde. Là où une politique permissive nomme
|
|
607
|
+
-- `anon`, la RLS dit oui et le droit dit non : la surface publique tombe, sans erreur de
|
|
608
|
+
-- configuration nulle part. Sur les tables de CE fichier la condition est remplie — zéro politique,
|
|
609
|
+
-- rien d'ouvert à refermer — mais un hôte qui généralise le geste à SA base applicative peut y
|
|
610
|
+
-- perdre une carte publique. Un hôte l'a mesuré le 28/08 : il a posé le `revoke` sur les dix tables
|
|
611
|
+
-- du player, et ne l'a PAS posé sur ses 221 tables applicatives, dont trois portent une politique
|
|
612
|
+
-- nommant `anon`.
|
|
613
|
+
--
|
|
614
|
+
-- ⚠️ ET LA VÉRIFICATION QUI TRANCHE N'EST PAS CELLE QU'ON CROIT — TROIS ÉCRITURES, TROIS PORTÉES.
|
|
615
|
+
-- La première proposée ici filtrait `roles::text like '%anon%'`. Or le geste conseillé retire le
|
|
616
|
+
-- droit à `anon` ET à `authenticated` : une base dont toutes les politiques nomment
|
|
617
|
+
-- `authenticated` obtient donc ZÉRO LIGNE — un feu vert — alors qu'elle est au maximum exposée.
|
|
618
|
+
-- Relevé par un hôte le 28/08, chez qui 28 politiques sur 28 nomment `authenticated` et aucune
|
|
619
|
+
-- `anon`. Une sonde dont le filtre est plus étroit que le geste qu'elle vérifie rend un zéro qui
|
|
620
|
+
-- veut dire « je n'ai pas regardé », pas « il n'y a rien ».
|
|
621
|
+
--
|
|
622
|
+
-- ⚠️ ET LA DEUXIÈME ÉCRITURE AVAIT LE MÊME DÉFAUT D'UN CRAN PLUS BAS : elle choisissait UN rôle par
|
|
623
|
+
-- politique (`case when … then 'anon' else 'authenticated' end`). Sur une politique qui nomme les
|
|
624
|
+
-- deux et où seul `authenticated` a perdu le droit, elle regarde `anon`, le trouve intact, et ne
|
|
625
|
+
-- dit rien. Mesuré contre un vrai Postgres : elle voit 1 des 2 tables cassées.
|
|
626
|
+
--
|
|
627
|
+
-- La forme qui tient déplie CHAQUE rôle nommé, et mesure l'ÉTAT RÉSULTANT plutôt que de demander si
|
|
628
|
+
-- le `revoke` a été posé — c'est la seule chose qui compte pour l'hôte :
|
|
629
|
+
--
|
|
630
|
+
-- ⚠️ LES DEUX MARQUES CI-DESSOUS NE SONT PAS DÉCORATIVES : LA CI EXTRAIT CE QUI EST ENTRE ELLES ET
|
|
631
|
+
-- L'EXÉCUTE. Ce bloc n'est donc pas la transcription d'une requête éprouvée ailleurs — c'est
|
|
632
|
+
-- l'artefact éprouvé lui-même. Le job `schema` monte trois profils de politique contre un vrai
|
|
633
|
+
-- Postgres, en casse un dans chacun, et exige que CETTE requête nomme la table morte à chaque fois.
|
|
634
|
+
-- Si vous l'éditez, c'est votre version qui est mesurée à la course suivante ; si vous la cassez, la
|
|
635
|
+
-- CI le dit avant l'hôte. Aucune des trois écritures de cette requête n'a été trouvée fautive par
|
|
636
|
+
-- son auteur — d'où le banc plutôt qu'une relecture de plus.
|
|
637
|
+
-- ⚠️ ET IL Y AVAIT UN QUATRIÈME PROFIL, QUI EST LA FORME PAR DÉFAUT — TROUVÉ PAR UN HÔTE LE 29/08
|
|
638
|
+
-- ALORS QUE LE BANC CI-DESSOUS TOURNAIT DÉJÀ SUR TROIS. Une politique écrite SANS clause `TO`
|
|
639
|
+
-- s'applique à `PUBLIC`, donc à tous les rôles, `anon` compris — et `pg_policies` l'affiche
|
|
640
|
+
-- `{public}`, jamais `{anon}`. Un filtre `in ('anon','authenticated')` la saute intégralement. Ce
|
|
641
|
+
-- n'est pas un cas exotique : c'est ce que produit l'interface Supabase pour un bucket public, et
|
|
642
|
+
-- l'hôte en a trouvé cinq chez lui, dont les trois qui servent ses buckets publics. Un hôte qui pose
|
|
643
|
+
-- le `revoke` puis lance la requête obtient alors zéro ligne pendant que ses buckets sont morts.
|
|
644
|
+
--
|
|
645
|
+
-- ⚠️ ET LE `case` NE CHOISIT PAS UN RÔLE, IL EN DÉPLIE DEUX. Tester `anon` comme représentant de
|
|
646
|
+
-- `public` raterait le cas où seul `authenticated` est privé — l'hôte l'a écrit lui-même en
|
|
647
|
+
-- proposant sa piste, et c'est mesuré : sur une politique `{public}` dont seul `authenticated` a
|
|
648
|
+
-- perdu le droit, la version « un représentant » se tait, celle qui déplie rend la ligne.
|
|
649
|
+
--
|
|
650
|
+
-- ⚠️ ET `to_regrole` N'EST PAS UNE COQUETTERIE. Sur une installation qui n'a NI `anon` NI
|
|
651
|
+
-- `authenticated` — tout ce qui n'est pas Supabase — `has_table_privilege('anon', …)` ne rend pas
|
|
652
|
+
-- faux : il LÈVE « role "anon" does not exist », et la requête entière échoue. Filtrer sur
|
|
653
|
+
-- `pg_roles` ne suffit pas, mesuré : le planificateur évalue la fonction dans le même filtre de
|
|
654
|
+
-- jointure et la sous-requête ne la court-circuite pas. `to_regrole` rend NULL sur un rôle absent,
|
|
655
|
+
-- le prédicat devient NULL, la ligne sort du résultat — quel que soit l'ordre d'évaluation.
|
|
656
|
+
--
|
|
657
|
+
-- [banc:requete-diagnostic]
|
|
658
|
+
-- select p.schemaname, p.tablename, p.policyname, e.role
|
|
659
|
+
-- from pg_policies p
|
|
660
|
+
-- cross join lateral unnest(p.roles) as r(role)
|
|
661
|
+
-- cross join lateral unnest(
|
|
662
|
+
-- case when r.role::text = 'public' then array['anon', 'authenticated']
|
|
663
|
+
-- else array[r.role::text] end
|
|
664
|
+
-- ) as e(role)
|
|
665
|
+
-- join pg_class c on c.relname = p.tablename
|
|
666
|
+
-- join pg_namespace n on n.oid = c.relnamespace and n.nspname = p.schemaname
|
|
667
|
+
-- where e.role in ('anon', 'authenticated')
|
|
668
|
+
-- and not has_table_privilege(to_regrole(e.role), c.oid, 'SELECT');
|
|
669
|
+
-- [/banc:requete-diagnostic]
|
|
670
|
+
--
|
|
671
|
+
-- Rien en retour : aucune politique n'est privée du droit qu'elle suppose. Une ligne : cette
|
|
672
|
+
-- politique est MORTE — la RLS dit oui, le droit dit non — et la ligne nomme la table et le rôle.
|
|
673
|
+
--
|
|
674
|
+
-- ⚠️ ET CETTE POSTURE EST DÉSORMAIS MESURÉE, PAS SEULEMENT ÉCRITE. Elle l'était ici dix fois et
|
|
675
|
+
-- confrontée zéro fois — aucun banc, aucune étape ne demandait à la base si elle en avait tenu
|
|
676
|
+
-- compte. Le job `schema` relève maintenant ce que ces fichiers DÉCLARENT et le confronte à ce que
|
|
677
|
+
-- le moteur a RETENU, politiques comprises.
|
|
591
678
|
alter table public.commercial_doc_shares enable row level security;
|
|
592
679
|
alter table public.commercial_doc_views enable row level security;
|
|
593
680
|
alter table public.commercial_doc_sessions enable row level security;
|
|
@@ -689,6 +776,17 @@ alter table public.commercial_doc_shares
|
|
|
689
776
|
add column if not exists revoked_at timestamptz;
|
|
690
777
|
update public.commercial_doc_shares set revoked_at = now() where revoked = true and revoked_at is null;
|
|
691
778
|
|
|
779
|
+
-- 0024, et le rattrapage a été appris ICI, par le job `schema` : la colonne était déclarée dans le
|
|
780
|
+
-- corps de `doc_presentations` ci-dessus — ce qui suffit pour une base VIERGE — et le scénario
|
|
781
|
+
-- « init v0.1.64 → rejeu de l'init actuel » a refusé la forme obtenue. `create table if not
|
|
782
|
+
-- exists` ne fait RIEN sur une table déjà là, donc un hôte installé avant aujourd'hui n'aurait
|
|
783
|
+
-- jamais vu `view_rotation` en rejouant ce fichier, et sa sonde de schéma aurait dégradé la
|
|
784
|
+
-- rotation en silence : exactement le mode de panne que tout le paragraphe ci-dessus décrit.
|
|
785
|
+
-- Le type, la nullabilité et le défaut sont ceux de la 0024, au caractère près — c'est ce que le
|
|
786
|
+
-- job compare.
|
|
787
|
+
alter table public.doc_presentations
|
|
788
|
+
add column if not exists view_rotation integer not null default 0;
|
|
789
|
+
|
|
692
790
|
-- ⚠️ ET LE RATTRAPAGE VAUT AUSSI POUR LES CONTRAINTES, PAS SEULEMENT POUR LES COLONNES (0020).
|
|
693
791
|
-- Elles sont déclarées dans le corps des tables ci-dessus, ce qui règle la base VIERGE — et ne
|
|
694
792
|
-- touche pas une base déjà installée, où `create table if not exists` ne fait rien. Le scénario
|
|
@@ -41,6 +41,33 @@ create index if not exists doc_tts_objects_created_idx
|
|
|
41
41
|
-- politique n'est pas « ouverte par défaut » : sous RLS, l'absence de politique REFUSE tout. C'est
|
|
42
42
|
-- la même posture que `player_rate_limits`, et elle est délibérée : cette table n'a aucune raison
|
|
43
43
|
-- d'être lue par un visiteur, ni par l'équipe.
|
|
44
|
+
--
|
|
45
|
+
-- ⚠️ ET « PERSONNE » EST VRAI EN EFFET, PAS EN DROIT — LA NUANCE EST TOUT CE QUI VOUS PROTÈGE.
|
|
46
|
+
-- Un hôte l'a relevé le 27/08 en appliquant cette migration : sur une installation de type
|
|
47
|
+
-- Supabase, `anon` possède le droit SELECT sur cette table, hérité des privilèges par défaut du
|
|
48
|
+
-- schéma `public`. Ce n'est donc PAS l'absence de `grant` qui ferme la table — c'est UNIQUEMENT la
|
|
49
|
+
-- RLS, et il n'y a rien en dessous. Le jour où quelqu'un ajoute une politique permissive « pour
|
|
50
|
+
-- débloquer un cas », il ne retire pas une protection sur deux : il retire la seule. Si vous
|
|
51
|
+
-- voulez la seconde couche, elle s'écrit chez vous (`revoke select on public.doc_tts_objects from
|
|
52
|
+
-- anon, authenticated;`) — ce dépôt ne la pose pas à votre place, parce que ces rôles sont ceux de
|
|
53
|
+
-- votre installation, pas de Postgres.
|
|
54
|
+
--
|
|
55
|
+
-- ⚠️ ET LA PRÉCONDITION DE CE CONSEIL, QUI MANQUAIT : un `revoke` n'est sûr QUE là où aucune
|
|
56
|
+
-- politique n'accorde. Sur cette table la condition est remplie (zéro politique). Sur VOTRE base
|
|
57
|
+
-- applicative, une politique permissive qui nomme `anon` OU `authenticated` ferait dire oui à la RLS
|
|
58
|
+
-- et non au droit : la surface publique tombe sans qu'aucune configuration paraisse fautive.
|
|
59
|
+
--
|
|
60
|
+
-- ⚠️ ET LA REQUÊTE DE VÉRIFICATION DOIT COUVRIR CHAQUE RÔLE NOMMÉ — Y COMPRIS CEUX QU'UNE POLITIQUE
|
|
61
|
+
-- NE NOMME PAS. Sans quoi elle rend un zéro rassurant sur une base exposée : TROIS écritures s'y
|
|
62
|
+
-- sont cassées, chacune trouvée par quelqu'un d'autre que son auteur, toutes mesurées contre un vrai
|
|
63
|
+
-- Postgres. La dernière butait sur le cas le plus courant de tous — une politique écrite sans clause
|
|
64
|
+
-- `TO` porte `{public}`, jamais `{anon}`, et c'est la forme que produit l'interface Supabase pour un
|
|
65
|
+
-- bucket public. La forme qui tient est dans le bloc « Accès » d'`init.sql` ; elle déplie `public`
|
|
66
|
+
-- en ses rôles concrets et mesure l'ÉTAT RÉSULTANT (`has_table_privilege`) au lieu de demander si le
|
|
67
|
+
-- `revoke` a été posé. La forge l'EXÉCUTE à chaque course contre quatre profils de politique.
|
|
68
|
+
--
|
|
69
|
+
-- Ce que la forge vérifie désormais à chaque course, contre une vraie base : que la RLS déclarée
|
|
70
|
+
-- ici est bien RETENUE par le moteur, et qu'AUCUNE politique n'ouvre cette table.
|
|
44
71
|
alter table public.doc_tts_objects enable row level security;
|
|
45
72
|
|
|
46
73
|
comment on table public.doc_tts_objects is
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- LA ROTATION DU PRÉSENTATEUR NE SUIVAIT PAS SON AUDIENCE — ELLE N'AVAIT NULLE PART OÙ VOYAGER.
|
|
2
|
+
--
|
|
3
|
+
-- ⚠️ CE QUI MANQUAIT. Depuis la 0.1.143, un présentateur peut redresser un document à 90° dans sa
|
|
4
|
+
-- visionneuse. Sa rotation restait LOCALE : l'audience continuait de voir le document couché
|
|
5
|
+
-- pendant qu'il commentait un document droit. Le pilotage en direct transporte la page courante, le
|
|
6
|
+
-- document et la vue carte — pas l'orientation.
|
|
7
|
+
--
|
|
8
|
+
-- ⚠️ POURQUOI `view_rotation` ET NON `rotation`. Le nom dit ROTATION DE LA VUE, par opposition au
|
|
9
|
+
-- `/Rotate` que porte le fichier PDF lui-même : ce sont deux choses différentes, et le player les
|
|
10
|
+
-- COMPOSE au lieu de les confondre. Le nom court aurait aussi collisionné avec l'option `rotation`
|
|
11
|
+
-- de pdf.js, présente partout dans le code de la visionneuse — la garde qui vérifie qu'une colonne
|
|
12
|
+
-- migrée n'est jamais écrite sans condition aurait alors crié en permanence sur des lignes qui ne
|
|
13
|
+
-- touchent pas la base. Une alerte qui sonne quand tout va bien apprend à cliquer à côté.
|
|
14
|
+
--
|
|
15
|
+
-- ⚠️ POURQUOI UNE COLONNE ET PAS LA COLONNE `content`. `content` porte un type VALIDÉ (carte ou
|
|
16
|
+
-- Street View) et il est ré-assaini à la réception. Y glisser une orientation reviendrait à faire
|
|
17
|
+
-- porter deux sens au même champ, et à affaiblir le validateur qui protège toute l'audience d'une
|
|
18
|
+
-- vue imposée par un participant. Une orientation n'est pas un contenu.
|
|
19
|
+
--
|
|
20
|
+
-- ⚠️ Sans lui : RIEN NE CASSE, ET LA ROTATION NE SE PROPAGE SIMPLEMENT PAS. Le player interroge la
|
|
21
|
+
-- présence de cette colonne avant d'écrire (`GET /api/doc?schema=1` la nomme). Chez un hôte non
|
|
22
|
+
-- migré, le présentateur tourne son document pour lui seul, exactement comme avant — mais PostgREST
|
|
23
|
+
-- rejetterait le PATCH ENTIER si on la nommait sans qu'elle existe, ce qui ferait perdre AUSSI le
|
|
24
|
+
-- changement de page. C'est pourquoi le champ est conditionnel côté serveur et non systématique.
|
|
25
|
+
--
|
|
26
|
+
-- ⚠️ IDEMPOTENTE : `if not exists`, rejouable sans effet.
|
|
27
|
+
|
|
28
|
+
alter table public.doc_presentations
|
|
29
|
+
add column if not exists view_rotation integer not null default 0;
|
|
30
|
+
|
|
31
|
+
comment on column public.doc_presentations.view_rotation is
|
|
32
|
+
'Orientation du document imposée par le présentateur, en degrés (0, 90, 180, 270). Normalisée au '
|
|
33
|
+
'quart de tour à la RÉCEPTION : sur la voie broadcast, cette valeur vient du navigateur du '
|
|
34
|
+
'présentateur, et un viewport oblique casserait la couche de texte de toute l''audience.';
|