<commit_before>###
# Handle the thumbnail view, and navigation for the main view.
#
# Handle a large number (thousands) of entries cleanly.  Thumbnail nodes are created
# as needed, and destroyed when they scroll off screen.  This gives us constant
# startup time, loads thumbnails on demand, allows preloading thumbnails in advance
# by creating more nodes in advance, and keeps memory usage constant.
###

window.ThumbnailView = (container, view) ->
  @container = container
  @view = view
  @post_ids = []
  @post_frames = []
  @expanded_post_idx = null
  @centered_post_idx = null
  @centered_post_offset = 0
  @last_mouse_x = 0
  @last_mouse_y = 0
  @thumb_container_shown = true
  @allow_wrapping = true
  @thumb_preload_container = new PreloadContainer
  @unused_thumb_pool = []

  ### The [first, end) range of posts that are currently inside .post-browser-posts. ###

  @posts_populated = [
    0
    0
  ]
  document.on 'DOMMouseScroll', @document_mouse_wheel_event.bindAsEventListener(this)
  document.on 'mousewheel', @document_mouse_wheel_event.bindAsEventListener(this)
  document.on 'viewer:displayed-image-loaded', @displayed_image_loaded_event.bindAsEventListener(this)
  document.on 'viewer:set-active-post', ((e) ->
    post_id_and_frame = [
      e.memo.post_id
      e.memo.post_frame
    ]
    @set_active_post post_id_and_frame, e.memo.lazy, e.memo.center_thumbs
    return
  ).bindAsEventListener(this)
  document.on 'viewer:show-next-post', ((e) ->
    @show_next_post e.memo.prev
    return
  ).bindAsEventListener(this)
  document.on 'viewer:scroll', ((e) ->
    @scroll e.memo.left
    return
  ).bindAsEventListener(this)
  document.on 'viewer:set-thumb-bar', ((e) ->
    if e.memo.toggle
      @show_thumb_bar !@thumb_container_shown
    else
      @show_thumb_bar e.memo.set
    return
  ).bindAsEventListener(this)
  document.on 'viewer:loaded-posts', @loaded_posts_event.bindAsEventListener(this)
  @hashchange_post_id = @hashchange_post_id.bind(this)
  UrlHash.observe 'post-id', @hashchange_post_id
  UrlHash.observe 'post-frame', @hashchange_post_id
  new DragElement(@container, ondrag: @container_ondrag.bind(this))
  Element.on window, 'resize', @window_resize_event.bindAsEventListener(this)
  @container.on 'mousemove', @container_mousemove_event.bindAsEventListener(this)
  @container.on 'mouseover', @container_mouseover_event.bindAsEventListener(this)
  @container.on 'mouseout', @container_mouseout_event.bindAsEventListener(this)
  @container.on 'click', @container_click_event.bindAsEventListener(this)
  @container.on 'dblclick', '.post-thumb,.browser-thumb-hover-overlay', @container_dblclick_event.bindAsEventListener(this)

  ### Prevent the default behavior of left-clicking on the expanded thumbnail overlay.  It's
  # handled by container_click_event. 
  ###

  @container.down('.browser-thumb-hover-overlay').on 'click', ((event) ->
    if event.isLeftClick()
      event.preventDefault()
    return
  ).bindAsEventListener(this)

  ###
  # For Android browsers, we're set to 150 DPI, which (in theory) scales us to a consistent UI size
  # based on the screen DPI.  This means that we can determine the physical screen size from the
  # window resolution: 150x150 is 1"x1".  Set a thumbnail scale based on this.  On a 320x480 HVGA
  # phone screen the thumbnails are about 2x too big, so set thumb_scale to 0.5.
  #
  # For iOS browsers, there's no way to set the viewport based on the DPI, so it's fixed at 1x.
  # (Note that on Retina screens the browser lies: even though we request 1x, it's actually at
  # 0.5x and our screen dimensions work as if we're on the lower-res iPhone screen.  We can mostly
  # ignore this.)  CSS inches aren't implemented (the DPI is fixed at 96), so that doesn't help us.
  # Fall back on special-casing individual iOS devices.
  ###

  @config = {}
  if navigator.userAgent.indexOf('iPad') != -1
    @config.thumb_scale = 1.0
  else if navigator.userAgent.indexOf('iPhone') != -1 or navigator.userAgent.indexOf('iPod') != -1
    @config.thumb_scale = 0.5
  else if navigator.userAgent.indexOf('Android') != -1

    ### We may be in landscape or portrait; use out the narrower dimension. ###

    width = Math.min(window.innerWidth, window.innerHeight)

    ### Scale a 320-width screen to 0.5, up to 1.0 for a 640-width screen.  Remember
    # that this width is already scaled by the DPI of the screen due to target-densityDpi,
    # so these numbers aren't actually real pixels, and this scales based on the DPI
    # and size of the screen rather than the pixel count. 
    ###

    @config.thumb_scale = scale(width, 320, 640, 0.5, 1.0)
    debug 'Unclamped thumb scale: ' + @config.thumb_scale

    ### Clamp to [0.5,1.0]. ###

    @config.thumb_scale = Math.min(@config.thumb_scale, 1.0)
    @config.thumb_scale = Math.max(@config.thumb_scale, 0.5)
    debug 'startup, window size: ' + window.innerWidth + 'x' + window.innerHeight
  else

    ### Unknown device, or not a mobile device. ###

    @config.thumb_scale = 1.0
  debug 'Thumb scale: ' + @config.thumb_scale
  @config_changed()

  ### Send the initial viewer:thumb-bar-changed event. ###

  @thumb_container_shown = false
  @show_thumb_bar true
  return

