From db4c6407699fe1685bbf0957756e6c5f17387238 Mon Sep 17 00:00:00 2001 From: kodi Date: Tue, 10 Mar 2026 09:56:15 +0100 Subject: [PATCH] feat (ui): selecteer meerdere episodes tegelijkertijd --- app/static/app.js | 154 ++++++++++++++++++++++------- app/static/styles.css | 8 ++ app/templates/index.html | 3 +- data/session_state.sqlite3 | Bin 274432 -> 303104 bytes feature_tests_episode_selection.sh | 43 ++++++++ 5 files changed, 172 insertions(+), 36 deletions(-) create mode 100755 feature_tests_episode_selection.sh diff --git a/app/static/app.js b/app/static/app.js index ff8704f..f3639b3 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -7,6 +7,8 @@ selectedSeries: null, selectedSeriesSummary: null, episodes: [], + selectedEpisodeIds: new Set(), + episodeSelectionAnchorId: null, selectedEpisodes: [], selectedFiles: [], selectedPairIndex: null, @@ -63,6 +65,7 @@ rememberMaxSeriesInput: document.getElementById("rememberMaxSeriesInput"), purgeRememberedSeriesBtn: document.getElementById("purgeRememberedSeriesBtn"), refreshEpisodesBtn: document.getElementById("refreshEpisodesBtn"), + addSelectedEpisodesBtn: document.getElementById("addSelectedEpisodesBtn"), episodesList: document.getElementById("episodesList"), episodeMeta: document.getElementById("episodeMeta"), refreshSelectedEpisodesBtn: document.getElementById("refreshSelectedEpisodesBtn"), @@ -159,14 +162,6 @@ console.log(text); } - function makeBtn(label, handler, secondary) { - const btn = document.createElement("button"); - btn.textContent = label; - if (secondary) btn.className = "secondary"; - btn.addEventListener("click", handler); - return btn; - } - function setBusy(btn, busy) { if (btn) btn.disabled = busy; } @@ -653,12 +648,24 @@ out("Search result", data); } - async function loadEpisodes() { - if (!state.selectedSeries || !state.selectedSeries.id) { - throw new Error("Select a series first"); - } - const data = await api(`/api/tvdb/series/${encodeURIComponent(state.selectedSeries.id)}/episodes?order_type=aired`); - state.episodes = data.items || []; + function episodeKey(episode) { + const id = (episode && episode.id != null) ? String(episode.id).trim() : ""; + if (id) return id; + return `${episode.season_number || ""}-${episode.episode_number || ""}-${episode.title || ""}`; + } + + function updateAddSelectedEpisodesControl() { + if (!el.addSelectedEpisodesBtn) return; + el.addSelectedEpisodesBtn.disabled = state.selectedEpisodeIds.size === 0; + } + + function clearEpisodeSelection() { + state.selectedEpisodeIds = new Set(); + state.episodeSelectionAnchorId = null; + renderEpisodesList(); + } + + function renderEpisodesList() { el.episodesList.innerHTML = ""; let previousSeasonKey = null; state.episodes.forEach((episode) => { @@ -678,7 +685,14 @@ previousSeasonKey = seasonKey; } + const key = episodeKey(episode); const li = document.createElement("li"); + li.classList.add("episode-row"); + if (state.selectedEpisodeIds.has(key)) li.classList.add("selected"); + if (state.episodeSelectionAnchorId && state.episodeSelectionAnchorId === key) { + li.classList.add("episode-anchor"); + } + const left = document.createElement("span"); left.className = "episode-main"; const title = document.createElement("div"); @@ -694,30 +708,97 @@ dateLine.textContent = `${future ? "Airing" : "Aired"} ${formattedDate}`; left.appendChild(dateLine); } - - const right = document.createElement("div"); - right.appendChild(makeBtn("Add", async () => { - const payload = { - id: episode.id, - series: state.selectedSeries.name, - year: state.selectedSeries.year, - season_number: episode.season_number, - episode_number: episode.episode_number, - title: episode.title, - aired: episode.aired, - label: episode.label, - }; - await api(q("/api/session/selected-episodes"), { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ items: [payload] }), - }); - await loadSelectedEpisodes(); - })); li.appendChild(left); - li.appendChild(right); + + li.addEventListener("click", (event) => { + handleEpisodeRowClick(key, event); + }); el.episodesList.appendChild(li); }); + updateAddSelectedEpisodesControl(); + } + + function handleEpisodeRowClick(clickedId, event) { + const clicked = String(clickedId || "").trim(); + if (!clicked) return; + + const visibleEpisodeIds = (state.episodes || []).map((ep) => episodeKey(ep)); + const isShift = !!(event && event.shiftKey); + const isToggle = !!(event && (event.ctrlKey || event.metaKey)); + + if (isShift) { + const anchor = String(state.episodeSelectionAnchorId || "").trim(); + const from = visibleEpisodeIds.indexOf(anchor); + const to = visibleEpisodeIds.indexOf(clicked); + if (from >= 0 && to >= 0) { + const start = Math.min(from, to); + const end = Math.max(from, to); + state.selectedEpisodeIds = new Set(visibleEpisodeIds.slice(start, end + 1)); + } else { + state.selectedEpisodeIds = new Set([clicked]); + } + state.episodeSelectionAnchorId = clicked; + renderEpisodesList(); + return; + } + + if (isToggle) { + if (state.selectedEpisodeIds.has(clicked)) { + state.selectedEpisodeIds.delete(clicked); + } else { + state.selectedEpisodeIds.add(clicked); + } + state.episodeSelectionAnchorId = clicked; + renderEpisodesList(); + return; + } + + state.selectedEpisodeIds = new Set([clicked]); + state.episodeSelectionAnchorId = clicked; + renderEpisodesList(); + } + + async function addSelectedEpisodes() { + const selectedIds = new Set(state.selectedEpisodeIds); + if (!selectedIds.size) return; + if (!state.selectedSeries || !state.selectedSeries.name) { + throw new Error("Select a series first"); + } + + const items = (state.episodes || []) + .filter((episode) => selectedIds.has(episodeKey(episode))) + .map((episode) => ({ + id: episode.id, + series: state.selectedSeries.name, + year: state.selectedSeries.year, + season_number: episode.season_number, + episode_number: episode.episode_number, + title: episode.title, + aired: episode.aired, + label: episode.label, + })); + + if (!items.length) return; + + await api(q("/api/session/selected-episodes"), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ items }), + }); + + clearEpisodeSelection(); + await loadSelectedEpisodes(); + } + + async function loadEpisodes() { + if (!state.selectedSeries || !state.selectedSeries.id) { + throw new Error("Select a series first"); + } + const data = await api(`/api/tvdb/series/${encodeURIComponent(state.selectedSeries.id)}/episodes?order_type=aired`); + state.episodes = data.items || []; + state.selectedEpisodeIds = new Set(); + state.episodeSelectionAnchorId = null; + renderEpisodesList(); out("Episodes loaded", data); } @@ -1051,6 +1132,9 @@ if (e.target === el.settingsModal) closeSettingsModal(); }); el.refreshEpisodesBtn.addEventListener("click", () => withHandler(loadEpisodes, el.refreshEpisodesBtn)); + el.addSelectedEpisodesBtn.addEventListener("click", () => + withHandler(addSelectedEpisodes, el.addSelectedEpisodesBtn) + ); el.refreshSelectedEpisodesBtn.addEventListener("click", () => withHandler(loadSelectedEpisodes, el.refreshSelectedEpisodesBtn)); el.clearSelectedEpisodesBtn.addEventListener("click", () => diff --git a/app/static/styles.css b/app/static/styles.css index 4198750..2e30313 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -343,6 +343,10 @@ button.secondary { min-width: 0; } +#episodesList li.episode-row { + cursor: pointer; +} + #episodesList .episode-title { white-space: nowrap; overflow: hidden; @@ -359,6 +363,10 @@ button.secondary { color: var(--button-primary-bg); } +#episodesList li.episode-row.episode-anchor { + box-shadow: inset 0 0 0 1px var(--button-primary-border); +} + .badge { display: inline-block; font-size: 11px; diff --git a/app/templates/index.html b/app/templates/index.html index 36127af..e336791 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -69,7 +69,8 @@
- + +
    diff --git a/data/session_state.sqlite3 b/data/session_state.sqlite3 index bfdbd51c155df0a1b4d5916efed8fca03da47d02..4c53beeee4eede008171f45f6c7a180d7e61e14c 100644 GIT binary patch delta 40243 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 delta 11258 zcmcgy36LCDdEV`wJ+#-XCCe)xvYyr!vSD{Mvom{)2#IAQABuJO0GmX{dNciIrZqF& zqdrESO{lh0q2d(A&ay?ipaO^_Rm4dJFzaGTCD_J5L6O*oA|x>wf~lg2gDW8iq{0#B z``_O)nq4_mLZ#TL>79Q4-v9pldVT*>+wcG8wPWPNP%AJVHQ2Bt=?!|k?9#?zneOQql6mwqYz*Xj4CKeE1e^5OJFY16!_ z-&2lksC&K8@#oe)a$u)Z%kd>t5Bgeb-WcWdsm&!z*h; znd<1fhr6FY*gH9#x-7Lcy!O$Xr`F%S><*c^;Xhp)x~@9I7KdjS9K=K4#YzI@D_8@+sO=!^HPJ$>KQdS>$62zDJ?8~@t)dj4x) zOkF&f{uVaxTT1^Z{e$$sq+d>dC;crfekQ&4?SI~59v^>tZRmR+z`zyb>remW`rBVK zHt?3dkz?E5J^aG(RYS)Ht@Qqh`SI*ncJvS){7vVB1qTK;-twmYKRP%&R_@z2_@2Je z&yS`@uHW|d;nj5CcxmYO2j62ncmC7&4*pwllI zJN@;(q4bvt*Bg3p{k!y+>4RmTNFTdy98VX=a3R_7-|M#F|1|@+>`&oxwTa7KVYOMfB#mGpPhkEh?3{wC;uGW}?J4SHdx{j?95htskBWdEMMSTl+*{f(HH_U9M! z3yTXQ{Bt3bU)&Zy`egszS52Jm8z28s--JkSjQ?oj@Wk`!o6|2${MW?e6OW{?N;lH) zO1}$0|8_in=$Zb>or?z-3J2#G=5h;}#pT@VGMR&!%tS1n?LR_@?ps~mtLNRla4@@k zFf%_lzqnM$F7v#RtIebR^O^a@*-UOWQ@As$C(Re;a!ZBf#R5;-wsjJA%pF|F&lQ%l zi`j+vKc4OX;?5=PdN5O%%g)a)EG+RtW8;PA`o~UeC%~JZZ~SnNF>?P{YT(CxqkpyS zg`t-Rehi9U9>{%jYTMwY4?I~qnqMG>{Mxi_O&=7~d^bHKrpdTyCmuES4d|)yslZr_Kl-RK zx}fh5G+jSXo_}Dmkk99GizT&?%P-EIZdU#WEM5$ZOC#_xJ3F7HDQ-)7EnA)KAbZyJ z>XvZJqH0&FqAdNY?K&a|J;xW8^lIX4RXM_Tu&E;hJd<_dhrqM}FH!eJQB`b5SZ&*> zV3w$Oq4M2^64J3mqfNNl?r7+Xv$E|&?wO63KV*FEM$P-p^4fUd3FEc>QFcz;c>OKn zP(?X`FK%#MY$c9L%Lf(&^N8zN(gBYhM+Je9o)UNzh`<#mq-Xo|BXmkcrU)R6zzc)w zoH&FBRn->2xutMb1Qt}edQu6Zn9&oW?qUhJ_l2@vZv%#O{DvLCKZ=&^ftsKyu_XZ5we8y_ur3iX#%Dw^48&PGs0uL2v&))=VWdR$v2NGG1_{ivm8jHZ!wo$>Y0+2WE>*`wFz9B|i5pobX7p50Q5B)r ztkdMEycn$1X$0IZ+YU)P0I#7I4OR6NWV1hXsKhN@zS( zK0rvS%5p&H*gz@-n{TE0qUJUl3jFgU7k0xWKxbyP&ftEt(p+=61${Z8fSZ3Uq(G++g(3Qb@>7r_Qk zR*FzgU%Z(V2FwHBL8Pdtv&`qJL!1MkqhNvlp>?AMg0UUXajMD=eA`kII@OS+YQRFy ziL-7|*^w5>F=EPD+pC2RjHb&=PaKmlW8mpnb0@u%&KpAylCmKeJSkP7xY#yXqdXZ9 zDr{%Oq>h0>svrTxdZM*S$8K=FsOe%IR4>uAYYzF<0F!<>3_y@~?Tk>iqZ*(YbTk~> z4;#>TQWETSrz%xvkJ@$kLC+#`Ro(-e$SfP;&B`mf9>~E&;@n+8sUr{3)$piI%6--@ z)ktAsl8_X36t^_MN*nrp7T^US;^%2)0oDl*x+Fk13o2N_7*Xr1U?spW;6`2sn%n3x zr$fFkeekhMrV~_Mr!At}VK;t4CiD`%5h|;xyBSdG5&{8Dq$GGk!aA)Sn}j5t#GIHx zw<|KDiqdwf@>Ne8)mh~c&b34rfSfZ&E?hYk_TCW;=I7wP6YqVYV~&2Fp7~WAbM*7{ z%)O3Ti=k{s4$0vTS(i|A2ug;I3VqM5xiI2J7zTlq`0oaGLrS~efFJVlkCmOTfvTyN z&A$@>By%qe8#dX7AK*vLx91S}oEmgno(7a!SO@ShlKAD;5eH=p@yNGZVN1fX2Vo5g z4V|m$@mgp@b!f4xF=7OR5WuBqK{EkS*;>t1UIaGoDYi-|E-n-qahIUz+MWVv?f%t8 z`D&nA2_d2mLgLkBW9MzCAuQFfCGZx~4{X4|Pe&wE>vqrvf^`OxXT1W}KDLt@&nBn< zUKHBvEqDwKHfl-fVHCjt2}!k~G#!D$9Vn&1!k8qOo(6N=Y9Q-wttM?vNQf{4L?nyP zlO(-cAt@IY56>g&5>6z9>`E?pNSTG(DB>Ry4qwz^ZKbMo@e7_45gIEH=ZIHlLh_QE zL!A#G6sAg)oM@CX*eLXDkHOL6lI@XJ1(Jfj&X)AVf@edGFak!y=}VyC&xuGX zP@;U|&zW$)jbN{wGogM!K_r+FG}a53SP(+|I(14yr2=n~6slpvxfUU9@bP?71#y62 z(A&wbBB&ni#W=`-?yUVVxt^qo6D<;sEc$Legzh#ZJictHl9m_oo0*a1SV{6_OSo5G zD@IinNii^MLCw?ETF}?{mxfXU@pIc!!|{7>G%kw8r_8}&mC?`gar9YpS3LQMxqYvt z^wK5$Agiw>!g)bSea*+mFn3umRC#``uX#PoD(YDUJ(SZ|-k)(%@gx7*e`WmSFB*gK z_-`3Q@u`oS1M#!>rglv;o=P&9l3uED={|@rdeD$@{rml+3)Z6EhJp6})|)9g$0%zw z5+VyCKKHnJ$sOHWBwaH+jfv%@nk2Q%Sn&Ru48^=C^Miz@{Y+qd|Lcw2S0sR!8K}i9 zV4#cfC;!seeKGS(E$dbMd@=sd$ILxDGzO}7NiKf)G2_x97LbY`d)C+y|JiSWz*y5QinM|Cy8vA`?KLmNzYk<~orc!%i@f>d7|4w6KoM$or^YIg; zUOPW!Za2yzKKd#1>K()+6jbxjPbN`{_^pGfT|(=Rm1qGAgW19mi}9%kjj4G09maM; zEyoW$V(c2{=S);SKC;iu#{cy1{ey;;joaVv-yI)0Y>eNPV9%RDqqT)tyn)v21P`X0 zLFP^(VbQ{3b!R^GaK-)m3{{99-Vb)%a~LZ9wsl;-ylzfi%penavlcDKuYb_k z*DvDZ&lpotVnEC$#$t&#WU29$4Ubhau|meAS7VuDn=CTZ3yLWM7npDjd9hbXt&UoD zXl5N}+*nr_Mm~NlmD&-1hBWdOC~xe1%)Bg7)IxmjjJZ4h$i1n7bjOr6T*zuPG^gWJ zPnko*%yugmzxW2@lHmjiE57XwfcV76O%dPq1!F30ZDPfIEcQY0OHZ5Q@$bDgH5$M7 zT4SVtIzIO?>@0-2<<}H^;vA!rWf`v4w2nK}eux>^`z8&)meu-CNMTleCl5*m5KrFiwv!SwHZ6inZ> z<Nni4Keu^I<;zh6n35AA%FO|8a95)A7CuYk3w+fuZv1Qo@qFcFXabzH45bSTd`% z_L*14x4`G^Utq>FkmRwp%OvO|Oo}gh9P0QdXUy%u- ztwbRbJK}|G{fQ)cMU>#kz$SPytNzjqIr%H&4~(StOl&eaEB@ZI#%trR{^^Od zB|e*VE1Ah#B__jY^R~Q2A^!Sv#;)klin#G^&`dp)L&9GH#Tiv-`ux9ga$1lkWRyku zdv+;{t=g)XFO=Nb0>6q|Gbcc1c~;lEf@qcVtcI0#;=MKv;2|3ZsEN8MZGxbvoIn-bS8O zl?fKesn7|j@w{x^>QWG8TTIE^mi|cx;+EW!8ZPvxT&lED7~{ucf_P1wU`U?5;uobr zR=)z2XGGPlv_%c2fv2BaLBiXbkSijxD{_SgvK5!QNWy8m4VUGRMbYl^vMh=f!0}hm za^ZbY5omhKl9~@F;Zf`3Np>Ndu*^aS#*^r`p%QR3D(E$#uO~&>UO}aStn*}ao&;4u zNu~Ub4pj*io3d-ds)_PImq_UK zC8a@T4jm(hR=6mQkaIVio?AvWWfK)H>HvYxsyl=BAj3vADFF_pligbo_+0cPf9&1aoks{ z^|p$Sl@%zV+_R+xJenw$lTN1oiGmYiKuaH3K!ldG6d9TAv=mD8O8og5GaY%VrBDK* zU&h{mEC)q6^})O1p;mZe92RDS30g+n3a3L1-*?fn(-K0x22i%fuDn3hB-I+XJSZGx z6tL#Tmlcr=NosmIi>*^yIwc&K2vnG!2l}8B;F7>USPD81auy^f&bYvaml1~>O%#b> zu82v{ZffkfLv zD4FHC6$lrtlZxkJM>Gl==*uHl>^3XgC#UDc8*QHsa9qeB2q9T585m8V&?yvdq_MOc zP0bgS;I4of^rD1b46HyqT;f|*`PA9~lhHp10KFc)O5Ls`#-RT(gW;3YogSu3XdFht z4*e%II=vdK6fi>Y(}M(#PzSWlOp+c>cyM$91yVjt3*1kfUI7*_ohk+dISxe}gD0RK z3B>HVuoWp#qx6B-xYbmZbHLao6i~pU^AP|TiH^LLib(D-kOqk!hye3IHJacPq9eH@ zG}7M;FS!B~Z+F3Ww6fin6t}n@X`$vOmbJhNk_rSwc^lcD7G4w15|SnxfXY#eM~k?M zxsFW+5wSxE$WI^d9E)7w5Y8J(ok-VBB9IP+X9I^9tAYx>v$hay+VSF075MmWSO?|3 z{^GPvTHWEs&u;tw3&W3}oH2Jr*q@9Cc>n*JJX|sxdlBl|IoX7!Zn5p?RgBw3+Q7{i z*f)g_QjZc$u%tb3!-iVGvT*!Brw=S%ea%6WHXHR8+qux3Bf<6zg+qMs6r_2fE%(~+ zL2)aNQ-BbpcvQP`ZB8&rI8lv77P0N8**LkOu7O8n;eof+#Jhg1q5KA+q>Y&h}2+fjymRC41KWo0yBlT>39mz%r^Qnn&?|x zoFylL2tJ(cGy1<8sJ2hPsS~NgNTfJm>$F5?Qk|F$XBOxXQ_kwnuFG*-TB5`un{6=C z=@UrnH9~4T!s*!1TgvGXv8iX<3D|IfC?&fv@AY84ObgL(%g>Kv6@a*yRjM%umh)7m|6{2IWQ^AFkcd{cLsfmx} zT!khOu}Iqxs>w}jvM6lR`vFP95oM895~w6_BVytxoM!V;Ur+F+y3m4=AsYwKq!htX zfZoMNr$*~`HISDFKRua*%C<*CpaV(^ewAo=es`v3VA&C8@DeO>(MNK z=O(t~Vk=Lh3o{|jSw1gC5I z1*Y>JF)o/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: panel 2 controls include Add Selected and no static row Add button ==" +grep -q 'id="addSelectedEpisodesBtn"' app/templates/index.html || { echo "Add Selected button missing"; exit 1; } +grep -q 'id="refreshEpisodesBtn" class="secondary"' app/templates/index.html || { echo "Refresh Episodes should be secondary"; exit 1; } +echo "panel controls validation passed" + +echo +echo "== Feature test 2: episode selection modifiers are implemented ==" +grep -q "selectedEpisodeIds: new Set()" app/static/app.js || { echo "selectedEpisodeIds state missing"; exit 1; } +grep -q "episodeSelectionAnchorId" app/static/app.js || { echo "episode anchor state missing"; exit 1; } +grep -q "event.shiftKey" app/static/app.js || { echo "shift selection handling missing"; exit 1; } +grep -q "event.ctrlKey || event.metaKey" app/static/app.js || { echo "ctrl/meta toggle handling missing"; exit 1; } +echo "modifier selection validation passed" + +echo +echo "== Feature test 3: Add Selected bulk add flow is wired and clears selection ==" +grep -q "async function addSelectedEpisodes()" app/static/app.js || { echo "addSelectedEpisodes function missing"; exit 1; } +grep -q "clearEpisodeSelection();" app/static/app.js || { echo "selection clear after add missing"; exit 1; } +grep -q "await loadSelectedEpisodes();" app/static/app.js || { echo "selected episodes reload missing"; exit 1; } +echo "bulk add flow validation passed" + +echo +echo "All episode selection feature tests passed."