From c843f0073841bb4e189373a3987b8267221c9ca2 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 4 Nov 2025 20:34:42 -0500 Subject: [PATCH] release(v1.8.2): media progress tracking + watched badges; PWA scaffolding; mobile switcher (closes #37) --- CHANGELOG.md | 32 + README.md | 5 +- public/api/media/getProgress.php | 7 + public/api/media/getViewedMap.php | 7 + public/api/media/updateProgress.php | 7 + public/assets/icons/apple-touch-icon.png | Bin 0 -> 5066 bytes public/assets/icons/base-1024.png | Bin 0 -> 55264 bytes public/assets/icons/icon-192.png | Bin 0 -> 5072 bytes public/assets/icons/icon-512.png | Bin 0 -> 18735 bytes public/assets/icons/maskable-512.png | Bin 0 -> 15265 bytes public/css/styles.css | 27 +- public/index.html | 10 +- public/js/fileListView.js | 141 +++- public/js/filePreview.js | 858 +++++++++++++---------- public/js/i18n.js | 12 +- public/js/main.js | 48 ++ public/js/mobile/switcher.js | 287 ++++++++ public/js/pwa/register-sw.js | 5 + public/js/pwa/sw.js | 9 + public/manifest.webmanifest | 14 + public/sw.js | 6 + src/controllers/MediaController.php | 135 ++++ src/models/MediaModel.php | 94 +++ 23 files changed, 1320 insertions(+), 384 deletions(-) create mode 100644 public/api/media/getProgress.php create mode 100644 public/api/media/getViewedMap.php create mode 100644 public/api/media/updateProgress.php create mode 100644 public/assets/icons/apple-touch-icon.png create mode 100644 public/assets/icons/base-1024.png create mode 100644 public/assets/icons/icon-192.png create mode 100644 public/assets/icons/icon-512.png create mode 100644 public/assets/icons/maskable-512.png create mode 100644 public/js/mobile/switcher.js create mode 100644 public/js/pwa/register-sw.js create mode 100644 public/js/pwa/sw.js create mode 100644 public/manifest.webmanifest create mode 100644 public/sw.js create mode 100644 src/controllers/MediaController.php create mode 100644 src/models/MediaModel.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eca25c..0815adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## Changes 11/4/2025 (v1.8.2) + +release(v1.8.2): media progress tracking + watched badges; PWA scaffolding; mobile switcher (closes #37) + +- **Highlights** + - Video: auto-save playback progress and mark “Watched”, with resume-on-open and inline status chips on list/gallery. + - Mobile: introduced FileRise Mobile (Capacitor) companion repo + in-app server switcher and PWA bits. + +- **Details** + - API (new): + - POST /api/media/updateProgress.php — persist per-user progress (seconds/duration/completed). + - GET /api/media/getProgress.php — fetch per-file progress. + - GET /api/media/getViewedMap.php — folder map for badges. + +- **Frontend (media):** + - Video previews now resume from last position, periodically save progress, and mark completed on end, with toasts. + - Added status badges (“Watched” / %-complete) in table & gallery; CSS polish for badges. + - Badges render during list/gallery refresh; safer filename wrapping for badge injection. + +- **Mobile & PWA:** + - New in-app server switcher (Capacitor-aware) loaded only in app/standalone contexts. + - Service Worker + manifest added (root scope via /public/sw.js; worker body in /js/pwa/sw.js; manifest icons). + - main.js conditionally imports the mobile switcher and registers the SW on web origins only. + +- **Notes** + - Companion repo: **filerise-mobile** (Capacitor app shell) created for iOS/Android distribution. + - No breaking changes expected; endpoints are additive. + +Closes #37. + +--- + ## Changes 11/3/2025 (V1.8.1) release(v1.8.1): fix(security,onlyoffice): sanitize DS origin; safe api.js/iframe probes; better UX placeholder diff --git a/README.md b/README.md index 7142e43..34e3615 100644 --- a/README.md +++ b/README.md @@ -369,12 +369,13 @@ FileRise can open & edit office docs using your **self-hosted ONLYOFFICE Documen **Apache** ```apache - Header always set Content-Security-Policy "default-src 'self'; frame-src 'self' https://docs.example.com; script-src 'self' https://docs.example.com https://docs.example.com/web-apps/apps/api/documents/api.js; connect-src 'self' https://docs.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'" + Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; object-src 'none'; script-src 'self' 'sha256-ajmGY+5VJOY6+8JHgzCqsqI8w9dCQfAmqIkFesOKItM=' https://your-onlyoffice-server.example.com https://your-onlyoffice-server.example.com/web-apps/apps/api/documents/api.js; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' https://your-onlyoffice-server.example.com; media-src 'self' blob:; worker-src 'self' blob:; form-action 'self'; frame-src 'self' https://your-onlyoffice-server.example.com" ``` **Nginx** - ```add_header Content-Security-Policy "default-src 'self'; frame-src 'self' https://docs.example.com; script-src 'self' https://docs.example.com https://docs.example.com/web-apps/apps/api/documents/api.js; connect-src 'self' https://docs.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'" always; + ```nginx + add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; object-src 'none'; script-src 'self' 'sha256-ajmGY+5VJOY6+8JHgzCqsqI8w9dCQfAmqIkFesOKItM=' https://your-onlyoffice-server.example.com https://your-onlyoffice-server.example.com/web-apps/apps/api/documents/api.js; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' https://your-onlyoffice-server.example.com; media-src 'self' blob:; worker-src 'self' blob:; form-action 'self'; frame-src 'self' https://your-onlyoffice-server.example.com" always; ``` **Notes** diff --git a/public/api/media/getProgress.php b/public/api/media/getProgress.php new file mode 100644 index 0000000..9ef3a3a --- /dev/null +++ b/public/api/media/getProgress.php @@ -0,0 +1,7 @@ +getProgress(); \ No newline at end of file diff --git a/public/api/media/getViewedMap.php b/public/api/media/getViewedMap.php new file mode 100644 index 0000000..737b2bb --- /dev/null +++ b/public/api/media/getViewedMap.php @@ -0,0 +1,7 @@ +getViewedMap(); \ No newline at end of file diff --git a/public/api/media/updateProgress.php b/public/api/media/updateProgress.php new file mode 100644 index 0000000..72564ce --- /dev/null +++ b/public/api/media/updateProgress.php @@ -0,0 +1,7 @@ +updateProgress(); \ No newline at end of file diff --git a/public/assets/icons/apple-touch-icon.png b/public/assets/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..969aed795c370f828a399fe725f32a06534376ca GIT binary patch literal 5066 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 literal 0 HcmV?d00001 diff --git a/public/assets/icons/base-1024.png b/public/assets/icons/base-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..fbfd1046f2ca31d92ee082a80d7e31a11ccb20fb GIT binary patch literal 55264 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 NkDoR#Ip%rme*i!hGLirQ literal 0 HcmV?d00001 diff --git a/public/assets/icons/icon-192.png b/public/assets/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..4a72fdf69b369cb6d677b40a7350b2a00dee337f GIT binary patch literal 5072 zcmZ`-cQo5y|Nn?+s1>v}jlFk#jYI3_S^1)B*_6s)8!UT_c}w4pZSCoT92n?k8cMK}O3`=L=9<^f+x zc?&6A)4shUt%Qa5t7#IiuYAgMB=!4BmDNbSN2UumaJ6Vx@9~LYK8E_xufAdolo&nZ zI5t^*B{lkw5j|WcW(lZwd==pePar8L;jD@|_8lxTNjGae^MfA%xP=3EA4{6Q-v>#M)~=a_Vumi|_vRyr$QW zQvG^I^#H*S6e0m+-F9}V27ttSj9yZH$H9P$u784w$Q3{^LP;%M$u$1&zO0+|g3X9w zqUqFNO(TpL?Ib8)bjg$zh;xxmOLpkFW+0RiKHD2m%}~6qf8_amzFn%;t%}iqvYZFM z_sSyzOCfZLz9=gKPhl**bSkl2w6Z*w{`FMCa`c0|m%d11;!D~-M~^Kf6e^-Z;x9($ z6gv>K!qPf<%P=p_=1xOB#6>7&^Y(6ca8s=tmbW7Q15A%9-X$SCWReod5hnXB ztr>1E)VxsIsB{ML>pQ!{0De)YrHGsZHb$p{`B9?`q?k6?MDdCstR-lxS5FLeMRT@d zLjCM(2yUSAklIr9Hw^`#(Hw4PJ)(Ag(%PtKZpxer1!0WQ&WI4p0ZS2k-J62=}M(%Jjv)F#oK>)C5zY?Q`R{V#vRGhAb zF((pi(>Ze1`-gmcrG=hZGiu3mn`$PVC0N7N-hCgM3Jqw1gq_y}9%tgj9d=7Efn~4S zob~;A>_7A#0R_do+|z155@eoZ@nx@sFc}Eh&NqcN;5aj1^X9)Shk!?=f^ZXm^KS2M zwN}SsUHcAj2jHRhL$xv22sN>5)(Dhf^npfR=-+nYdOZm(LVas{ET#5^l3xb2lh@xG z7?xX^a&e$r#qOMWKC!(P?kMaa?BlBzKJ>`9W7qYu@vdY)zLJ z^cM~yHX;`nhb*^8P=mlTq={xXVzayzcWLq? zg7XUmMPO;;j}GXz-%lsPus7p;ELslb*;w@Ey^k?kw?R$rsgALRMpOFP{q?>3*7b8% zQ0U@BGJbtkFq$7cwgPkLjlNkhzSVlbHe1%b!Td>jqakVN2sKC=mM54kA=r|cK!91K zbjlwy9=g&imaL=!2>}>ZUexrhVx>m}9|cC{(l74C0J(F6CLNyyU2%}!3nqSotiK<& ziptAwfzfcHe{E|+{JWRVsmU@z26nZg{z0;bC`p}`iwVjEm~N!0mu9p!I=U?gl^y&{ zgrc`8Z~2k1{ad4+Z`)pC?`d^ze=eD*FFBKi#57^6(0UYf3Elz=fUX!3AJvrnhrmsJ z#X%#~7@4mX#WLf~$j=mRuE2F0Bo=?b6)TeeP$Ts3oCUob@hpz_y{%o z(H-satEgVJ9lz2nR-v(ZN@D=dOL^=e$!+z?|2#d@46OXuVeshLjQ-HbgF$906@S9D zt@r$LiZ`A28oN*H#{=?ZQQFIU?4pJXRZ)B~8grtsmgZ;g^TbNNZmV%>#f8g$cvv>2s#>(*}b12^BWV^b8^P@l+u)137V@(n?Q z4;gprJ}i8wFC}bGl~D{Q_C5X;sJy8^gh=gCEe#iS%XSeCPo3Sp!m1eO6iI^^rR4$S z8wnr>r%0--e?(l=_?&o#I`t^OfUV^;y01#n zi$!$jktT;dkhVQ9-YTk8`;ttIBvn%T27CRZiIP1?WT6tGtr&6Q`E&WS)hoZGbaj0E zXaD0rPqq)-Nk*G4EMZ}VG`xw_nZJ*=2xUKHGBUlY^Gh4+bNC`VPHK&;Mb61nkGWAS z$Yz-Ie1YTsAeFw#hX7$asW{6JOG!|1B5-ie8zq#&!DQ^X{B|QEfcy+ah9_0M72oy-YcT)O0svz|5pXNV#Hk5!-&1$~dos%iE!y7&$ z>QcMiZU;rf!nUZl5g zW8m@KITJ>Jx~pBxF03}xw@T%$nm2b{`1t3kym^w1R)93aH9PQX;!i7@yS-R6DR21)ORK^)quTB}WQh;4Yar z5e{kc{X=x>n7}Izd6s zAAYzmsmT3BNQU@9w!||9?{ZWZ z#=taHWg^3M(M0#CoHt?x*{;#Tna>Hd0{v_Gmku99n;NVRY@q6pqNtaqDjUXq+{&p3 ziP|=_E8R8Si-ni_v$PjKUhe;EmJS!S;M~*2G7r}kQ;Im}Xss2xb+S!Plokj5C?5!{ zW^GQys6W5eM^2qd=W*P@6?}0M`XELvQTQt7ZEa2_pJ>K|Fd(T5 zy161wo2l&nDiG)3@2O}6v(4aF3DsB_Cxro8%7$m7)!wP_m4sQ(M3JNq+I11fz=`n1 zHBQS}YI!5A@6^t%GXx15@%xxF`#KgeQjd;JGA`gjAG?$IWd=mDo$T8DN=`~ztZf`; zu->@>Kh49c>>q9a*bh6yDOB<7qaB1D^(qdrO6{TViokHmv^-13RUg33OE4n<3@l>$ zKvSk#dXoK9-|u%U^TvClK9;Jc`7Q^+lvl|H-Lq#WJ$6}X6|c5W{f zNMpr++gnTXkUs$|{ zuf?Buy|d;RlPARf-T|~yl@r%^z1K^_{K?%+LM7Ny+{Fi%*xg%Cv)+F9ptaXR+C8J6 z%Ho#S{r$-RV>4J;KiMPY#{b>}pt zwhR2mT8rAT9xqFjfs8AXK5VTt}GO(%zrND&!el_>n7QY|e*yMxb8t zkt|~+ugfy2!kAq_50la*4bZXD3(sj1%*wkRYIPh8+!LuXj6lJHnSjF+v?cPVV4aSG zP!CKs1aWI;|GP4^9;;c9pgR`<7mc2lq~n^txxt8<<$su*H7@XQlwS_U!gG0;vMd{l zV0IQt=8dP$yztO(RHKZcc74XP?g?z;{IlmCP1mc70t;VP-KlNawBgVfc!G%fpxAgAd!%GkBdhwkD_s*K60)X*E1^g^(R!uZb`Z~zD~S85N&ofL$c zjNoTqO15UDZP*a$rSA)kg7Hha6FwA7CPoBJi>5zjYUb~2?>_3*-bV{#O&-Y!G;o^6 z)#pmX%FM-5CX4-Aa8%b+)>Hc~xtzVqY%1dxS433gxtB}oPbDmW(TkNDh(rSA;zYcO zWoB-W=hJX#Dqt|7)?S*Yl1ZD`n$?612R3`XUG;PB=YYc_7(5bawSb9+N)FYq$~sf% z`?i4Zp#~0ok$CiVe{s9MTZ9JByKh%LBDTtjhk9a!QJGw6_v*D7po|wZqnsbSH(zYO z?nk%EN>H|?;440%iPJ>|*`0?No|Gi5Ik2{V))apj+nhy^DX=*UF`PJ2WeV()l`es? z#cKHwM0HojkAJOkk?bDLdh0c0!|k8nQwwO=**likwNawui>`H<-Lirm7UlP%zL5O> zNxnaFuVPA%(>chLhN)WmU&*~1e^J!s)Mc%>7B8Cp$6xnOf~2S0d_s>;jZb7w8;bP1 z*?M$Yz!@WPwP8hGKW7`t{7)X)Ou(J@NRAz@*w1_@<)$-q-g-FA>$r?z5m@NAf#Yp#}6;{8+Z_Tn5!=`cU8Nv)*V{AZ|6YgN~Mepkmi-t|tjhil-$g8oJmb{ZAUiD-B83hVn(bJ>BZi znt-zpkCkQ|P?lxz2>Pn#r5b-?IR!6fYZ1?zwO1;1s8c^I$E04)?*C7;&4nb#-0aE@x+2Wc}s-gyztFyH;em3W5-~=N1cVF_U73VHTBI& z$LlgJjovyY2fNkJN^l|emx0;j&jL9QM--WNqpgzsugjkd<%&Y?-Ss<>X$hZ=_#(Jp zSIH2GzS+4FKHKlvi(GlrE61(eD(Ir80s&@it-&vC#BH#MSZiy zVDuQvIcaq0-HYD(u1LVx(-|aw%I?zViqQ1E7HlCsta2_6q-CE*b9yzP+~)%R-l?N? zWMJWbE#gDlw=z~-35lcvmvDmaB##LQ*nLyTw|8Jj-(B>dWe6-?>9DLsi_ZFLoFMgG;Qt0}+459e07U=q1)uwFIBy?&H_!i#d9SbLe}Uon t7h~q(0cdR_RE;Jk@;@v7L1+K~ literal 0 HcmV?d00001 diff --git a/public/assets/icons/icon-512.png b/public/assets/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..43edf97c61b0198080669029c183870a11507ed7 GIT binary patch literal 18735 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

bKX+itJFiDutrUVl8c(1o9z-G;-sjN zmP2FI;yr6757)%zLu0xKYmpqXd0C59Wn8LRH`=-(TvR|^*^~T|tSi-YX}zFur^N!v zoko6F#jNtL)nv;4*GgSyRmOsY-Gr{}eRBHIVXa633 zFlo2F&h)AF(kbuoc-X3WwWfmas?C&hvIO{stW4?qNgg8OU~dNgfB{r3=rQ$$hpvUtk`r?EDt?QyR{`;h5 z!>(Y0-Z6DU%Zh3_YKh$O-c#Sz zPvC&MN5fpIrZkzdub9UfhjPEA!YKfK!dDqsub@~QQ^ZDZjigP-9^-<3>DuzNVU0t} z^YLo-_L1*NjAwEuJw#`~=b1OQc)C*rxNWm?5}kxm6pTN7Z27Gx>`#_UAHU!%CODm4 zD=^msOY7vx`-V4j=1)~5rU;|Dlba^SYq1cp3WtVE@dVg|P1R-OucoHHmCbBOWQP#8)u zl->McJmwN%rT;g|@2VnHt$V*yA8>URl8dZAOZ0GkO?Kmk!M`ufFAvbCNNHtyaf=3c zCo->u?_fxKxMe-KoP6S168c+uy^ZBHCncBu^Ltc;sL4}~YsgD(35LwW3w_CWw~p3B zw9Vt+X6#Rrm$qK_92u`d>RT=z2q&UYP}E+X-P@P#Lk(>8=eRKe?yVcRcemQp8$s6a zlDm;TLHLN&y1{z-ZoCOrI?KcmFaN~_&`|5PymhTGEyM@ixWh~K{9~Bs)+7eR2M30{ zC-lNyC>y!uT^$F)Uq6g6kF_o1jgb>SZ~!iT5Z#eAF&Hid4lT!*@$AYJWjaDF;FvE3$DJq+?0emVJ3tMtna~#Ob!+Gm*mU z!n-_n>llTPhjR((vm9vEBiFjf@bzTnzZo~l@R_I$sfS6iIBPoYfwwC%r+b2tf+pGV zJRKwDYUx%p4ikZ@4v{%X0Y3BX_f&g{fF9DJ*P6dMOf)(KCC$a#G#PLgI*p%?+kh=j z-7oWjY1n@Hv&Iu@wxnoemox z@WI@)xv`!hTzZjl3iFx*`X>^FfV`p*;%;8|Gw=Qpq>@OrbOi)FphvaddRn1ntmcr83@h_JT}zo35=ULUZH`jnvxWO*PuX^c?psgIc4 z!*-GKDBXeSR!Q=B6iGxS+3sk2S~=itJFX!>AJBgBK;5-@T+6bLs;xPmMh(Hg#LKJ%k>ZPXfXSgs))O`9FZuDwirs#F% z9iLqJ-JUN5_RiVAr~dxfnRO*p(U4)vb(O9EyqRA=P;PfCn6+$=vD!qDUq8=i9f(~h z?{FAs{vsfC>&NtMe~-ssol)Khv_)y=hxy-Dq!eV@C%pLk*q!g#^M=ape~Zm8Fu&N> zxKfPeOP#m*j?Wu%)r%&pGhF%0D0Z*qG2>(vhFzCtd{`Qv|15cC?up%p_AI!!e^yG^ z`5F6VtzLeLdxbTUs{Sc{q@&|gh;iN$qncT@Nq+PtkzrOssYL8Fi8i~dyfr$XJ4%Gb;@Zq^EPpH6Q*|hCZ=>ONh66>r zioPt*JY~K3Sry+|#?N`bpXaS%cVO=QwJ)&IaCO;lyNMs~b}T$^{PqJggU+_V)^@|~ z>YmKNoc!mtSX!LB)1?=l(-tdzx^O;el1af;q5y!rPgro2czw6w#Rb?PDYn0o6k^A=wI>2AAVd$nO!tGHVO;{yNq2npa8 z9J#WWPyU{?7H;n94BWnj{X%!UETjF8$0zf7&R%MsDi0i-esNjayp8Rbp?RC#N#Feo zIqwyHWc>&}=H>dFyZx{18G6if57+$ZYFrt&=*OIe>;8+FXWZH)IN23I4H|G8N-}d(i%Sx73vlb%?QK&I)FTPf6P%w` pQl40p%1~Zju9umYU7Va)kgAtols@~NjTBH3gQu&X%Q~loCIHU?Q(yo9 literal 0 HcmV?d00001 diff --git a/public/assets/icons/maskable-512.png b/public/assets/icons/maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..db415d2bbb87a34f273986a97c0119c50e4fd474 GIT binary patch literal 15265 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