ThumbnailView::window_resize_event = (e) ->
  if e.stopped
    return
  if @thumb_container_shown
    @center_on_post_for_scroll @centered_post_idx
  return

### Show the given posts.  If extending is true, post_ids are meant to extend a previous
# search; attempt to continue where we left off. 
###

ThumbnailView::loaded_posts_event = (event) ->
  post_ids = event.memo.post_ids
  old_post_ids = @post_ids
  old_centered_post_idx = @centered_post_idx
  @remove_all_posts()

  ### Filter blacklisted posts. ###

  post_ids = post_ids.reject(Post.is_blacklisted)
  @post_ids = []
  @post_frames = []
  i = 0
  while i < post_ids.length
    post_id = post_ids[i]
    post = Post.posts.get(post_id)
    if post.frames.length > 0
      frame_idx = 0
      while frame_idx < post.frames.length
        @post_ids.push post_id
        @post_frames.push frame_idx
        ++frame_idx
    else
      @post_ids.push post_id
      @post_frames.push -1
    ++i
  @allow_wrapping = !event.memo.can_be_extended_further

  ### Show the results box or "no results".  Do this before updating the results box to make sure
  # the results box isn't hidden when we update, which will make offsetLeft values inside it zero
  # and break things.  If the reason we have no posts is because we didn't do a search at all,
  # don't show no-results. 
  ###

  @container.down('.post-browser-no-results').show event.memo.tags? and @post_ids.length == 0
  @container.down('.post-browser-posts').show @post_ids.length != 0
  if event.memo.extending

    ###
    # We're extending a previous search with more posts.  The new post list we get may
    # not line up with the old one: the post we're focused on may no longer be in the
    # search, or may be at a different index.
    #
    # Find a nearby post in the new results.  Start searching at the post we're already
    # centered on.  If that doesn't match, move outwards from there.  Only look forward
    # a little bit, or we may match a post that was never seen and jump forward too far
    # in the results.
    ###

    post_id_search_order = sort_array_by_distance(old_post_ids.slice(0, old_centered_post_idx + 3), old_centered_post_idx)
    initial_post_id = null
    i = 0
    while i < post_id_search_order.length
      post_id_to_search = post_id_search_order[i]
      post = Post.posts.get(post_id_to_search)
      if post?
        initial_post_id = post.id
        break
      ++i
    debug 'center-on-' + initial_post_id

    ### If we didn't find anything that matched, go back to the start. ###

    if !initial_post_id?
      @centered_post_offset = 0
      initial_post_id = new_post_ids[0]
    center_on_post_idx = @post_ids.indexOf(initial_post_id)
    @center_on_post_for_scroll center_on_post_idx
  else

    ###
    # A new search has completed.
    #
    # results_mode can be one of the following:
    #
    # "center-on-first"
    # Don't change the active post.  Center the results on the first result.  This is used
    # when performing a search by clicking on a tag, where we don't want to center on the
    # post we're on (since it'll put us at some random spot in the results when the user
    # probably wants to browse from the beginning), and we don't want to change the displayed
    # post either.
    #
    # "center-on-current"
    # Don't change the active post.  Center the results on the existing current item,
    # if possible.  This is used when we want to show a new search without disrupting the
    # shown post, such as the "child posts" link in post info, and when loading the initial
    # URL hash when we start up.
    #
    # "jump-to-first"
    # Set the active post to the first result, and center on it.  This is used after making
    # a search in the tags box.
    ###

    results_mode = event.memo.load_options.results_mode or 'center-on-current'
    initial_post_id_and_frame = undefined
    if results_mode == 'center-on-first' or results_mode == 'jump-to-first'
      initial_post_id_and_frame = [
        @post_ids[0]
        @post_frames[0]
      ]
    else
      initial_post_id_and_frame = @get_current_post_id_and_frame()
    center_on_post_idx = @get_post_idx(initial_post_id_and_frame)
    if !center_on_post_idx?
      center_on_post_idx = 0
    @centered_post_offset = 0
    @center_on_post_for_scroll center_on_post_idx

    ### If no post is currently displayed and we just completed a search, set the current post.
    # This happens when first initializing; we wait for the first search to complete to retrieve
    # info about the post we're starting on, instead of making a separate query. 
    ###

    if results_mode == 'jump-to-first' or !@view.wanted_post_id?
      @set_active_post initial_post_id_and_frame, false, false, true
  if !event.memo.tags?

    ### If tags is null then no search has been done, which means we're on a URL
    # with a post ID and no search, eg. "/post/browse#12345".  Hide the thumb
    # bar, so we'll just show the post. 
    ###

    @show_thumb_bar false
  return

