##// END OF EJS Templates
hgweb: add CSS class to the last entry on a page...
Alexander Plavin -
r19760:2ac4e89a default
parent child Browse files
Show More
@@ -1,422 +1,423
1 // mercurial.js - JavaScript utility functions
1 // mercurial.js - JavaScript utility functions
2 //
2 //
3 // Rendering of branch DAGs on the client side
3 // Rendering of branch DAGs on the client side
4 // Display of elapsed time
4 // Display of elapsed time
5 // Show or hide diffstat
5 // Show or hide diffstat
6 //
6 //
7 // Copyright 2008 Dirkjan Ochtman <dirkjan AT ochtman DOT nl>
7 // Copyright 2008 Dirkjan Ochtman <dirkjan AT ochtman DOT nl>
8 // Copyright 2006 Alexander Schremmer <alex AT alexanderweb DOT de>
8 // Copyright 2006 Alexander Schremmer <alex AT alexanderweb DOT de>
9 //
9 //
10 // derived from code written by Scott James Remnant <scott@ubuntu.com>
10 // derived from code written by Scott James Remnant <scott@ubuntu.com>
11 // Copyright 2005 Canonical Ltd.
11 // Copyright 2005 Canonical Ltd.
12 //
12 //
13 // This software may be used and distributed according to the terms
13 // This software may be used and distributed according to the terms
14 // of the GNU General Public License, incorporated herein by reference.
14 // of the GNU General Public License, incorporated herein by reference.
15
15
16 var colors = [
16 var colors = [
17 [ 1.0, 0.0, 0.0 ],
17 [ 1.0, 0.0, 0.0 ],
18 [ 1.0, 1.0, 0.0 ],
18 [ 1.0, 1.0, 0.0 ],
19 [ 0.0, 1.0, 0.0 ],
19 [ 0.0, 1.0, 0.0 ],
20 [ 0.0, 1.0, 1.0 ],
20 [ 0.0, 1.0, 1.0 ],
21 [ 0.0, 0.0, 1.0 ],
21 [ 0.0, 0.0, 1.0 ],
22 [ 1.0, 0.0, 1.0 ]
22 [ 1.0, 0.0, 1.0 ]
23 ];
23 ];
24
24
25 function Graph() {
25 function Graph() {
26
26
27 this.canvas = document.getElementById('graph');
27 this.canvas = document.getElementById('graph');
28 if (window.G_vmlCanvasManager) this.canvas = window.G_vmlCanvasManager.initElement(this.canvas);
28 if (window.G_vmlCanvasManager) this.canvas = window.G_vmlCanvasManager.initElement(this.canvas);
29 this.ctx = this.canvas.getContext('2d');
29 this.ctx = this.canvas.getContext('2d');
30 this.ctx.strokeStyle = 'rgb(0, 0, 0)';
30 this.ctx.strokeStyle = 'rgb(0, 0, 0)';
31 this.ctx.fillStyle = 'rgb(0, 0, 0)';
31 this.ctx.fillStyle = 'rgb(0, 0, 0)';
32 this.cur = [0, 0];
32 this.cur = [0, 0];
33 this.line_width = 3;
33 this.line_width = 3;
34 this.bg = [0, 4];
34 this.bg = [0, 4];
35 this.cell = [2, 0];
35 this.cell = [2, 0];
36 this.columns = 0;
36 this.columns = 0;
37 this.revlink = '';
37 this.revlink = '';
38
38
39 this.scale = function(height) {
39 this.scale = function(height) {
40 this.bg_height = height;
40 this.bg_height = height;
41 this.box_size = Math.floor(this.bg_height / 1.2);
41 this.box_size = Math.floor(this.bg_height / 1.2);
42 this.cell_height = this.box_size;
42 this.cell_height = this.box_size;
43 }
43 }
44
44
45 function colorPart(num) {
45 function colorPart(num) {
46 num *= 255
46 num *= 255
47 num = num < 0 ? 0 : num;
47 num = num < 0 ? 0 : num;
48 num = num > 255 ? 255 : num;
48 num = num > 255 ? 255 : num;
49 var digits = Math.round(num).toString(16);
49 var digits = Math.round(num).toString(16);
50 if (num < 16) {
50 if (num < 16) {
51 return '0' + digits;
51 return '0' + digits;
52 } else {
52 } else {
53 return digits;
53 return digits;
54 }
54 }
55 }
55 }
56
56
57 this.setColor = function(color, bg, fg) {
57 this.setColor = function(color, bg, fg) {
58
58
59 // Set the colour.
59 // Set the colour.
60 //
60 //
61 // If color is a string, expect an hexadecimal RGB
61 // If color is a string, expect an hexadecimal RGB
62 // value and apply it unchanged. If color is a number,
62 // value and apply it unchanged. If color is a number,
63 // pick a distinct colour based on an internal wheel;
63 // pick a distinct colour based on an internal wheel;
64 // the bg parameter provides the value that should be
64 // the bg parameter provides the value that should be
65 // assigned to the 'zero' colours and the fg parameter
65 // assigned to the 'zero' colours and the fg parameter
66 // provides the multiplier that should be applied to
66 // provides the multiplier that should be applied to
67 // the foreground colours.
67 // the foreground colours.
68 var s;
68 var s;
69 if(typeof color == "string") {
69 if(typeof color == "string") {
70 s = "#" + color;
70 s = "#" + color;
71 } else { //typeof color == "number"
71 } else { //typeof color == "number"
72 color %= colors.length;
72 color %= colors.length;
73 var red = (colors[color][0] * fg) || bg;
73 var red = (colors[color][0] * fg) || bg;
74 var green = (colors[color][1] * fg) || bg;
74 var green = (colors[color][1] * fg) || bg;
75 var blue = (colors[color][2] * fg) || bg;
75 var blue = (colors[color][2] * fg) || bg;
76 red = Math.round(red * 255);
76 red = Math.round(red * 255);
77 green = Math.round(green * 255);
77 green = Math.round(green * 255);
78 blue = Math.round(blue * 255);
78 blue = Math.round(blue * 255);
79 s = 'rgb(' + red + ', ' + green + ', ' + blue + ')';
79 s = 'rgb(' + red + ', ' + green + ', ' + blue + ')';
80 }
80 }
81 this.ctx.strokeStyle = s;
81 this.ctx.strokeStyle = s;
82 this.ctx.fillStyle = s;
82 this.ctx.fillStyle = s;
83 return s;
83 return s;
84
84
85 }
85 }
86
86
87 this.edge = function(x0, y0, x1, y1, color, width) {
87 this.edge = function(x0, y0, x1, y1, color, width) {
88
88
89 this.setColor(color, 0.0, 0.65);
89 this.setColor(color, 0.0, 0.65);
90 if(width >= 0)
90 if(width >= 0)
91 this.ctx.lineWidth = width;
91 this.ctx.lineWidth = width;
92 this.ctx.beginPath();
92 this.ctx.beginPath();
93 this.ctx.moveTo(x0, y0);
93 this.ctx.moveTo(x0, y0);
94 this.ctx.lineTo(x1, y1);
94 this.ctx.lineTo(x1, y1);
95 this.ctx.stroke();
95 this.ctx.stroke();
96
96
97 }
97 }
98
98
99 this.render = function(data) {
99 this.render = function(data) {
100
100
101 var backgrounds = '';
101 var backgrounds = '';
102 var nodedata = '';
102 var nodedata = '';
103
103
104 for (var i in data) {
104 for (var i in data) {
105
105
106 var parity = i % 2;
106 var parity = i % 2;
107 this.cell[1] += this.bg_height;
107 this.cell[1] += this.bg_height;
108 this.bg[1] += this.bg_height;
108 this.bg[1] += this.bg_height;
109
109
110 var cur = data[i];
110 var cur = data[i];
111 var node = cur[1];
111 var node = cur[1];
112 var edges = cur[2];
112 var edges = cur[2];
113 var fold = false;
113 var fold = false;
114
114
115 var prevWidth = this.ctx.lineWidth;
115 var prevWidth = this.ctx.lineWidth;
116 for (var j in edges) {
116 for (var j in edges) {
117
117
118 line = edges[j];
118 line = edges[j];
119 start = line[0];
119 start = line[0];
120 end = line[1];
120 end = line[1];
121 color = line[2];
121 color = line[2];
122 var width = line[3];
122 var width = line[3];
123 if(width < 0)
123 if(width < 0)
124 width = prevWidth;
124 width = prevWidth;
125 var branchcolor = line[4];
125 var branchcolor = line[4];
126 if(branchcolor)
126 if(branchcolor)
127 color = branchcolor;
127 color = branchcolor;
128
128
129 if (end > this.columns || start > this.columns) {
129 if (end > this.columns || start > this.columns) {
130 this.columns += 1;
130 this.columns += 1;
131 }
131 }
132
132
133 if (start == this.columns && start > end) {
133 if (start == this.columns && start > end) {
134 var fold = true;
134 var fold = true;
135 }
135 }
136
136
137 x0 = this.cell[0] + this.box_size * start + this.box_size / 2;
137 x0 = this.cell[0] + this.box_size * start + this.box_size / 2;
138 y0 = this.bg[1] - this.bg_height / 2;
138 y0 = this.bg[1] - this.bg_height / 2;
139 x1 = this.cell[0] + this.box_size * end + this.box_size / 2;
139 x1 = this.cell[0] + this.box_size * end + this.box_size / 2;
140 y1 = this.bg[1] + this.bg_height / 2;
140 y1 = this.bg[1] + this.bg_height / 2;
141
141
142 this.edge(x0, y0, x1, y1, color, width);
142 this.edge(x0, y0, x1, y1, color, width);
143
143
144 }
144 }
145 this.ctx.lineWidth = prevWidth;
145 this.ctx.lineWidth = prevWidth;
146
146
147 // Draw the revision node in the right column
147 // Draw the revision node in the right column
148
148
149 column = node[0]
149 column = node[0]
150 color = node[1]
150 color = node[1]
151
151
152 radius = this.box_size / 8;
152 radius = this.box_size / 8;
153 x = this.cell[0] + this.box_size * column + this.box_size / 2;
153 x = this.cell[0] + this.box_size * column + this.box_size / 2;
154 y = this.bg[1] - this.bg_height / 2;
154 y = this.bg[1] - this.bg_height / 2;
155 var add = this.vertex(x, y, color, parity, cur);
155 var add = this.vertex(x, y, color, parity, cur);
156 backgrounds += add[0];
156 backgrounds += add[0];
157 nodedata += add[1];
157 nodedata += add[1];
158
158
159 if (fold) this.columns -= 1;
159 if (fold) this.columns -= 1;
160
160
161 }
161 }
162
162
163 document.getElementById('nodebgs').innerHTML += backgrounds;
163 document.getElementById('nodebgs').innerHTML += backgrounds;
164 document.getElementById('graphnodes').innerHTML += nodedata;
164 document.getElementById('graphnodes').innerHTML += nodedata;
165
165
166 }
166 }
167
167
168 }
168 }
169
169
170
170
171 process_dates = (function(document, RegExp, Math, isNaN, Date, _false, _true){
171 process_dates = (function(document, RegExp, Math, isNaN, Date, _false, _true){
172
172
173 // derived from code from mercurial/templatefilter.py
173 // derived from code from mercurial/templatefilter.py
174
174
175 var scales = {
175 var scales = {
176 'year': 365 * 24 * 60 * 60,
176 'year': 365 * 24 * 60 * 60,
177 'month': 30 * 24 * 60 * 60,
177 'month': 30 * 24 * 60 * 60,
178 'week': 7 * 24 * 60 * 60,
178 'week': 7 * 24 * 60 * 60,
179 'day': 24 * 60 * 60,
179 'day': 24 * 60 * 60,
180 'hour': 60 * 60,
180 'hour': 60 * 60,
181 'minute': 60,
181 'minute': 60,
182 'second': 1
182 'second': 1
183 };
183 };
184
184
185 function format(count, string){
185 function format(count, string){
186 var ret = count + ' ' + string;
186 var ret = count + ' ' + string;
187 if (count > 1){
187 if (count > 1){
188 ret = ret + 's';
188 ret = ret + 's';
189 }
189 }
190 return ret;
190 return ret;
191 }
191 }
192
192
193 function shortdate(date){
193 function shortdate(date){
194 var ret = date.getFullYear() + '-';
194 var ret = date.getFullYear() + '-';
195 // getMonth() gives a 0-11 result
195 // getMonth() gives a 0-11 result
196 var month = date.getMonth() + 1;
196 var month = date.getMonth() + 1;
197 if (month <= 9){
197 if (month <= 9){
198 ret += '0' + month;
198 ret += '0' + month;
199 } else {
199 } else {
200 ret += month;
200 ret += month;
201 }
201 }
202 ret += '-';
202 ret += '-';
203 var day = date.getDate();
203 var day = date.getDate();
204 if (day <= 9){
204 if (day <= 9){
205 ret += '0' + day;
205 ret += '0' + day;
206 } else {
206 } else {
207 ret += day;
207 ret += day;
208 }
208 }
209 return ret;
209 return ret;
210 }
210 }
211
211
212 function age(datestr){
212 function age(datestr){
213 var now = new Date();
213 var now = new Date();
214 var once = new Date(datestr);
214 var once = new Date(datestr);
215 if (isNaN(once.getTime())){
215 if (isNaN(once.getTime())){
216 // parsing error
216 // parsing error
217 return datestr;
217 return datestr;
218 }
218 }
219
219
220 var delta = Math.floor((now.getTime() - once.getTime()) / 1000);
220 var delta = Math.floor((now.getTime() - once.getTime()) / 1000);
221
221
222 var future = _false;
222 var future = _false;
223 if (delta < 0){
223 if (delta < 0){
224 future = _true;
224 future = _true;
225 delta = -delta;
225 delta = -delta;
226 if (delta > (30 * scales.year)){
226 if (delta > (30 * scales.year)){
227 return "in the distant future";
227 return "in the distant future";
228 }
228 }
229 }
229 }
230
230
231 if (delta > (2 * scales.year)){
231 if (delta > (2 * scales.year)){
232 return shortdate(once);
232 return shortdate(once);
233 }
233 }
234
234
235 for (unit in scales){
235 for (unit in scales){
236 var s = scales[unit];
236 var s = scales[unit];
237 var n = Math.floor(delta / s);
237 var n = Math.floor(delta / s);
238 if ((n >= 2) || (s == 1)){
238 if ((n >= 2) || (s == 1)){
239 if (future){
239 if (future){
240 return format(n, unit) + ' from now';
240 return format(n, unit) + ' from now';
241 } else {
241 } else {
242 return format(n, unit) + ' ago';
242 return format(n, unit) + ' ago';
243 }
243 }
244 }
244 }
245 }
245 }
246 }
246 }
247
247
248 return function(){
248 return function(){
249 var nodes = document.getElementsByTagName('*');
249 var nodes = document.getElementsByTagName('*');
250 var ageclass = new RegExp('\\bage\\b');
250 var ageclass = new RegExp('\\bage\\b');
251 var dateclass = new RegExp('\\bdate\\b');
251 var dateclass = new RegExp('\\bdate\\b');
252 for (var i=0; i<nodes.length; ++i){
252 for (var i=0; i<nodes.length; ++i){
253 var node = nodes[i];
253 var node = nodes[i];
254 var classes = node.className;
254 var classes = node.className;
255 if (ageclass.test(classes)){
255 if (ageclass.test(classes)){
256 var agevalue = age(node.textContent);
256 var agevalue = age(node.textContent);
257 if (dateclass.test(classes)){
257 if (dateclass.test(classes)){
258 // We want both: date + (age)
258 // We want both: date + (age)
259 node.textContent += ' ('+agevalue+')';
259 node.textContent += ' ('+agevalue+')';
260 } else {
260 } else {
261 node.title = node.textContent;
261 node.title = node.textContent;
262 node.textContent = agevalue;
262 node.textContent = agevalue;
263 }
263 }
264 }
264 }
265 }
265 }
266 }
266 }
267 })(document, RegExp, Math, isNaN, Date, false, true)
267 })(document, RegExp, Math, isNaN, Date, false, true)
268
268
269 function toggleDiffstat() {
269 function toggleDiffstat() {
270 var curdetails = document.getElementById('diffstatdetails').style.display;
270 var curdetails = document.getElementById('diffstatdetails').style.display;
271 var curexpand = curdetails == 'none' ? 'inline' : 'none';
271 var curexpand = curdetails == 'none' ? 'inline' : 'none';
272 document.getElementById('diffstatdetails').style.display = curexpand;
272 document.getElementById('diffstatdetails').style.display = curexpand;
273 document.getElementById('diffstatexpand').style.display = curdetails;
273 document.getElementById('diffstatexpand').style.display = curdetails;
274 }
274 }
275
275
276 function toggleLinewrap() {
276 function toggleLinewrap() {
277 function getLinewrap() {
277 function getLinewrap() {
278 var nodes = document.getElementsByClassName('sourcelines');
278 var nodes = document.getElementsByClassName('sourcelines');
279 // if there are no such nodes, error is thrown here
279 // if there are no such nodes, error is thrown here
280 return nodes[0].classList.contains('wrap');
280 return nodes[0].classList.contains('wrap');
281 }
281 }
282
282
283 function setLinewrap(enable) {
283 function setLinewrap(enable) {
284 var nodes = document.getElementsByClassName('sourcelines');
284 var nodes = document.getElementsByClassName('sourcelines');
285 for (var i = 0; i < nodes.length; i++) {
285 for (var i = 0; i < nodes.length; i++) {
286 if (enable) {
286 if (enable) {
287 nodes[i].classList.add('wrap');
287 nodes[i].classList.add('wrap');
288 } else {
288 } else {
289 nodes[i].classList.remove('wrap');
289 nodes[i].classList.remove('wrap');
290 }
290 }
291 }
291 }
292
292
293 var links = document.getElementsByClassName('linewraplink');
293 var links = document.getElementsByClassName('linewraplink');
294 for (var i = 0; i < links.length; i++) {
294 for (var i = 0; i < links.length; i++) {
295 links[i].innerHTML = enable ? 'on' : 'off';
295 links[i].innerHTML = enable ? 'on' : 'off';
296 }
296 }
297 }
297 }
298
298
299 setLinewrap(!getLinewrap());
299 setLinewrap(!getLinewrap());
300 }
300 }
301
301
302 function format(str, replacements) {
302 function format(str, replacements) {
303 return str.replace(/%(\w+)%/g, function(match, p1) {
303 return str.replace(/%(\w+)%/g, function(match, p1) {
304 return String(replacements[p1]);
304 return String(replacements[p1]);
305 });
305 });
306 }
306 }
307
307
308 function makeRequest(url, method, onstart, onsuccess, onerror, oncomplete) {
308 function makeRequest(url, method, onstart, onsuccess, onerror, oncomplete) {
309 xfr = new XMLHttpRequest();
309 xfr = new XMLHttpRequest();
310 xfr.onreadystatechange = function() {
310 xfr.onreadystatechange = function() {
311 if (xfr.readyState === 4) {
311 if (xfr.readyState === 4) {
312 try {
312 try {
313 if (xfr.status === 200) {
313 if (xfr.status === 200) {
314 onsuccess(xfr.responseText);
314 onsuccess(xfr.responseText);
315 } else {
315 } else {
316 throw 'server error';
316 throw 'server error';
317 }
317 }
318 } catch (e) {
318 } catch (e) {
319 onerror(e);
319 onerror(e);
320 } finally {
320 } finally {
321 oncomplete();
321 oncomplete();
322 }
322 }
323 }
323 }
324 };
324 };
325
325
326 xfr.open(method, url);
326 xfr.open(method, url);
327 xfr.send();
327 xfr.send();
328 onstart();
328 onstart();
329 return xfr;
329 return xfr;
330 }
330 }
331
331
332 function removeByClassName(className) {
332 function removeByClassName(className) {
333 var nodes = document.getElementsByClassName(className);
333 var nodes = document.getElementsByClassName(className);
334 while (nodes.length) {
334 while (nodes.length) {
335 nodes[0].parentNode.removeChild(nodes[0]);
335 nodes[0].parentNode.removeChild(nodes[0]);
336 }
336 }
337 }
337 }
338
338
339 function docFromHTML(html) {
339 function docFromHTML(html) {
340 var doc = document.implementation.createHTMLDocument('');
340 var doc = document.implementation.createHTMLDocument('');
341 doc.documentElement.innerHTML = html;
341 doc.documentElement.innerHTML = html;
342 return doc;
342 return doc;
343 }
343 }
344
344
345 function appendFormatHTML(element, formatStr, replacements) {
345 function appendFormatHTML(element, formatStr, replacements) {
346 element.insertAdjacentHTML('beforeend', format(formatStr, replacements));
346 element.insertAdjacentHTML('beforeend', format(formatStr, replacements));
347 }
347 }
348
348
349 function ajaxScrollInit(urlFormat,
349 function ajaxScrollInit(urlFormat,
350 nextHash,
350 nextHash,
351 nextHashRegex,
351 nextHashRegex,
352 containerSelector,
352 containerSelector,
353 messageFormat) {
353 messageFormat) {
354 updateInitiated = false;
354 updateInitiated = false;
355 container = document.querySelector(containerSelector);
355 container = document.querySelector(containerSelector);
356
356
357 function scrollHandler() {
357 function scrollHandler() {
358 if (updateInitiated) {
358 if (updateInitiated) {
359 return;
359 return;
360 }
360 }
361
361
362 var scrollHeight = document.documentElement.scrollHeight;
362 var scrollHeight = document.documentElement.scrollHeight;
363 var clientHeight = document.documentElement.clientHeight;
363 var clientHeight = document.documentElement.clientHeight;
364 var scrollTop = document.body.scrollTop
364 var scrollTop = document.body.scrollTop
365 || document.documentElement.scrollTop;
365 || document.documentElement.scrollTop;
366
366
367 if (scrollHeight - (scrollTop + clientHeight) < 50) {
367 if (scrollHeight - (scrollTop + clientHeight) < 50) {
368 updateInitiated = true;
368 updateInitiated = true;
369 removeByClassName('scroll-loading-error');
369 removeByClassName('scroll-loading-error');
370 container.lastElementChild.classList.add('scroll-separator');
370
371
371 if (!nextHash) {
372 if (!nextHash) {
372 var message = {
373 var message = {
373 class: 'scroll-loading-info',
374 class: 'scroll-loading-info',
374 text: 'No more entries'
375 text: 'No more entries'
375 };
376 };
376 appendFormatHTML(container, messageFormat, message);
377 appendFormatHTML(container, messageFormat, message);
377 return;
378 return;
378 }
379 }
379
380
380 makeRequest(
381 makeRequest(
381 format(urlFormat, {hash: nextHash}),
382 format(urlFormat, {hash: nextHash}),
382 'GET',
383 'GET',
383 function onstart() {
384 function onstart() {
384 var message = {
385 var message = {
385 class: 'scroll-loading',
386 class: 'scroll-loading',
386 text: 'Loading...'
387 text: 'Loading...'
387 };
388 };
388 appendFormatHTML(container, messageFormat, message);
389 appendFormatHTML(container, messageFormat, message);
389 },
390 },
390 function onsuccess(htmlText) {
391 function onsuccess(htmlText) {
391 var m = htmlText.match(nextHashRegex);
392 var m = htmlText.match(nextHashRegex);
392 nextHash = m ? m[1] : null;
393 nextHash = m ? m[1] : null;
393
394
394 var doc = docFromHTML(htmlText);
395 var doc = docFromHTML(htmlText);
395 var nodes = doc.querySelector(containerSelector).children;
396 var nodes = doc.querySelector(containerSelector).children;
396 while (nodes.length) {
397 while (nodes.length) {
397 var node = nodes[0];
398 var node = nodes[0];
398 node = document.adoptNode(node);
399 node = document.adoptNode(node);
399 container.appendChild(node);
400 container.appendChild(node);
400 }
401 }
401 process_dates();
402 process_dates();
402 },
403 },
403 function onerror(errorText) {
404 function onerror(errorText) {
404 var message = {
405 var message = {
405 class: 'scroll-loading-error',
406 class: 'scroll-loading-error',
406 text: 'Error: ' + errorText
407 text: 'Error: ' + errorText
407 };
408 };
408 appendFormatHTML(container, messageFormat, message);
409 appendFormatHTML(container, messageFormat, message);
409 },
410 },
410 function oncomplete() {
411 function oncomplete() {
411 removeByClassName('scroll-loading');
412 removeByClassName('scroll-loading');
412 updateInitiated = false;
413 updateInitiated = false;
413 scrollHandler();
414 scrollHandler();
414 }
415 }
415 );
416 );
416 }
417 }
417 }
418 }
418
419
419 window.addEventListener('scroll', scrollHandler);
420 window.addEventListener('scroll', scrollHandler);
420 window.addEventListener('resize', scrollHandler);
421 window.addEventListener('resize', scrollHandler);
421 scrollHandler();
422 scrollHandler();
422 }
423 }
General Comments 0
You need to be logged in to leave comments. Login now