-
- \ No newline at end of file diff --git a/public/js/fileListView.js b/public/js/fileListView.js index 6550c2e..4d5db68 100644 --- a/public/js/fileListView.js +++ b/public/js/fileListView.js @@ -157,7 +157,121 @@ function wireSelectAll(fileListContent) { } return body ?? {}; } - + // ---- Viewed badges (table + gallery) ---- +// ---------- Badge factory (center text vertically) ---------- +function makeBadge(state) { + if (!state) return null; + const el = document.createElement('span'); + el.className = 'status-badge'; + el.style.cssText = [ + 'display:inline-flex', + 'align-items:center', + 'justify-content:center', + 'vertical-align:middle', + 'margin-left:6px', + 'padding:2px 8px', + 'min-height:18px', + 'line-height:1', + 'border-radius:999px', + 'font-size:.78em', + 'border:1px solid rgba(0,0,0,.2)', + 'background:rgba(0,0,0,.06)' + ].join(';'); + + if (state.completed) { + el.classList.add('watched'); + el.textContent = (t('watched') || t('viewed') || 'Watched'); + el.style.borderColor = 'rgba(34,197,94,.45)'; + el.style.background = 'rgba(34,197,94,.12)'; + el.style.color = '#22c55e'; + return el; + } + + if (Number.isFinite(state.seconds) && Number.isFinite(state.duration) && state.duration > 0) { + const pct = Math.max(1, Math.min(99, Math.round((state.seconds / state.duration) * 100))); + el.classList.add('progress'); + el.textContent = `${pct}%`; + el.style.borderColor = 'rgba(245,158,11,.45)'; + el.style.background = 'rgba(245,158,11,.12)'; + el.style.color = '#f59e0b'; + return el; + } + + return null; +} + +// ---------- Public: set/clear badges for one file (table + gallery) ---------- +function applyBadgeToDom(name, state) { + const safe = CSS.escape(name); + + // Table + document.querySelectorAll(`tr[data-file-name="${safe}"] .name-cell, tr[data-file-name="${safe}"] .file-name-cell`) + .forEach(cell => { + cell.querySelector('.status-badge')?.remove(); + const b = makeBadge(state); + if (b) cell.appendChild(b); + }); + + // Gallery + document.querySelectorAll(`.gallery-card[data-file-name="${safe}"] .gallery-file-name`) + .forEach(title => { + title.querySelector('.status-badge')?.remove(); + const b = makeBadge(state); + if (b) title.appendChild(b); + }); +} + +export function setFileWatchedBadge(name, watched = true) { + applyBadgeToDom(name, watched ? { completed: true } : null); +} + +export function setFileProgressBadge(name, seconds, duration) { + if (duration > 0 && seconds >= 0) { + applyBadgeToDom(name, { seconds, duration, completed: seconds >= duration - 1 }); + } else { + applyBadgeToDom(name, null); + } +} + +export async function refreshViewedBadges(folder) { + let map = null; + try { + const res = await fetch(`/api/media/getViewedMap.php?folder=${encodeURIComponent(folder)}&t=${Date.now()}`, { credentials: 'include' }); + const j = await res.json(); + map = j?.map || null; + } catch { /* ignore */ } + + // Clear any existing badges + document.querySelectorAll( + '#fileList tr[data-file-name] .file-name-cell .status-badge, ' + + '#fileList tr[data-file-name] .name-cell .status-badge, ' + + '.gallery-card[data-file-name] .gallery-file-name .status-badge' + ).forEach(n => n.remove()); + + if (!map) return; + + // Table rows + document.querySelectorAll('#fileList tr[data-file-name]').forEach(tr => { + const name = tr.getAttribute('data-file-name'); + const state = map[name]; + if (!state) return; + const cell = tr.querySelector('.name-cell, .file-name-cell'); + if (!cell) return; + const badge = makeBadge(state); + if (badge) cell.appendChild(badge); + }); + + // Gallery cards + document.querySelectorAll('.gallery-card[data-file-name]').forEach(card => { + const name = card.getAttribute('data-file-name'); + const state = map[name]; + if (!state) return; + const title = card.querySelector('.gallery-file-name'); + if (!title) return; + const badge = makeBadge(state); + if (badge) title.appendChild(badge); + }); +} /** * Convert a file size string (e.g. "456.9KB", "1.2 MB", "1024") into bytes. */ @@ -548,6 +662,7 @@ function searchFiles(searchTerm) { } updateFileActionButtons(); fileListContainer.style.visibility = "visible"; + // ----- FOLDERS NEXT (populate strip when ready; doesn't block rows) ----- try { @@ -712,9 +827,14 @@ function searchFiles(searchTerm) { if (totalFiles > 0) { filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => { // Build row with a neutral base, then correct the links/preview below. - let rowHTML = buildFileTableRow(file, fakeBase); // Give the row an ID so we can patch attributes safely - rowHTML = rowHTML.replace(" 0) { @@ -724,9 +844,13 @@ function searchFiles(searchTerm) { }); tagBadgesHTML += ""; } - rowsHTML += rowHTML.replace(/()(.*?)(<\/td>)/, (match, p1, p2, p3) => { - return p1 + p2 + tagBadgesHTML + p3; - }); + rowsHTML += rowHTML.replace( + /()([\s\S]*?)(<\/td>)/, + (m, open, inner, close) => { + // keep the original filename content, then add your tag badges, then close + return `${open}${inner}${tagBadgesHTML}${close}`; + } + ); }); } else { rowsHTML += `No files found.`; @@ -904,6 +1028,7 @@ function searchFiles(searchTerm) { }); }); updateFileActionButtons(); + document.querySelectorAll("#fileList tbody tr").forEach(row => { row.setAttribute("draggable", "true"); import('./fileDragDrop.js?v={{APP_QVER}}').then(module => { @@ -914,6 +1039,7 @@ function searchFiles(searchTerm) { btn.addEventListener("click", e => e.stopPropagation()); }); bindFileListContextMenu(); + refreshViewedBadges(folder).catch(() => {}); } // A helper to compute the max image height based on the current column count. @@ -1040,6 +1166,7 @@ function searchFiles(searchTerm) { // card with checkbox, preview, info, buttons galleryHTML += `