ThumbnailView::container_ondrag = (e) ->
  @centered_post_offset -= e.dX
  @center_on_post_for_scroll @centered_post_idx
  return

ThumbnailView::container_mouseover_event = (event) ->
  li = $(event.target)
  if !li.hasClassName('.post-thumb')
    li = li.up('.post-thumb')
  if li
    @expand_post li.post_idx
  return

ThumbnailView::container_mouseout_event = (event) ->

  ### If the mouse is leaving the hover overlay, hide it. ###

  target = $(event.target)
  if !target.hasClassName('.browser-thumb-hover-overlay')
    target = target.up('.browser-thumb-hover-overlay')
  if target
    @expand_post null
  return

ThumbnailView::hashchange_post_id = ->
  post_id_and_frame = @get_current_post_id_and_frame()
  if !post_id_and_frame[0]?
    return

  ### If we're already displaying this post, ignore the hashchange.  Don't center on the
  # post if this is just a side-effect of clicking a post, rather than the user actually
  # changing the hash. 
  ###

  post_id = post_id_and_frame[0]
  post_frame = post_id_and_frame[1]
  if post_id == @view.displayed_post_id and post_frame == @view.displayed_post_frame
    #    debug("ignored-hashchange");
    return
  new_post_idx = @get_post_idx(post_id_and_frame)
  @centered_post_offset = 0
  @center_on_post_for_scroll new_post_idx
  @set_active_post post_id_and_frame, false, false, true
  return

### Search for the given post ID and frame in the current search results, and return its
# index.  If the given post isn't in post_ids, return null. 
###

ThumbnailView::get_post_idx = (post_id_and_frame) ->
  post_id = post_id_and_frame[0]
  post_frame = post_id_and_frame[1]
  post_idx = @post_ids.indexOf(post_id)
  if post_idx == -1
    return null
  if post_frame == -1
    return post_idx

  ### A post-frame is specified.  Search for a matching post-id and post-frame.  We assume
  # here that all frames for a post are grouped together in post_ids. 
  ###

  post_frame_idx = post_idx
  while post_frame_idx < @post_ids.length and @post_ids[post_frame_idx] == post_id
    if @post_frames[post_frame_idx] == post_frame
      return post_frame_idx
    ++post_frame_idx

  ### We found a matching post, but not a matching frame.  Return the post. ###

  post_idx

### Return the post and frame that's currently being displayed in the main view, based
# on the URL hash.  If no post is displayed and no search results are available,
# return [null, null]. 
###

ThumbnailView::get_current_post_id_and_frame = ->
  post_id = UrlHash.get('post-id')
  if !post_id?
    if @post_ids.length == 0
      return [
        null
        null
      ]
    else
      return [
        @post_ids[0]
        @post_frames[0]
      ]
  post_id = parseInt(post_id)
  post_frame = UrlHash.get('post-frame')
  # If no frame is set, attempt to resolve the post_frame we'll display, if the post data
  # is already loaded.  Otherwise, post_frame will remain null.
  if !post_frame?
    post_frame = @view.get_default_post_frame(post_id)
  [
    post_id
    post_frame
  ]

### Track the mouse cursor when it's within the container. ###

ThumbnailView::container_mousemove_event = (e) ->
  x = e.pointerX() - (document.documentElement.scrollLeft)
  y = e.pointerY() - (document.documentElement.scrollTop)
  @last_mouse_x = x
  @last_mouse_y = y
  return

ThumbnailView::document_mouse_wheel_event = (event) ->
  event.stop()
  val = undefined
  if event.wheelDelta
    val = event.wheelDelta
  else if event.detail
    val = -event.detail
  if @thumb_container_shown
    document.fire 'viewer:scroll', left: val >= 0
  else
    document.fire 'viewer:show-next-post', prev: val >= 0
  return

### Set the post that's shown in the view.  The thumbs will be centered on the post
# if center_thumbs is true.  See BrowserView.prototype.set_post for an explanation
# of no_hash_change. 
###

ThumbnailView::set_active_post = (post_id_and_frame, lazy, center_thumbs, no_hash_change, replace_history) ->

  ### If no post is specified, do nothing.  This will happen if a search returns
  # no results. 
  ###

  if !post_id_and_frame[0]?
    return
  @view.set_post post_id_and_frame[0], post_id_and_frame[1], lazy, no_hash_change, replace_history
  if center_thumbs
    post_idx = @get_post_idx(post_id_and_frame)
    @centered_post_offset = 0
    @center_on_post_for_scroll post_idx
  return

ThumbnailView::set_active_post_idx = (post_idx, lazy, center_thumbs, no_hash_change, replace_history) ->
  if !post_idx?
    return
  post_id = @post_ids[post_idx]
  post_frame = @post_frames[post_idx]
  @set_active_post [
    post_id
    post_frame
  ], lazy, center_thumbs, no_hash_change, replace_history
  return

