showoff 0.16.1 → 0.16.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  # No namespace here since ShowOff is a class and I'd have to inherit from
2
2
  # Sinatra::Application (which we don't want to load here)
3
- SHOWOFF_VERSION = '0.16.1'
3
+ SHOWOFF_VERSION = '0.16.2'
@@ -78,6 +78,9 @@
78
78
  #links a.enabled {
79
79
  background-color: #003d96;
80
80
  }
81
+ #links a.warning {
82
+ background-color: #8e030a;
83
+ }
81
84
 
82
85
  #links .mobile {
83
86
  display: none;
@@ -109,6 +112,10 @@
109
112
  color: #ffffff;
110
113
  }
111
114
 
115
+ #presenterPopup #stats .zoomline {
116
+ color: #000;
117
+ }
118
+
112
119
  #nextWindowConfirmation {
113
120
  right: 0.25em;
114
121
  left: auto;
@@ -843,6 +843,7 @@ form .element {
843
843
  -webkit-border-radius: 5px;
844
844
  -khtml-border-radius: 5px;
845
845
  border-radius: 5px;
846
+ cursor: default;
846
847
  }
847
848
  /* and to the bar inside the row */
848
849
  #stats div.bar {
@@ -864,6 +865,68 @@ form .element {
864
865
  right: 0.25em;
865
866
  }
866
867
 
868
+ /* Add the disclosure triangles */
869
+ #stats div.row:before {
870
+ font-family: FontAwesome;
871
+ font-style: normal;
872
+ font-weight: normal;
873
+ font-size: 1.25em;
874
+ text-decoration: inherit;
875
+ }
876
+ #stats div.row.top:hover:before {
877
+ position: absolute;
878
+ content: "\f0da"; /* fa-caret-right */
879
+ margin-left: -0.5em;
880
+ }
881
+ #stats div.row.top.active:before {
882
+ position: absolute;
883
+ content: "\f0d7"; /* fa-caret-down */
884
+ margin-left: -0.75em;
885
+ }
886
+
887
+ #stats h2 {
888
+ border-bottom: 1px solid #eee;
889
+ }
890
+ #stats #viewers,
891
+ #stats #elapsed {
892
+ text-align: center;
893
+ min-height: 2em;
894
+ }
895
+ #stats #viewers.zoomline,
896
+ #stats #elapsed.zoomline {
897
+ text-align: left;
898
+ }
899
+ #stats .zoomline .col.current,
900
+ #stats .zoomline .col.current .inner {
901
+ background-color: #ffb105;
902
+ min-width: 5px;
903
+ }
904
+ #stats .zoomline .col.current:hover {
905
+ min-width: 10px;
906
+ background: #ffb105;
907
+ background: linear-gradient(#efefec, #ffb105);
908
+ }
909
+ #stats .zoomline .col.current:hover .inner,
910
+ #stats .zoomline .col.current .inner:hover {
911
+ background-color: #f3708d;
912
+ }
913
+
914
+
915
+ #stats #stray,
916
+ #stats #idle {
917
+ display: none;
918
+ width: 75%;
919
+ margin: 1em auto;
920
+ padding: 0.5em;
921
+ border: 1px solid #ccc;
922
+ }
923
+ #stats #stray {
924
+ background-color: #8e030a;
925
+ }
926
+ #stats #idle {
927
+ background-color: #3a64c1;
928
+ }
929
+
867
930
  /* style all three boxes */
868
931
  #stats div#least,
869
932
  #stats div#most,
@@ -903,6 +966,7 @@ form .element {
903
966
  }
904
967
  #stats div#all div.bar {
905
968
  background: #66b366;
969
+ border-color: #000;
906
970
  }
907
971
 
908
972
  /* detail view */
