guide.coffee 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. #
  2. # Config
  3. #
  4. HOUR_WIDTH = 200
  5. CHANNEL_LABEL_WIDTH = 180
  6. STORAGE_CHANNELS = 'tvgids-channels'
  7. STORAGE_PROGRAMS = 'tvgids-programs'
  8. HOURS_BEFORE = HOURS_AFTER = 2
  9. DEFAULT_CHANNELS = [1, 2, 3, 4, 31, 46, 92, 36, 37, 34, 29, 18, 91]
  10. DEFAULT_CHANNELS = _.map(DEFAULT_CHANNELS, String)
  11. DETAILS_WINDOW_PADDING = 22 # top/bottom margin between details div and window edge
  12. #
  13. # Utils
  14. #
  15. day_start = -> (new Date()).setHours(0, 0, 0, 0)
  16. day_offset = -> Settings.get('day') * 24 * 60 * 60 * 1000
  17. seconds_today = (time) -> (time - day_start() - day_offset()) / 1000
  18. time2px = (seconds) -> HOUR_WIDTH / 3600 * seconds
  19. zeropad = (digit) -> if digit < 10 then '0' + digit else String(digit)
  20. format_time = (time) ->
  21. date = new Date(time)
  22. zeropad(date.getHours()) + ':' + zeropad(date.getMinutes())
  23. parse_date = (str) ->
  24. [date, time] = str.split(' ')
  25. [year, month, day] = date.split('-')
  26. [hours, minutes, seconds] = time.split(':')
  27. (new Date(year, month - 1, day, hours, minutes, seconds)).getTime()
  28. store_list = (name, values) -> localStorage.setItem(name, values.join(';'))
  29. load_stored_list = (name, def) ->
  30. store_list(name, def) if not localStorage.hasOwnProperty(name)
  31. value = localStorage.getItem(name)
  32. if value.length > 0 then value.split(';') else []
  33. #
  34. # Models & collections
  35. #
  36. Channel = Backbone.Model.extend(
  37. defaults:
  38. id: null
  39. name: 'Some channel'
  40. visible: true
  41. programs: []
  42. )
  43. Program = Backbone.Model.extend(
  44. defaults:
  45. id: null
  46. title: 'Some program'
  47. genre: ''
  48. sort: ''
  49. start: 0
  50. end: 0
  51. article_id: null
  52. article_title: null
  53. )
  54. ChannelList = Backbone.Collection.extend(
  55. model: Channel
  56. #comparator: (a, b) -> parseInt(a.get('id')) - parseInt(b.get('id'))
  57. initialize: ->
  58. @listenTo(Settings, 'change:favourite_channels', @propagateVisible)
  59. fetch: ->
  60. @reset(CHANNELS)
  61. #$.getJSON('channels.php', (data) => @reset(data))
  62. @propagateVisible()
  63. propagateVisible: ->
  64. visible = Settings.get('favourite_channels')
  65. for id in visible
  66. @findWhere(id: id)?.set(visible: true)
  67. for id in _.difference(@pluck('id'), visible)
  68. @findWhere(id: id)?.set(visible: false)
  69. fetchPrograms: (day) ->
  70. # Sometimes a program list is an object (PHP's json_encode() is
  71. # probably given an associative array)
  72. to_array = (o) -> if o.length? then o else _.map(o, ((v, k) -> v))
  73. $('#loading-screen').show()
  74. $.getJSON(
  75. 'programs.php'
  76. channels: Settings.get('favourite_channels').join(','), day: day
  77. (channels) ->
  78. _.each channels, (programs, id) ->
  79. channel = Channels.findWhere(id: id)
  80. channel.set(programs: (
  81. new Program(
  82. id: p.db_id
  83. title: p.titel
  84. genre: p.genre
  85. sort: p.soort
  86. start: parse_date(p.datum_start)
  87. end: parse_date(p.datum_end)
  88. article_id: p.artikel_id
  89. article_title: p.artikel_titel
  90. ) for p in to_array(programs)
  91. )) if channel?
  92. $('#loading-screen').hide()
  93. )
  94. )
  95. #
  96. # Views
  97. #
  98. ChannelView = Backbone.View.extend(
  99. tagName: 'div'
  100. className: 'channel'
  101. initialize: ->
  102. @listenTo(@model, 'change:programs', @render)
  103. @listenTo(@model, 'change:visible', @toggleVisible)
  104. #@$el.text(@model.get('title'))
  105. render: ->
  106. @$el.empty()
  107. _.each @model.get('programs'), (program) =>
  108. view = new ProgramView(model: program)
  109. view.render()
  110. @$el.append(view.el)
  111. toggleVisible: ->
  112. @$el.toggle(@model.get('visible'))
  113. )
  114. ProgramView = Backbone.View.extend(
  115. tagName: 'div'
  116. className: 'program'
  117. events:
  118. 'click .favlink': 'toggleFavourite'
  119. 'click': -> Settings.set(selected_program: @model.get('id'))
  120. initialize: ->
  121. $('<span class="title"/>').text(@model.get('title')).appendTo(@el)
  122. from = format_time(@model.get('start'))
  123. to = format_time(@model.get('end'))
  124. @$el.attr('title', @model.get('title') + " (#{from} - #{to})")
  125. @$fav = $('<a class="favlink icon-heart"/>').appendTo(@el)
  126. @$fav.attr('title', 'Als favoriet instellen')
  127. @updateFavlink()
  128. left = time2px(Math.max(-HOURS_BEFORE * 60 * 60,
  129. seconds_today(@model.get('start'))))
  130. width = time2px(seconds_today(@model.get('end'))) - left
  131. @$el.css(
  132. left: ((HOURS_BEFORE * HOUR_WIDTH) + left) + 'px'
  133. width: (width - 10) + 'px'
  134. )
  135. @listenTo(Settings, 'change:favourite_programs', @updateFavlink)
  136. @listenTo(Clock, 'tick', @render)
  137. toggleFavourite: (e) ->
  138. Settings.toggleFavouriteProgram(@model.get('title'))
  139. e.stopPropagation()
  140. updateFavlink: ->
  141. isfav = Settings.isFavouriteProgram(@model.get('title'))
  142. @$el.toggleClass('favourite', isfav)
  143. render: ->
  144. if @model.get('start') <= Date.now()
  145. if @model.get('end') < Date.now()
  146. @$el.removeClass('current').addClass('past')
  147. @stopListening(Clock, 'tick')
  148. else
  149. @$el.addClass('current')
  150. )
  151. ChannelLabelsView = Backbone.View.extend(
  152. el: $('#channel-labels')
  153. initialize: (options) ->
  154. @listenTo(Channels, 'reset', @addChannels)
  155. @listenTo(options.app, 'scroll', @moveTop)
  156. addChannels: ->
  157. @$el.empty()
  158. Channels.each((channel) ->
  159. elem = $('<div id="label-' + channel.get('id') + '" class="label"/>')
  160. elem.html(channel.get('name')).toggle(channel.get('visible')).appendTo(@el)
  161. @listenTo(channel, 'change:visible', -> @toggleVisible(channel))
  162. , @)
  163. moveTop: (delta) ->
  164. @$el.css('top', (@$el.position().top - delta) + 'px')
  165. toggleVisible: (channel) ->
  166. @$('#label-' + channel.get('id')).toggle(channel.get('visible'))
  167. )
  168. ProgramDetailsView = Backbone.View.extend(
  169. el: $('#program-details')
  170. template: _.template($('#details-template').html())
  171. events:
  172. 'click .bg': -> Settings.set(selected_program: null)
  173. initialize: (options) ->
  174. @listenTo(Settings, 'change:selected_program', @toggleDetails)
  175. @setBounds()
  176. $(window).resize(=> @setBounds())
  177. toggleDetails: ->
  178. id = Settings.get('selected_program')
  179. if id
  180. $('#loading-screen').show()
  181. $.getJSON(
  182. 'details.php'
  183. id: id
  184. (data) =>
  185. $('#loading-screen').hide()
  186. @$el.show()
  187. @$('.content').html(@template(_.extend(id: id, data)))
  188. @alignMiddle()
  189. # Align again after images are loaded
  190. @$('.content img').load(-> $(@).css(height: 'auto'))
  191. @$('.content img').load(=> @alignMiddle())
  192. )
  193. else
  194. @$el.hide()
  195. @$('.content').empty()
  196. setBounds: ->
  197. max = $(window).height() - 2 * DETAILS_WINDOW_PADDING
  198. @$('.content').css(maxHeight: max)
  199. @alignMiddle()
  200. alignMiddle: ->
  201. height = @$('.content').outerHeight()
  202. @$('.content').css(marginTop: "-#{height / 2}px")
  203. )
  204. AppView = Backbone.View.extend(
  205. el: $('#guide')
  206. events:
  207. # TODO: move to initialize
  208. 'scroll': 'moveTimeline'
  209. initialize: ->
  210. @prevScrollTop = null
  211. @listenTo(Channels, 'reset', @addChannels)
  212. @listenTo(Settings, 'change:day', @fetchPrograms)
  213. @labelview = new ChannelLabelsView(app: @)
  214. @detailsview = new ProgramDetailsView(app: @)
  215. #@$el.smoothTouchScroll(
  216. # scrollableAreaClass: 'channels'
  217. # scrollWrapperClass: 'guide'
  218. #)
  219. #@iscroll = new iScroll('guide', vScroll: false, hScrollbar: true)
  220. $('#beforeyesterday').click(-> Settings.set(day: -2))
  221. $('#yesterday').click(-> Settings.set(day: -1))
  222. $('#today').click(-> Settings.set(day: 0))
  223. $('#tomorrow').click(-> Settings.set(day: 1))
  224. $('#overmorrow').click(-> Settings.set(day: 2))
  225. $('#help').click((e) -> e.stopPropagation(); $('#help-popup').show())
  226. $(document).click(-> $('#help-popup').hide())
  227. Channels.fetch()
  228. @centerIndicator()
  229. @updateIndicator()
  230. @listenTo(Clock, 'tick', @updateIndicator)
  231. addChannels: ->
  232. @$('.channels > .channel').remove()
  233. Channels.each((channel) ->
  234. view = new ChannelView(model: channel)
  235. view.render()
  236. @$('.channels').append(view.el)
  237. , @)
  238. @fetchPrograms()
  239. updateIndicator: ->
  240. if Settings.get('day') == 0
  241. left = time2px(seconds_today(Date.now())) + CHANNEL_LABEL_WIDTH - 1
  242. @$('.indicator')
  243. .css(left: ((HOURS_BEFORE * HOUR_WIDTH) + left) + 'px')
  244. .height(@$('.channels').height() - 2)
  245. .show()
  246. else
  247. @$('.indicator').hide()
  248. centerIndicator: ->
  249. @$el.scrollLeft(@$('.indicator').position().left - @$el.width() / 2)
  250. fetchPrograms: ->
  251. day = Settings.get('day')
  252. Channels.fetchPrograms(day)
  253. @updateIndicator()
  254. $('.navbar .active').removeClass('active')
  255. $($('.navbar .navitem')[day + 2]).addClass('active')
  256. moveTimeline: ->
  257. if @$el.scrollTop() != @prevScrollTop
  258. @trigger('scroll', @$el.scrollTop() - @prevScrollTop)
  259. @prevScrollTop = @$el.scrollTop()
  260. @$('.timeline').css('top', (@$el.scrollTop() + 37) + 'px')
  261. )
  262. #
  263. # Main
  264. #
  265. Settings = new (Backbone.Model.extend(
  266. defaults:
  267. day: 0
  268. favourite_channels: load_stored_list(STORAGE_CHANNELS, DEFAULT_CHANNELS)
  269. favourite_programs: load_stored_list(STORAGE_PROGRAMS, [])
  270. selected_program: null
  271. toggleFavouriteProgram: (title) ->
  272. list = @get('favourite_programs')
  273. if @isFavouriteProgram(title)
  274. list.splice(list.indexOf(title), 1)
  275. else
  276. list.push(title)
  277. @attributes.favourite_programs = list
  278. @trigger('change:favourite_programs')
  279. store_list(STORAGE_PROGRAMS, list)
  280. isFavouriteProgram: (title) ->
  281. _.contains(@get('favourite_programs'), title)
  282. ))()
  283. Clock = new (->
  284. _.extend(@, Backbone.Events)
  285. setInterval((=> @trigger('tick')), 60 * 60 * 1000 / HOUR_WIDTH)
  286. )()
  287. Channels = new ChannelList()
  288. App = new AppView()