ThumbnailView::show_next_post = (prev) ->
  if @post_ids.length == 0
    return
  current_idx = @get_post_idx([
    @view.wanted_post_id
    @view.wanted_post_frame
  ])

  ### If the displayed post isn't in the thumbnails and we're changing posts, start
  # at the beginning. 
  ###

  if !current_idx?
    current_idx = 0
  add = if prev then -1 else +1
  if @post_frames[current_idx] != @view.wanted_post_frame and add == +1

    ###
    # We didn't find an exact match for the frame we're displaying, which usually means
    # we viewed a post frame, and then the user changed the view to the main post, and
    # the main post isn't in the thumbnails.
    #
    # It's strange to be on the main post, to hit pgdn, and to end up on the second frame
    # because the nearest match was the first frame.  Instead, we should end up on the first
    # frame.  To do that, just don't add anything to the index.
    ###

    debug 'Snapped the display to the nearest frame'
    if add == +1
      add = 0
  new_idx = current_idx
  new_idx += add
  new_idx += @post_ids.length
  new_idx %= @post_ids.length
  wrapped = prev and new_idx > current_idx or !prev and new_idx < current_idx
  if wrapped

    ### Only allow wrapping over the edge if we've already expanded the results. ###

    if !@allow_wrapping
      return
    if !@thumb_container_shown and prev
      notice 'Continued from the end'
    else if !@thumb_container_shown and !prev
      notice 'Starting over from the beginning'
  @set_active_post_idx new_idx, true, true, false, true
  return

### Scroll the thumbnail view left or right.  Don't change the displayed post. ###

ThumbnailView::scroll = (left) ->

  ### There's no point in scrolling the list if it's not visible. ###

  if !@thumb_container_shown
    return
  new_idx = @centered_post_idx

  ### If we're not centered on the post, and we're moving towards the center,
  # don't jump past the post. 
  ###

  if @centered_post_offset > 0 and left
    # do nothing
  else if @centered_post_offset < 0 and !left
    # do nothing
  else
    new_idx += if left then -1 else +1
  # Snap to the nearest post.
  @centered_post_offset = 0

  ### Wrap the new index. ###

  if new_idx < 0

    ### Only allow scrolling over the left edge if we've already expanded the results. ###

    if !@allow_wrapping
      new_idx = 0
    else
      new_idx = @post_ids.length - 1
  else if new_idx >= @post_ids.length
    if !@allow_wrapping
      new_idx = @post_ids.length - 1
    else
      new_idx = 0
  @center_on_post_for_scroll new_idx
  return

### Hide the hovered post, if any, call center_on_post(post_idx), then hover over the correct post again. ###

ThumbnailView::center_on_post_for_scroll = (post_idx) ->
  if @thumb_container_shown
    @expand_post null
  @center_on_post post_idx

  ###
  # Now that we've re-centered, we need to expand the correct image.  Usually, we can just
  # wait for the mouseover event to fire, since we hid the expanded thumb overlay and the
  # image underneith it is now under the mouse.  However, browsers are badly broken here.
  # Opera doesn't fire mouseover events when the element under the cursor is hidden.  FF
  # fires the mouseover on hide, but misses the mouseout when the new overlay is shown, so
  # the next time it's hidden mouseover events are lost.
  #
  # Explicitly figure out which item we're hovering over and expand it.
  ###

  if @thumb_container_shown
    element = document.elementFromPoint(@last_mouse_x, @last_mouse_y)
    element = $(element)
    if element
      li = element.up('.post-thumb')
      if li
        @expand_post li.post_idx
  return

ThumbnailView::remove_post = (right) ->
  if @posts_populated[0] == @posts_populated[1]
    return false

  ### none to remove ###

  node = @container.down('.post-browser-posts')
  if right
    --@posts_populated[1]
    node_to_remove = node.lastChild
  else
    ++@posts_populated[0]
    node_to_remove = node.firstChild

  ### Remove the thumbnail that's no longer visible, and put it in unused_thumb_pool
  # so we can reuse it later.  This won't grow out of control, since we'll always use
  # an item from the pool if available rather than creating a new one. 
  ###

  item = node.removeChild(node_to_remove)
  @unused_thumb_pool.push item
  true

ThumbnailView::remove_all_posts = ->
  while @remove_post(true)
      return

### Add the next thumbnail to the left or right side. ###

ThumbnailView::add_post_to_display = (right) ->
  node = @container.down('.post-browser-posts')
  if right
    post_idx_to_populate = @posts_populated[1]
    if post_idx_to_populate == @post_ids.length
      return false
    ++@posts_populated[1]
    thumb = @create_thumb(post_idx_to_populate)
    node.insertBefore thumb, null
  else
    if @posts_populated[0] == 0
      return false
    --@posts_populated[0]
    post_idx_to_populate = @posts_populated[0]
    thumb = @create_thumb(post_idx_to_populate)
    node.insertBefore thumb, node.firstChild
  true

### Fill the container so post_idx is visible. ###

ThumbnailView::populate_post = (post_idx) ->
  if @is_post_idx_shown(post_idx)
    return
