Set up a new loading/progress system for views.

This commit is contained in:
Scott Erickson 2014-04-03 18:43:29 -07:00
parent c434325ec0
commit 3b3b825be0
8 changed files with 246 additions and 58 deletions

View file

@ -12,6 +12,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
manual: "Manual"
fork: "Fork"
play: "Play"
retry: "Retry"
units:
second: "second"
@ -602,3 +603,27 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
tutorial: "tutorial"
new_to_programming: ". New to programming? Hit our beginner campaign to skill up."
so_ready: "I Am So Ready for This"
loading_error:
could_not_load: "Error loading from server"
connection_failure: "Connection failed."
unauthorized: "You need to be signed in. Do you have cookies disabled?"
forbidden: "You do not have the permissions."
not_found: "Not found."
not_allowed: "Method not allowed."
timeout: "Server timeout."
conflict: "Resource conflict."
bad_input: "Bad input."
server_error: "Server error."
unknown: "Unknown error."
resources:
your_sessions: "Your Sessions"
level: "Level"
social_network_apis: "Social Network APIs"
facebook_status: "Facebook Status"
facebook_friends: "Facebook Friends"
facebook_friend_sessions: "Facebook Friend Sessions"
gplus_friends: "G+ Friends"
gplus_friend_sessions: "G+ Friend Sessions"
leaderboard: 'leaderboard'

View file

@ -167,7 +167,15 @@ a[data-toggle="modal"]
width: 50%
margin: 0 25%
.progress-bar
width: 100%
width: 0%
transition: width 0.1s ease
.errors .alert
padding: 5px
display: block
margin: 10px auto
.btn
margin-left: 10px
.modal
.wait

View file

@ -1,4 +1,6 @@
.loading-screen
h1(data-i18n="common.loading") Loading...
.progress.progress-striped.active
.progress-bar
.progress
.progress-bar
.errors

View file

@ -0,0 +1,31 @@
.alert.alert-danger.loading-error-alert
span(data-i18n="loading_error.could_not_load") Error loading from server
span (
span(data-i18n="resources.#{name}")
span )
if !responseText
strong(data-i18n="loading_error.connection_failure") Connection failed.
else if status === 401
strong(data-i18n="loading_error.unauthorized") You need to be signed in. Do you have cookies disabled?
else if status === 403
strong(data-i18n="loading_error.forbidden") You do not have the permissions.
else if status === 404
strong(data-i18n="loading_error.not_found") Not found.
else if status === 405
strong(data-i18n="loading_error.not_allowed") Method not allowed.
else if status === 408
strong(data-i18n="loading_error.timeout") Server timeout.
else if status === 409
strong(data-i18n="loading_error.conflict") Resource conflict.
else if status === 422
strong(data-i18n="loading_error.bad_input") Bad input.
else if status >= 500
strong(data-i18n="loading_error.server_error") Server error.
else
strong(data-i18n="loading_error.unknown") Unknown error.
if resourceIndex !== undefined
button.btn.btn-sm.retry-loading-resource(data-i18n="common.retry", data-resource-index=resourceIndex) Retry
if requestIndex !== undefined
button.btn.btn-sm.retry-loading-request(data-i18n="common.retry", data-request-index=requestIndex) Retry

View file

