|
192 | 192 | if isValid(task) and task.subtype() = "RecordProgramTask" |
193 | 193 | handleRecordResult(task) |
194 | 194 | end if |
195 | | - else if isNodeEvent(msg, "isDone") |
196 | | - resultNode = msg.getRoSGNode() |
197 | | - if isValid(resultNode) and resultNode.isSameNode(m.watchedResultNode) |
198 | | - handleWatchedToggleDone() |
199 | | - end if |
200 | 195 | else if isNodeEvent(msg, "selectedItem") |
201 | 196 | ' If you select a library from ANYWHERE, follow this flow |
202 | 197 | selectedItem = msg.getData() |
|
540 | 535 | else if btn.id = "watchedButton" |
541 | 536 | item = group.itemContent |
542 | 537 | if isValid(item) and isValidAndNotEmpty(item.id) |
543 | | - ' Series: confirm before marking all episodes watched/unwatched (can affect hundreds of episodes) |
| 538 | + ' Series: confirm before marking all episodes watched/unwatched (can affect hundreds of |
| 539 | + ' episodes). The dialog routing stays here (#550 sgRouter territory); the fetch itself is |
| 540 | + ' delegated to ItemDetails.toggleWatched() on confirm (see the isDataReturned handler). |
544 | 541 | if item.type = "Series" |
545 | 542 | watchedButton = group.findNode("watchedButton") |
546 | | - m.pendingWatchedItemId = item.id |
547 | 543 | ' Read current state from button — writing to itemContent triggers a full rebuild |
548 | | - m.pendingWatchedIsCurrentlyWatched = isValid(watchedButton) and watchedButton.isButtonSelected |
549 | | - if m.pendingWatchedIsCurrentlyWatched |
| 544 | + isCurrentlyWatched = isValid(watchedButton) and watchedButton.isButtonSelected |
| 545 | + if isCurrentlyWatched |
550 | 546 | confirmMsg = translate(translationKeys.MessageMarkAllEpisodesInThisSeries) |
551 | 547 | confirmBtn = translate(translationKeys.ButtonMarkUnwatched) |
552 | 548 | else |
553 | 549 | confirmMsg = translate(translationKeys.MessageMarkAllEpisodesInThisSeries2) |
554 | 550 | confirmBtn = translate(translationKeys.ButtonMarkWatched) |
555 | 551 | end if |
| 552 | + m.pendingWatchedConfirmation = true |
556 | 553 | m.global.sceneManager.callFunc("showConfirmationDialog", translate(translationKeys.LabelWatched), [confirmMsg], [translate(translationKeys.ButtonCancel), confirmBtn]) |
557 | 554 | else |
558 | | - watchedButton = group.findNode("watchedButton") |
559 | | - if isValid(watchedButton) and watchedButton.isLoading then continue while ' Already in-flight |
560 | | - ' Read current state from button — writing to itemContent triggers a full rebuild |
561 | | - isCurrentlyWatched = isValid(watchedButton) and watchedButton.isButtonSelected |
562 | | - if isCurrentlyWatched |
563 | | - req = GetApi().BuildUnmarkPlayedRequest(item.id) |
564 | | - else |
565 | | - req = GetApi().BuildMarkPlayedRequest(item.id) |
566 | | - end if |
567 | | - newWatchedState = not isCurrentlyWatched |
568 | | - if isValid(watchedButton) then watchedButton.isLoading = true |
569 | | - ' Set resume button to loading while we wait for async response |
570 | | - if newWatchedState |
571 | | - resumeButton = group.findNode("resumeButton") |
572 | | - if isValid(resumeButton) then resumeButton.isLoading = true |
573 | | - end if |
574 | | - resultNode = submitApiRequest(req, "watchedToggle") |
575 | | - if isValid(resultNode) |
576 | | - m.watchedResultNode = resultNode |
577 | | - m.pendingWatchedButton = watchedButton |
578 | | - m.pendingWatchedNewState = newWatchedState |
579 | | - m.pendingWatchedItemType = item.type |
580 | | - m.watchedResultNode.observeField("isDone", m.port) |
581 | | - else |
582 | | - ' Pool not ready — fall back to fire-and-forget + direct update |
583 | | - SubmitSideEffect(req) |
584 | | - if isValid(watchedButton) |
585 | | - watchedButton.isButtonSelected = newWatchedState |
586 | | - watchedButton.isLoading = false |
587 | | - end if |
588 | | - ' Remove stale resume button when marking as watched |
589 | | - if newWatchedState |
590 | | - removeResumeButtonFromGroup(group) |
591 | | - end if |
| 555 | + ' Movie/Episode: toggle immediately on the active screen's render-thread method |
| 556 | + ' (callFunc rendezvouses there, where the toggle runs as a promise). The in-flight |
| 557 | + ' guard, optimistic state, stale-resume removal, and error contract all live in |
| 558 | + ' ItemDetails.toggleWatched(). |
| 559 | + if isValid(group) |
| 560 | + group.callFunc("toggleWatched") |
592 | 561 | end if |
593 | 562 | end if |
594 | 563 | end if |
|
884 | 853 | m.pendingDeleteItemId = invalid |
885 | 854 | end if |
886 | 855 | ' Handle watched confirmation dialog (Series only) |
887 | | - else if isValid(m.pendingWatchedItemId) |
| 856 | + else if m.pendingWatchedConfirmation = true |
| 857 | + m.pendingWatchedConfirmation = false |
888 | 858 | if popupNode.returnData.indexSelected = 1 |
889 | | - ' User confirmed — mark/unmark all episodes |
890 | | - if m.pendingWatchedIsCurrentlyWatched |
891 | | - req = GetApi().BuildUnmarkPlayedRequest(m.pendingWatchedItemId) |
892 | | - else |
893 | | - req = GetApi().BuildMarkPlayedRequest(m.pendingWatchedItemId) |
894 | | - end if |
895 | | - newWatchedState = not m.pendingWatchedIsCurrentlyWatched |
896 | | - m.pendingWatchedItemId = invalid |
897 | | - m.pendingWatchedIsCurrentlyWatched = invalid |
| 859 | + ' User confirmed — delegate the mark/unmark of all episodes to the active screen's |
| 860 | + ' render-thread method (callFunc rendezvouses there, where it runs as a promise). |
| 861 | + ' toggleWatched() re-reads the button state to pick mark vs unmark, so no fetch state |
| 862 | + ' needs threading through the dialog. |
898 | 863 | activeGroup = m.global.sceneManager.callFunc("getActiveScene") |
899 | | - watchedButton = invalid |
900 | 864 | if isValid(activeGroup) |
901 | | - watchedButton = activeGroup.findNode("watchedButton") |
| 865 | + activeGroup.callFunc("toggleWatched") |
902 | 866 | end if |
903 | | - if isValid(watchedButton) then watchedButton.isLoading = true |
904 | | - ' Set resume button to loading while we wait for async data |
905 | | - resumeButton = invalid |
906 | | - if isValid(activeGroup) then resumeButton = activeGroup.findNode("resumeButton") |
907 | | - if isValid(resumeButton) then resumeButton.isLoading = true |
908 | | - resultNode = submitApiRequest(req, "watchedToggle") |
909 | | - if isValid(resultNode) |
910 | | - m.watchedResultNode = resultNode |
911 | | - m.pendingWatchedButton = watchedButton |
912 | | - m.pendingWatchedNewState = newWatchedState |
913 | | - m.pendingWatchedItemType = "Series" |
914 | | - m.watchedResultNode.observeField("isDone", m.port) |
915 | | - else |
916 | | - ' Pool not ready — fall back to fire-and-forget + direct update |
917 | | - SubmitSideEffect(req) |
918 | | - if isValid(watchedButton) |
919 | | - watchedButton.isButtonSelected = newWatchedState |
920 | | - watchedButton.isLoading = false |
921 | | - end if |
922 | | - ' Series: refresh resume data to reflect new watched state |
923 | | - if isValid(activeGroup) |
924 | | - activeGroup.refreshResumeData = not activeGroup.refreshResumeData |
925 | | - end if |
926 | | - end if |
927 | | - else |
928 | | - ' User cancelled |
929 | | - m.pendingWatchedItemId = invalid |
930 | | - m.pendingWatchedIsCurrentlyWatched = invalid |
931 | 867 | end if |
932 | 868 | else |
933 | 869 | selectedItem = m.global.queueManager.callFunc("getHold") |
|
1145 | 1081 | end if |
1146 | 1082 | end sub |
1147 | 1083 |
|
1148 | | -sub handleWatchedToggleDone() |
1149 | | - res = m.watchedResultNode.result |
1150 | | - m.watchedResultNode.unobserveField("isDone") |
1151 | | - m.watchedResultNode = invalid |
1152 | | - |
1153 | | - ' Debug error injection — compiled out in production (bs_const=debug=false) |
1154 | | - #if debug |
1155 | | - if isValid(m.global.debug) and m.global.debug.shouldForceWatchedFail |
1156 | | - res = { ok: false } |
1157 | | - end if |
1158 | | - #end if |
1159 | | - |
1160 | | - if isValid(res) and res.ok |
1161 | | - if isValid(m.pendingWatchedButton) |
1162 | | - m.pendingWatchedButton.isButtonSelected = m.pendingWatchedNewState |
1163 | | - end if |
1164 | | - ' Update resume button to reflect new watched state |
1165 | | - activeGroup = m.global.sceneManager.callFunc("getActiveScene") |
1166 | | - if isValid(activeGroup) |
1167 | | - if m.pendingWatchedItemType = "Series" |
1168 | | - ' Series: re-fetch next-up episode (may change or disappear) |
1169 | | - activeGroup.refreshResumeData = not activeGroup.refreshResumeData |
1170 | | - else if m.pendingWatchedNewState |
1171 | | - ' Non-Series marking watched: server clears playback position, remove stale resume button |
1172 | | - removeResumeButtonFromGroup(activeGroup) |
1173 | | - end if |
1174 | | - end if |
1175 | | - else |
1176 | | - ' Revert button to original state on failure |
1177 | | - if isValid(m.pendingWatchedButton) |
1178 | | - m.pendingWatchedButton.isButtonSelected = not m.pendingWatchedNewState |
1179 | | - end if |
1180 | | - ' Revert resume button loading state on failure |
1181 | | - activeGroup = m.global.sceneManager.callFunc("getActiveScene") |
1182 | | - if isValid(activeGroup) |
1183 | | - resumeButton = activeGroup.findNode("resumeButton") |
1184 | | - if isValid(resumeButton) then resumeButton.isLoading = false |
1185 | | - end if |
1186 | | - displayToast(translate(translationKeys.ErrorFailedToUpdateWatchedStatus), "error") |
1187 | | - end if |
1188 | | - |
1189 | | - if isValid(m.pendingWatchedButton) |
1190 | | - m.pendingWatchedButton.isLoading = false |
1191 | | - end if |
1192 | | - m.pendingWatchedButton = invalid |
1193 | | - m.pendingWatchedNewState = invalid |
1194 | | - m.pendingWatchedItemType = invalid |
1195 | | -end sub |
1196 | | - |
1197 | 1084 | sub handleRecordResult(task as object) |
1198 | 1085 | res = task.result |
1199 | 1086 | task.unobserveField("result") |
|
1235 | 1122 | m.pendingRecordIsCancel = invalid |
1236 | 1123 | end sub |
1237 | 1124 |
|
1238 | | -' removeResumeButtonFromGroup: Remove the resume button from an ItemDetails button group. |
1239 | | -' Used after marking an item as watched — the server clears playback position so the |
1240 | | -' resume button is stale. Focus is preserved on the nearest remaining button. |
1241 | | -sub removeResumeButtonFromGroup(group as object) |
1242 | | - resumeButton = group.findNode("resumeButton") |
1243 | | - if not isValid(resumeButton) then return |
1244 | | - |
1245 | | - buttonGrp = group.findNode("buttons") |
1246 | | - if not isValid(buttonGrp) then return |
1247 | | - |
1248 | | - currentFocusIndex = buttonGrp.buttonFocused |
1249 | | - resumeIndex = -1 |
1250 | | - for i = 0 to buttonGrp.getChildCount() - 1 |
1251 | | - child = buttonGrp.getChild(i) |
1252 | | - if isValid(child) and child.isSameNode(resumeButton) |
1253 | | - resumeIndex = i |
1254 | | - exit for |
1255 | | - end if |
1256 | | - end for |
1257 | | - |
1258 | | - wasFocused = resumeButton.isInFocusChain() |
1259 | | - buttonGrp.removeChild(resumeButton) |
1260 | | - |
1261 | | - if resumeIndex >= 0 and currentFocusIndex >= resumeIndex |
1262 | | - focusIdx = currentFocusIndex - 1 |
1263 | | - if focusIdx < 0 then focusIdx = 0 |
1264 | | - buttonGrp.buttonFocused = focusIdx |
1265 | | - end if |
1266 | | - if wasFocused |
1267 | | - nextButton = buttonGrp.getChild(buttonGrp.buttonFocused) |
1268 | | - if isValid(nextButton) |
1269 | | - nextButton.setFocus(true) |
1270 | | - end if |
1271 | | - end if |
1272 | | -end sub |
1273 | | - |
1274 | 1125 | ' Shared handler for menu actions from both the OptionsSlider and the user dropdown. |
1275 | 1126 | ' Returns true if the caller should goto appStart (session-ending actions). |
1276 | 1127 | function handleMenuAction(actionId as string) as boolean |
|
0 commit comments