source_GridView_GridPresenterBase.bs

' GridPresenterBase: Base class for library grid presenters
'
' Defines the interface contract for presenter classes used by BaseGridView.
' Each media type (Movie, Music, TVShow, etc.) has its own presenter that
' extends this base class and implements media-specific behavior.
'
' Usage:
'   presenter = new MoviePresenter(baseGridView)
'   baseGridView.setPresenter(presenter)

class GridPresenterBase
  ' Reference to the BaseGridView component
  protected view

  ' Reference to the log instance
  protected log

  ' Cached filter options from API (invalid = not loaded, {} = load failed)
  protected apiFilters

  ' Constructor - view is set later via onInit()
  sub new()
    m.view = invalid
    m.log = invalid
    m.apiFilters = invalid
  end sub

  ' ============================================================================
  ' Backdrop Configuration
  ' ============================================================================

  ' Returns the backdrop mode for this presenter
  ' @return {string} "presentation" | "fullscreen" | "none"
  function getBackdropMode() as string
    return "fullscreen"
  end function

  ' ============================================================================
  ' Initialization
  ' ============================================================================

  ' Called after presenter is attached to BaseGridView
  ' Stores view reference and creates shared task nodes
  ' @param {object} view - Reference to BaseGridView component
  ' Override in subclass but call super.onInit(view) first
  sub onInit(view as object)
    m.view = view
    ' Task node stored on view (component scope) so observer callbacks work
    m.view.getFiltersTask = CreateObject("roSGNode", "GetFiltersTask")
  end sub

  ' ============================================================================
  ' Options Configuration
  ' ============================================================================

  ' Returns the options configuration for ItemGridOptions
  ' @param {object} parentItem - The library item being displayed
  ' @return {object} Options object with views, sort, filter arrays
  function getOptions(_parentItem as object) as object
    return {
      views: [],
      sort: [],
      filter: []
    }
  end function

  ' Called when options dialog is closed
  ' Override to handle presenter-specific option changes
  ' @param {object} options - The ItemGridOptions component
  sub onOptionsClosed(_options as object)
    ' Override in subclass
  end sub

  ' ============================================================================
  ' Load Task Configuration
  ' ============================================================================

  ' Configures the LoadItemsTask2 for data loading
  ' @param {object} task - The LoadItemsTask2 node
  ' @param {object} parentItem - The library item being displayed
  ' @param {string} viewMode - Current view mode (e.g., "Movies", "MoviesGrid")
  sub configureLoadTask(_task as object, _parentItem as object, _viewMode as string)
    ' Override in subclass
  end sub

  ' Returns the item type(s) to load
  ' @return {string} Item type(s) for API query (e.g., "Movie", "Series,Movie")
  function getItemType() as string
    return ""
  end function

  ' ============================================================================
  ' Grid Configuration
  ' ============================================================================

  ' Returns grid layout configuration for the specified view mode
  ' @param {string} viewMode - Current view mode
  ' @return {object} Grid config with properties:
  '   - translation: [x, y] grid position
  '   - itemSize: [width, height] size of each grid cell
  '   - rowHeights: [height] array of row heights
  '   - numRows: string number of visible rows
  '   - numColumns: string number of columns
  '   - imageDisplayMode: "scaleToZoom" | "scaleToFit"
  function getGridConfig(_viewMode as string) as object
    return {
      translation: [96, 60],
      itemSize: [264, 396],
      rowHeights: [396],
      numRows: "4",
      numColumns: "6",
      imageDisplayMode: "scaleToZoom"
    }
  end function

  ' Returns whether to show presentation info panel for the view mode
  ' @param {string} viewMode - Current view mode
  ' @return {boolean} True to show info panel
  function shouldShowPresentationInfo(_viewMode as string) as boolean
    return false
  end function

  ' ============================================================================
  ' Focus Handling
  ' ============================================================================

  ' Called when an item receives focus
  ' Override to update presentation info display
  ' @param {object} item - The focused ContentNode item
  ' @param {string} currentView - The current view mode (e.g., "Movies", "MoviesGrid")
  sub onItemFocused(_item as object, _currentView as string)
    ' Override in subclass
  end sub

  ' ============================================================================
  ' Presentation Info
  ' ============================================================================

  ' Creates presenter-specific info nodes in the presentationInfo group
  ' Called during onInit() if presenter needs custom info display
  ' @param {object} infoGroup - The presentationInfo Group node
  sub createInfoNodes(_infoGroup as object)
    ' Override in subclass
  end sub

  ' Clears/hides the presentation info display
  sub clearPresentationInfo()
    ' Override in subclass
  end sub

  ' ============================================================================
  ' Dynamic Filters
  ' ============================================================================

  ' Load dynamic filter options (Genres, Years, Ratings) from the Jellyfin API.
  ' Call from configureLoadTask() in subclasses that need dynamic filters.
  ' Results arrive via onFiltersLoaded() and are cached in m.apiFilters.
  ' @param {object} parentItem - The library item to load filters for
  ' @param {string} itemType - The item type to filter (e.g., "Movie", "Series")
  sub loadFilters(parentItem as object, itemType as string)
    if not isValid(m.view) or not isValid(m.view.getFiltersTask) then return

    m.view.getFiltersTask.observeField("filters", "onPresenterFiltersLoaded")
    m.view.getFiltersTask.params = {
      userid: m.view.global.user.id,
      parentid: parentItem.Id,
      includeitemtypes: itemType
    }
    m.view.getFiltersTask.control = "RUN"
  end sub

  ' Called when filters are loaded from API (via onPresenterFiltersLoaded bridge in BaseGridView).
  ' Caches the result in m.apiFilters and shows a toast on failure.
  ' @param {object} event - roSGNodeEvent for the "filters" field change
  sub onFiltersLoaded(event as object)
    m.apiFilters = event.getData()
    if not isValid(m.view) or not isValid(m.view.getFiltersTask) then return

    m.view.getFiltersTask.unobserveField("filters")

    errorMsg = m.view.getFiltersTask.error
    if errorMsg <> ""
      m.showToast(errorMsg)
    end if
  end sub

  ' ============================================================================
  ' Toast Notifications
  ' ============================================================================

  ' Show a toast notification via the scene-level Toast component.
  ' Safe to call from presenter classes (routes through view's scene reference).
  ' @param {string} message - The message to display
  ' @param {string} [toastType="error"] - "error", "success", or "info"
  sub showToast(message as string, toastType = "error" as string)
    if not isValid(m.view) then return
    scene = m.view.top.getScene()
    if isValid(scene)
      scene.callFunc("showToast", message, toastType)
    end if
  end sub

  ' ============================================================================
  ' Cleanup
  ' ============================================================================

  ' Called when presenter is being destroyed
  ' Override to clean up resources, stop tasks, unobserve fields
  ' Always call super.onDestroy() at the end of overrides
  sub onDestroy()
    if isValid(m.view) and isValid(m.view.getFiltersTask)
      m.view.getFiltersTask.control = "stop"
      m.view.getFiltersTask.unobserveField("filters")
      m.view.getFiltersTask = invalid
    end if
    m.apiFilters = invalid
    m.view = invalid
    m.log = invalid
  end sub
end class