source_utils_searchOutcome.bs

import "pkg:/source/translationKeys.bs"
import "pkg:/source/utils/misc.bs"

' What a search ended with, and what the search screen shows for it. SearchTask reports the
' status; SearchResults shows the view. Pure, so the rules are unit-tested without a network
' seam. Also which reads a search sends (canSearchLiveTv), since a read the server refuses
' would make every search look partial.
'
' A read that failed says nothing about what the server holds, so a search that got no answer
' must never read as one that found nothing: "failed" offers Try again, "empty" says there are
' no results. A search whose reads partly answered shows what arrived and says some is missing.
namespace searchOutcome

  const STATUS_OK = "ok"
  const STATUS_PARTIAL = "partial"
  const STATUS_FAILED = "failed"

  const SHOW_RESULTS = "results"
  const SHOW_EMPTY = "empty"
  const SHOW_FAILED = "failed"

  ' How a search's reads ended: every one answered, some did, or none did. A search that sent
  ' nothing (no read could be built) failed, since nothing answered for it.
  '
  ' @param {integer} sentCount - the reads asked for
  ' @param {integer} failedCount - the reads that did not answer
  ' @return {string} a STATUS_ constant
  function statusOf(sentCount as integer, failedCount as integer) as string
    if sentCount < 1 or failedCount >= sentCount then return searchOutcome.STATUS_FAILED
    if failedCount > 0 then return searchOutcome.STATUS_PARTIAL
    return searchOutcome.STATUS_OK
  end function

  ' What the screen shows for a search that ended with `status` and `itemCount` items, the
  ' toast to raise with it, and what the screen reader announces ("" for none). A partial search
  ' that found nothing is a failure: the reads that answered say nothing about the ones that did
  ' not. A status that is none of these reads as failed, since nobody can vouch for its empty
  ' list.
  '
  ' Failed and empty are announced because focus stays on the keyboard, and the screen reader
  ' speaks only what has focus: without it, neither is heard. Results need no announcement (the
  ' viewer moves into them), and a partial search's toast is its notice.
  '
  ' @param {dynamic} status - a STATUS_ constant
  ' @param {integer} itemCount - the items the search found
  ' @return {object} { show: a SHOW_ constant, toastKey and announceKey: a translation key or "" }
  function view(status as dynamic, itemCount as integer) as object
    if not isValidAndNotEmpty(status) then status = searchOutcome.STATUS_FAILED
    if status = searchOutcome.STATUS_OK
      if itemCount > 0 then return { show: searchOutcome.SHOW_RESULTS, toastKey: "", announceKey: "" }
      return { show: searchOutcome.SHOW_EMPTY, toastKey: "", announceKey: translationKeys.MessageSearchNoResults }
    end if
    if status = searchOutcome.STATUS_PARTIAL and itemCount > 0
      return { show: searchOutcome.SHOW_RESULTS, toastKey: translationKeys.MessageSearchPartial, announceKey: "" }
    end if
    return { show: searchOutcome.SHOW_FAILED, toastKey: "", announceKey: translationKeys.MessageSearchFailed }
  end function

  ' Whether a search sends its Live TV program read. Jellyfin refuses that read to a user
  ' without Live TV access (its LiveTvAccess policy), so asking would count every one of that
  ' user's searches as partial. A policy, or a field, that is missing reads as allowed, as
  ' SessionDataTransformer defaults it.
  '
  ' @param {dynamic} policy - m.global.user.policy (JellyfinUserPolicy node or AA), or invalid
  ' @return {boolean} true when the search asks for Live TV programs
  function canSearchLiveTv(policy as dynamic) as boolean
    if not isValid(policy) then return true
    access = policy.enableLiveTvAccess
    if not isValid(access) then return true
    return access <> false
  end function

end namespace