<commit_msg>Fix remove_all_posts in posts browser not removing all thumbnails<commit_after>###
# Handle the thumbnail view, and navigation for the main view.
#
# Handle a large number (thousands) of entries cleanly.  Thumbnail nodes are created
# as needed, and destroyed when they scroll off screen.  This gives us constant
# startup time, loads thumbnails on demand, allows preloading thumbnails in advance
# by creating more nodes in advance, and keeps memory usage constant.
###

window.ThumbnailView = (container, view) ->
  @container = container
  @view = view
  @post_ids = []
  @post_frames = []
  @expanded_post_idx = null
  @centered_post_idx = null
  @centered_post_offset = 0
  @last_mouse_x = 0
  @last_mouse_y = 0
  @thumb_container_shown = true
  @allow_wrapping = true
  @thumb_preload_container = new PreloadContainer
  @unused_thumb_pool = []

  ### The [first, end) range of posts that are currently inside .post-browser-posts. ###

  @posts_populated = [
    0
    0
  ]
  document.on 'DOMMouseScroll', @document_mouse_wheel_event.bindAsEventListener(this)
  document.on 'mousewheel', @document_mouse_wheel_event.bindAsEventListener(this)
  document.on 'viewer:displayed-image-loaded', @displayed_image_loaded_event.bindAsEventListener(this)
  document.on 'viewer:set-active-post', ((e) ->
    post_id_and_frame = [
      e.memo.post_id
      e.memo.post_frame
    ]
    @set_active_post post_id_and_frame, e.memo.lazy, e.memo.center_thumbs
    return
  ).bindAsEventListener(this)
  document.on 'viewer:show-next-post', ((e) ->
    @show_next_post e.memo.prev
    return
  ).bindAsEventListener(this)
  document.on 'viewer:scroll', ((e) ->
    @scroll e.memo.left
    return
  ).bindAsEventListener(this)
  document.on 'viewer:set-thumb-bar', ((e) ->
    if e.memo.toggle
      @show_thumb_bar !@thumb_container_shown
    else
      @show_thumb_bar e.memo.set
    return
  ).bindAsEventListener(this)
  document.on 'viewer:loaded-posts', @loaded_posts_event.bindAsEventListener(this)
  @hashchange_post_id = @hashchange_post_id.bind(this)
  UrlHash.observe 'post-id', @hashchange_post_id
  UrlHash.observe 'post-frame', @hashchange_post_id
  new DragElement(@container, ondrag: @container_ondrag.bind(this))
  Element.on window, 'resize', @window_resize_event.bindAsEventListener(this)
  @container.on 'mousemove', @container_mousemove_event.bindAsEventListener(this)
  @container.on 'mouseover', @container_mouseover_event.bindAsEventListener(this)
  @container.on 'mouseout', @container_mouseout_event.bindAsEventListener(this)
  @container.on 'click', @container_click_event.bindAsEventListener(this)
  @container.on 'dblclick', '.post-thumb,.browser-thumb-hover-overlay', @container_dblclick_event.bindAsEventListener(this)

  ### Prevent the default behavior of left-clicking on the expanded thumbnail overlay.  It's
  # handled by container_click_event. 
  ###

  @container.down('.browser-thumb-hover-overlay').on 'click', ((event) ->
    if event.isLeftClick()
      event.preventDefault()
    return
  ).bindAsEventListener(this)

  ###
  # For Android browsers, we're set to 150 DPI, which (in theory) scales us to a consistent UI size
  # based on the screen DPI.  This means that we can determine the physical screen size from the
  # window resolution: 150x150 is 1"x1".  Set a thumbnail scale based on this.  On a 320x480 HVGA
  # phone screen the thumbnails are about 2x too big, so set thumb_scale to 0.5.
  #
  # For iOS browsers, there's no way to set the viewport based on the DPI, so it's fixed at 1x.
  # (Note that on Retina screens the browser lies: even though we request 1x, it's actually at
  # 0.5x and our screen dimensions work as if we're on the lower-res iPhone screen.  We can mostly
  # ignore this.)  CSS inches aren't implemented (the DPI is fixed at 96), so that doesn't help us.
  # Fall back on special-casing individual iOS devices.
  ###

  @config = {}
  if navigator.userAgent.indexOf('iPad') != -1
    @config.thumb_scale = 1.0
  else if navigator.userAgent.indexOf('iPhone') != -1 or navigator.userAgent.indexOf('iPod') != -1
    @config.thumb_scale = 0.5
  else if navigator.userAgent.indexOf('Android') != -1

    ### We may be in landscape or portrait; use out the narrower dimension. ###

    width = Math.min(window.innerWidth, window.innerHeight)

    ### Scale a 320-width screen to 0.5, up to 1.0 for a 640-width screen.  Remember
    # that this width is already scaled by the DPI of the screen due to target-densityDpi,
    # so these numbers aren't actually real pixels, and this scales based on the DPI
    # and size of the screen rather than the pixel count. 
    ###

    @config.thumb_scale = scale(width, 320, 640, 0.5, 1.0)
    debug 'Unclamped thumb scale: ' + @config.thumb_scale

    ### Clamp to [0.5,1.0]. ###

    @config.thumb_scale = Math.min(@config.thumb_scale, 1.0)
    @config.thumb_scale = Math.max(@config.thumb_scale, 0.5)
    debug 'startup, window size: ' + window.innerWidth + 'x' + window.innerHeight
  else

    ### Unknown device, or not a mobile device. ###

    @config.thumb_scale = 1.0
  debug 'Thumb scale: ' + @config.thumb_scale
  @config_changed()

  ### Send the initial viewer:thumb-bar-changed event. ###

  @thumb_container_shown = false
  @show_thumb_bar true
  return