@ -2,6 +2,7 @@ SuperModel = require 'models/SuperModel'
utils = require 'lib/utils'
CocoClass = require 'lib/CocoClass'
loadingScreenTemplate = require 'templates/loading'
loadingErrorTemplate = require 'templates/loading_error'
visibleModal = null
waitingModal = null
@ -18,13 +19,26 @@ module.exports = class CocoView extends Backbone.View
'click a': 'toggleModal'
'click button': 'toggleModal'
'click li': 'toggleModal'
'click .retry-loading-resource': 'onRetryResource'
'click .retry-loading-request': 'onRetryRequest'
subscriptions: {}
shortcuts: {}
# load progress properties
loadProgress:
num: 0
denom: 0
showing: false
resources: [] # models and collections
requests: [] # jqxhr's
somethings: [] # everything else
progress: 0
# Setup, Teardown
constructor: (options) ->
@loadProgress = _.cloneDeep @loadProgress
@supermodel ?= options?.supermodel or new SuperModel()
@options = options
@subscriptions = utils.combineAncestralObject(@, 'subscriptions')
@ -33,6 +47,7 @@ module.exports = class CocoView extends Backbone.View
@shortcuts = utils.combineAncestralObject(@, 'shortcuts')
@subviews = {}
@listenToShortcuts()
@updateProgressBar = _.debounce @updateProgressBar, 100
# Backbone.Mediator handles subscription setup/teardown automatically
super options
@ -74,7 +89,7 @@ module.exports = class CocoView extends Backbone.View
return @template if _.isString(@template)
@$el.html @template(@getRenderData())
@afterRender()
@showLoading() if @startsLoading
@showLoading() if @startsLoading or @loading() # TODO: Remove startsLoading entirely
@$el.i18n()
@
@ -89,6 +104,101 @@ module.exports = class CocoView extends Backbone.View
context
afterRender: ->
# Resource and request loading management for any given view
addResourceToLoad: (modelOrCollection, name, value=1) ->
@loadProgress.resources.push {resource:modelOrCollection, value:value, name:name}
@listenToOnce modelOrCollection, 'sync', @updateProgress
@listenTo modelOrCollection, 'error', @onResourceLoadFailed
@updateProgress()
addRequestToLoad: (jqxhr, name, retryFunc, value=1) ->
@loadProgress.requests.push {request:jqxhr, value:value, name: name, retryFunc: retryFunc}
jqxhr.done @updateProgress
jqxhr.fail @onRequestLoadFailed
addSomethingToLoad: (name, value=1) ->
@loadProgress.somethings.push {loaded: false, name: name, value: value}
@updateProgress()
somethingLoaded: (name) ->
r = _.find @loadProgress.somethings, {name: name}
return console.error 'Could not find something called', name if not r
r.loaded = true
@updateProgress(name)
loading: ->
return false if @loaded
for r in @loadProgress.resources
return true if not r.resource.loaded
for r in @loadProgress.requests
return true if not r.request.status
for r in @loadProgress.somethings
return true if not r.loaded
return false
updateProgress: =>
console.debug 'Loaded', r.name if arguments[0] and r = _.find @loadProgress.resources, {resource:arguments[0]}
console.debug 'Loaded', r.name if arguments[2] and r = _.find @loadProgress.requests, {request:arguments[2]}
console.debug 'Loaded', r.name if arguments[0] and r = _.find @loadProgress.somethings, {name:arguments[0]}
denom = 0
denom += r.value for r in @loadProgress.resources
denom += r.value for r in @loadProgress.requests
denom += r.value for r in @loadProgress.somethings
num = @loadProgress.num
num += r.value for r in @loadProgress.resources when r.resource.loaded
num += r.value for r in @loadProgress.requests when r.request.status
num += r.value for r in @loadProgress.somethings when r.loaded
#console.log 'update progress', @, num, denom, arguments
progress = if denom then num / denom else 0
# sometimes the denominator isn't known from the outset, so make sure the overall progress only goes up
@loadProgress.progress = progress if progress > @loadProgress.progress
@updateProgressBar()
if num is denom and not @loaded
@loaded = true
@onLoaded()
updateProgressBar: =>
prog = "#{parseInt(@loadProgress.progress*100)}%"
@$el.find('.loading-screen .progress-bar').css('width', prog)
onLoaded: ->
@render()
# Error handling for loading
onResourceLoadFailed: (resource, jqxhr) ->
for r, index in @loadProgress.resources
break if r.resource is resource
@$el.find('.loading-screen .errors').append(loadingErrorTemplate({
status:jqxhr.status,
name: r.name
resourceIndex: index,
responseText: jqxhr.responseText
})).i18n()
onRetryResource: (e) ->
r = @loadProgress.resources[$(e.target).data('resource-index')]
r.resource.fetch()
$(e.target).closest('.loading-error-alert').remove()
onRequestLoadFailed: (jqxhr) =>
for r, index in @loadProgress.requests
break if r.request is jqxhr
@$el.find('.loading-screen .errors').append(loadingErrorTemplate({
status:jqxhr.status,
name: r.name
requestIndex: index,
responseText: jqxhr.responseText
}))
onRetryRequest: (e) ->
r = @loadProgress.requests[$(e.target).data('request-index')]
@[r.retryFunc]?()
$(e.target).closest('.loading-error-alert').remove()
# Modals

View file

