From 4941b4161cd597380182d950fd0851a872e7be91 Mon Sep 17 00:00:00 2001 From: kodi Date: Mon, 9 Mar 2026 18:24:00 +0100 Subject: [PATCH] feat (ui): selecteer meerdere bestanden tegelijkertijd --- app/static/app.js | 62 ++++++++++++++++++++++---- app/static/styles.css | 4 ++ data/session_state.sqlite3 | Bin 274432 -> 274432 bytes feature_tests_file_modal_selection.sh | 44 ++++++++++++++++++ 4 files changed, 101 insertions(+), 9 deletions(-) create mode 100755 feature_tests_file_modal_selection.sh diff --git a/app/static/app.js b/app/static/app.js index 270d1ae..ff8704f 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -17,6 +17,7 @@ modalKnownFiles: {}, modalFileFilter: "", modalVisibleFiles: [], + modalSelectionAnchorPath: null, syncScrolling: false, settings: { set_file_date_to_first_aired_date: false, @@ -755,6 +756,7 @@ }; }); state.modalFileFilter = ""; + state.modalSelectionAnchorPath = null; el.fileModalTitle.textContent = "File Discovery"; el.modalAddSelectedFilesBtn.style.display = ""; if (el.modalFileFilterInput) { @@ -819,22 +821,62 @@ const isSelected = state.modalSelectedFilePaths.has(file.path); if (isSelected) li.classList.add("selected"); + if (state.modalSelectionAnchorPath && state.modalSelectionAnchorPath === file.path) { + li.classList.add("modal-anchor"); + } - li.addEventListener("click", () => { - if (state.modalSelectedFilePaths.has(file.path)) { - state.modalSelectedFilePaths.delete(file.path); - li.classList.remove("selected"); - } else { - state.modalSelectedFilePaths.add(file.path); - li.classList.add("selected"); - } - updateModalSelectionCount(); + li.addEventListener("click", (event) => { + handleModalFileRowClick(file.path, event); }); el.modalFilesList.appendChild(li); }); updateModalSelectionCount(); } + function handleModalFileRowClick(path, event) { + const clickedPath = (path || "").toString().trim(); + if (!clickedPath) return; + + const visiblePaths = (state.modalVisibleFiles || []) + .map((f) => (f && f.path ? f.path : "")) + .filter((p) => p); + const isShift = !!(event && event.shiftKey); + const isToggle = !!(event && (event.ctrlKey || event.metaKey)); + + if (isShift) { + const anchor = state.modalSelectionAnchorPath || ""; + const from = visiblePaths.indexOf(anchor); + const to = visiblePaths.indexOf(clickedPath); + if (from >= 0 && to >= 0) { + const start = Math.min(from, to); + const end = Math.max(from, to); + const rangePaths = visiblePaths.slice(start, end + 1); + state.modalSelectedFilePaths = new Set(rangePaths); + } else { + // Fallback: no valid anchor in current visible list. + state.modalSelectedFilePaths = new Set([clickedPath]); + } + state.modalSelectionAnchorPath = clickedPath; + renderModalFiles(); + return; + } + + if (isToggle) { + if (state.modalSelectedFilePaths.has(clickedPath)) { + state.modalSelectedFilePaths.delete(clickedPath); + } else { + state.modalSelectedFilePaths.add(clickedPath); + } + state.modalSelectionAnchorPath = clickedPath; + renderModalFiles(); + return; + } + + state.modalSelectedFilePaths = new Set([clickedPath]); + state.modalSelectionAnchorPath = clickedPath; + renderModalFiles(); + } + async function loadModalFiles(subpath) { const rootId = el.modalRootSelect.value; const chosenSubpath = (subpath || "").trim(); @@ -845,6 +887,7 @@ const data = await api( `/api/files/discover?root_id=${encodeURIComponent(rootId)}&subpath=${encodeURIComponent(chosenSubpath)}&recursive=${recursive}&limit=200` ); + state.modalSelectionAnchorPath = null; state.modalFiles = data.items || []; state.modalFiles.forEach((file) => { const path = file.path || ""; @@ -891,6 +934,7 @@ function clearModalSelection() { state.modalSelectedFilePaths.clear(); + state.modalSelectionAnchorPath = null; renderModalFiles(); } diff --git a/app/static/styles.css b/app/static/styles.css index 0741c15..4198750 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -506,6 +506,10 @@ button.secondary { cursor: pointer; } +#modalFilesList li.modal-anchor { + box-shadow: inset 0 0 0 1px var(--button-primary-border); +} + .modal-files-tools { margin-bottom: 8px; } diff --git a/data/session_state.sqlite3 b/data/session_state.sqlite3 index b869528817ea95ada4b622bc08fddd732e5948cb..bfdbd51c155df0a1b4d5916efed8fca03da47d02 100644 GIT binary patch delta 786 zcmaJF|7=LfZ-TAXS^KDRBEaq++TDG%qMt8>9A}xu$q#hyyk-{R{{vJx`=pLk? zLyi+oM!C@?Iz-w?vo{4hM4$%JpUnan+&|!4vaTc1-=H3xMci2%is=r*@lH|v@eLd`Rsr_k%>FQ z5AZdoe+=iCoz?b^r}g&QJ;`2uaom->Up%X@MVp)MW!NIk9byl*DIzkC3WC7%!aydW z8b&iQ^j2c(s-7^KbweYXo*>N$VjfeCw5Fwrsc3}iTABz+6XNc9^(n+Ep{UKh?PWqy zYU_#ivraKmVWimIi`(6E?L?`jh0))=6n!S}0YX^1l-+(Z&&5WI21<9HhbSE<8ca57W4HbfOZ{pHwG z_;Q_m9!XLe$uaPxvV2%BKqq{b*X2q1Hq^l-7=aO*{y}oevp6Nw8LDZuMbTPyHDx*n z=I~y>@-EAY`+|s$G~P1}F?l|Mp}*a;xpN>wVx4oTT@%J@M9CN?%W_<;@aqf&VQ z!TEjt%}OU>CzYyH5H~y1{q@)1yTAUr?|$}zyPv(_{THu&WYwxw*W*w9ci+xx;gN^m zesE7=_wt|bsTWS1cwx^DBQCxb`PI;p!wL) z1;hWc;$LyNh^{+_Pdt6sRn@|}qn9!}k?et@VcfXz(M2JJ0;X*m&^dNA53t z^}O@%x%iI3RfmS}$(A1uw=Tcqv*`fXzw^HPZkOIi3%^?a!Dm1IK;h$s!e+FdUwnc@ zbA9oZ;=dMuQGBuZqvH3A&lJB^d;$pnqImGSwdj4r8a%EZ!NXaN$5rR!vGY7Ub`aw2 zIv!g!>KQGDtBPMK{$BA^@u$Vl6yH^R7FZuGK3H5XeykW46W}~hl;NX=EhDS-O@&Im zUaQnb_H@~vs@F`@GUYXo7j|8?UB9VNHp|X#v$ET)A1gW2mHKq0 zG*z=ryIQ)+G^b5-=8-Eclm-8naFB_cA?IE?TUC+=Jpw?iY2 z47b@5UDw#}M$w{ieKVfoSHQaSahRq;L@vQ4uIsx-D@?O?=qAQu(+8qXD4O``icTD1 zw#9Ck_7-WTqbTthLO=+oDZX2hDg&Kq{+XLf$5Ua=(;TtrLK$l z+n9FVjhdp<3EOQD!i*gQoM;hf7=pE|<#t0i18ePY)@}9BG3T~t-6YL2qaz}bq>Jl$;S+XQxQ2-DEm+gUW;>}I=+*|?>cPvbPrqAUgVoyD*%It$)>rSO8MNMuloXt#lo-|-KyG7nzE zR!f9XLSNLK#lVYlLCSTv_%RPWJa2Swbm{SA=PM7IQ`ja&m=W@z>+|z-MqA9Y&8zB!8=nC00 zhylY(W0#Fc>9@~)nCt5_Z>=!tgsHMD!-Wip&Rs?;%OWK?nhQ#pB_r$epR347vN$0# zhW)^LMLVUz0Ea&DN=BSd*6yncr1#kZ|1B7ql1!E*Ig5SrT48qt=Bavw^Kn$0cJj+;)@H(U}XC zl-gm_ZD+IY)V5v5wxH{7n>M!H0w~W@0<9&6FO1%N2<1keFyKaAfu`&(!;2$7^HPy@ z6C>zmNy_@GCvcUn(N^tq-8c|Q62?&ihfd~%mxl8KI2H1`M@XB($Rc6Pi^zxdxW+rO z&}fGZo*t6{(`HN&MoAh*UYei*Q?#?hNa9WdSmn}tv~8Mi$s7Mp+ah23qP}K*6{Bt6 zrl+W>cT2XXwaet!AJHaCqNJL*qpse$B{0+nH<{`^Fm@i_CO`3rHZFg$Q(NB?s?}0Y z5|Ql_rMAyw{NvlMU>s_0pK&l;ePsue#@%I_LLuRAp2B4C6-~s6d|-vU~vRSGyyd@tU7?`+-1;AYK^vs*6%lrOP8{grqB$`JuA50i?uW& zi|^Og?94gjflQ);60RC9$_k@z$REF;Ir2SMYU?Wu#gb6(f@E--R-t$kOsXPMl;sz{ zqD}P9(hO6_tT2uI&a>AMze)Z|Be`M~RQ}Gh=QJ`~Oo~gx1P@d1ew8taATt9z=)wxA8Ks=!=Pp)s}xWw@S+_eQk}OKFtr z7O*rdn}#$@Hvm+rtj3!z0aR-=zo$ODYL%#{+BE{M5J`<%PZ8MgYxGsBQWrzJn46%P z06$O!5@$IAro*ecHVv^GG#+%|-Dwa}p6|lU8Wr*vU{Q4XHNsLApbW&QXRm@E-*jns z&W(cd9h;i!RYkvV5-zVqu(m_*Wsgun)JphK+b7}gC(U(31kt!XlN#v))+J;qw1W9) zrL|!p7QWcsHw+`}A!~xAG~+n%jkZgx7eKIj;syUVMQSv}40MjpXlBWEI|_bA_^#I^ zXaaH(vfE)agLl~4Jgr0+uPB>~+sZ7q?OlMK!MKlaGrIB8TtWJsk_-H3fM z*F`ed%$VygA?@=P4Zj=D#0z2;3pvC2OK4n(XJ;`Af)yj=~TD z5*sZVU{N?SgGw5~X1lHzFGOL>2*RaBZwWc+ez%vWrpC^rsleJ}yj5h;6~Su1o&b`QPZ;hiF3h^q6ErcOr!Q<4g=Hu%0ftluiM!2#_eRY+EqACASN9npr3A zDigq3Zg5HwRJvygFA6&$@)<%2sW0{1cqR&w@F3M)I*rw6!qMYyA1VYvrx|fU!_tE4 zGVLXjGUU6ED_g~YTRJNu57GvA2CH)t&Lg_eS4`$fL5H%49%4(@L7YK+3z0M-#vuFf z+$3?KL+Ya-gicOHOGw9o9I*`14+Gphg(#tsC=n1~Y`AI|0^}UhR3s7tNyofE;ERzg z2xJThr&&9}SEq+02qI0qtQ#lHL`nvG77qxmc~(=7knkalQV!DvZUjvw0NN&iT3|eb znlP)>Bt$b9N7hTf2@gc!j%nNFPjANJ{pwHf z*!-k6ET5kkSSPR8psiuX^Lc4G0XuZ6gRZ_72O3YQcr^4e4Srpqad zu&j#eXZf;J7}ivYidB~1`yf)Pu`%sJIWVoS(<(K2_hD_*4u)dcv1lvIrloM@@)7cr zN3`|wcwq6d)l}@9Zl-UghD%CuxymU0JY#}6%ktqL7Pc35 z$$PHV$K}t*v1mzQqP)=lS(DUck##A0FPvuYRx&q<3cQOy+o{`E-C-t>H7LVoI0VPv<({1MD) zJ71||wp*4+AfI*g@nPlxdGp8qvoKNSFSbJV#9Xo~tRweLCQ!(S?$@>yki(yXCpMnY z*U2}04o2FzMql?^EQnkyjGFx?AIo|w<>FF8U{=}VnVGsFzj3Ot@qCN9D*Lkcbm3C@ zhhNo)h8QA{-!S#b%UNz^=6$8$SaZ48%Yp;pFMLjo}#*pYKyDdB`38@Z>f)TEM7 zCHHqrPQF~&D4+R>Hnj09l5?I&&OJ1*J4*;+B|v9i&$5s#8 z#mYmaBX3@hozEw#`k+?cCBOKs!bIP~?WYS9qd6Gcazpm6)5kB#dCXOHc2JZb7#p}s z-a9ccatY94r!4~%E_l7E!KtF%p(p`px6v1Kl!9q zmKXjQ<8#^f9asL@etn~S`Ur^df26IGy~EnjCYA%cFuyZ7Hie%_`IreKN)BV02_^<6 zuljZCJo~q@4rBwX@}N|c??0t)mT%oOu%_V2i*5b4uRdE;N?RZmdx#iu<#DznHq7kkvhnKqJHY&7~ww%3ezQQC?U9nyE~ z*)sgC%&ilo>qvyVqMdmmwjXEZ*EF#xA;n&iVPhLeDfT|1s$>Ulmwr(QNa(jxS~40y zqiU=H>WXf&8K&4&8mgEdcuChKA_%sGtpdSe5IyZcjhqR2<6;M4#|w8Mb$0_4L9qdI z=Vu@(s>_D0&{%XEZbVYY@Wob7(8)mrAmDFvYm0Xmf!9sZUL6uMEN zB}}Az^wJPt*?T~PUlVJtyu{B37Z+Hi>Ws-w84;!NEp zMFasQyZ<}X@_`o$Tk8(hzML{iKG#~K0p3{tR$xL5mY}4~R_)A*Q#I@I@t+hf>aqSD zzk>0s6xMVtp!!nXr23fye;_PmJ+)E*1{^;i%TPM53$hZB4wh`YP7*CUP!>zHFF9Y? z78R(@vXY)c;h0oV=R+ymxxI#pzU?lFDs8Td-UN@4Y1a*@ytc~I@fYm*o{;7!3_w) z^spO}lkysv8zx|H!j!cYbtfz(^%@b1c9OL67m_78XUPPmPQ#<8=Yc=8fD}wX0%Qew zlw8yu60+@+JeVhz3gQI0ZkaX^PT+wv5J7lQu*6e2KO4ARuF>=*l2$dS2{$5|3Ok2#yG*;)oc{mT^zCvLWIA;R5?*Qgb(2#6nq>n|wc1lrP(?c{p!ioS zQ>d!5L6)d4%JN}3tTaFNmTHwSPi^*bl=BHhzV-TpPE_Z!YyF^~`f;Q4q`%tHX*-WRbk~N16 z+jK18z*FsaA+9q`YE$p1wc4TTK7f>`qYz4t{uZImXiIiIB>%_ZqRJA42k%>wgL7r zg22*w+1sp5PcAGhOyx~eUfkJ}hG|>usbO5za!T^To>m_usU3TZeC;LO94y(`$=UMe zo;D?aeo$Y1o^6%!{i}m|Y4zb!sa$euxJe@)pVZ1~+?_k+*ZxXNF5wF{d*)&s!FQ8A zu&s)NY{>Rarz$U;)Hh82x|=a;v27^~-n(k>Ufgr}ox$6G>5dF;y65?Yn;6|Yw}t9t zg};dN$n6Lc#=cz6i^5fEMUGyfe#(s7?&6&*;f$TsD{uZ2&De4V6H2{qqD=qxZ6pAL zfgaSh4>FK^dx|5}haS{co80j0nVXuOn>m*<`CO`P>fu6=wcAu%hZv_a^Px~H>Rz0{ zxIRt+XXq%Aj|g$fL&rrpz*&e5oZ;iJ2U0{>SH(PaHb^Ht`QE|9;edftAN*lE5M=Fu zF46c@(C-R5sl=(jaeSHwW7pDcB~&UGnQx>`7e`7+qi3)wz$KLwH!iw3m+ZE1nI^BE z<1GKVFKF@QXJmD*l#_O*!U5Tv{N(NW37Wai*enagM4j!X&684@nA0 zoy%bag@e39?avX!XkQ0Za?4}d#-o(D0w9lRavnQAvn-gtTy@q|XR5%sm{VnYR3$S?36! zldD2|SuFs}ZC^etTGoZba#XOYM%)kmszY9VTp!)UG$|eAh;j_FbX*^=^MdmF!F^0h zH@vvyYfr9c8a~nEMTyrbUX5~^)H+pG%2bty{>r7UQ1nekb+i81uFle>Iy1@D%SKe; z0n9v2*UK@Lt7_+$FLpO&=qgol97giN3+=3zXcXQ;MxRDAuxIiXI zn#u&MbZSbOS)MK-)c5{hV*PIp^>6r}V!fg_;B@Y&tFFD#t9lKN8Yq9 zwZb_!gbGnSYh`g0v*Icu6~h Qd(OF*d-CO<>4Vq(7YNc-o&W#< diff --git a/feature_tests_file_modal_selection.sh b/feature_tests_file_modal_selection.sh new file mode 100755 index 0000000..2f50a46 --- /dev/null +++ b/feature_tests_file_modal_selection.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${BASE_URL:-http://127.0.0.1:8085}" +ALT_BASE_URL="http://host.containers.internal:8085" + +detect_base_url() { + if curl -fsS --max-time 2 "$BASE_URL/api/health" >/dev/null 2>&1; then + echo "$BASE_URL" + return + fi + if curl -fsS --max-time 2 "$ALT_BASE_URL/api/health" >/dev/null 2>&1; then + echo "$ALT_BASE_URL" + return + fi + echo "$BASE_URL" +} + +BASE_URL="$(detect_base_url)" +echo "Using BASE_URL=$BASE_URL" + +echo "== Feature test 1: modal selection modifiers are implemented ==" +grep -q "modalSelectionAnchorPath" app/static/app.js || { echo "anchor state missing"; exit 1; } +grep -q "event.shiftKey" app/static/app.js || { echo "shift handling missing"; exit 1; } +grep -q "event.ctrlKey || event.metaKey" app/static/app.js || { echo "ctrl/meta handling missing"; exit 1; } +echo "modifier selection validation passed" + +echo +echo "== Feature test 2: anchor reset on clear and folder reload ==" +grep -q "state.modalSelectionAnchorPath = null;" app/static/app.js || { echo "anchor reset missing"; exit 1; } +grep -q "function clearModalSelection()" app/static/app.js || { echo "clearModalSelection missing"; exit 1; } +grep -q "async function loadModalFiles(subpath)" app/static/app.js || { echo "loadModalFiles missing"; exit 1; } +echo "anchor reset validation passed" + +echo +echo "== Feature test 3: modal file list UI still present with batch actions ==" +page="$(curl -fsS "$BASE_URL/")" +echo "$page" | grep -q 'id="modalFilesList"' || { echo "modalFilesList missing"; exit 1; } +echo "$page" | grep -q 'id="modalSelectAllFilesBtn"' || { echo "Select All button missing"; exit 1; } +echo "$page" | grep -q 'id="modalClearSelectionBtn"' || { echo "Clear Selection button missing"; exit 1; } +echo "modal UI validation passed" + +echo +echo "All file modal selection feature tests passed."