${t("password_optional")}

- + - @@ -79,392 +69,524 @@ export function openShareModal(file, folder) { document.body.appendChild(modal); modal.style.display = "block"; - // Close handler - document.getElementById("closeShareModal") - .addEventListener("click", () => modal.remove()); + document.getElementById("closeShareModal").addEventListener("click", () => modal.remove()); + document.getElementById("shareExpiration").addEventListener("change", e => { + const container = document.getElementById("customExpirationContainer"); + container.style.display = e.target.value === "custom" ? "block" : "none"; + }); - // Show/hide custom-duration inputs - document.getElementById("shareExpiration") - .addEventListener("change", e => { - const container = document.getElementById("customExpirationContainer"); - container.style.display = e.target.value === "custom" ? "block" : "none"; - }); + document.getElementById("generateShareLinkBtn").addEventListener("click", () => { + const sel = document.getElementById("shareExpiration"); + let value, unit; - // Generate share link - document.getElementById("generateShareLinkBtn") - .addEventListener("click", () => { - const sel = document.getElementById("shareExpiration"); - let value, unit; - - if (sel.value === "custom") { - value = parseInt(document.getElementById("customExpirationValue").value, 10); - unit = document.getElementById("customExpirationUnit").value; - } else { - value = parseInt(sel.value, 10); - unit = "minutes"; - } - - const password = document.getElementById("sharePassword").value; - - fetch("/api/file/createShareLink.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify({ - folder, - file: file.name, - expirationValue: value, - expirationUnit: unit, - password - }) - }) - .then(res => res.json()) - .then(data => { - if (data.token) { - const url = `${window.location.origin}/api/file/share.php?token=${encodeURIComponent(data.token)}`; - document.getElementById("shareLinkInput").value = url; - document.getElementById("shareLinkDisplay").style.display = "block"; - } else { - showToast(t("error_generating_share") + ": " + (data.error || "Unknown")); - } - }) - .catch(err => { - console.error(err); - showToast(t("error_generating_share")); - }); - }); - - // Copy to clipboard - document.getElementById("copyShareLinkBtn") - .addEventListener("click", () => { - const input = document.getElementById("shareLinkInput"); - input.select(); - document.execCommand("copy"); - showToast(t("link_copied")); - }); -} - -export function previewFile(fileUrl, fileName) { - let modal = document.getElementById("filePreviewModal"); - if (!modal) { - modal = document.createElement("div"); - modal.id = "filePreviewModal"; - Object.assign(modal.style, { - position: "fixed", - top: "0", - left: "0", - width: "100vw", - height: "100vh", - backgroundColor: "rgba(0,0,0,0.7)", - display: "flex", - justifyContent: "center", - alignItems: "center", - zIndex: "1000" - }); - modal.innerHTML = ` - `; - document.body.appendChild(modal); - - function closeModal() { - const mediaElements = modal.querySelectorAll("video, audio"); - mediaElements.forEach(media => { - media.pause(); - if (media.tagName.toLowerCase() !== 'video') { - try { media.currentTime = 0; } catch (e) { } - } - }); - modal.remove(); + if (sel.value === "custom") { + value = parseInt(document.getElementById("customExpirationValue").value, 10); + unit = document.getElementById("customExpirationUnit").value; + } else { + value = parseInt(sel.value, 10); + unit = "minutes"; } - document.getElementById("closeFileModal").addEventListener("click", closeModal); - modal.addEventListener("click", function (e) { - if (e.target === modal) { - closeModal(); - } - }); - } - modal.querySelector("h4").textContent = fileName; - const container = modal.querySelector(".file-preview-container"); - container.innerHTML = ""; + const password = document.getElementById("sharePassword").value; - const extension = fileName.split('.').pop().toLowerCase(); - const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(fileName); + fetch("/api/file/createShareLink.php", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, + body: JSON.stringify({ folder, file: file.name, expirationValue: value, expirationUnit: unit, password }) + }) + .then(res => res.json()) + .then(data => { + if (data.token) { + const url = `${window.location.origin}/api/file/share.php?token=${encodeURIComponent(data.token)}`; + document.getElementById("shareLinkInput").value = url; + document.getElementById("shareLinkDisplay").style.display = "block"; + } else { + showToast(t("error_generating_share") + ": " + (data.error || "Unknown")); + } + }) + .catch(err => { + console.error(err); + showToast(t("error_generating_share")); + }); + }); + + document.getElementById("copyShareLinkBtn").addEventListener("click", () => { + const input = document.getElementById("shareLinkInput"); + input.select(); + document.execCommand("copy"); + showToast(t("link_copied")); + }); +} + +/* -------------------------------- Media modal viewer -------------------------------- */ +const IMG_RE = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i; +const VID_RE = /\.(mp4|mkv|webm|mov|ogv)$/i; +const AUD_RE = /\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i; + +function ensureMediaModal() { + let overlay = document.getElementById("filePreviewModal"); + if (overlay) return overlay; + + overlay = document.createElement("div"); + overlay.id = "filePreviewModal"; + Object.assign(overlay.style, { + position: "fixed", + inset: "0", + width: "100vw", + height: "100vh", + backgroundColor: "rgba(0,0,0,0.7)", + display: "flex", + justifyContent: "center", + alignItems: "center", + zIndex: "1000" + }); + + const root = document.documentElement; + const styles = getComputedStyle(root); + const isDark = root.classList.contains('dark-mode'); + const panelBg = styles.getPropertyValue('--panel-bg').trim() || styles.getPropertyValue('--bg-color').trim() || (isDark ? '#121212' : '#ffffff'); + const textCol = styles.getPropertyValue('--text-color').trim() || (isDark ? '#eaeaea' : '#111111'); + + const navBg = isDark ? 'rgba(255,255,255,.28)' : 'rgba(0,0,0,.45)'; + const navFg = '#fff'; + const navBorder = isDark ? 'rgba(255,255,255,.35)' : 'rgba(0,0,0,.25)'; + + overlay.innerHTML = ` + `; + + document.body.appendChild(overlay); + + function closeModal() { + try { overlay.querySelectorAll("video,audio").forEach(m => { try{m.pause()}catch(_){}}); } catch {} + if (overlay._onKey) window.removeEventListener('keydown', overlay._onKey); + overlay.remove(); + } + overlay.querySelector("#closeFileModal").addEventListener("click", closeModal); + overlay.addEventListener("click", (e) => { if (e.target === overlay) closeModal(); }); + + return overlay; +} + +function setTitle(overlay, name) { + const el = overlay.querySelector('.media-title-badge'); + if (el) el.textContent = name || ''; +} + +function makeMI(name, title) { + const b = document.createElement('button'); + b.className = `material-icons ${name}`; + b.textContent = name; // Material Icons font + b.title = title; + Object.assign(b.style, { + width: "32px", + height: "32px", + display: "flex", + alignItems: "center", + justifyContent: "center", + background: "rgba(0,0,0,.25)", + border: "1px solid rgba(255,255,255,.25)", + cursor: "pointer", + userSelect: "none", + fontSize: "20px", + padding: "0", + borderRadius: "8px", + color: "#fff", + lineHeight: "1" + }); + return b; +} + +function setNavVisibility(overlay, showPrev, showNext) { + const prev = overlay.querySelector('.nav-left'); + const next = overlay.querySelector('.nav-right'); + prev.style.display = showPrev ? 'inline-flex' : 'none'; + next.style.display = showNext ? 'inline-flex' : 'none'; +} + +function setRowWatchedBadge(name, watched) { + try { + const cell = document.querySelector(`tr[data-file-name="${CSS.escape(name)}"] .name-cell`); + if (!cell) return; + const old = cell.querySelector('.status-badge.watched'); + if (watched) { + if (!old) { + const b = document.createElement('span'); + b.className = 'status-badge watched'; + b.textContent = t("watched") || t("viewed") || "Watched"; + b.style.marginLeft = "6px"; + cell.appendChild(b); + } + } else if (old) { + old.remove(); + } + } catch {} +} + +/* -------------------------------- Entry -------------------------------- */ +export function previewFile(fileUrl, fileName) { + const overlay = ensureMediaModal(); + const container = overlay.querySelector(".file-preview-container"); + const actionWrap = overlay.querySelector(".media-actions-bar .action-group"); + const statusChip = overlay.querySelector(".media-actions-bar .status-chip"); + + // replace nav buttons to clear old listeners + let prevBtn = overlay.querySelector('.nav-left'); + let nextBtn = overlay.querySelector('.nav-right'); + const newPrev = prevBtn.cloneNode(true); + const newNext = nextBtn.cloneNode(true); + prevBtn.replaceWith(newPrev); + nextBtn.replaceWith(newNext); + prevBtn = newPrev; nextBtn = newNext; + + // reset + container.innerHTML = ""; + actionWrap.innerHTML = ""; + if (statusChip) statusChip.style.display = 'none'; + if (overlay._onKey) window.removeEventListener('keydown', overlay._onKey); + overlay._onKey = null; + + const folder = window.currentFolder || 'root'; + const name = fileName; + const lower = (name || '').toLowerCase(); + const isImage = IMG_RE.test(lower); + const isVideo = VID_RE.test(lower); + const isAudio = AUD_RE.test(lower); + + setTitle(overlay, name); + + /* -------------------- IMAGES -------------------- */ if (isImage) { - // Create the image element with default transform data. const img = document.createElement("img"); img.src = fileUrl; img.className = "image-modal-img"; - img.style.maxWidth = "80vw"; - img.style.maxHeight = "80vh"; + img.style.maxWidth = "88vw"; + img.style.maxHeight = "88vh"; img.style.transition = "transform 0.3s ease"; img.dataset.scale = 1; img.dataset.rotate = 0; - img.style.position = 'relative'; - img.style.zIndex = '1'; + container.appendChild(img); - // Filter gallery images for navigation. - const images = fileData.filter(file => /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)); + const zoomInBtn = makeMI('zoom_in', t('zoom_in') || 'Zoom In'); + const zoomOutBtn = makeMI('zoom_out', t('zoom_out') || 'Zoom Out'); + const rotateLeft = makeMI('rotate_left', t('rotate_left') || 'Rotate Left'); + const rotateRight = makeMI('rotate_right', t('rotate_right') || 'Rotate Right'); + actionWrap.appendChild(zoomInBtn); + actionWrap.appendChild(zoomOutBtn); + actionWrap.appendChild(rotateLeft); + actionWrap.appendChild(rotateRight); - // Create a flex wrapper to hold left panel, center image, and right panel. - const wrapper = document.createElement('div'); - wrapper.className = 'image-wrapper'; - wrapper.style.display = 'flex'; - wrapper.style.alignItems = 'center'; - wrapper.style.justifyContent = 'center'; - wrapper.style.position = 'relative'; + zoomInBtn.addEventListener('click', (e) => { + e.stopPropagation(); + let s = parseFloat(img.dataset.scale) || 1; s += 0.1; + img.dataset.scale = s; + img.style.transform = `scale(${s}) rotate(${img.dataset.rotate}deg)`; + }); + zoomOutBtn.addEventListener('click', (e) => { + e.stopPropagation(); + let s = parseFloat(img.dataset.scale) || 1; s = Math.max(0.1, s - 0.1); + img.dataset.scale = s; + img.style.transform = `scale(${s}) rotate(${img.dataset.rotate}deg)`; + }); + rotateLeft.addEventListener('click', (e) => { + e.stopPropagation(); + let r = parseFloat(img.dataset.rotate) || 0; r = (r - 90 + 360) % 360; + img.dataset.rotate = r; + img.style.transform = `scale(${img.dataset.scale}) rotate(${r}deg)`; + }); + rotateRight.addEventListener('click', (e) => { + e.stopPropagation(); + let r = parseFloat(img.dataset.rotate) || 0; r = (r + 90) % 360; + img.dataset.rotate = r; + img.style.transform = `scale(${img.dataset.scale}) rotate(${r}deg)`; + }); - // --- Left Panel: Contains Zoom controls (top) and Prev button (bottom) --- - const leftPanel = document.createElement('div'); - leftPanel.className = 'left-panel'; - leftPanel.style.display = 'flex'; - leftPanel.style.flexDirection = 'column'; - leftPanel.style.justifyContent = 'space-between'; - leftPanel.style.alignItems = 'center'; - leftPanel.style.width = '60px'; - leftPanel.style.height = '100%'; - leftPanel.style.zIndex = '10'; + const images = (Array.isArray(fileData) ? fileData : []).filter(f => IMG_RE.test(f.name)); + overlay.mediaType = 'image'; + overlay.mediaList = images; + overlay.mediaIndex = Math.max(0, images.findIndex(f => f.name === name)); + setNavVisibility(overlay, images.length > 1, images.length > 1); - // Top container for zoom buttons. - const leftTop = document.createElement('div'); - leftTop.style.display = 'flex'; - leftTop.style.flexDirection = 'column'; - leftTop.style.gap = '4px'; - // Zoom In button. - const zoomInBtn = document.createElement('button'); - zoomInBtn.className = 'material-icons zoom_in'; - zoomInBtn.title = 'Zoom In'; - zoomInBtn.style.background = 'transparent'; - zoomInBtn.style.border = 'none'; - zoomInBtn.style.cursor = 'pointer'; - zoomInBtn.textContent = 'zoom_in'; - // Zoom Out button. - const zoomOutBtn = document.createElement('button'); - zoomOutBtn.className = 'material-icons zoom_out'; - zoomOutBtn.title = 'Zoom Out'; - zoomOutBtn.style.background = 'transparent'; - zoomOutBtn.style.border = 'none'; - zoomOutBtn.style.cursor = 'pointer'; - zoomOutBtn.textContent = 'zoom_out'; - leftTop.appendChild(zoomInBtn); - leftTop.appendChild(zoomOutBtn); - leftPanel.appendChild(leftTop); + const navigate = (dir) => { + if (!overlay.mediaList || overlay.mediaList.length < 2) return; + overlay.mediaIndex = (overlay.mediaIndex + dir + overlay.mediaList.length) % overlay.mediaList.length; + const newFile = overlay.mediaList[overlay.mediaIndex].name; + setTitle(overlay, newFile); + img.dataset.scale = 1; + img.dataset.rotate = 0; + img.style.transform = 'scale(1) rotate(0deg)'; + img.src = buildPreviewUrl(folder, newFile); + }; - // Bottom container for prev button. - const leftBottom = document.createElement('div'); - leftBottom.style.display = 'flex'; - leftBottom.style.justifyContent = 'center'; - leftBottom.style.alignItems = 'center'; - leftBottom.style.width = '100%'; if (images.length > 1) { - const prevBtn = document.createElement("button"); - prevBtn.textContent = "‹"; - prevBtn.className = "gallery-nav-btn"; - prevBtn.style.background = 'transparent'; - prevBtn.style.border = 'none'; - prevBtn.style.color = 'white'; - prevBtn.style.fontSize = '48px'; - prevBtn.style.cursor = 'pointer'; - prevBtn.addEventListener("click", function (e) { - e.stopPropagation(); - // Safety check: - if (!modal.galleryImages || modal.galleryImages.length === 0) return; - modal.galleryCurrentIndex = (modal.galleryCurrentIndex - 1 + modal.galleryImages.length) % modal.galleryImages.length; - let newFile = modal.galleryImages[modal.galleryCurrentIndex]; - modal.querySelector("h4").textContent = newFile.name; - img.src = buildPreviewUrl(window.currentFolder || 'root', newFile.name); - // Reset transforms. - img.dataset.scale = 1; - img.dataset.rotate = 0; - img.style.transform = 'scale(1) rotate(0deg)'; - }); - leftBottom.appendChild(prevBtn); - } else { - // Insert an empty placeholder for consistent layout. - leftBottom.innerHTML = ' '; + prevBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(-1); }); + nextBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(+1); }); + const onKey = (e) => { + if (!document.body.contains(overlay)) { window.removeEventListener("keydown", onKey); return; } + if (e.key === "ArrowLeft") navigate(-1); + if (e.key === "ArrowRight") navigate(+1); + }; + window.addEventListener("keydown", onKey); + overlay._onKey = onKey; } - leftPanel.appendChild(leftBottom); - // --- Center Panel: Contains the image --- - const centerPanel = document.createElement('div'); - centerPanel.className = 'center-image-container'; - centerPanel.style.flexGrow = '1'; - centerPanel.style.textAlign = 'center'; - centerPanel.style.position = 'relative'; - centerPanel.style.zIndex = '1'; - centerPanel.appendChild(img); + overlay.style.display = "flex"; + return; + } - // --- Right Panel: Contains Rotate controls (top) and Next button (bottom) --- - const rightPanel = document.createElement('div'); - rightPanel.className = 'right-panel'; - rightPanel.style.display = 'flex'; - rightPanel.style.flexDirection = 'column'; - rightPanel.style.justifyContent = 'space-between'; - rightPanel.style.alignItems = 'center'; - rightPanel.style.width = '60px'; - rightPanel.style.height = '100%'; - rightPanel.style.zIndex = '10'; + /* -------------------- PDF => new tab -------------------- */ + if (lower.endsWith('.pdf')) { + const separator = fileUrl.includes('?') ? '&' : '?'; + const urlWithTs = fileUrl + separator + 't=' + Date.now(); + window.open(urlWithTs, "_blank"); + overlay.remove(); + return; + } - // Top container for rotate buttons. - const rightTop = document.createElement('div'); - rightTop.style.display = 'flex'; - rightTop.style.flexDirection = 'column'; - rightTop.style.gap = '4px'; - // Rotate Left button. - const rotateLeftBtn = document.createElement('button'); - rotateLeftBtn.className = 'material-icons rotate_left'; - rotateLeftBtn.title = 'Rotate Left'; - rotateLeftBtn.style.background = 'transparent'; - rotateLeftBtn.style.border = 'none'; - rotateLeftBtn.style.cursor = 'pointer'; - rotateLeftBtn.textContent = 'rotate_left'; - // Rotate Right button. - const rotateRightBtn = document.createElement('button'); - rotateRightBtn.className = 'material-icons rotate_right'; - rotateRightBtn.title = 'Rotate Right'; - rotateRightBtn.style.background = 'transparent'; - rotateRightBtn.style.border = 'none'; - rotateRightBtn.style.cursor = 'pointer'; - rotateRightBtn.textContent = 'rotate_right'; - rightTop.appendChild(rotateLeftBtn); - rightTop.appendChild(rotateRightBtn); - rightPanel.appendChild(rightTop); + /* -------------------- VIDEOS -------------------- */ + if (isVideo) { + let video = document.createElement("video"); // let so we can rebind + video.controls = true; + video.style.maxWidth = "88vw"; + video.style.maxHeight = "88vh"; + video.style.objectFit = "contain"; + container.appendChild(video); - // Bottom container for next button. - const rightBottom = document.createElement('div'); - rightBottom.style.display = 'flex'; - rightBottom.style.justifyContent = 'center'; - rightBottom.style.alignItems = 'center'; - rightBottom.style.width = '100%'; - if (images.length > 1) { - const nextBtn = document.createElement("button"); - nextBtn.textContent = "›"; - nextBtn.className = "gallery-nav-btn"; - nextBtn.style.background = 'transparent'; - nextBtn.style.border = 'none'; - nextBtn.style.color = 'white'; - nextBtn.style.fontSize = '48px'; - nextBtn.style.cursor = 'pointer'; - nextBtn.addEventListener("click", function (e) { - e.stopPropagation(); - // Safety check: - if (!modal.galleryImages || modal.galleryImages.length === 0) return; - modal.galleryCurrentIndex = (modal.galleryCurrentIndex + 1) % modal.galleryImages.length; - let newFile = modal.galleryImages[modal.galleryCurrentIndex]; - modal.querySelector("h4").textContent = newFile.name; - img.src = buildPreviewUrl(window.currentFolder || 'root', newFile.name); - // Reset transforms. - img.dataset.scale = 1; - img.dataset.rotate = 0; - img.style.transform = 'scale(1) rotate(0deg)'; - }); - rightBottom.appendChild(nextBtn); - } else { - // Insert a placeholder so that center remains properly aligned. - rightBottom.innerHTML = ' '; + const markBtn = document.createElement('button'); + const clearBtn = document.createElement('button'); + markBtn.className = 'btn btn-sm btn-success'; + clearBtn.className = 'btn btn-sm btn-secondary'; + markBtn.textContent = t("mark_as_viewed") || "Mark as viewed"; + clearBtn.textContent = t("clear_progress") || "Clear progress"; + actionWrap.appendChild(markBtn); + actionWrap.appendChild(clearBtn); + + const videos = (Array.isArray(fileData) ? fileData : []).filter(f => VID_RE.test(f.name)); + overlay.mediaType = 'video'; + overlay.mediaList = videos; + overlay.mediaIndex = Math.max(0, videos.findIndex(f => f.name === name)); + setNavVisibility(overlay, videos.length > 1, videos.length > 1); + + const setVideoSrc = (nm) => { video.src = buildPreviewUrl(folder, nm); setTitle(overlay, nm); }; + + const SAVE_INTERVAL_MS = 5000; + let lastSaveAt = 0; + let pending = false; + + async function getProgress(nm) { + try { + const res = await fetch(`/api/media/getProgress.php?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(nm)}&t=${Date.now()}`, { credentials: "include" }); + const data = await res.json(); + return data && data.state ? data.state : null; + } catch { return null; } } - rightPanel.appendChild(rightBottom); - - // Assemble panels into the wrapper. - wrapper.appendChild(leftPanel); - wrapper.appendChild(centerPanel); - wrapper.appendChild(rightPanel); - container.appendChild(wrapper); - - // --- Set up zoom controls event listeners --- - zoomInBtn.addEventListener('click', function (e) { - e.stopPropagation(); - let scale = parseFloat(img.dataset.scale) || 1; - scale += 0.1; - img.dataset.scale = scale; - img.style.transform = 'scale(' + scale + ') rotate(' + img.dataset.rotate + 'deg)'; - }); - zoomOutBtn.addEventListener('click', function (e) { - e.stopPropagation(); - let scale = parseFloat(img.dataset.scale) || 1; - scale = Math.max(0.1, scale - 0.1); - img.dataset.scale = scale; - img.style.transform = 'scale(' + scale + ') rotate(' + img.dataset.rotate + 'deg)'; - }); - - // Attach rotation control listeners (always present now). - rotateLeftBtn.addEventListener('click', function (e) { - e.stopPropagation(); - let rotate = parseFloat(img.dataset.rotate) || 0; - rotate = (rotate - 90 + 360) % 360; - img.dataset.rotate = rotate; - img.style.transform = 'scale(' + img.dataset.scale + ') rotate(' + rotate + 'deg)'; - }); - rotateRightBtn.addEventListener('click', function (e) { - e.stopPropagation(); - let rotate = parseFloat(img.dataset.rotate) || 0; - rotate = (rotate + 90) % 360; - img.dataset.rotate = rotate; - img.style.transform = 'scale(' + img.dataset.scale + ') rotate(' + rotate + 'deg)'; - }); - - // Save gallery details if there is more than one image. - if (images.length > 1) { - modal.galleryImages = images; - modal.galleryCurrentIndex = images.findIndex(f => f.name === fileName); + async function sendProgress({nm, seconds, duration, completed, clear}) { + try { + pending = true; + const res = await fetch("/api/media/updateProgress.php", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, + body: JSON.stringify({ folder, file: nm, seconds, duration, completed, clear }) + }); + const data = await res.json(); + pending = false; + return data; + } catch (e) { pending = false; console.error(e); return null; } } - } else { - // Handle non-image file previews. - if (extension === "pdf") { - // build a cache‐busted URL - const separator = fileUrl.includes('?') ? '&' : '?'; - const urlWithTs = fileUrl + separator + 't=' + Date.now(); + const lsKey = (nm) => `videoProgress-${folder}/${nm}`; - // open in a new tab (avoids CSP frame-ancestors) - window.open(urlWithTs, "_blank"); + function renderStatus(state) { + if (!statusChip) return; + // Completed + if (state && state.completed) { + + statusChip.textContent = (t('viewed') || 'Viewed') + ' ✓'; + statusChip.style.display = 'inline-block'; + statusChip.style.borderColor = 'rgba(34,197,94,.45)'; + statusChip.style.background = 'rgba(34,197,94,.15)'; + statusChip.style.color = '#22c55e'; + markBtn.style.display = 'none'; + clearBtn.style.display = ''; + clearBtn.textContent = t('reset_progress') || t('clear_progress') || 'Reset'; + return; + } + // In progress + if (state && Number.isFinite(state.seconds) && Number.isFinite(state.duration) && state.duration > 0) { + const pct = Math.max(1, Math.min(99, Math.round((state.seconds / state.duration) * 100))); + statusChip.textContent = `${pct}%`; + statusChip.style.display = 'inline-block'; + statusChip.style.borderColor = 'rgba(250,204,21,.45)'; + statusChip.style.background = 'rgba(250,204,21,.15)'; + statusChip.style.color = '#facc15'; + markBtn.style.display = ''; + clearBtn.style.display = ''; + clearBtn.textContent = t('reset_progress') || t('clear_progress') || 'Reset'; + return; + } + // No progress + statusChip.style.display = 'none'; + markBtn.style.display = ''; + clearBtn.style.display = 'none'; + } - // tear down the just-created modal - const modal = document.getElementById("filePreviewModal"); - if (modal) modal.remove(); + function bindVideoEvents(nm) { + const nv = video.cloneNode(true); + video.replaceWith(nv); + video = nv; - // stop further preview logic - return; - } else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) { - const video = document.createElement("video"); - video.src = fileUrl; - video.controls = true; - video.className = "image-modal-img"; - - const progressKey = 'videoProgress-' + fileUrl; - video.addEventListener("loadedmetadata", () => { - const savedTime = localStorage.getItem(progressKey); - if (savedTime) { - video.currentTime = parseFloat(savedTime); + video.addEventListener("loadedmetadata", async () => { + try { + const state = await getProgress(nm); + if (state && Number.isFinite(state.seconds) && state.seconds > 0 && state.seconds < (video.duration || Infinity)) { + video.currentTime = state.seconds; + const seconds = Math.floor(video.currentTime || 0); +const duration = Math.floor(video.duration || 0); +setFileProgressBadge(nm, seconds, duration); + showToast((t("resumed_from") || "Resumed from") + " " + Math.floor(state.seconds) + "s"); + } else { + const ls = localStorage.getItem(lsKey(nm)); + if (ls) video.currentTime = parseFloat(ls); + } + renderStatus(state || null); + } catch { + renderStatus(null); } }); - video.addEventListener("timeupdate", () => { - localStorage.setItem(progressKey, video.currentTime); + + video.addEventListener("timeupdate", async () => { + const now = Date.now(); + if ((now - lastSaveAt) < SAVE_INTERVAL_MS || pending) return; + lastSaveAt = now; + const seconds = Math.floor(video.currentTime || 0); + const duration = Math.floor(video.duration || 0); + sendProgress({ nm, seconds, duration }); + setFileProgressBadge(nm, seconds, duration); + try { localStorage.setItem(lsKey(nm), String(seconds)); } catch {} + renderStatus({ seconds, duration, completed: false }); }); - video.addEventListener("ended", () => { - localStorage.removeItem(progressKey); + + video.addEventListener("ended", async () => { + const duration = Math.floor(video.duration || 0); + await sendProgress({ nm, seconds: duration, duration, completed: true }); + try { localStorage.removeItem(lsKey(nm)); } catch {} + showToast(t("marked_viewed") || "Marked as viewed"); + setFileWatchedBadge(nm, true); + renderStatus({ seconds: duration, duration, completed: true }); }); - container.appendChild(video); - } else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(fileName)) { - const audio = document.createElement("audio"); - audio.src = fileUrl; - audio.controls = true; - audio.className = "audio-modal"; - audio.style.maxWidth = "80vw"; - container.appendChild(audio); - } else { - container.textContent = "Preview not available for this file type."; + + markBtn.onclick = async () => { + const duration = Math.floor(video.duration || 0); + await sendProgress({ nm, seconds: duration, duration, completed: true }); + showToast(t("marked_viewed") || "Marked as viewed"); + setFileWatchedBadge(nm, true); + renderStatus({ seconds: duration, duration, completed: true }); + }; + clearBtn.onclick = async () => { + await sendProgress({ nm, seconds: 0, duration: null, completed: false, clear: true }); + try { localStorage.removeItem(lsKey(nm)); } catch {} + showToast(t("progress_cleared") || "Progress cleared"); + setFileWatchedBadge(nm, false); + renderStatus(null); + }; } + + const navigate = (dir) => { + if (!overlay.mediaList || overlay.mediaList.length < 2) return; + overlay.mediaIndex = (overlay.mediaIndex + dir + overlay.mediaList.length) % overlay.mediaList.length; + const nm = overlay.mediaList[overlay.mediaIndex].name; + setVideoSrc(nm); + bindVideoEvents(nm); + }; + + if (videos.length > 1) { + prevBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(-1); }); + nextBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(+1); }); + const onKey = (e) => { + if (!document.body.contains(overlay)) { window.removeEventListener("keydown", onKey); return; } + if (e.key === "ArrowLeft") navigate(-1); + if (e.key === "ArrowRight") navigate(+1); + }; + window.addEventListener("keydown", onKey); + overlay._onKey = onKey; + } + + setVideoSrc(name); + renderStatus(null); + bindVideoEvents(name); + overlay.style.display = "flex"; + return; + } + + /* -------------------- AUDIO / OTHER -------------------- */ + if (isAudio) { + const audio = document.createElement("audio"); + audio.src = fileUrl; + audio.controls = true; + audio.className = "audio-modal"; + audio.style.maxWidth = "88vw"; + container.appendChild(audio); + overlay.style.display = "flex"; + } else { + container.textContent = t("preview_not_available") || "Preview not available for this file type."; + overlay.style.display = "flex"; } - modal.style.display = "flex"; } -// Preserve original functionality. +/* -------------------------------- Small display helper -------------------------------- */ export function displayFilePreview(file, container) { const actualFile = file.file || file; if (!(actualFile instanceof File)) { @@ -472,10 +594,9 @@ export function displayFilePreview(file, container) { return; } container.style.display = "inline-block"; - while (container.firstChild) { - container.removeChild(container.firstChild); - } - if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(actualFile.name)) { + while (container.firstChild) container.removeChild(container.firstChild); + + if (IMG_RE.test(actualFile.name)) { const img = document.createElement("img"); img.src = URL.createObjectURL(actualFile); img.classList.add("file-preview-img"); @@ -488,5 +609,6 @@ export function displayFilePreview(file, container) { } } +// expose for HTML onclick usage window.previewFile = previewFile; window.openShareModal = openShareModal; \ No newline at end of file diff --git a/public/js/i18n.js b/public/js/i18n.js index ba66d38..3484e48 100644 --- a/public/js/i18n.js +++ b/public/js/i18n.js @@ -302,7 +302,17 @@ const translations = { "acl_move_folder_info": "Moving folders is restricted to folder owners or managers. Destination folders must also allow moves in.", "context_move_folder": "Move Folder...", "context_move_here": "Move Here", - "context_move_cancel": "Cancel Move" + "context_move_cancel": "Cancel Move", + "mark_as_viewed": "Mark as viewed", + "viewed": "Viewed", + "resumed_from": "Resumed from", + "clear_progress": "Clear progress", + "marked_viewed": "Marked as viewed", + "progress_cleared": "Progress cleared", + "previous": "Previous", + "next": "Next", + "watched": "Watched", + "reset_progress": "Reset Progress" }, es: { "please_log_in_to_continue": "Por favor, inicie sesión para continuar.", diff --git a/public/js/main.js b/public/js/main.js index 90d3fda..70e4989 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1057,4 +1057,52 @@ function bindDarkMode() { if (overlay) overlay.style.display = 'none'; }, { once: true }); +})(); + + +// --- Mobile switcher + PWA SW (mobile-only) --- +(() => { + // keep it simple + robust + const qs = new URLSearchParams(location.search); + const hasFrAppHint = qs.get('frapp') === '1'; + + const isStandalone = + (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) || + (typeof navigator.standalone === 'boolean' && navigator.standalone); + + const isCapUA = /\bCapacitor\b/i.test(navigator.userAgent); + const hasCapBridge = !!(window.Capacitor && window.Capacitor.Plugins); + + // “mobile-ish”: native mobile UAs OR touch + reasonably narrow viewport (covers iPad-on-Mac UA) + const isMobileish = + /Android|iPhone|iPad|iPod|Mobile|Silk|IEMobile|Opera Mini/i.test(navigator.userAgent) || + (navigator.maxTouchPoints > 1 && Math.min(screen.width, screen.height) <= 900); + + // load the switcher only in the mobile app, or mobile standalone PWA, or when explicitly hinted + const shouldLoadSwitcher = + hasCapBridge || isCapUA || (isStandalone && isMobileish) || (hasFrAppHint && isMobileish); + + // expose a flag to inspect later + window.FR_APP = !!(hasCapBridge || isCapUA || (isStandalone && isMobileish)); + + const QVER = (window.APP_QVER && String(window.APP_QVER)) || '{{APP_QVER}}'; + + if (shouldLoadSwitcher) { + import(`/js/mobile/switcher.js?v=${encodeURIComponent(QVER)}`) + .then(() => { + if (hasFrAppHint && !sessionStorage.getItem('frx_opened_once')) { + sessionStorage.setItem('frx_opened_once', '1'); + window.dispatchEvent(new CustomEvent('frx:openSwitcher')); + } + }) + .catch(err => console.info('[FileRise] switcher import failed:', err)); + } + + // SW only for web (https or localhost), never in Capacitor + const onHttps = location.protocol === 'https:' || location.hostname === 'localhost'; + if ('serviceWorker' in navigator && onHttps && !hasCapBridge && !isCapUA) { + window.addEventListener('load', () => { + navigator.serviceWorker.register(`/js/pwa/sw.js?v=${encodeURIComponent(QVER)}`).catch(() => {}); + }); + } })(); \ No newline at end of file diff --git a/public/js/mobile/switcher.js b/public/js/mobile/switcher.js new file mode 100644 index 0000000..447df3c --- /dev/null +++ b/public/js/mobile/switcher.js @@ -0,0 +1,287 @@ +(function(){ + const isCap = !!window.Capacitor || /Capacitor/i.test(navigator.userAgent); + if (!isCap) return; + if ((location.origin || '').startsWith('capacitor://')) return; + + const Plugins = (window.Capacitor && window.Capacitor.Plugins) || {}; + const Pref = Plugins.Preferences ? { + get: ({key}) => Plugins.Preferences.get({key}), + set: ({key,value}) => Plugins.Preferences.set({key,value}), + remove:({key}) => Plugins.Preferences.remove({key}) + } : { + get: async ({key}) => ({ value: localStorage.getItem(key) || null }), + set: async ({key,value}) => localStorage.setItem(key, value), + remove: async ({key}) => localStorage.removeItem(key) + }; + const Http = (Plugins.Http || Plugins.CapacitorHttp) || null; + + const K_INST='fr_instances_v1', K_ACTIVE='fr_active_v1', K_STATUS='fr_status_v1'; + + const $ = s => document.querySelector(s); + const el = (t,a={},html='') => { const n=document.createElement(t); for (const k in a) n.setAttribute(k,a[k]); n.innerHTML=html; return n; }; + const normalize = u => { if(!u) return ''; let v=u.trim(); if(!/^https?:\/\//i.test(v)) v='https://'+v; return v.replace(/\/+$/,''); }; + const host = u => { try{ return new URL(normalize(u)).hostname }catch{ return '' } }; + const originOf = u => { try{ return new URL(normalize(u)).origin }catch{ return '' } }; + const faviconUrl = u => { try{ const x=new URL(normalize(u)); return x.origin+'/favicon.ico' }catch{ return '' } }; + const initialsIcon = (hn='FR') => { + const t=(hn||'FR').replace(/^www\./,'').slice(0,2).toUpperCase(); + const svg=` + + ${t}`; + return 'data:image/svg+xml;utf8,'+encodeURIComponent(svg); + }; + + async function getStatusCache(){ const raw=(await Pref.get({key:K_STATUS})).value; try{ return raw?JSON.parse(raw):{} }catch{ return {}; } } + async function writeStatus(origin, ok){ const cache=await getStatusCache(); cache[origin]={ ok, ts: Date.now() }; await Pref.set({key:K_STATUS, value:JSON.stringify(cache)}); } + + async function verifyFileRise(u, timeout=5000){ + if (!u || !Http) return {ok:false}; + const base = normalize(u), origin = originOf(base); + const tryJson = async (url, validate) => { + try{ + const r = await Http.get({ url, connectTimeout:timeout, readTimeout:timeout, headers:{'Accept':'application/json','Cache-Control':'no-cache'} }); + if (r && r.data) { + const j = (typeof r.data === 'string') ? JSON.parse(r.data) : r.data; + return !!validate(j); + } + }catch(_){} + return false; + }; + if (await tryJson(origin + '/siteConfig.json', j => j && (j.appTitle || j.headerTitle || j.auth || j.oidc || j.basicAuth))) return {ok:true, origin}; + if (await tryJson(origin + '/api/ping.php', j => j && (j.ok===true || j.status==='ok' || j.pong || j.app==='FileRise'))) return {ok:true, origin}; + if (await tryJson(origin + '/api/version.php', j => j && (j.version || j.app==='FileRise'))) return {ok:true, origin}; + try{ + const r = await Http.get({ url: origin+'/', connectTimeout:timeout, readTimeout:timeout, headers:{'Cache-Control':'no-cache'} }); + if (typeof r.data === 'string' && /FileRise/i.test(r.data)) return {ok:true, origin}; + }catch(_){} + return {ok:false, origin}; + } + + async function probeReachable(u, timeout=3000){ + try{ + const base = new URL(normalize(u)).origin, ico=base+'/favicon.ico'; + if (Http){ + try{ const r=await Http.get({ url: ico, connectTimeout:timeout, readTimeout:timeout, headers:{'Cache-Control':'no-cache'} }); + if (r && typeof r.status==='number' && r.status<500) return true; }catch(e){} + try{ const r2=await Http.get({ url: base+'/', connectTimeout:timeout, readTimeout:timeout, headers:{'Cache-Control':'no-cache'} }); + if (r2 && typeof r2.status==='number' && r2.status<500) return true; }catch(e){} + return false; + } + return await new Promise(res=>{ + const img=new Image(), t=setTimeout(()=>done(false), timeout); + function done(ok){ clearTimeout(t); img.onload=img.onerror=null; res(ok); } + img.onload=()=>done(true); img.onerror=()=>done(false); + img.src = ico + (ico.includes('?')?'&':'?') + '__fr=' + Date.now(); + }); + }catch{ return false; } + } + + async function loadInstances(){ const raw=(await Pref.get({key:K_INST})).value; try{ return raw?JSON.parse(raw):[] }catch{ return [] } } + async function saveInstances(list){ await Pref.set({key:K_INST, value:JSON.stringify(list)}); } + async function getActive(){ return (await Pref.get({key:K_ACTIVE})).value } + async function setActive(id){ await Pref.set({key:K_ACTIVE, value:id||''}) } + + // ---- Styles (slide-up sheet + disabled buttons + safe-area) ---- + if (!$('#frx-mobile-style')) { + const css = ` + .frx-fab { position:fixed; right:16px; bottom:calc(env(safe-area-inset-bottom,0px) + 18px); width:52px; height:52px; border-radius:26px; + background: linear-gradient(180deg,#64B5F6,#2196F3 65%,#1976D2); color:#fff; display:grid; place-items:center; + box-shadow:0 10px 22px rgba(33,150,243,.38); z-index:2147483647; cursor:pointer; user-select:none; } + .frx-fab:active { transform: translateY(1px) scale(.98); } + .frx-fab svg { width:26px; height:26px; fill:white } + .frx-scrim{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:2147483645;opacity:0;visibility:hidden;transition:opacity .24s ease} + .frx-scrim.show{opacity:1;visibility:visible} + .frx-sheet{position:fixed;left:0;right:0;bottom:0;background:#0f172a;color:#e5e7eb; + border-top-left-radius:16px;border-top-right-radius:16px;box-shadow:0 -10px 30px rgba(0,0,0,.3); + z-index:2147483646;transform:translateY(100%);opacity:0;visibility:hidden; + transition:transform .28s cubic-bezier(.2,.8,.2,1), opacity .28s ease; will-change:transform} + .frx-sheet.show{transform:translateY(0);opacity:1;visibility:visible} + .frx-sheet .hdr{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid rgba(255,255,255,.08)} + .frx-title{display:flex;align-items:center;gap:10px;font-weight:800} + .frx-title img{width:22px;height:22px} + .frx-list{max-height:60vh;overflow:auto;padding:8px 12px} + .frx-chip{border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:12px;margin:8px 4px;background:rgba(255,255,255,.04)} + .frx-chip.active{outline:3px solid rgba(33,150,243,.35); border-color:#2196F3} + .frx-top{display:flex;gap:10px;align-items:center;justify-content:space-between;margin-bottom:10px} + .frx-left{display:flex;gap:10px;align-items:center} + .frx-ico{width:20px;height:20px;border-radius:6px;overflow:hidden;background:#fff;display:grid;place-items:center} + .frx-ico img{width:100%;height:100%;object-fit:cover;display:block} + .frx-name{font-weight:800} + .frx-host{font-size:12px;opacity:.8;margin-top:2px} + .frx-status{display:flex;align-items:center;gap:6px;font-size:12px;opacity:.9} + .frx-dot{width:10px;height:10px;border-radius:50%;} + .frx-dot.on{background:#10B981;box-shadow:0 0 0 3px rgba(16,185,129,.18)} + .frx-dot.off{background:#ef4444;box-shadow:0 0 0 3px rgba(239,68,68,.18)} + .frx-actions{display:flex;gap:8px;flex-wrap:wrap} + .frx-btn{appearance:none;border:0;border-radius:10px;padding:10px 12px;font-weight:700;cursor:pointer;transition:.15s ease opacity, .15s ease filter} + .frx-btn[disabled]{opacity:.5;cursor:not-allowed;filter:grayscale(20%)} + .frx-primary{background:linear-gradient(180deg,#64B5F6,#2196F3);color:#fff} + .frx-ghost{background:transparent;color:#cbd5e1;border:1px solid rgba(255,255,255,.12)} + .frx-danger{background:transparent;color:#f44336;border:1px solid rgba(244,67,54,.45)} + .frx-row{display:flex;gap:8px;align-items:center} + .frx-field{display:grid;gap:6px;margin:8px 4px} + .frx-input{width:100%;padding:12px;border-radius:10px;border:1px solid rgba(255,255,255,.12);background:transparent;color:inherit} + .frx-footer{display:flex;justify-content:flex-end;gap:8px;padding:10px 12px;border-top:1px solid rgba(255,255,255,.08)} + @media (pointer:coarse) { .frx-fab { width:58px; height:58px; border-radius:29px; } } + `; + document.head.appendChild(el('style',{id:'frx-mobile-style'}, css)); + } + + // DOM + const scrim = el('div',{class:'frx-scrim', id:'frx-scrim'}); + const sheet = el('div',{class:'frx-sheet', id:'frx-sheet'}, ` +
+
+ FileRiseFileRise Switcher +
+
+ + +
+
+
+
+
+ + +
+
+ + `); + const fab = el('div',{class:'frx-fab', id:'frx-fab', title:'Switch server'}, ``); + document.body.appendChild(scrim); document.body.appendChild(sheet); document.body.appendChild(fab); + + function show(){ scrim.classList.add('show'); sheet.classList.add('show'); fab.style.display='none'; } + function hide(){ scrim.classList.remove('show'); sheet.classList.remove('show'); fab.style.display='grid'; } + $('#frx-close').addEventListener('click', hide); + $('#frx-add-cancel').addEventListener('click', hide); + $('#frx-home').addEventListener('click', ()=>{ try{ location.href='capacitor://localhost/index.html'; }catch{} }); + scrim.addEventListener('click', hide); + document.addEventListener('keydown', e=>{ if(e.key==='Escape') hide(); }); + + function chipNode(item, isActive){ + const hv=host(item.url); + const node = el('div',{class:'frx-chip'+(isActive?' active':''), 'data-id':item.id}); + const top = el('div',{class:'frx-top'}); + const left = el('div',{class:'frx-left'}); + const ico = el('div',{class:'frx-ico'}); + const img = new Image(); + img.alt=''; img.src=item.favicon||faviconUrl(item.url)||initialsIcon(hv); + img.onerror=()=>{ img.onerror=null; img.src=initialsIcon(hv); }; + ico.appendChild(img); + const txt = el('div',{}, `
${item.name || hv}
${hv}
`); + left.appendChild(ico); left.appendChild(txt); + const status = el('div',{class:'frx-status'}, `Checking…`); + top.appendChild(left); top.appendChild(status); + const actions = el('div',{class:'frx-actions'}); + const bOpen = el('button',{class:'frx-btn frx-primary', 'data-act':'open', disabled:true}, 'Open'); + const bRen = el('button',{class:'frx-btn frx-ghost', 'data-act':'rename'}, 'Rename'); + const bDel = el('button',{class:'frx-btn frx-danger', 'data-act':'remove'}, 'Remove'); + actions.appendChild(bOpen); actions.appendChild(bRen); actions.appendChild(bDel); + node.appendChild(top); node.appendChild(actions); + return node; + } + + async function renderList(){ + const listEl=$('#frx-list'); listEl.innerHTML=''; + const list=await loadInstances(); const active=await getActive(); + const cache=await getStatusCache(); + + list.sort((a,b)=>(b.lastUsed||0)-(a.lastUsed||0)).forEach(item=>{ + const chip = chipNode(item, item.id===active); + const o = originOf(item.url), cached = cache[o]; + const dot = chip.querySelector(`#frx-dot-${item.id}`), lbl = chip.querySelector(`#frx-lbl-${item.id}`); + const openBtn = chip.querySelector('[data-act="open"]'); + + if (cached){ + dot.classList.add(cached.ok ? 'on':'off'); + lbl.textContent = cached.ok ? 'Online' : 'Offline'; + openBtn.disabled = !cached.ok; + } else { + lbl.textContent = 'Unknown'; + openBtn.disabled = true; + } + + chip.addEventListener('click', async (e)=>{ + const act = e.target?.dataset?.act; + if (!act) return; + + if (act==='open'){ + if (openBtn.disabled) return; + await setActive(item.id); + const url=normalize(item.url), withFlag=url+(url.includes('?')?'&':'?')+'frapp=1'; + window.location.replace(withFlag); + } else if (act==='rename'){ + const nn=prompt('New display name:', item.name || host(item.url)); + if (nn!=null){ + const L=await loadInstances(); const it=L.find(x=>x.id===item.id); + if (it){ it.name=nn.trim(); it.lastUsed=Date.now(); await saveInstances(L); renderList(); } + } + } else if (act==='remove'){ + if (!confirm('Remove this server?')) return; + let L=await loadInstances(); L=L.filter(x=>x.id!==item.id); await saveInstances(L); + const a=await getActive(); if (a===item.id) await setActive(L[0]?.id||''); renderList(); + } + }); + + listEl.appendChild(chip); + + // Live refresh (best effort) + (async ()=>{ + const ok = await probeReachable(item.url, 2500); + const d = document.getElementById(`frx-dot-${item.id}`); + const l = document.getElementById(`frx-lbl-${item.id}`); + const b = chip.querySelector('[data-act="open"]'); + if (d && l && b){ + d.classList.remove('on','off'); + d.classList.add(ok?'on':'off'); + l.textContent = ok ? 'Online' : 'Offline'; + b.disabled = !ok; + } + const o2 = originOf(item.url); if (o2) writeStatus(o2, ok); + })(); + }); + } + + $('#frx-add-save').addEventListener('click', async ()=>{ + const name = $('#frx-name').value.trim(); + const url = $('#frx-url').value.trim(); + if (!url) { alert('Enter a valid URL'); return; } + + // Verify: must be FileRise + const vf = await verifyFileRise(url); + if (!vf.ok) { alert('That address does not look like a FileRise server.'); return; } + + let L = await loadInstances(); + const h = host(url); + const dupe = L.find(i => host(i.url)===h); + const inst = dupe || { id:'i'+Math.random().toString(36).slice(2)+Date.now().toString(36) }; + inst.name = name || inst.name || h; + inst.url = normalize(url); + inst.favicon = faviconUrl(url); + inst.lastUsed = Date.now(); + if (!dupe) L.push(inst); + await saveInstances(L); + await setActive(inst.id); + + if (vf.origin) await writeStatus(vf.origin, true); + + window.location.replace(inst.url + (inst.url.includes('?')?'&':'?') + 'frapp=1'); + }); + + fab.addEventListener('click', async ()=>{ await renderList(); show(); }); + + + // Ensure zoom gestures work if the host page tried to disable them + (function ensureZoomable(){ + let m = document.querySelector('meta[name=viewport]'); + const desired = 'width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=yes, minimum-scale=1, maximum-scale=5'; + if (!m){ m = document.createElement('meta'); m.setAttribute('name','viewport'); document.head.appendChild(m); } + const c = m.getAttribute('content') || ''; + if (/user-scalable=no|maximum-scale=1/.test(c)) m.setAttribute('content', desired); + })(); + })(); \ No newline at end of file diff --git a/public/js/pwa/register-sw.js b/public/js/pwa/register-sw.js new file mode 100644 index 0000000..304b149 --- /dev/null +++ b/public/js/pwa/register-sw.js @@ -0,0 +1,5 @@ +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js?v={{APP_QVER}}').catch(() => {}); + }); + } \ No newline at end of file diff --git a/public/js/pwa/sw.js b/public/js/pwa/sw.js new file mode 100644 index 0000000..a5d6c84 --- /dev/null +++ b/public/js/pwa/sw.js @@ -0,0 +1,9 @@ +// public/js/pwa/sw.js +const SW_VERSION = '{{APP_QVER}}'; +const STATIC_CACHE = `fr-static-${SW_VERSION}`; +const STATIC_ASSETS = [ + '/', '/index.html', + '/css/styles.css?v={{APP_QVER}}', + '/js/main.js?v={{APP_QVER}}', + '/assets/logo.svg?v={{APP_QVER}}' +]; \ No newline at end of file diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..aada9e4 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,14 @@ +{ + "name": "FileRise", + "short_name": "FileRise", + "start_url": "/?pwa=1", + "scope": "/", + "display": "standalone", + "background_color": "#111111", + "theme_color": "#0b5ed7", + "icons": [ + { "src": "/assets/icons/icon-192.png?v={{APP_QVER}}", "sizes": "192x192", "type": "image/png" }, + { "src": "/assets/icons/icon-512.png?v={{APP_QVER}}", "sizes": "512x512", "type": "image/png" }, + { "src": "/assets/icons/maskable-512.png?v={{APP_QVER}}", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] + } \ No newline at end of file diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..760999a --- /dev/null +++ b/public/sw.js @@ -0,0 +1,6 @@ +// Root-scoped stub. Keeps the worker’s scope at “/” level +try { + self.importScripts('/js/pwa/sw.js?v={{APP_QVER}}'); +} catch (_) { + // no-op +} \ No newline at end of file diff --git a/src/controllers/MediaController.php b/src/controllers/MediaController.php new file mode 100644 index 0000000..ff5f201 --- /dev/null +++ b/src/controllers/MediaController.php @@ -0,0 +1,135 @@ +out(['error'=>'Unauthorized'], 401); return 'no'; + } + return null; + } + private function checkCsrf(): ?string { + $headers = function_exists('getallheaders') ? array_change_key_case(getallheaders(), CASE_LOWER) : []; + $received = $headers['x-csrf-token'] ?? ''; + if (!isset($_SESSION['csrf_token']) || $received !== $_SESSION['csrf_token']) { + $this->out(['error'=>'Invalid CSRF token'], 403); return 'no'; + } + return null; + } + private function normalizeFolder($f): string { + $f = trim((string)$f); + return ($f==='' || strtolower($f)==='root') ? 'root' : $f; + } + private function validFolder($f): bool { + return $f==='root' || (bool)preg_match(REGEX_FOLDER_NAME, $f); + } + private function validFile($f): bool { + $f = basename((string)$f); + return $f !== '' && (bool)preg_match(REGEX_FILE_NAME, $f); + } + private function enforceRead(string $folder, string $username): ?string { + $perms = loadUserPermissions($username) ?: []; + return ACL::canRead($username, $perms, $folder) ? null : "Forbidden"; + } + + /** POST /api/media/updateProgress.php */ + public function updateProgress(): void { + $this->jsonStart(); + try { + if ($this->requireAuth()) return; + if ($this->checkCsrf()) return; + + $u = $_SESSION['username'] ?? ''; + $d = $this->readJson(); + $folder = $this->normalizeFolder($d['folder'] ?? 'root'); + $file = (string)($d['file'] ?? ''); + $seconds = isset($d['seconds']) ? floatval($d['seconds']) : 0.0; + $duration = isset($d['duration']) ? floatval($d['duration']) : null; + $completed = isset($d['completed']) ? (bool)$d['completed'] : null; + $clear = isset($d['clear']) ? (bool)$d['clear'] : false; + + if (!$this->validFolder($folder) || !$this->validFile($file)) { + $this->out(['error'=>'Invalid folder/file'], 400); return; + } + if ($this->enforceRead($folder, $u)) { $this->out(['error'=>'Forbidden'], 403); return; } + + if ($clear) { + $ok = MediaModel::clearProgress($u, $folder, $file); + $this->out(['success'=>$ok]); return; + } + + $row = MediaModel::saveProgress($u, $folder, $file, $seconds, $duration, $completed); + $this->out(['success'=>true, 'state'=>$row]); + } catch (Throwable $e) { + error_log('MediaController::updateProgress: '.$e->getMessage()); + $this->out(['error'=>'Internal server error'], 500); + } finally { $this->jsonEnd(); } + } + + /** GET /api/media/getProgress.php?folder=…&file=… */ + public function getProgress(): void { + $this->jsonStart(); + try { + if ($this->requireAuth()) return; + $u = $_SESSION['username'] ?? ''; + $folder = $this->normalizeFolder($_GET['folder'] ?? 'root'); + $file = (string)($_GET['file'] ?? ''); + + if (!$this->validFolder($folder) || !$this->validFile($file)) { + $this->out(['error'=>'Invalid folder/file'], 400); return; + } + if ($this->enforceRead($folder, $u)) { $this->out(['error'=>'Forbidden'], 403); return; } + + $row = MediaModel::getProgress($u, $folder, $file); + $this->out(['state'=>$row]); + } catch (Throwable $e) { + error_log('MediaController::getProgress: '.$e->getMessage()); + $this->out(['error'=>'Internal server error'], 500); + } finally { $this->jsonEnd(); } + } + + /** GET /api/media/getViewedMap.php?folder=… (optional, for badges) */ + public function getViewedMap(): void { + $this->jsonStart(); + try { + if ($this->requireAuth()) return; + $u = $_SESSION['username'] ?? ''; + $folder = $this->normalizeFolder($_GET['folder'] ?? 'root'); + + if (!$this->validFolder($folder)) { + $this->out(['error'=>'Invalid folder'], 400); return; + } + if ($this->enforceRead($folder, $u)) { $this->out(['error'=>'Forbidden'], 403); return; } + + $map = MediaModel::getFolderMap($u, $folder); + $this->out(['map'=>$map]); + } catch (Throwable $e) { + error_log('MediaController::getViewedMap: '.$e->getMessage()); + $this->out(['error'=>'Internal server error'], 500); + } finally { $this->jsonEnd(); } + } +} \ No newline at end of file diff --git a/src/models/MediaModel.php b/src/models/MediaModel.php new file mode 100644 index 0000000..7837508 --- /dev/null +++ b/src/models/MediaModel.php @@ -0,0 +1,94 @@ +1, "items"=>[]]; + $json = file_get_contents($path); + $data = json_decode($json, true); + return (is_array($data) && isset($data['items'])) ? $data : ["version"=>1, "items"=>[]]; + } + + private static function saveState(string $username, array $state): bool { + $path = self::filePathFor($username); + $tmp = $path . '.tmp'; + $ok = file_put_contents($tmp, json_encode($state, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX); + if ($ok === false) return false; + return @rename($tmp, $path); + } + + /** Save/merge a single file progress record. */ + public static function saveProgress(string $username, string $folder, string $file, float $seconds, ?float $duration, ?bool $completed): array { + $folderKey = ($folder === '' || strtolower($folder)==='root') ? 'root' : $folder; + $nowIso = date('c'); + + $state = self::loadState($username); + if (!isset($state['items'][$folderKey])) $state['items'][$folderKey] = []; + if (!isset($state['items'][$folderKey][$file])) { + $state['items'][$folderKey][$file] = [ + "seconds" => 0, + "duration" => $duration ?? 0, + "completed" => false, + "updatedAt" => $nowIso + ]; + } + + $row =& $state['items'][$folderKey][$file]; + if ($duration !== null && $duration > 0) $row['duration'] = $duration; + if ($seconds >= 0) $row['seconds'] = $seconds; + if ($completed !== null) $row['completed'] = (bool)$completed; + // auto-complete if we’re basically done + if (!$row['completed'] && $row['duration'] > 0 && $row['seconds'] >= max(0, $row['duration'] * 0.95)) { + $row['completed'] = true; + } + $row['updatedAt'] = $nowIso; + + self::saveState($username, $state); + return $row; + } + + /** Get a single file progress record. */ + public static function getProgress(string $username, string $folder, string $file): array { + $folderKey = ($folder === '' || strtolower($folder)==='root') ? 'root' : $folder; + $state = self::loadState($username); + $row = $state['items'][$folderKey][$file] ?? null; + return is_array($row) ? $row : ["seconds"=>0,"duration"=>0,"completed"=>false,"updatedAt"=>null]; + } + + /** Folder map: filename => {seconds,duration,completed,updatedAt} */ + public static function getFolderMap(string $username, string $folder): array { + $folderKey = ($folder === '' || strtolower($folder)==='root') ? 'root' : $folder; + $state = self::loadState($username); + $items = $state['items'][$folderKey] ?? []; + return is_array($items) ? $items : []; + } + + /** Clear one file’s progress (e.g., “mark unviewed”). */ + public static function clearProgress(string $username, string $folder, string $file): bool { + $folderKey = ($folder === '' || strtolower($folder)==='root') ? 'root' : $folder; + $state = self::loadState($username); + if (isset($state['items'][$folderKey][$file])) { + unset($state['items'][$folderKey][$file]); + return self::saveState($username, $state); + } + return true; + } +} \ No newline at end of file