@ -1,4 +1,5 @@
CocoView = require 'views/kinds/CocoView'
CocoClass = require 'lib/CocoClass'
Level = require 'models/Level'
LevelSession = require 'models/LevelSession'
CocoCollection = require 'models/CocoCollection'
@ -18,7 +19,6 @@ class LevelSessionsCollection extends CocoCollection
module.exports = class LadderTabView extends CocoView
id: 'ladder-tab-view'
template: require 'templates/play/ladder/ladder_tab'
startsLoading: true
events:
'click .connect-facebook': 'onConnectFacebook'
@ -32,6 +32,7 @@ module.exports = class LadderTabView extends CocoView
constructor: (options, @level, @sessions) ->
super(options)
@addSomethingToLoad("social_network_apis")
@teams = teamDataFromLevel @level
@leaderboards = {}
@refreshLadder()
@ -39,15 +40,16 @@ module.exports = class LadderTabView extends CocoView
checkFriends: ->
return if @checked or (not window.FB) or (not window.gapi)
@somethingLoaded("social_network_apis")
@checked = true
@loadingFacebookFriends = true
@addSomethingToLoad("facebook_status")
FB.getLoginStatus (response) =>
@facebookStatus = response.status
if @facebookStatus is 'connected' then @loadFacebookFriendSessions() else @loadingFacebookFriends = false
@somethingLoaded("facebook_status")
@loadFacebookFriends() if @facebookStatus is 'connected'
if application.gplusHandler.loggedIn is undefined
@loadingGPlusFriends = true
@listenToOnce(application.gplusHandler, 'checked-state', @gplusSessionStateLoaded)
else
@gplusSessionStateLoaded()
@ -60,16 +62,24 @@ module.exports = class LadderTabView extends CocoView
onConnectedWithFacebook: -> location.reload() if @connecting
loadFacebookFriends: ->
@addSomethingToLoad("facebook_friends")
FB.api '/me/friends', @onFacebookFriendsLoaded
onFacebookFriendsLoaded: (response) =>
@somethingLoaded("facebook_friends")
@facebookData = response.data
@loadFacebookFriendSessions()
loadFacebookFriendSessions: ->
FB.api '/me/friends', (response) =>
@facebookData = response.data
levelFrag = "#{@level.get('original')}.#{@level.get('version').major}"
url = "/db/level/#{levelFrag}/leaderboard_facebook_friends"
$.ajax url, {
data: { friendIDs: (f.id for f in @facebookData) }
method: 'POST'
success: @onFacebookFriendSessionsLoaded
}
levelFrag = "#{@level.get('original')}.#{@level.get('version').major}"
url = "/db/level/#{levelFrag}/leaderboard_facebook_friends"
jqxhr = $.ajax url, {
data: { friendIDs: (f.id for f in @facebookData) }
method: 'POST'
success: @onFacebookFriendSessionsLoaded
}
@addRequestToLoad(jqxhr, 'facebook_friend_sessions', 'loadFacebookFriendSessions')
onFacebookFriendSessionsLoaded: (result) =>
friendsMap = {}
@ -79,9 +89,7 @@ module.exports = class LadderTabView extends CocoView
friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans'
friend.imageSource = "http://graph.facebook.com/#{friend.facebookID}/picture"
@facebookFriendSessions = result
@loadingFacebookFriends = false
@renderMaybe()
# GOOGLE PLUS
onConnectGPlus: ->
@ -93,21 +101,23 @@ module.exports = class LadderTabView extends CocoView
gplusSessionStateLoaded: ->
if application.gplusHandler.loggedIn
@loadingGPlusFriends = true
@addSomethingToLoad("gplus_friends")
application.gplusHandler.loadFriends @gplusFriendsLoaded
else
@loadingGPlusFriends = false
@renderMaybe()
gplusFriendsLoaded: (friends) =>
@somethingLoaded("gplus_friends")
@gplusData = friends.items
@loadGPlusFriendSessions()
loadGPlusFriendSessions: ->
levelFrag = "#{@level.get('original')}.#{@level.get('version').major}"
url = "/db/level/#{levelFrag}/leaderboard_gplus_friends"
$.ajax url, {
jqxhr = $.ajax url, {
data: { friendIDs: (f.id for f in @gplusData) }
method: 'POST'
success: @onGPlusFriendSessionsLoaded
}
@addRequestToLoad(jqxhr, 'gplus_friend_sessions', 'loadGPlusFriendSessions')
onGPlusFriendSessionsLoaded: (result) =>
friendsMap = {}
@ -117,29 +127,15 @@ module.exports = class LadderTabView extends CocoView
friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans'
friend.imageSource = friendsMap[friend.gplusID].image.url
@gplusFriendSessions = result
@loadingGPlusFriends = false
@renderMaybe()
# LADDER LOADING
refreshLadder: ->
promises = []
for team in @teams
@leaderboards[team.id]?.off 'sync'
@leaderboards[team.id]?.destroy()
teamSession = _.find @sessions.models, (session) -> session.get('team') is team.id
@leaderboards[team.id] = new LeaderboardData(@level, team.id, teamSession)
promises.push @leaderboards[team.id].promise
@loadingLeaderboards = true
$.when(promises...).then(@leaderboardsLoaded)
leaderboardsLoaded: =>
@loadingLeaderboards = false
@renderMaybe()
renderMaybe: ->
return if @loadingFacebookFriends or @loadingLeaderboards or @loadingGPlusFriends
@startsLoading = false
@render()
@addResourceToLoad @leaderboards[team.id], 'leaderboard', 3
getRenderData: ->
ctx = super()
@ -160,9 +156,16 @@ module.exports = class LadderTabView extends CocoView
sessions.reverse()
sessions
class LeaderboardData
class LeaderboardData extends CocoClass
###
Consolidates what you need to load for a leaderboard into a single Backbone Model-like object.
###
constructor: (@level, @team, @session) ->
_.extend @, Backbone.Events
super()
@fetch()
fetch: ->
@topPlayers = new LeaderboardCollection(@level, {order:-1, scoreOffset: HIGHEST_SCORE, team: @team, limit: 20})
promises = []
promises.push @topPlayers.fetch()
@ -173,18 +176,24 @@ class LeaderboardData
promises.push @playersAbove.fetch()
@playersBelow = new LeaderboardCollection(@level, {order:-1, scoreOffset: score, limit: 4, team: @team})
promises.push @playersBelow.fetch()
level = "#{level.get('original')}.#{level.get('version').major}"
level = "#{@level.get('original')}.#{@level.get('version').major}"
success = (@myRank) =>
promises.push $.ajax "/db/level/#{level}/leaderboard_rank?scoreOffset=#{@session.get('totalScore')}&team=#{@team}", {success}
@promise = $.when(promises...)
@promise.then @onLoad
@promise.fail @onFail
@promise
onLoad: =>
return if @destroyed
@loaded = true
@trigger 'sync'
@trigger 'sync', @
# TODO: cache user ids -> names mapping, and load them here as needed,
# and apply them to sessions. Fetching each and every time is too costly.
onFail: (resource, jqxhr) =>
return if @destroyed
@trigger 'error', @, jqxhr
inTopSessions: ->
return me.id in (session.attributes.creator for session in @topPlayers.models)
@ -201,3 +210,7 @@ class LeaderboardData
startRank = @myRank - 4
session.rank = startRank + i for session, i in l
l
allResources: ->
resources = [@topPlayers, @playersAbove, @playersBelow]
return (r for r in resources when r)

View file

@ -24,7 +24,6 @@ class LevelSessionsCollection extends CocoCollection
module.exports = class LadderView extends RootView
id: 'ladder-view'
template: require 'templates/play/ladder'
startsLoading: true
subscriptions:
'application:idle-changed': 'onIdleChanged'
@ -38,18 +37,18 @@ module.exports = class LadderView extends RootView
constructor: (options, @levelID) ->
super(options)
@level = new Level(_id:@levelID)
p1 = @level.fetch()
@level.fetch()
@sessions = new LevelSessionsCollection(levelID)
p2 = @sessions.fetch({})
@sessions.fetch({})
@addResourceToLoad(@sessions, 'your_sessions')
@addResourceToLoad(@level, 'level')
@simulator = new Simulator()
@listenTo(@simulator, 'statusUpdate', @updateSimulationStatus)
@teams = []
$.when(p1, p2).then @onLoaded
onLoaded: =>
onLoaded: ->
@teams = teamDataFromLevel @level
@startsLoading = false
@render()
super()
getRenderData: ->
ctx = super()
@ -63,7 +62,7 @@ module.exports = class LadderView extends RootView
afterRender: ->
super()
return if @startsLoading
return if @loading()
@insertSubView(@ladderTab = new LadderTabView({}, @level, @sessions))
@insertSubView(@myMatchesTab = new MyMatchesTabView({}, @level, @sessions))
@refreshInterval = setInterval(@fetchSessionsAndRefreshViews.bind(@), 10 * 1000)
@ -72,7 +71,7 @@ module.exports = class LadderView extends RootView
@showPlayModal(hash) if @sessions.loaded
fetchSessionsAndRefreshViews: ->
return if @destroyed or application.userIsIdle or @$el.find('#simulate.active').length or (new Date() - 2000 < @lastRefreshTime) or @startsLoading
return if @destroyed or application.userIsIdle or @$el.find('#simulate.active').length or (new Date() - 2000 < @lastRefreshTime) or @loading()
@sessions.fetch({"success": @refreshViews})
refreshViews: =>

View file

@ -63,7 +63,7 @@ module.exports = class SpellView extends View
@createFirepad()
else
# needs to happen after the code generating this view is complete
setTimeout @onLoaded, 1
setTimeout @onAllLoaded, 1
createACE: ->
# Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html
@ -178,9 +178,9 @@ module.exports = class SpellView extends View
else
@ace.setValue @previousSource
@ace.clearSelection()
@onLoaded()
@onAllLoaded()
onLoaded: =>
onAllLoaded: =>
@spell.transpile @spell.source
@spell.loaded = true
Backbone.Mediator.publish 'tome:spell-loaded', spell: @spell