Skip to content

Commit d9a6792

Browse files
chore(docs): update code docs
1 parent 388dba6 commit d9a6792

5 files changed

Lines changed: 92 additions & 171 deletions

File tree

docs/components_ItemDetails.bs.html

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,76 @@
367367
displayToast(translate(translationKeys.ErrorFailedToUpdateFavorite), "error")
368368
end sub
369369

370+
' callFunc entry — toggles the watched/played state of the current item. Invoked from main.bs's
371+
' button router; Movie/Episode call it directly, while Series route through the "mark all
372+
' episodes" confirmation dialog first (main.bs keeps that dialog routing and re-invokes this on
373+
' confirm — #550 sgRouter territory). Because callFunc rendezvouses to this render-thread-owned
374+
' node, the body (and the fetchAsync().then() promise delivery + auto-abandon) runs on this
375+
' component's render thread, where the named-observer adapter works. This is the deferred twin of
376+
' toggleFavorite() — the Phase-3c "main thread → render thread delegation" pattern (see
377+
' docs/dev/promises.md). It moves the LAST raw submitApiRequest + isDone flow off main.bs's god-loop.
378+
sub toggleWatched()
379+
watchedButton = m.top.findNode("watchedButton")
380+
item = m.top.itemContent
381+
if not (isValid(item) and isValidAndNotEmpty(item.id) and isValid(watchedButton)) then return
382+
if watchedButton.isLoading then return ' already in-flight
383+
384+
isSeries = (item.type = "Series")
385+
' Read the desired state from the button (writing itemContent would trigger a full rebuild).
386+
newState = not watchedButton.isButtonSelected
387+
if newState
388+
req = GetApi().BuildMarkPlayedRequest(item.id)
389+
else
390+
req = GetApi().BuildUnmarkPlayedRequest(item.id)
391+
end if
392+
watchedButton.isLoading = true
393+
394+
' Show the resume button as loading while the toggle is in flight: always for a Series (its
395+
' next-up episode is re-fetched on success), and on mark-watched for a Movie/Episode (its now
396+
' stale resume button is removed on success).
397+
if isSeries or newState
398+
resumeButton = m.top.findNode("resumeButton")
399+
if isValid(resumeButton) then resumeButton.isLoading = true
400+
end if
401+
402+
' Carry the button + intended state + itemType through the promise context (no closures in BS).
403+
promises.chain(fetchAsync(req, "watchedToggle-" + item.id), { button: watchedButton, newState: newState, isSeries: isSeries }).then(sub(res as object, ctx as object)
404+
ok = res.ok
405+
' Debug error injection — compiled out in production (bs_const=debug=false).
406+
#if debug
407+
if isValid(m.global.debug) and m.global.debug.shouldForceWatchedFail then ok = false
408+
#end if
409+
if ok
410+
ctx.button.isButtonSelected = ctx.newState
411+
if ctx.isSeries
412+
' Series: re-fetch next-up episode (may change or disappear).
413+
m.top.refreshResumeData = not m.top.refreshResumeData
414+
else if ctx.newState
415+
' Non-Series marking watched: server clears playback position — drop the stale resume button.
416+
removeResumeButtonWithFocus(m.top.findNode("resumeButton"))
417+
end if
418+
else
419+
' 4xx/5xx resolve under the error contract — the server rejected the change.
420+
onWatchedToggleFailed(ctx.button, ctx.newState)
421+
end if
422+
ctx.button.isLoading = false
423+
end sub).catch(sub(err as object, ctx as object)
424+
' Transport failure / timeout / pool-unavailable — the request never completed.
425+
m.log.warn("watched toggle failed", err.reason)
426+
onWatchedToggleFailed(ctx.button, ctx.newState)
427+
ctx.button.isLoading = false
428+
end sub)
429+
end sub
430+
431+
' Reverts the watched button to its pre-press state, clears the resume button's loading
432+
' state, and surfaces an error toast.
433+
sub onWatchedToggleFailed(watchedButton as object, attemptedState as boolean)
434+
if isValid(watchedButton) then watchedButton.isButtonSelected = not attemptedState
435+
resumeButton = m.top.findNode("resumeButton")
436+
if isValid(resumeButton) then resumeButton.isLoading = false
437+
displayToast(translate(translationKeys.ErrorFailedToUpdateWatchedStatus), "error")
438+
end sub
439+
370440
' Triggered by refreshResumeData field toggle (watched toggle on Series).
371441
' Re-fetches ONLY the next-up/resume episode — no full rebuild, no setupButtons().
372442
' The existing onNextUpEpisodeChanged() handles adding/removing/updating the resume button.

docs/data/search.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-ItemDetails.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/module-main.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/source_main.bs.html

Lines changed: 19 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,6 @@
192192
if isValid(task) and task.subtype() = "RecordProgramTask"
193193
handleRecordResult(task)
194194
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
200195
else if isNodeEvent(msg, "selectedItem")
201196
' If you select a library from ANYWHERE, follow this flow
202197
selectedItem = msg.getData()
@@ -540,55 +535,29 @@
540535
else if btn.id = "watchedButton"
541536
item = group.itemContent
542537
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).
544541
if item.type = "Series"
545542
watchedButton = group.findNode("watchedButton")
546-
m.pendingWatchedItemId = item.id
547543
' 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
550546
confirmMsg = translate(translationKeys.MessageMarkAllEpisodesInThisSeries)
551547
confirmBtn = translate(translationKeys.ButtonMarkUnwatched)
552548
else
553549
confirmMsg = translate(translationKeys.MessageMarkAllEpisodesInThisSeries2)
554550
confirmBtn = translate(translationKeys.ButtonMarkWatched)
555551
end if
552+
m.pendingWatchedConfirmation = true
556553
m.global.sceneManager.callFunc("showConfirmationDialog", translate(translationKeys.LabelWatched), [confirmMsg], [translate(translationKeys.ButtonCancel), confirmBtn])
557554
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")
592561
end if
593562
end if
594563
end if
@@ -884,50 +853,17 @@
884853
m.pendingDeleteItemId = invalid
885854
end if
886855
' Handle watched confirmation dialog (Series only)
887-
else if isValid(m.pendingWatchedItemId)
856+
else if m.pendingWatchedConfirmation = true
857+
m.pendingWatchedConfirmation = false
888858
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.
898863
activeGroup = m.global.sceneManager.callFunc("getActiveScene")
899-
watchedButton = invalid
900864
if isValid(activeGroup)
901-
watchedButton = activeGroup.findNode("watchedButton")
865+
activeGroup.callFunc("toggleWatched")
902866
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
931867
end if
932868
else
933869
selectedItem = m.global.queueManager.callFunc("getHold")
@@ -1145,55 +1081,6 @@
11451081
end if
11461082
end sub
11471083

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-
11971084
sub handleRecordResult(task as object)
11981085
res = task.result
11991086
task.unobserveField("result")
@@ -1235,42 +1122,6 @@
12351122
m.pendingRecordIsCancel = invalid
12361123
end sub
12371124

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-
12741125
' Shared handler for menu actions from both the OptionsSlider and the user dropdown.
12751126
' Returns true if the caller should goto appStart (session-ending actions).
12761127
function handleMenuAction(actionId as string) as boolean

0 commit comments

Comments
 (0)