ThumbnailView::window_resize_event = (e) ->
  if e.stopped
    return
  if @thumb_container_shown
    @center_on_post_for_scroll @centered_post_idx
  return

### Show the given posts.  If extending is true, post_ids are meant to extend a previous
# search; attempt to continue where we left off. 
###

ThumbnailView::loaded_posts_event = (event) ->
  post_ids = event.memo.post_ids
  old_post_ids = @post_ids
  old_centered_post_idx = @centered_post_idx
  @remove_all_posts()

  ### Filter blacklisted posts. ###

  post_ids = post_ids.reject(Post.is_blacklisted)
  @post_ids = []
  @post_frames = []
  i = 0
  while i < post_ids.length
    post_id = post_ids[i]
    post = Post.posts.get(post_id)
    if post.frames.length > 0
      frame_idx = 0
      while frame_idx < post.frames.length
        @post_ids.push post_id
        @post_frames.push frame_idx
        ++frame_idx
    else
      @post_ids.push post_id
      @post_frames.push -1
    ++i
  @allow_wrapping = !event.memo.can_be_extended_further

  ### Show the results box or "no results".  Do this before updating the results box to make sure
  # the results box isn't hidden when we update, which will make offsetLeft values inside it zero
  # and break things.  If the reason we have no posts is because we didn't do a search at all,
  # don't show no-results. 
  ###

  @container.down('.post-browser-no-results').show event.memo.tags? and @post_ids.length == 0
  @container.down('.post-browser-posts').show @post_ids.length != 0
  if event.memo.extending

    ###
    # We're extending a previous search with more posts.  The new post list we get may
    # not line up with the old one: the post we're focused on may no longer be in the
    # search, or may be at a different index.
    #
    # Find a nearby post in the new results.  Start searching at the post we're already
    # centered on.  If that doesn't match, move outwards from there.  Only look forward
    # a little bit, or we may match a post that was never seen and jump forward too far
    # in the results.
    ###

    post_id_search_order = sort_array_by_distance(old_post_ids.slice(0, old_centered_post_idx + 3), old_centered_post_idx)
    initial_post_id = null
    i = 0
    while i < post_id_search_order.length
      post_id_to_search = post_id_search_order[i]
      post = Post.posts.get(post_id_to_search)
      if post?
        initial_post_id = post.id
        break
      ++i
    debug 'center-on-' + initial_post_id

    ### If we didn't find anything that matched, go back to the start. ###

    if !initial_post_id?
      @centered_post_offset = 0
      initial_post_id = new_post_ids[0]
    center_on_post_idx = @post_ids.indexOf(initial_post_id)
    @center_on_post_for_scroll center_on_post_idx
  else

    ###
    # A new search has completed.
    #
    # results_mode can be one of the following:
    #
    # "center-on-first"
    # Don't change the active post.  Center the results on the first result.  This is used
    # when performing a search by clicking on a tag, where we don't want to center on the
    # post we're on (since it'll put us at some random spot in the results when the user
    # probably wants to browse from the beginning), and we don't want to change the displayed
    # post either.
    #
    # "center-on-current"
    # Don't change the active post.  Center the results on the existing current item,
    # if possible.  This is used when we want to show a new search without disrupting the
    # shown post, such as the "child posts" link in post info, and when loading the initial
    # URL hash when we start up.
    #
    # "jump-to-first"
    # Set the active post to the first result, and center on it.  This is used after making
    # a search in the tags box.
    ###

    results_mode = event.memo.load_options.results_mode or 'center-on-current'
    initial_post_id_and_frame = undefined
    if results_mode == 'center-on-first' or results_mode == 'jump-to-first'
      initial_post_id_and_frame = [
        @post_ids[0]
        @post_frames[0]
      ]
    else
      initial_post_id_and_frame = @get_current_post_id_and_frame()
    center_on_post_idx = @get_post_idx(initial_post_id_and_frame)
    if !center_on_post_idx?
      center_on_post_idx = 0
    @centered_post_offset = 0
    @center_on_post_for_scroll center_on_post_idx

    ### If no post is currently displayed and we just completed a search, set the current post.
    # This happens when first initializing; we wait for the first search to complete to retrieve
    # info about the post we're starting on, instead of making a separate query. 
    ###

    if results_mode == 'jump-to-first' or !@view.wanted_post_id?
      @set_active_post initial_post_id_and_frame, false, false, true
  if !event.memo.tags?

    ### If tags is null then no search has been done, which means we're on a URL
    # with a post ID and no search, eg. "/post/browse#12345".  Hide the thumb
    # bar, so we'll just show the post. 
    ###

    @show_thumb_bar false
  return