@@ -944,7 +1008,8 @@ form .element {
944
1008
  .callout.exercise,
945
1009
  .callout.stop,
946
1010
  .callout.thumbsup,
947
- .callout.conversation {
1011
+ .callout.conversation,
1012
+ .callout.glossary {
948
1013
  padding-left: 3em;
949
1014
  }
950
1015
 
@@ -955,6 +1020,7 @@ form .element {
955
1020
  .callout.stop:before { content: "\f05e"; } /* fa-ban */
956
1021
  .callout.thumbsup:before { content: "\f164"; } /* fa-thumbs-up */
957
1022
  .callout.conversation:before { content: "\f0e5"; } /* fa-comment */
1023
+ .callout.glossary:before { content: "\f02d"; } /* fa-book */
958
1024
 
959
1025
  /* callouts used on error pages */
960
1026
  .error .callout {
@@ -970,6 +1036,32 @@ form .element {
970
1036
  /**********************
971
1037
  *** end callouts ***
972
1038
  **********************/
1039
+ .callout.glossary a.label,
1040
+ ul.glossary.terms a.label {
1041
+ display: block;
1042
+ font-weight: 600;
1043
+ text-decoration: none;
1044
+ }
1045
+ ul.glossary.terms a.return {
1046
+ text-decoration: none;
1047
+ padding-right: 3em;
1048
+ }
1049
+ a.term {
1050
+ text-decoration: none;
1051
+ }
1052
+ a.term:after {
1053
+ font-family: FontAwesome;
1054
+ font-size: 0.8em;
1055
+ vertical-align: super;
1056
+ content: "\f27b";
1057
+ text-decoration: none;/* fa-commenting-o */
1058
+ }
1059
+
1060
+
1061
+ /**********************
1062
+ *** glossary ***
1063
+ **********************/
1064
+
973
1065
 
974
1066
  /* Render hidden headlines so that when we print with wkhtmltopdf, we can use section
975
1067
  titles for page headers. it would make sense to put this in the print section, but
@@ -126,6 +126,27 @@ $(document).ready(function(){
126
126
 
127
127
  setInterval(function() { updatePace() }, 1000);
128
128
 
129
+ setInterval(function() {
130
+ $.getJSON("/stats_data", function( json ) {
131
+ var percent = json['stray_p'];
132
+ if(percent > 25) {
133
+ $('#topbar #statslink').addClass('warning');
134
+ $('#topbar #statslink').attr('title', percent + "% of your audience is not viewing the same slide you are.");
135
+ }
136
+ else {
137
+ $('#stray').hide(); // in case the popup is open
138
+ $('#topbar #statslink').removeClass('warning');
139
+ $('#topbar #statslink').attr('title', "");
140
+ }
141
+
142
+ if( $('#presenterPopup #stats').is(':visible') ) {
143
+ setupStats(json);
144
+ }
145
+ });
146
+
147
+ }, 30000);
148
+
149
+
129
150
  // Tell the showoff server that we're a presenter
130
151
  register();
131
152
 
@@ -172,8 +193,6 @@ function presenterPopupToggle(page, event) {
172
193
  content.append($(data).siblings('#wrapper').html());
173
194
  popup.append(content);
174
195
 
175
- setupStats(); // this function is in showoff.js because /stats does not load presenter.js
176
-
177
196
  $('body').append(popup);
178
197
  popup.slideDown(200); // #presenterPopup is display: none by default
179
198
  });
data/public/js/showoff.js CHANGED
@@ -448,6 +448,10 @@ function currentSlideFromName(name) {
448
448
  var count = 0;
449
449
  if(name.length > 0 ) {
450
450
  slides.each(function(s, slide) {
451
+ if (name == $(slide).attr("id") ) {
452
+ found = count;
453
+ return false;
454
+ }
451
455
  if (name == $(slide).find(".content").attr("ref") ) {
452
456
  found = count;
453
457
  return false;
@@ -460,12 +464,13 @@ function currentSlideFromName(name) {
460
464
 
461
465
  function currentSlideFromParams() {
462
466
  var result;
463
- if (result = window.location.hash.match(/#([0-9]+)/)) {
464
- return result[result.length - 1] - 1;
467
+ // Match numeric slide hashes: #241
468
+ if (result = window.location.hash.match(/^#([0-9]+)$/)) {
469
+ return result[1] - 1;
465
470
  }
466
- else {
467
- var hash = window.location.hash
468
- return currentSlideFromName(hash.substr(1, hash.length))
471
+ // Match slide, with optional internal mark: #slideName(+internal)
472
+ else if (result = window.location.hash.match(/^#([^+]+)\+?(.*)?$/)) {
473
+ return currentSlideFromName(result[1]);
469
474
  }
470
475
  }
471
476
 
@@ -1053,18 +1058,24 @@ function feedbackActivity() {
1053
1058
  setTimeout(function() { $("#hamburger").removeClass('highlight') }, 75);
1054
1059
  }
1055
1060
 
1056
- function track() {
1061
+ function track(current) {
1057
1062
  if (mode.track && ws.readyState == WebSocket.OPEN) {
1058
- var slideName = $("#slideFilename").text() || $("#slideFile").text(); // yey for consistency
1059
- var slideEndTime = new Date().getTime();
1060
- var elapsedTime = slideEndTime - slideStartTime;
1063
+ var slideName = $("#slideFilename").text() || $("#slideFile").text(); // yey for consistency
1064
+
1065
+ if(current) {
1066
+ ws.send(JSON.stringify({ message: 'track', slide: slideName}));
1067
+ }
1068
+ else {
1069
+ var slideEndTime = new Date().getTime();
1070
+ var elapsedTime = slideEndTime - slideStartTime;
1061
1071
 
1062
- // reset the timer
1063
- slideStartTime = slideEndTime;
1072
+ // reset the timer
1073
+ slideStartTime = slideEndTime;
1064
1074
 
1065
- if (elapsedTime > 1000) {
1066
- elapsedTime /= 1000;
1067
- ws.send(JSON.stringify({ message: 'track', slide: slideName, time: elapsedTime}));
1075
+ if (elapsedTime > 1000) {
1076
+ elapsedTime /= 1000;
1077
+ ws.send(JSON.stringify({ message: 'track', slide: slideName, time: elapsedTime}));
1078
+ }
1068
1079
  }
1069
1080
  }
1070
1081
  }
@@ -1170,6 +1181,9 @@ function postSlide() {
1170
1181
  }
1171
1182
 
1172
1183
  $('#notes').html(notes);
1184
+
1185
+ // tell Showoff what slide we ended up on
1186
+ track(true);
1173
1187
  }
1174
1188
  }
1175
1189
 
@@ -1620,12 +1634,50 @@ function togglePause() {
1620
1634
  Stats page
1621
1635
  ********************/
1622
1636
 
1623
- function setupStats()
1637
+ function setupStats(data)
1624
1638
  {
1625
1639
  $("#stats div#all div.detail").hide();
1626
1640
  $("#stats div#all div.row").click(function() {
1641
+ $(this).toggleClass('active');
1627
1642
  $(this).find("div.detail").slideToggle("fast");
1628
1643
  });
1644
+
1645
+ ['stray', 'idle'].forEach(function(stat){
1646
+ var percent = data[stat+'_p'];
1647
+ var selector = '#'+stat;
1648
+
1649
+ if(percent > 25) {
1650
+ $(selector).show();
1651
+ $(selector+' .label').text(percent+'%');
1652
+ }
1653
+ else {
1654
+ $(selector).hide();
1655
+ }
1656
+ });
1657
+
1658
+ var location = window.location.pathname == '/presenter' ? '#' : '/#';
1659
+ var viewers = data['viewers'];
1660
+ if (viewers) {
1661
+ if (viewers.length == 1 && viewers[0][3] == 'current') {
1662
+ $("#viewers").removeClass('zoomline');
1663
+ $("#viewers").text("All audience members are viewing the presenter's slide.");
1664
+ }
1665
+ else {
1666
+ $("#viewers").zoomline({
1667
+ max: data['viewmax'],
1668
+ data: viewers,
1669
+ click: function(element) { window.location = (location + element.attr("data-left")); }
1670
+ });
1671
+ }
1672
+ }
1673
+
1674
+ if (data['elapsed']) {
1675
+ $("#elapsed").zoomline({
1676
+ max: data['maxtime'],
1677
+ data: data['elapsed'],
1678
+ click: function(element) { window.location = (location + element.attr("data-left")); }
1679
+ });
1680
+ }
1629
1681
  }
1630
1682
 
1631
1683
  /* Is this a mobile device? */
data/views/header.erb CHANGED
@@ -11,6 +11,7 @@
11
11
  <link rel="stylesheet" type="text/css" href="<%= @asset_path %>/css/mermaid-6.0.0.css" />
12
12
  <link rel="stylesheet" type="text/css" href="<%= @asset_path %>/css/font-awesome-4.4.0/css/font-awesome.min.css">
13
13
  <link rel="stylesheet" type="text/css" href="<%= @asset_path %>/css/showoff.css?v=<%= SHOWOFF_VERSION %>" />
14
+ <link rel="stylesheet" type="text/css" href="<%= @asset_path %>/css/zoomline-0.0.1.css">
14
15
 
15
16
  <script type="text/javascript" src="<%= @asset_path %>/js/jquery-2.1.4.min.js"></script>
16
17
 
@@ -20,6 +21,7 @@
20
21
  <script type="text/javascript" src="<%= @asset_path %>/js/jquery.doubletap-4ff02c5.js"></script>
21
22
  <script type="text/javascript" src="<%= @asset_path %>/js/jTypeWriter-1.1.js"></script>
22
23
  <script type="text/javascript" src="<%= @asset_path %>/js/bigtext-0.1.8.js"></script>
24
+ <script type="text/javascript" src="<%= @asset_path %>/js/zoomline-0.0.1.js"></script>
23
25
  <script type="text/javascript" src="<%= @asset_path %>/js/highlight.pack-9.2.0.js"></script>
24
26
  <script type="text/javascript" src="<%= @asset_path %>/js/mermaid-6.0.0-min.js"></script>
25
27
 
@@ -9,8 +9,10 @@
9
9
 
10
10
  <link rel="stylesheet" type="text/css" href="<%= @asset_path %>/css/showoff.css?v=<%= SHOWOFF_VERSION %>" />
11
11
  <link rel="stylesheet" type="text/css" href="<%= @asset_path %>/css/font-awesome-4.4.0/css/font-awesome.min.css">
12
+ <link rel="stylesheet" type="text/css" href="<%= @asset_path %>/css/zoomline-0.0.1.css">
12
13
 
13
14
  <script type="text/javascript" src="<%= @asset_path %>/js/jquery-2.1.4.min.js"></script>
15
+ <script type="text/javascript" src="<%= @asset_path %>/js/zoomline-0.0.1.js"></script>
14
16
 
15
17
  <script type="text/javascript" src="<%= @asset_path %>/js/showoff.js?v=<%= SHOWOFF_VERSION %>"></script>
16
18
 
data/views/stats.erb CHANGED
@@ -3,68 +3,59 @@
3
3
  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
4
  <head>
5
5
  <%= erb :header_mini %>
6
-
7
- <script type="text/javascript">
8
- $(document).ready(function(){ setupStats(); });
9
- </script>
10
6
  </head>
11
7
 
12
8
  <body id="stats">
13
9
  <div id="wrapper">
14
- <h1>Viewing Statistics</h1>
15
10
 
16
- <div id="least">
17
- <h3>Least viewed slides</h3>
18
- <% if @least.size > 0 %>
19
- <% max = @least.sort_by {|slide, time| -time}[0][1].to_f %>
20
- <% timestr = (max > 3599) ? '%H:%M:%S' : '%M:%S' %>
21
- <% @least.each do |slide, time| %>
22
- <div class="row">
23
- <span class="label"><%= slide %></span>
24
- <div class="bar" style="width: <%= (time/max)*100 %>%;"> </div>
25
- <div class="time"><%= Time.at(time).gmtime.strftime(timestr) %></div>
26
- </div>
27
- <% end %>
28
- <% end %>
29
- </div>
11
+ <script type="text/javascript">
12
+ $(document).ready(function(){
13
+ $.getJSON("/stats_data", function( json ) {
14
+ setupStats(json);
15
+ });
16
+ });
17
+ </script>
30
18
 
31
- <div id="most">
32
- <h3>Most viewed slides</h3>
33
- <% if @least.size > 0 %>
34
- <% max = @most[0][1].to_f %>
35
- <% timestr = (max > 3599) ? '%H:%M:%S' : '%M:%S' %>
36
- <% @most.each do |slide, time| %>
37
- <div class="row">
38
- <span class="label"><%= slide %></span>
39
- <div class="bar" style="width: <%= (time/max)*100 %>%;"> </div>
40
- <div class="time"><%= Time.at(time).gmtime.strftime(timestr) %></div>
41
- </div>
42
- <% end %>
43
- <% end %>
44
- </div>
19
+ <h1>Viewing Statistics</h1>
20
+ <p id="stray"><span class="label"></span> of your audience is not viewing the same slide you are.</p>
21
+ <p id="idle"><span class="label"></span> of your audience is idle.</p>
22
+ <h2>Slides currently being viewed:</h2>
23
+ <div id="viewers">No data to display.</div>
24
+ <h2>Elapsed time spent on each slide:</h2>
25
+ <div id="elapsed">No data to display.</div>
26
+ </div>
45
27
 
46
- <div id="all">
47
- <h3>All slides</h3>
48
- <%# We reuse the max value calculated from the above step. %>
49
- <% @all.sort.map do |slide, time| %>
50
- <div class="row">
51
- <span class="label"><%= slide %></span>
52
- <div class="bar" style="width: <%= (time/max)*100 %>%;"> </div>
53
- <div class="time"><%= Time.at(time).gmtime.strftime(timestr) %></div>
54
- <% if @counter %>
55
- <div class="detail">
56
- <% @counter[slide].each do |host, count| %>
57
- <div class="row">
28
+ <div id="all">
29
+ <h3>All slides</h3>
30
+ <% max = @all.sort_by {|slide, time| -time}[0][1].to_f rescue 0 %>
31
+ <% @all.sort.map do |slide, time| %>
32
+ <% next if slide.empty? %>
33
+ <% timestr = (time < 60) ? ':%S' : (time < 3599) ? '%M:%S' : '%H:%M:%S' %>
34
+ <div class="row top">
35
+ <span class="label"><%= slide %></span>
36
+ <div class="bar" style="width: <%= (time/max)*100 %>%;"> </div>
37
+ <div class="time"><%= Time.at(time).gmtime.strftime(timestr) %></div>
38
+ <% if @counter %>
39
+ <div class="detail">
40
+ <% @counter[slide].each do |host, views| %>
41
+ <% count = views.inject(0) { |sum, view| sum += view['elapsed'] } %>
42
+ <% timestr = (count < 60) ? ':%S' : (count < 3599) ? '%M:%S' : '%H:%M:%S' %>
43
+ <div class="row">
44
+ <% if host == 'presenter' %>
45
+ <% bgcolor = '' %>
58
46
  <span class="label"><%= host %>:</span>
59
- <div class="bar" style="width: <%= (count/max)*100 %>%;"> </div>
60
- <div class="time"><%= Time.at(count).gmtime.strftime(timestr) %></div>
61
- </div>
62
- <% end %>
63
- </div>
64
- <% end %>
65
- </div>
66
- <% end %>
67
- </div>
47
+ <% else %>
48
+ <% bgcolor = "background-color: ##{host[0...6]}" %>
49
+ <% end %>
50
+ <div class="bar" style="width: <%= (count/max)*100 %>%; <%= bgcolor %>"> </div>
51
+ <div class="time"><%= Time.at(count).gmtime.strftime(timestr) %></div>
52
+ </div>
53
+ <% end %>
54
+ </div>
55
+ <% end %>
56
+ </div>
57
+ <% end %>
68
58
  </div>
59
+
69
60
  </body>
70
61
  </html>