##// END OF EJS Templates
diffs: added diff navigation to improve UX when browisng the full context diffs.
marcink -
r4441:114e65cb default
parent child Browse files
Show More
@@ -0,0 +1,91 b''
1 // jQuery Scrollstop Plugin v1.2.0
2 // https://github.com/ssorallen/jquery-scrollstop
3
4 (function (factory) {
5 // UMD[2] wrapper for jQuery plugins to work in AMD or in CommonJS.
6 //
7 // [2] https://github.com/umdjs/umd
8
9 if (typeof define === 'function' && define.amd) {
10 // AMD. Register as an anonymous module.
11 define(['jquery'], factory);
12 } else if (typeof exports === 'object') {
13 // Node/CommonJS
14 module.exports = factory(require('jquery'));
15 } else {
16 // Browser globals
17 factory(jQuery);
18 }
19 }(function ($) {
20 // $.event.dispatch was undocumented and was deprecated in jQuery 1.7[1]. It
21 // was replaced by $.event.handle in jQuery 1.9.
22 //
23 // Use the first of the available functions to support jQuery <1.8.
24 //
25 // [1] https://github.com/jquery/jquery-migrate/blob/master/src/event.js#L25
26 var dispatch = $.event.dispatch || $.event.handle;
27
28 var special = $.event.special,
29 uid1 = 'D' + (+new Date()),
30 uid2 = 'D' + (+new Date() + 1);
31
32 special.scrollstart = {
33 setup: function(data) {
34 var _data = $.extend({
35 latency: special.scrollstop.latency
36 }, data);
37
38 var timer,
39 handler = function(evt) {
40 var _self = this,
41 _args = arguments;
42
43 if (timer) {
44 clearTimeout(timer);
45 } else {
46 evt.type = 'scrollstart';
47 dispatch.apply(_self, _args);
48 }
49
50 timer = setTimeout(function() {
51 timer = null;
52 }, _data.latency);
53 };
54
55 $(this).bind('scroll', handler).data(uid1, handler);
56 },
57 teardown: function() {
58 $(this).unbind('scroll', $(this).data(uid1));
59 }
60 };
61
62 special.scrollstop = {
63 latency: 250,
64 setup: function(data) {
65 var _data = $.extend({
66 latency: special.scrollstop.latency
67 }, data);
68
69 var timer,
70 handler = function(evt) {
71 var _self = this,
72 _args = arguments;
73
74 if (timer) {
75 clearTimeout(timer);
76 }
77
78 timer = setTimeout(function() {
79 timer = null;
80 evt.type = 'scrollstop';
81 dispatch.apply(_self, _args);
82 }, _data.latency);
83 };
84
85 $(this).bind('scroll', handler).data(uid2, handler);
86 },
87 teardown: function() {
88 $(this).unbind('scroll', $(this).data(uid2));
89 }
90 };
91 }));
@@ -0,0 +1,171 b''
1 /**
2 * Within Viewport jQuery Plugin
3 *
4 * @description Companion plugin for withinviewport.js - determines whether an element is completely within the browser viewport
5 * @author Craig Patik, http://patik.com/
6 * @version 2.1.2
7 * @date 2019-08-16
8 */
9 (function ($) {
10 /**
11 * $.withinviewport()
12 * @description jQuery method
13 * @param {Object} [settings] optional settings
14 * @return {Collection} Contains all elements that were within the viewport
15 */
16 $.fn.withinviewport = function (settings) {
17 var opts;
18 var elems;
19
20 if (typeof settings === 'string') {
21 settings = {
22 sides: settings
23 };
24 }
25
26 opts = $.extend({}, settings, {
27 sides: 'all'
28 });
29 elems = [];
30
31 this.each(function () {
32 if (withinviewport(this, opts)) {
33 elems.push(this);
34 }
35 });
36
37 return $(elems);
38 };
39
40 // Main custom selector
41 $.extend($.expr[':'], {
42 'within-viewport': function (element) {
43 return withinviewport(element, 'all');
44 }
45 });
46
47 /**
48 * Optional enhancements and shortcuts
49 *
50 * @description Uncomment or comment these pieces as they apply to your project and coding preferences
51 */
52
53 // Shorthand jQuery methods
54
55 $.fn.withinviewporttop = function (settings) {
56 var opts;
57 var elems;
58
59 if (typeof settings === 'string') {
60 settings = {
61 sides: settings
62 };
63 }
64
65 opts = $.extend({}, settings, {
66 sides: 'top'
67 });
68 elems = [];
69
70 this.each(function () {
71 if (withinviewport(this, opts)) {
72 elems.push(this);
73 }
74 });
75
76 return $(elems);
77 };
78
79 $.fn.withinviewportright = function (settings) {
80 var opts;
81 var elems;
82
83 if (typeof settings === 'string') {
84 settings = {
85 sides: settings
86 };
87 }
88
89 opts = $.extend({}, settings, {
90 sides: 'right'
91 });
92 elems = [];
93
94 this.each(function () {
95 if (withinviewport(this, opts)) {
96 elems.push(this);
97 }
98 });
99
100 return $(elems);
101 };
102
103 $.fn.withinviewportbottom = function (settings) {
104 var opts;
105 var elems;
106
107 if (typeof settings === 'string') {
108 settings = {
109 sides: settings
110 };
111 }
112
113 opts = $.extend({}, settings, {
114 sides: 'bottom'
115 });
116 elems = [];
117
118 this.each(function () {
119 if (withinviewport(this, opts)) {
120 elems.push(this);
121 }
122 });
123
124 return $(elems);
125 };
126
127 $.fn.withinviewportleft = function (settings) {
128 var opts;
129 var elems;
130
131 if (typeof settings === 'string') {
132 settings = {
133 sides: settings
134 };
135 }
136
137 opts = $.extend({}, settings, {
138 sides: 'left'
139 });
140 elems = [];
141
142 this.each(function () {
143 if (withinviewport(this, opts)) {
144 elems.push(this);
145 }
146 });
147
148 return $(elems);
149 };
150
151 // Custom jQuery selectors
152 $.extend($.expr[':'], {
153 'within-viewport-top': function (element) {
154 return withinviewport(element, 'top');
155 },
156 'within-viewport-right': function (element) {
157 return withinviewport(element, 'right');
158 },
159 'within-viewport-bottom': function (element) {
160 return withinviewport(element, 'bottom');
161 },
162 'within-viewport-left': function (element) {
163 return withinviewport(element, 'left');
164 }
165 // Example custom selector:
166 //,
167 // 'within-viewport-top-left-45': function (element) {
168 // return withinviewport(element, {sides:'top left', top: 45, left: 45});
169 // }
170 });
171 }(jQuery)); No newline at end of file
@@ -0,0 +1,235 b''
1 /**
2 * Within Viewport
3 *
4 * @description Determines whether an element is completely within the browser viewport
5 * @author Craig Patik, http://patik.com/
6 * @version 2.1.2
7 * @date 2019-08-16
8 */
9 (function (root, name, factory) {
10 // AMD
11 if (typeof define === 'function' && define.amd) {
12 define([], factory);
13 }
14 // Node and CommonJS-like environments
15 else if (typeof module !== 'undefined' && typeof exports === 'object') {
16 module.exports = factory();
17 }
18 // Browser global
19 else {
20 root[name] = factory();
21 }
22 }(this, 'withinviewport', function () {
23 var canUseWindowDimensions = typeof window !== 'undefined' && window.innerHeight !== undefined; // IE 8 and lower fail this
24
25 /**
26 * Determines whether an element is within the viewport
27 * @param {Object} elem DOM Element (required)
28 * @param {Object} options Optional settings
29 * @return {Boolean} Whether the element was completely within the viewport
30 */
31 var withinviewport = function withinviewport(elem, options) {
32 var result = false;
33 var metadata = {};
34 var config = {};
35 var settings;
36 var isWithin;
37 var isContainerTheWindow;
38 var elemBoundingRect;
39 var containerBoundingRect;
40 var containerScrollTop;
41 var containerScrollLeft;
42 var scrollBarWidths = [0, 0];
43 var sideNamesPattern;
44 var sides;
45 var side;
46 var i;
47
48 // If invoked by the jQuery plugin, get the actual DOM element
49 if (typeof jQuery !== 'undefined' && elem instanceof jQuery) {
50 elem = elem.get(0);
51 }
52
53 if (typeof elem !== 'object' || elem.nodeType !== 1) {
54 throw new Error('First argument must be an element');
55 }
56
57 // Look for inline settings on the element
58 if (elem.getAttribute('data-withinviewport-settings') && window.JSON) {
59 metadata = JSON.parse(elem.getAttribute('data-withinviewport-settings'));
60 }
61
62 // Settings argument may be a simple string (`top`, `right`, etc)
63 if (typeof options === 'string') {
64 settings = {
65 sides: options
66 };
67 } else {
68 settings = options || {};
69 }
70
71 // Build configuration from defaults and user-provided settings and metadata
72 config.container = settings.container || metadata.container || withinviewport.defaults.container || window;
73 config.sides = settings.sides || metadata.sides || withinviewport.defaults.sides || 'all';
74 config.top = settings.top || metadata.top || withinviewport.defaults.top || 0;
75 config.right = settings.right || metadata.right || withinviewport.defaults.right || 0;
76 config.bottom = settings.bottom || metadata.bottom || withinviewport.defaults.bottom || 0;
77 config.left = settings.left || metadata.left || withinviewport.defaults.left || 0;
78
79 // Extract the DOM node from a jQuery collection
80 if (typeof jQuery !== 'undefined' && config.container instanceof jQuery) {
81 config.container = config.container.get(0);
82 }
83
84 // Use the window as the container if the user specified the body or a non-element
85 if (config.container === document.body || config.container.nodeType !== 1) {
86 config.container = window;
87 }
88
89 isContainerTheWindow = (config.container === window);
90
91 // Element testing methods
92 isWithin = {
93 // Element is below the top edge of the viewport
94 top: function _isWithin_top() {
95 if (isContainerTheWindow) {
96 return (elemBoundingRect.top >= config.top);
97 } else {
98 return (elemBoundingRect.top >= containerScrollTop - (containerScrollTop - containerBoundingRect.top) + config.top);
99 }
100 },
101
102 // Element is to the left of the right edge of the viewport
103 right: function _isWithin_right() {
104 // Note that `elemBoundingRect.right` is the distance from the *left* of the viewport to the element's far right edge
105
106 if (isContainerTheWindow) {
107 return (elemBoundingRect.right <= (containerBoundingRect.right + containerScrollLeft) - config.right);
108 } else {
109 return (elemBoundingRect.right <= containerBoundingRect.right - scrollBarWidths[0] - config.right);
110 }
111 },
112
113 // Element is above the bottom edge of the viewport
114 bottom: function _isWithin_bottom() {
115 var containerHeight = 0;
116
117 if (isContainerTheWindow) {
118 if (canUseWindowDimensions) {
119 containerHeight = config.container.innerHeight;
120 } else if (document && document.documentElement) {
121 containerHeight = document.documentElement.clientHeight;
122 }
123 } else {
124 containerHeight = containerBoundingRect.bottom;
125 }
126
127 // Note that `elemBoundingRect.bottom` is the distance from the *top* of the viewport to the element's bottom edge
128 return (elemBoundingRect.bottom <= containerHeight - scrollBarWidths[1] - config.bottom);
129 },
130
131 // Element is to the right of the left edge of the viewport
132 left: function _isWithin_left() {
133 if (isContainerTheWindow) {
134 return (elemBoundingRect.left >= config.left);
135 } else {
136 return (elemBoundingRect.left >= containerScrollLeft - (containerScrollLeft - containerBoundingRect.left) + config.left);
137 }
138 },
139
140 // Element is within all four boundaries
141 all: function _isWithin_all() {
142 // Test each boundary in order of efficiency and likeliness to be false. This way we can avoid running all four functions on most elements.
143 // 1. Top: Quickest to calculate + most likely to be false
144 // 2. Bottom: Note quite as quick to calculate, but also very likely to be false
145 // 3-4. Left and right are both equally unlikely to be false since most sites only scroll vertically, but left is faster to calculate
146 return (isWithin.top() && isWithin.bottom() && isWithin.left() && isWithin.right());
147 }
148 };
149
150 // Get the element's bounding rectangle with respect to the viewport
151 elemBoundingRect = elem.getBoundingClientRect();
152
153 // Get viewport dimensions and offsets
154 if (isContainerTheWindow) {
155 containerBoundingRect = document.documentElement.getBoundingClientRect();
156 containerScrollTop = document.body.scrollTop;
157 containerScrollLeft = window.scrollX || document.body.scrollLeft;
158 } else {
159 containerBoundingRect = config.container.getBoundingClientRect();
160 containerScrollTop = config.container.scrollTop;
161 containerScrollLeft = config.container.scrollLeft;
162 }
163
164 // Don't count the space consumed by scrollbars
165 if (containerScrollLeft) {
166 scrollBarWidths[0] = 18;
167 }
168
169 if (containerScrollTop) {
170 scrollBarWidths[1] = 16;
171 }
172
173 // Test the element against each side of the viewport that was requested
174 sideNamesPattern = /^top$|^right$|^bottom$|^left$|^all$/;
175
176 // Loop through all of the sides
177 sides = config.sides.split(' ');
178 i = sides.length;
179
180 while (i--) {
181 side = sides[i].toLowerCase();
182
183 if (sideNamesPattern.test(side)) {
184 if (isWithin[side]()) {
185 result = true;
186 } else {
187 result = false;
188
189 // Quit as soon as the first failure is found
190 break;
191 }
192 }
193 }
194
195 return result;
196 };
197
198 // Default settings
199 withinviewport.prototype.defaults = {
200 container: typeof document !== 'undefined' ? document.body : {},
201 sides: 'all',
202 top: 0,
203 right: 0,
204 bottom: 0,
205 left: 0
206 };
207
208 withinviewport.defaults = withinviewport.prototype.defaults;
209
210 /**
211 * Optional enhancements and shortcuts
212 *
213 * @description Uncomment or comment these pieces as they apply to your project and coding preferences
214 */
215
216 // Shortcut methods for each side of the viewport
217 // Example: `withinviewport.top(elem)` is the same as `withinviewport(elem, 'top')`
218 withinviewport.prototype.top = function _withinviewport_top(element) {
219 return withinviewport(element, 'top');
220 };
221
222 withinviewport.prototype.right = function _withinviewport_right(element) {
223 return withinviewport(element, 'right');
224 };
225
226 withinviewport.prototype.bottom = function _withinviewport_bottom(element) {
227 return withinviewport(element, 'bottom');
228 };
229
230 withinviewport.prototype.left = function _withinviewport_left(element) {
231 return withinviewport(element, 'left');
232 };
233
234 return withinviewport;
235 })); No newline at end of file
@@ -51,9 +51,12 b''
51 51 "<%= dirs.js.src %>/plugins/jquery.pjax.js",
52 52 "<%= dirs.js.src %>/plugins/jquery.dataTables.js",
53 53 "<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
54 "<%= dirs.js.src %>/plugins/within_viewport.js",
54 55 "<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js",
55 56 "<%= dirs.js.src %>/plugins/jquery.autocomplete.js",
56 57 "<%= dirs.js.src %>/plugins/jquery.debounce.js",
58 "<%= dirs.js.src %>/plugins/jquery.scrollstop.js",
59 "<%= dirs.js.src %>/plugins/jquery.within-viewport.js",
57 60 "<%= dirs.js.node_modules %>/mark.js/dist/jquery.mark.min.js",
58 61 "<%= dirs.js.src %>/plugins/jquery.timeago.js",
59 62 "<%= dirs.js.src %>/plugins/jquery.timeago-extension.js",
@@ -540,10 +540,11 b' class DiffSet(object):'
540 540 })
541 541
542 542 file_chunks = patch['chunks'][1:]
543 for hunk in file_chunks:
543 for i, hunk in enumerate(file_chunks, 1):
544 544 hunkbit = self.parse_hunk(hunk, source_file, target_file)
545 545 hunkbit.source_file_path = source_file_path
546 546 hunkbit.target_file_path = target_file_path
547 hunkbit.index = i
547 548 filediff.hunks.append(hunkbit)
548 549
549 550 # Simulate hunk on OPS type line which doesn't really contain any diff
@@ -53,7 +53,7 b' from pygments.lexers import ('
53 53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
54 54
55 55 from pyramid.threadlocal import get_current_request
56
56 from tempita import looper
57 57 from webhelpers2.html import literal, HTML, escape
58 58 from webhelpers2.html._autolink import _auto_link_urls
59 59 from webhelpers2.html.tools import (
@@ -998,6 +998,21 b' input.filediff-collapse-state {'
998 998
999 999 /**** END COMMENTS ****/
1000 1000
1001
1002 .nav-chunk {
1003 position: absolute;
1004 right: 20px;
1005 margin-top: -17px;
1006 }
1007
1008 .nav-chunk.selected {
1009 visibility: visible !important;
1010 }
1011
1012 #diff_nav {
1013 color: @grey3;
1014 }
1015
1001 1016 }
1002 1017
1003 1018
@@ -314,6 +314,7 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
314 314 ${hunk.section_header}
315 315 </td>
316 316 </tr>
317
317 318 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
318 319 % endfor
319 320
@@ -657,21 +658,28 b' def get_comments_for(diff_type, comments'
657 658 %>
658 659
659 660 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
660 %for i, line in enumerate(hunk.sideside):
661
662 <% chunk_count = 1 %>
663 %for loop_obj, item in h.looper(hunk.sideside):
661 664 <%
665 line = item
666 i = loop_obj.index
667 prev_line = loop_obj.previous
662 668 old_line_anchor, new_line_anchor = None, None
663 669
664 670 if line.original.lineno:
665 671 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
666 672 if line.modified.lineno:
667 673 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
674
675 line_action = line.modified.action or line.original.action
676 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
668 677 %>
669 678
670 679 <tr class="cb-line">
671 680 <td class="cb-data ${action_class(line.original.action)}"
672 681 data-line-no="${line.original.lineno}"
673 682 >
674 <div>
675 683
676 684 <% line_old_comments = None %>
677 685 %if line.original.get_comment_args:
@@ -685,7 +693,6 b' def get_comments_for(diff_type, comments'
685 693 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
686 694 % endif
687 695 %endif
688 </div>
689 696 </td>
690 697 <td class="cb-lineno ${action_class(line.original.action)}"
691 698 data-line-no="${line.original.lineno}"
@@ -751,6 +758,12 b' def get_comments_for(diff_type, comments'
751 758 %if use_comments and line.modified.lineno and line_new_comments:
752 759 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries)}
753 760 %endif
761 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
762 <div class="nav-chunk" style="visibility: hidden">
763 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
764 </div>
765 <% chunk_count +=1 %>
766 % endif
754 767 </td>
755 768 </tr>
756 769 %endfor
@@ -903,12 +916,21 b' def get_comments_for(diff_type, comments'
903 916 </div>
904 917 </div>
905 918 </div>
906 <div class="fpath-placeholder">
919 <div class="fpath-placeholder pull-left">
907 920 <i class="icon-file-text"></i>
908 921 <strong class="fpath-placeholder-text">
909 922 Context file:
910 923 </strong>
911 924 </div>
925 <div class="pull-right noselect">
926 <span id="diff_nav">Loading diff...:</span>
927 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
928 <i class="icon-angle-up"></i>
929 </span>
930 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
931 <i class="icon-angle-down"></i>
932 </span>
933 </div>
912 934 <div class="sidebar_inner_shadow"></div>
913 935 </div>
914 936 </div>
@@ -1031,10 +1053,84 b' def get_comments_for(diff_type, comments'
1031 1053 e.preventDefault();
1032 1054 });
1033 1055
1056 getCurrentChunk = function () {
1057
1058 var chunksAll = $('.nav-chunk').filter(function () {
1059 return $(this).parents('.filediff').prev().get(0).checked !== true
1060 })
1061 var chunkSelected = $('.nav-chunk.selected');
1062 var initial = false;
1063
1064 if (chunkSelected.length === 0) {
1065 // no initial chunk selected, we pick first
1066 chunkSelected = $(chunksAll.get(0));
1067 var initial = true;
1068 }
1069
1070 return {
1071 'all': chunksAll,
1072 'selected': chunkSelected,
1073 'initial': initial,
1074 }
1075 }
1076
1077 animateDiffNavText = function () {
1078 var $diffNav = $('#diff_nav')
1079
1080 var callback = function () {
1081 $diffNav.animate({'opacity': 1.00}, 200)
1082 };
1083 $diffNav.animate({'opacity': 0.15}, 200, callback);
1084 }
1085
1086 scrollToChunk = function (moveBy) {
1087 var chunk = getCurrentChunk();
1088 var all = chunk.all
1089 var selected = chunk.selected
1090
1091 var curPos = all.index(selected);
1092 var newPos = curPos;
1093 if (!chunk.initial) {
1094 var newPos = curPos + moveBy;
1095 }
1096
1097 var curElem = all.get(newPos);
1098
1099 if (curElem === undefined) {
1100 // end or back
1101 $('#diff_nav').html('No next diff element.')
1102 animateDiffNavText()
1103 return
1104 } else if (newPos < 0) {
1105 $('#diff_nav').html('No previous diff element.')
1106 animateDiffNavText()
1107 return
1108 } else {
1109 $('#diff_nav').html('Diff navigation:')
1110 }
1111
1112 curElem = $(curElem)
1113 var offset = 100;
1114 $(window).scrollTop(curElem.position().top - offset);
1115
1116 //clear selection
1117 all.removeClass('selected')
1118 curElem.addClass('selected')
1119 }
1120
1121 scrollToPrevChunk = function () {
1122 scrollToChunk(-1)
1123 }
1124 scrollToNextChunk = function () {
1125 scrollToChunk(1)
1126 }
1127
1034 1128 </script>
1035 1129 % endif
1036 1130
1037 1131 <script type="text/javascript">
1132 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1133
1038 1134 $(document).ready(function () {
1039 1135
1040 1136 var contextPrefix = _gettext('Context file: ');
@@ -1213,6 +1309,46 b' def get_comments_for(diff_type, comments'
1213 1309 $('.toggle-wide-diff').addClass('btn-active');
1214 1310 updateSticky();
1215 1311 }
1312
1313 // DIFF NAV //
1314
1315 // element to detect scroll direction of
1316 var $window = $(window);
1317
1318 // initialize last scroll position
1319 var lastScrollY = $window.scrollTop();
1320
1321 $window.on('resize scrollstop', {latency: 350}, function () {
1322 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1323
1324 // get current scroll position
1325 var currentScrollY = $window.scrollTop();
1326
1327 // determine current scroll direction
1328 if (currentScrollY > lastScrollY) {
1329 var y = 'down'
1330 } else if (currentScrollY !== lastScrollY) {
1331 var y = 'up';
1332 }
1333
1334 var pos = -1; // by default we use last element in viewport
1335 if (y === 'down') {
1336 pos = -1;
1337 } else if (y === 'up') {
1338 pos = 0;
1339 }
1340
1341 if (visibleChunks.length > 0) {
1342 $('.nav-chunk').removeClass('selected');
1343 $(visibleChunks.get(pos)).addClass('selected');
1344 }
1345
1346 // update last scroll position to current position
1347 lastScrollY = currentScrollY;
1348
1349 });
1350 $('#diff_nav').html('Diff navigation:')
1351
1216 1352 });
1217 1353 </script>
1218 1354
General Comments 0
You need to be logged in to leave comments. Login now