ThumbnailView::container_ondrag = (e) ->
  @centered_post_offset -= e.dX
  @center_on_post_for_scroll @centered_post_idx
  return

ThumbnailView::container_mouseover_event = (event) ->
  li = $(event.target)
  if !li.hasClassName('.post-thumb')
    li = li.up('.post-thumb')
  if li
    @expand_post li.post_idx
  return

ThumbnailView::container_mouseout_event = (event) ->

  ### If the mouse is leaving the hover overlay, hide it. ###

  target = $(event.target)
  if !target.hasClassName('.browser-thumb-hover-overlay')
    target = target.up('.browser-thumb-hover-overlay')
  if target
    @expand_post null
  return

ThumbnailView::hashchange_post_id = ->
  post_id_and_frame = @get_current_post_id_and_frame()
  if !post_id_and_frame[0]?
    return

  ### If we're already displaying this post, ignore the hashchange.  Don't center on the
  # post if this is just a side-effect of clicking a post, rather than the user actually
  # changing the hash. 
  ###

  post_id = post_id_and_frame[0]
  post_frame = post_id_and_frame[1]
  if post_id == @view.displayed_post_id and post_frame == @view.displayed_post_frame
    #    debug("ignored-hashchange");
    return
  new_post_idx = @get_post_idx(post_id_and_frame)
  @centered_post_offset = 0
  @center_on_post_for_scroll new_post_idx
  @set_active_post post_id_and_frame, false, false, true
  return

### Search for the given post ID and frame in the current search results, and return its
# index.  If the given post isn't in post_ids, return null. 
###

ThumbnailView::get_post_idx = (post_id_and_frame) ->
  post_id = post_id_and_frame[0]
  post_frame = post_id_and_frame[1]
  post_idx = @post_ids.indexOf(post_id)
  if post_idx == -1
    return null
  if post_frame == -1
    return post_idx

  ### A post-frame is specified.  Search for a matching post-id and post-frame.  We assume
  # here that all frames for a post are grouped together in post_ids. 
  ###

  post_frame_idx = post_idx
  while post_frame_idx < @post_ids.length and @post_ids[post_frame_idx] == post_id
    if @post_frames[post_frame_idx] == post_frame
      return post_frame_idx
    ++post_frame_idx

  ### We found a matching post, but not a matching frame.  Return the post. ###

  post_idx

### Return the post and frame that's currently being displayed in the main view, based
# on the URL hash.  If no post is displayed and no search results are available,
# return [null, null]. 
###

ThumbnailView::get_current_post_id_and_frame = ->
  post_id = UrlHash.get('post-id')
  if !post_id?
    if @post_ids.length == 0
      return [
        null
        null
      ]
    else
      return [
        @post_ids[0]
        @post_frames[0]
      ]
  post_id = parseInt(post_id)
  post_frame = UrlHash.get('post-frame')
  # If no frame is set, attempt to resolve the post_frame we'll display, if the post data
  # is already loaded.  Otherwise, post_frame will remain null.
  if !post_frame?
    post_frame = @view.get_default_post_frame(post_id)
  [
    post_id
    post_frame
  ]

### Track the mouse cursor when it's within the container. ###

ThumbnailView::container_mousemove_event = (e) ->
  x = e.pointerX() - (document.documentElement.scrollLeft)
  y = e.pointerY() - (document.documentElement.scrollTop)
  @last_mouse_x = x
  @last_mouse_y = y
  return

ThumbnailView::document_mouse_wheel_event = (event) ->
  event.stop()
  val = undefined
  if event.wheelDelta
    val = event.wheelDelta
  else if event.detail
    val = -event.detail
  if @thumb_container_shown
    document.fire 'viewer:scroll', left: val >= 0
  else
    document.fire 'viewer:show-next-post', prev: val >= 0
  return

### Set the post that's shown in the view.  The thumbs will be centered on the post
# if center_thumbs is true.  See BrowserView.prototype.set_post for an explanation
# of no_hash_change. 
###

ThumbnailView::set_active_post = (post_id_and_frame, lazy, center_thumbs, no_hash_change, replace_history) ->

  ### If no post is specified, do nothing.  This will happen if a search returns
  # no results. 
  ###

  if !post_id_and_frame[0]?
    return
  @view.set_post post_id_and_frame[0], post_id_and_frame[1], lazy, no_hash_change, replace_history
  if center_thumbs
    post_idx = @get_post_idx(post_id_and_frame)
    @centered_post_offset = 0
    @center_on_post_for_scroll post_idx
  return

ThumbnailView::set_active_post_idx = (post_idx, lazy, center_thumbs, no_hash_change, replace_history) ->
  if !post_idx?
    return
  post_id = @post_ids[post_idx]
  post_frame = @post_frames[post_idx]
  @set_active_post [
    post_id
    post_frame
  ], lazy, center_thumbs, no_hash_change, replace_history
  return

ThumbnailView::show_next_post = (prev) ->
  if @post_ids.length == 0
    return
  current_idx = @get_post_idx([
    @view.wanted_post_id
    @view.wanted_post_frame
  ])

  ### If the displayed post isn't in the thumbnails and we're changing posts, start
  # at the beginning. 
  ###

  if !current_idx?
    current_idx = 0
  add = if prev then -1 else +1
  if @post_frames[current_idx] != @view.wanted_post_frame and add == +1

    ###
    # We didn't find an exact match for the frame we're displaying, which usually means
    # we viewed a post frame, and then the user changed the view to the main post, and
    # the main post isn't in the thumbnails.
    #
    # It's strange to be on the main post, to hit pgdn, and to end up on the second frame
    # because the nearest match was the first frame.  Instead, we should end up on the first
    # frame.  To do that, just don't add anything to the index.
    ###

    debug 'Snapped the display to the nearest frame'
    if add == +1
      add = 0
  new_idx = current_idx
  new_idx += add
  new_idx += @post_ids.length
  new_idx %= @post_ids.length
  wrapped = prev and new_idx > current_idx or !prev and new_idx < current_idx
  if wrapped

    ### Only allow wrapping over the edge if we've already expanded the results. ###

    if !@allow_wrapping
      return
    if !@thumb_container_shown and prev
      notice 'Continued from the end'
    else if !@thumb_container_shown and !prev
      notice 'Starting over from the beginning'
  @set_active_post_idx new_idx, true, true, false, true
  return

### Scroll the thumbnail view left or right.  Don't change the displayed post. ###

ThumbnailView::scroll = (left) ->

  ### There's no point in scrolling the list if it's not visible. ###

  if !@thumb_container_shown
    return
  new_idx = @centered_post_idx

  ### If we're not centered on the post, and we're moving towards the center,
  # don't jump past the post. 
  ###

  if @centered_post_offset > 0 and left
    # do nothing
  else if @centered_post_offset < 0 and !left
    # do nothing
  else
    new_idx += if left then -1 else +1
  # Snap to the nearest post.
  @centered_post_offset = 0

  ### Wrap the new index. ###

  if new_idx < 0

    ### Only allow scrolling over the left edge if we've already expanded the results. ###

    if !@allow_wrapping
      new_idx = 0
    else
      new_idx = @post_ids.length - 1
  else if new_idx >= @post_ids.length
    if !@allow_wrapping
      new_idx = @post_ids.length - 1
    else
      new_idx = 0
  @center_on_post_for_scroll new_idx
  return

### Hide the hovered post, if any, call center_on_post(post_idx), then hover over the correct post again. ###

ThumbnailView::center_on_post_for_scroll = (post_idx) ->
  if @thumb_container_shown
    @expand_post null
  @center_on_post post_idx

  ###
  # Now that we've re-centered, we need to expand the correct image.  Usually, we can just
  # wait for the mouseover event to fire, since we hid the expanded thumb overlay and the
  # image underneith it is now under the mouse.  However, browsers are badly broken here.
  # Opera doesn't fire mouseover events when the element under the cursor is hidden.  FF
  # fires the mouseover on hide, but misses the mouseout when the new overlay is shown, so
  # the next time it's hidden mouseover events are lost.
  #
  # Explicitly figure out which item we're hovering over and expand it.
  ###

  if @thumb_container_shown
    element = document.elementFromPoint(@last_mouse_x, @last_mouse_y)
    element = $(element)
    if element
      li = element.up('.post-thumb')
      if li
        @expand_post li.post_idx
  return

ThumbnailView::remove_post = (right) ->
  if @posts_populated[0] == @posts_populated[1]
    return false

  ### none to remove ###

  node = @container.down('.post-browser-posts')
  if right
    --@posts_populated[1]
    node_to_remove = node.lastChild
  else
    ++@posts_populated[0]
    node_to_remove = node.firstChild

  ### Remove the thumbnail that's no longer visible, and put it in unused_thumb_pool
  # so we can reuse it later.  This won't grow out of control, since we'll always use
  # an item from the pool if available rather than creating a new one. 
  ###

  item = node.removeChild(node_to_remove)
  @unused_thumb_pool.push item
  true

ThumbnailView::remove_all_posts = ->
  continue while @remove_post(true)

### Add the next thumbnail to the left or right side. ###

ThumbnailView::add_post_to_display = (right) ->
  node = @container.down('.post-browser-posts')
  if right
    post_idx_to_populate = @posts_populated[1]
    if post_idx_to_populate == @post_ids.length
      return false
    ++@posts_populated[1]
    thumb = @create_thumb(post_idx_to_populate)
    node.insertBefore thumb, null
  else
    if @posts_populated[0] == 0
      return false
    --@posts_populated[0]
    post_idx_to_populate = @posts_populated[0]
    thumb = @create_thumb(post_idx_to_populate)
    node.insertBefore thumb, node.firstChild
  true

### Fill the container so post_idx is visible. ###

ThumbnailView::populate_post = (post_idx) ->
  if @is_post_idx_shown(post_idx)
    return

{'fraction_non_alphanumeric': 0.09250199680511183, 'fraction_numerical': 0.006639376996805112, 'mean_word_length': 3.4790385690329795, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 65, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3626306', 'n_tokens_mistral': 13383, 'n_tokens_neox': 12884, 'n_words': 4104}