##// END OF EJS Templates
Add ambiplayer
Bohdan Horbeshko -
r2148:06e50007 ambiplayer
parent child Browse files
Show More
@@ -0,0 +1,125 b''
1 div.ambiplayer {
2 position: relative;
3 }
4
5 div.ambiplayer > * {
6 position: absolute;
7 }
8
9 div.ambiplayer, .ambiplayer-background {
10 width: 200px;
11 height: 131px;
12 }
13
14 .ambiplayer-background {
15 filter: brightness(.6);
16 }
17
18 .ambiplayer-input-playpause {
19 background: #333;
20 width: 50px;
21 height: 50px;
22 border-radius: 25px;
23 top: calc(50% - 25px);
24 left: calc(50% - 25px);
25 border: 1px solid #ffd37d;
26 cursor: pointer;
27 opacity: .75;
28 }
29
30 .ambiplayer-input-playpause:before, .ambiplayer-input-playpause:after {
31 content: "";
32 color: #ffd37d;
33 position: absolute;
34 display: block;
35 transition: border-width .1s ease, width .5s ease, height .5s ease;
36 }
37
38 .ambiplayer-input-playpause.ambiplayer-state-play:before, .ambiplayer-input-playpause.ambiplayer-state-play:after {
39 width: 6px;
40 height: 19px;
41 background-color: #ffd37d;
42 top: 14px;
43 }
44
45 .ambiplayer-input-playpause.ambiplayer-state-play:before {
46 left: 15px;
47 }
48
49 .ambiplayer-input-playpause.ambiplayer-state-play:after {
50 left: 27px;
51 }
52
53 .ambiplayer-input-playpause.ambiplayer-state-pause:before {
54 left: 18px;
55 border-style: solid;
56 border-color: transparent;
57 border-width: 13px 18px;
58 border-left-color: #ffd37d;
59 top: 11px;
60 background: transparent;
61 }
62
63 .ambiplayer-input-playpause.ambiplayer-state-pause:before, .ambiplayer-input-playpause.ambiplayer-state-pause:after {
64 width: 0px;
65 height: 0px;
66 }
67
68 .ambiplayer-input-volume {
69 transform: rotateZ(270deg);
70 width: 80px;
71 left: -15px;
72 top: 50px;
73 }
74
75 .ambiplayer-input-track {
76 width: 100px;
77 left: 50px;
78 bottom: calc(1em - 3px);
79 }
80
81 .ambiplayer-input-volume, .ambiplayer-input-track {
82 background: #333;
83 opacity: .75;
84 cursor: pointer;
85 height: 7px;
86 -webkit-appearance: none;
87 appearance: none;
88 border: 0;
89 }
90
91 .ambiplayer-input-volume::-webkit-progress-bar, .ambiplayer-input-track::-webkit-progress-bar {
92 background: #333;
93 }
94
95 .ambiplayer-input-volume:hover, .ambiplayer-input-track:hover, .ambiplayer-input-playpause:hover {
96 opacity: 1;
97 }
98
99 .ambiplayer-input-volume::-webkit-progress-value, .ambiplayer-input-track::-webkit-progress-value {
100 background-color: #ffd37d;
101 }
102
103 .ambiplayer-input-volume::-moz-progress-bar, .ambiplayer-input-track::-moz-progress-bar {
104 background-color: #ffd37d;
105 }
106
107 .ambiplayer-input-volume::-ms-fill, .ambiplayer-input-track::-ms-fill {
108 background-color: #ffd37d;
109 }
110
111 .ambiplayer-output-currenttime {
112 left: .5em;
113 }
114
115 .ambiplayer-output-duration {
116 right: .5em;
117 }
118
119 .ambiplayer > output {
120 bottom: .5em;
121 background-color: #333;
122 color: #ffd37d;
123 padding: 2px;
124 border-radius: 3px;
125 }
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,145 b''
1 var ambiplayer_init_hook = function() {
2 $('audio.ambiplayer').each(function() {
3 $(this).data('ambiplayer-instance', new Ambiplayer(this));
4 })
5 }
6
7 var ambiplayer_delete_hook = function(node) {
8 $('.ambiplayer audio', node).each(function() {
9 $(this).data('ambiplayer-instance').destroy();
10 });
11 }
12
13 var _AMBIPLAYER_HTML_TEMPLATE =
14 '<div class="ambiplayer">'+
15 ' <div class="ambiplayer-background"></div>'+
16 ' <button class="ambiplayer-input-playpause ambiplayer-state-pause"></button>'+
17 ' <progress class="ambiplayer-input-volume"/>'+
18 ' <output class="ambiplayer-output-currenttime">0:00</output>'+
19 ' <progress class="ambiplayer-input-track"/>'+
20 ' <output class="ambiplayer-output-duration">0:00</output>'+
21 '</div>';
22
23 var _timeformatter = function(seconds) {
24 var minutes = (seconds / 60)|0;
25 seconds = (seconds % 60)|0;
26 return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
27 }
28
29 var ambiplayer_ctx;
30
31 var Ambiplayer = function(audioNode) {
32 var mediaNode = new Audio(audioNode.src);
33 this.$audioNode = $(audioNode);
34 this._state = 'pause';
35
36 this.destroy = function() {
37 mediaNode.pause();
38 }
39
40 this.$playerNode = $(_AMBIPLAYER_HTML_TEMPLATE);
41
42 audioNode.parentNode.replaceChild(this.$playerNode.get(0), audioNode);
43 this.$playerNode.get(0).appendChild(audioNode);
44 this.$audioNode.removeClass('ambiplayer').hide();
45
46 this.$background = $('.ambiplayer-background', this.$playerNode);
47 this.$playPauseButton = $('.ambiplayer-input-playpause', this.$playerNode);
48 this.$volumeControl = $('.ambiplayer-input-volume', this.$playerNode);
49 this.$trackControl = $('.ambiplayer-input-track', this.$playerNode);
50 this.$outputCurrentTime = $('.ambiplayer-output-currenttime', this.$playerNode);
51 this.$outputDuration = $('.ambiplayer-output-duration', this.$playerNode);
52
53 this.$background.css('background-image', 'url('+this.$audioNode.data('bgimage')+')');
54
55 var playpause_state_hook = function() {
56 if (this._state == 'pause') {
57 this.$playPauseButton.removeClass('ambiplayer-state-play').addClass('ambiplayer-state-pause');
58 } else {
59 this.$playPauseButton.removeClass('ambiplayer-state-pause').addClass('ambiplayer-state-play');
60 }
61 }.bind(this);
62 var audionode_state_hook = function() {
63 if (this._state == 'pause') {
64 mediaNode.pause();
65 } else {
66 mediaNode.play();
67 }
68 }.bind(this);
69
70 this.$volumeControl.attr({
71 min: 0,
72 max: 1,
73 step: 0.01
74 }).val(mediaNode.volume);
75
76 this.$trackControl.attr({
77 min: 0,
78 max: 1,
79 step: 0.001
80 }).val(0);
81
82 this.$playPauseButton.on('click', function(e) {
83 this._state = this._state == 'play' ? 'pause' : 'play';
84 playpause_state_hook();
85 audionode_state_hook();
86 return false;
87 }.bind(this));
88 this.$volumeControl.on('click', function(e) {
89 var $this = $(this);
90 mediaNode.volume = 1 - (e.pageY - $this.offset().top) / $this.width();
91 $this.val(mediaNode.volume);
92 });
93 this.$trackControl.on('click', function(e) {
94 var $this = $(this);
95 mediaNode.currentTime = mediaNode.duration * (e.pageX - $this.offset().left) / $this.width();
96 });
97
98 mediaNode.addEventListener('canplay', function() {
99 this.$outputDuration.val(_timeformatter(mediaNode.duration));
100 }.bind(this));
101 mediaNode.addEventListener('timeupdate', function() {
102 this.$trackControl.val(mediaNode.currentTime/mediaNode.duration);
103 this.$outputCurrentTime.val(_timeformatter(mediaNode.currentTime));
104 }.bind(this));
105 mediaNode.addEventListener('ended', function(e) {
106 this._state = 'pause';
107 playpause_state_hook();
108 audionode_state_hook();
109 }.bind(this));
110
111 if (!ambiplayer_ctx) {
112 ambiplayer_ctx = new AudioContext();
113 }
114 var source = ambiplayer_ctx.createMediaElementSource(mediaNode);
115 var analyser = ambiplayer_ctx.createAnalyser();
116
117 source.connect(analyser);
118 analyser.connect(ambiplayer_ctx.destination);
119
120 var frequencyBuffer = new Uint8Array(analyser.frequencyBinCount);
121 var prevAver = 0;
122
123 var ambilight_update = function() {
124 requestAnimationFrame(ambilight_update);
125 analyser.getByteFrequencyData(frequencyBuffer);
126
127 var aver = 0;
128 for (var i=0; i<analyser.frequencyBinCount; i++) {
129 aver += frequencyBuffer[i];
130 }
131 aver /= analyser.frequencyBinCount;
132
133 if (Math.abs(prevAver-aver) >=5) {
134 this.$playPauseButton.css('box-shadow', '#ffd37d 0 0 30px ' + (aver/5 |0) + 'px');
135 prevAver = aver;
136 }
137 }.bind(this)
138 ambilight_update();
139 }
140
141 $(function() {
142 if (AudioContext) {
143 ambiplayer_init_hook();
144 }
145 })
@@ -1,238 +1,255 b''
1 import re
1 import re
2
2
3 from PIL import Image
3 from PIL import Image
4
4
5 from django.contrib.staticfiles import finders
5 from django.contrib.staticfiles import finders
6 from django.contrib.staticfiles.templatetags.staticfiles import static
6 from django.contrib.staticfiles.templatetags.staticfiles import static
7 from django.core.files.images import get_image_dimensions
7 from django.core.files.images import get_image_dimensions
8 from django.template.defaultfilters import filesizeformat
8 from django.template.defaultfilters import filesizeformat
9 from django.core.urlresolvers import reverse
9 from django.core.urlresolvers import reverse
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11
11
12 from boards.utils import get_domain, cached_result
12 from boards.utils import get_domain, cached_result
13 from boards import settings
13 from boards import settings
14
14
15
15
16 FILE_STUB_IMAGE = 'images/file.png'
16 FILE_STUB_IMAGE = 'images/file.png'
17 FILE_STUB_URL = 'url'
17 FILE_STUB_URL = 'url'
18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
19
19
20
20
21 FILE_TYPES_VIDEO = (
21 FILE_TYPES_VIDEO = (
22 'webm',
22 'webm',
23 'mp4',
23 'mp4',
24 'mpeg',
24 'mpeg',
25 'ogv',
25 'ogv',
26 )
26 )
27 FILE_TYPE_SVG = 'svg'
27 FILE_TYPE_SVG = 'svg'
28 FILE_TYPES_AUDIO = (
28 FILE_TYPES_AUDIO = (
29 'ogg',
29 'ogg',
30 'mp3',
30 'mp3',
31 'opus',
31 'opus',
32 'flac',
32 )
33 )
33 FILE_TYPES_IMAGE = (
34 FILE_TYPES_IMAGE = (
34 'jpeg',
35 'jpeg',
35 'jpg',
36 'jpg',
36 'png',
37 'png',
37 'bmp',
38 'bmp',
38 'gif',
39 'gif',
39 )
40 )
40
41
41 PLAIN_FILE_FORMATS = {
42 PLAIN_FILE_FORMATS = {
42 'zip': 'archive',
43 'zip': 'archive',
43 'tar': 'archive',
44 'tar': 'archive',
44 'gz': 'archive',
45 'gz': 'archive',
45 'mid' : 'midi',
46 'mid' : 'midi',
46 }
47 }
47
48
48 URL_PROTOCOLS = {
49 URL_PROTOCOLS = {
49 'magnet': 'magnet',
50 'magnet': 'magnet',
50 }
51 }
51
52
52 CSS_CLASS_IMAGE = 'image'
53 CSS_CLASS_IMAGE = 'image'
53 CSS_CLASS_THUMB = 'thumb'
54 CSS_CLASS_THUMB = 'thumb'
54
55
55 ABSTRACT_VIEW = '<div class="image">'\
56 ABSTRACT_VIEW = '<div class="image">'\
56 '{}'\
57 '{}'\
57 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
58 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
58 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}">πŸ” </a></div>'\
59 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}">πŸ” </a></div>'\
59 '</div>'
60 '</div>'
60 URL_VIEW = '<div class="image">' \
61 URL_VIEW = '<div class="image">' \
61 '{}' \
62 '{}' \
62 '<div class="image-metadata">{}</div>' \
63 '<div class="image-metadata">{}</div>' \
63 '</div>'
64 '</div>'
64 ABSTRACT_FORMAT_VIEW = '<a href="{}">'\
65 ABSTRACT_FORMAT_VIEW = '<a href="{}">'\
65 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
66 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
66 '</a>'
67 '</a>'
67 VIDEO_FORMAT_VIEW = '<video width="200" height="150" controls src="{}"></video>'
68 VIDEO_FORMAT_VIEW = '<video width="200" height="150" controls src="{}"></video>'
68 AUDIO_FORMAT_VIEW = '<audio controls src="{}"></audio>'
69 AUDIO_FORMAT_VIEW = '<audio controls src="{}" class="ambiplayer" data-bgimage="{}"></audio>'
69 IMAGE_FORMAT_VIEW = '<a class="{}" href="{full}">' \
70 IMAGE_FORMAT_VIEW = '<a class="{}" href="{full}">' \
70 '<img class="post-image-preview"' \
71 '<img class="post-image-preview"' \
71 ' src="{}"' \
72 ' src="{}"' \
72 ' alt="{}"' \
73 ' alt="{}"' \
73 ' width="{}"' \
74 ' width="{}"' \
74 ' height="{}"' \
75 ' height="{}"' \
75 ' data-width="{}"' \
76 ' data-width="{}"' \
76 ' data-height="{}" />' \
77 ' data-height="{}" />' \
77 '</a>'
78 '</a>'
78 SVG_FORMAT_VIEW = '<a class="thumb" href="{}">'\
79 SVG_FORMAT_VIEW = '<a class="thumb" href="{}">'\
79 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
80 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
80 '</a>'
81 '</a>'
81 URL_FORMAT_VIEW = '<a href="{}">' \
82 URL_FORMAT_VIEW = '<a href="{}">' \
82 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
83 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
83 '</a>'
84 '</a>'
84
85
85
86
86 def get_viewers():
87 def get_viewers():
87 return AbstractViewer.__subclasses__()
88 return AbstractViewer.__subclasses__()
88
89
89
90
90 def get_static_dimensions(filename):
91 def get_static_dimensions(filename):
91 file_path = finders.find(filename)
92 file_path = finders.find(filename)
92 return get_image_dimensions(file_path)
93 return get_image_dimensions(file_path)
93
94
94
95
95 # TODO Move this to utils
96 # TODO Move this to utils
96 def file_exists(filename):
97 def file_exists(filename):
97 return finders.find(filename) is not None
98 return finders.find(filename) is not None
98
99
99
100
100 class AbstractViewer:
101 class AbstractViewer:
101 def __init__(self, file, file_type, hash, url):
102 def __init__(self, file, file_type, hash, url):
102 self.file = file
103 self.file = file
103 self.file_type = file_type
104 self.file_type = file_type
104 self.hash = hash
105 self.hash = hash
105 self.url = url
106 self.url = url
106
107
107 @staticmethod
108 @staticmethod
108 def supports(file_type):
109 def supports(file_type):
109 return True
110 return True
110
111
111 def get_view(self):
112 def get_view(self):
112 search_host = settings.get('External', 'ImageSearchHost')
113 search_host = settings.get('External', 'ImageSearchHost')
113 if search_host:
114 if search_host:
114 if search_host.endswith('/'):
115 if search_host.endswith('/'):
115 search_host = search_host[:-1]
116 search_host = search_host[:-1]
116 search_url = search_host + self.file.url
117 search_url = search_host + self.file.url
117 else:
118 else:
118 search_url = ''
119 search_url = ''
119
120
120 return ABSTRACT_VIEW.format(self.get_format_view(), self.file.url,
121 return ABSTRACT_VIEW.format(self.get_format_view(), self.file.url,
121 self.file_type, filesizeformat(self.file.size),
122 self.file_type, filesizeformat(self.file.size),
122 self.file_type, search_url, self.file.name)
123 self.file_type, search_url, self.file.name)
123
124
124 def get_format_view(self):
125 def get_format_image(self):
125 image_name = PLAIN_FILE_FORMATS.get(self.file_type, self.file_type)
126 image_name = PLAIN_FILE_FORMATS.get(self.file_type, self.file_type)
126 file_name = FILE_FILEFORMAT.format(image_name)
127 file_name = FILE_FILEFORMAT.format(image_name)
127
128
128 if file_exists(file_name):
129 if file_exists(file_name):
129 image = file_name
130 image = file_name
130 else:
131 else:
131 image = FILE_STUB_IMAGE
132 image = FILE_STUB_IMAGE
132
133
134 return image
135
136 def get_format_view(self):
137 image = self.get_format_image()
133 w, h = get_static_dimensions(image)
138 w, h = get_static_dimensions(image)
134
139
135 return ABSTRACT_FORMAT_VIEW.format(self.file.url, static(image), w, h)
140 return ABSTRACT_FORMAT_VIEW.format(self.file.url, static(image), w, h)
136
141
137
142
138 class VideoViewer(AbstractViewer):
143 class VideoViewer(AbstractViewer):
139 @staticmethod
144 @staticmethod
140 def supports(file_type):
145 def supports(file_type):
141 return file_type in FILE_TYPES_VIDEO
146 return file_type in FILE_TYPES_VIDEO
142
147
143 def get_format_view(self):
148 def get_format_view(self):
144 return VIDEO_FORMAT_VIEW.format(self.file.url)
149 return VIDEO_FORMAT_VIEW.format(self.file.url)
145
150
146
151
147 class AudioViewer(AbstractViewer):
152 class AudioViewer(AbstractViewer):
148 @staticmethod
153 @staticmethod
149 def supports(file_type):
154 def supports(file_type):
150 return file_type in FILE_TYPES_AUDIO
155 return file_type in FILE_TYPES_AUDIO
151
156
157 def get_format_image(self):
158 file_name = FILE_FILEFORMAT.format(self.file_type)
159
160 if file_exists(file_name):
161 image = file_name
162 else:
163 image = FILE_STUB_IMAGE
164
165 return image
166
152 def get_format_view(self):
167 def get_format_view(self):
153 return AUDIO_FORMAT_VIEW.format(self.file.url)
168 image = self.get_format_image()
169
170 return AUDIO_FORMAT_VIEW.format(self.file.url, static(image))
154
171
155
172
156 class SvgViewer(AbstractViewer):
173 class SvgViewer(AbstractViewer):
157 @staticmethod
174 @staticmethod
158 def supports(file_type):
175 def supports(file_type):
159 return file_type == FILE_TYPE_SVG
176 return file_type == FILE_TYPE_SVG
160
177
161 def get_format_view(self):
178 def get_format_view(self):
162 return SVG_FORMAT_VIEW.format(self.file.url, self.file.url)
179 return SVG_FORMAT_VIEW.format(self.file.url, self.file.url)
163
180
164
181
165 class ImageViewer(AbstractViewer):
182 class ImageViewer(AbstractViewer):
166 @staticmethod
183 @staticmethod
167 def supports(file_type):
184 def supports(file_type):
168 return file_type in FILE_TYPES_IMAGE
185 return file_type in FILE_TYPES_IMAGE
169
186
170 def get_format_view(self):
187 def get_format_view(self):
171 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
188 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
172 filesizeformat(self.file.size))
189 filesizeformat(self.file.size))
173
190
174 try:
191 try:
175 width, height = get_image_dimensions(self.file.path)
192 width, height = get_image_dimensions(self.file.path)
176 except Exception:
193 except Exception:
177 # If the image is a decompression bomb, treat it as just a regular
194 # If the image is a decompression bomb, treat it as just a regular
178 # file
195 # file
179 return super().get_format_view()
196 return super().get_format_view()
180
197
181 preview_path = self.file.path.replace('.', '.200x150.')
198 preview_path = self.file.path.replace('.', '.200x150.')
182 pre_width, pre_height = get_image_dimensions(preview_path)
199 pre_width, pre_height = get_image_dimensions(preview_path)
183
200
184 split = self.file.url.rsplit('.', 1)
201 split = self.file.url.rsplit('.', 1)
185 w, h = 200, 150
202 w, h = 200, 150
186 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
203 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
187
204
188 return IMAGE_FORMAT_VIEW.format(CSS_CLASS_THUMB,
205 return IMAGE_FORMAT_VIEW.format(CSS_CLASS_THUMB,
189 thumb_url,
206 thumb_url,
190 self.hash,
207 self.hash,
191 str(pre_width),
208 str(pre_width),
192 str(pre_height), str(width), str(height),
209 str(pre_height), str(width), str(height),
193 full=self.file.url, image_meta=metadata)
210 full=self.file.url, image_meta=metadata)
194
211
195
212
196 class UrlViewer(AbstractViewer):
213 class UrlViewer(AbstractViewer):
197 @staticmethod
214 @staticmethod
198 def supports(file_type):
215 def supports(file_type):
199 return file_type is None
216 return file_type is None
200
217
201 def get_view(self):
218 def get_view(self):
202 return URL_VIEW.format(self.get_format_view(), get_domain(self.url))
219 return URL_VIEW.format(self.get_format_view(), get_domain(self.url))
203
220
204 def get_format_view(self):
221 def get_format_view(self):
205 protocol = self.url.split(':')[0]
222 protocol = self.url.split(':')[0]
206
223
207 domain = get_domain(self.url)
224 domain = get_domain(self.url)
208
225
209 if protocol in URL_PROTOCOLS:
226 if protocol in URL_PROTOCOLS:
210 url_image_name = URL_PROTOCOLS.get(protocol)
227 url_image_name = URL_PROTOCOLS.get(protocol)
211 elif domain:
228 elif domain:
212 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
229 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
213 else:
230 else:
214 url_image_name = FILE_STUB_URL
231 url_image_name = FILE_STUB_URL
215
232
216 image_path = 'images/{}.png'.format(url_image_name)
233 image_path = 'images/{}.png'.format(url_image_name)
217 image = static(image_path)
234 image = static(image_path)
218 w, h = get_static_dimensions(image_path)
235 w, h = get_static_dimensions(image_path)
219
236
220 return URL_FORMAT_VIEW.format(self.url, image, w, h)
237 return URL_FORMAT_VIEW.format(self.url, image, w, h)
221
238
222 @cached_result()
239 @cached_result()
223 def _find_image_for_domains(self, domain):
240 def _find_image_for_domains(self, domain):
224 """
241 """
225 Searches for the domain image for every domain level except top.
242 Searches for the domain image for every domain level except top.
226 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
243 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
227 example.co.uk, then co.uk
244 example.co.uk, then co.uk
228 """
245 """
229 levels = domain.split('.')
246 levels = domain.split('.')
230 while len(levels) > 1:
247 while len(levels) > 1:
231 domain = '.'.join(levels)
248 domain = '.'.join(levels)
232
249
233 filename = 'images/domains/{}.png'.format(domain)
250 filename = 'images/domains/{}.png'.format(domain)
234 if file_exists(filename):
251 if file_exists(filename):
235 return 'domains/' + domain
252 return 'domains/' + domain
236 else:
253 else:
237 del levels[0]
254 del levels[0]
238
255
@@ -1,134 +1,140 b''
1 var LOADING_MSG = "<div class=\"post\">" + gettext('Loading...') + "</div>";
1 var LOADING_MSG = "<div class=\"post\">" + gettext('Loading...') + "</div>";
2
2
3 var CLS_PREVIEW = 'post_preview';
3 var CLS_PREVIEW = 'post_preview';
4
4
5 function $X(path, root) {
5 function $X(path, root) {
6 return document.evaluate(path, root || document, null, 6, null);
6 return document.evaluate(path, root || document, null, 6, null);
7 }
7 }
8 function $x(path, root) {
8 function $x(path, root) {
9 return document.evaluate(path, root || document, null, 8, null).singleNodeValue;
9 return document.evaluate(path, root || document, null, 8, null).singleNodeValue;
10 }
10 }
11
11
12 function $del(el) {
12 function $del(el) {
13 if(el) el.parentNode.removeChild(el);
13 if (el) {
14 ambiplayer_delete_hook && ambiplayer_delete_hook(el);
15 el.parentNode.removeChild(el);
16 }
14 }
17 }
15
18
16 function $each(list, fn) {
19 function $each(list, fn) {
17 if(!list) return;
20 if(!list) return;
18 var i = list.snapshotLength;
21 var i = list.snapshotLength;
19 if(i > 0) while(i--) fn(list.snapshotItem(i), i);
22 if(i > 0) while(i--) fn(list.snapshotItem(i), i);
20 }
23 }
21
24
22 function mkPreview(cln, html) {
25 function mkPreview(cln, html) {
23 cln.innerHTML = html;
26 cln.innerHTML = html;
24
27
28 setTimeout(function() {
29 ambiplayer_init_hook && ambiplayer_init_hook();
30 }, 0);
25 addScriptsToPost($(cln));
31 addScriptsToPost($(cln));
26 }
32 }
27
33
28 function isElementInViewport (el) {
34 function isElementInViewport (el) {
29 //special bonus for those using jQuery
35 //special bonus for those using jQuery
30 if (typeof jQuery === "function" && el instanceof jQuery) {
36 if (typeof jQuery === "function" && el instanceof jQuery) {
31 el = el[0];
37 el = el[0];
32 }
38 }
33
39
34 var rect = el.getBoundingClientRect();
40 var rect = el.getBoundingClientRect();
35
41
36 return (
42 return (
37 rect.top >= 0 &&
43 rect.top >= 0 &&
38 rect.left >= 0 &&
44 rect.left >= 0 &&
39 rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
45 rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
40 rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
46 rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
41 );
47 );
42 }
48 }
43
49
44 function addRefLinkPreview(node) {
50 function addRefLinkPreview(node) {
45 $each($X('.//a[starts-with(text(),">>")]', node || document), function(link) {
51 $each($X('.//a[starts-with(text(),">>")]', node || document), function(link) {
46 link.addEventListener('mouseover', showPostPreview, false);
52 link.addEventListener('mouseover', showPostPreview, false);
47 link.addEventListener('mouseout', delPostPreview, false);
53 link.addEventListener('mouseout', delPostPreview, false);
48 });
54 });
49 }
55 }
50
56
51 function showPostPreview(e) {
57 function showPostPreview(e) {
52 var doc = document;
58 var doc = document;
53
59
54 var reflink = $(this);
60 var reflink = $(this);
55 var pNum = reflink.text().match(/\d+/);
61 var pNum = reflink.text().match(/\d+/);
56
62
57 if (pNum == null || pNum.length == 0) {
63 if (pNum == null || pNum.length == 0) {
58 return;
64 return;
59 }
65 }
60
66
61 var post = $('#' + pNum);
67 var post = $('#' + pNum);
62 if (post.length > 0 && isElementInViewport(post)) {
68 if (post.length > 0 && isElementInViewport(post)) {
63 // If post is on the same page and visible, just highlight it
69 // If post is on the same page and visible, just highlight it
64 post.addClass('highlight');
70 post.addClass('highlight');
65 } else {
71 } else {
66 var x = reflink.offset().left;
72 var x = reflink.offset().left;
67 var y = reflink.offset().top;
73 var y = reflink.offset().top;
68
74
69 var cln = doc.createElement('div');
75 var cln = doc.createElement('div');
70 cln.id = 'pstprev_' + pNum;
76 cln.id = 'pstprev_' + pNum;
71 cln.className = CLS_PREVIEW;
77 cln.className = CLS_PREVIEW;
72
78
73 cln.style.cssText = 'left:' + x + 'px; top:' + y + 'px';
79 cln.style.cssText = 'left:' + x + 'px; top:' + y + 'px';
74
80
75 cln.addEventListener('mouseout', delPostPreview, false);
81 cln.addEventListener('mouseout', delPostPreview, false);
76
82
77 cln.innerHTML = LOADING_MSG;
83 cln.innerHTML = LOADING_MSG;
78
84
79 if (post.length > 0) {
85 if (post.length > 0) {
80 // If post is on the same page but not visible, generate preview from it
86 // If post is on the same page but not visible, generate preview from it
81 var postClone = post.clone();
87 var postClone = post.clone(true);
82 postClone.removeAttr('style');
88 postClone.removeAttr('style');
83 var postdata = postClone.wrap("<div/>").parent().html();
89 var postdata = postClone.wrap("<div/>").parent().html();
84
90
85 mkPreview(cln, postdata);
91 mkPreview(cln, postdata);
86 } else {
92 } else {
87 // If post is from other page, load it
93 // If post is from other page, load it
88 $.ajax({
94 $.ajax({
89 url: '/api/post/' + pNum + '/?truncated'
95 url: '/api/post/' + pNum + '/?truncated'
90 })
96 })
91 .success(function(data) {
97 .success(function(data) {
92 var postdata = $(data).wrap("<div/>").parent().html();
98 var postdata = $(data).wrap("<div/>").parent().html();
93
99
94 //make preview
100 //make preview
95 mkPreview(cln, postdata);
101 mkPreview(cln, postdata);
96 })
102 })
97 .error(function() {
103 .error(function() {
98 cln.innerHTML = "<div class=\"post\">"
104 cln.innerHTML = "<div class=\"post\">"
99 + gettext('Post not found') + "</div>";
105 + gettext('Post not found') + "</div>";
100 });
106 });
101 }
107 }
102
108
103 $del(doc.getElementById(cln.id));
109 $del(doc.getElementById(cln.id));
104
110
105 //add preview
111 //add preview
106 $(cln).fadeIn(200);
112 $(cln).fadeIn(200);
107 $('body').append(cln);
113 $('body').append(cln);
108 }
114 }
109 }
115 }
110
116
111 function delPostPreview(e) {
117 function delPostPreview(e) {
112 var el = $x('ancestor-or-self::*[starts-with(@id,"pstprev")]', e.relatedTarget);
118 var el = $x('ancestor-or-self::*[starts-with(@id,"pstprev")]', e.relatedTarget);
113 if(!el) {
119 if(!el) {
114 $each($X('.//div[starts-with(@id,"pstprev")]'), function(clone) {
120 $each($X('.//div[starts-with(@id,"pstprev")]'), function(clone) {
115 $del(clone)
121 $del(clone)
116 });
122 });
117 } else {
123 } else {
118 while (el.nextSibling) {
124 while (el.nextSibling) {
119 if (el.nextSibling.className == CLS_PREVIEW) {
125 if (el.nextSibling.className == CLS_PREVIEW) {
120 $del(el.nextSibling);
126 $del(el.nextSibling);
121 } else {
127 } else {
122 break;
128 break;
123 }
129 }
124 }
130 }
125 }
131 }
126
132
127 $('.highlight').removeClass('highlight');
133 $('.highlight').removeClass('highlight');
128 }
134 }
129
135
130 function addPreview() {
136 function addPreview() {
131 $('.post').find('a').each(function() {
137 $('.post').find('a').each(function() {
132 showPostPreview($(this));
138 showPostPreview($(this));
133 });
139 });
134 }
140 }
@@ -1,155 +1,156 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var CLOSE_BUTTON = '#form-close-button';
26 var CLOSE_BUTTON = '#form-close-button';
27 var REPLY_TO_MSG = '.reply-to-message';
27 var REPLY_TO_MSG = '.reply-to-message';
28 var REPLY_TO_MSG_ID = '#reply-to-message-id';
28 var REPLY_TO_MSG_ID = '#reply-to-message-id';
29
29
30 var $html = $("html, body");
30 var $html = $("html, body");
31
31
32 function moveCaretToEnd(el) {
32 function moveCaretToEnd(el) {
33 var newPos = el.val().length;
33 var newPos = el.val().length;
34 el[0].setSelectionRange(newPos, newPos);
34 el[0].setSelectionRange(newPos, newPos);
35 }
35 }
36
36
37 function getForm() {
37 function getForm() {
38 return $('.post-form-w');
38 return $('.post-form-w');
39 }
39 }
40
40
41 function resetFormPosition() {
41 function resetFormPosition() {
42 var form = getForm();
42 var form = getForm();
43 form.insertAfter($('.thread'));
43 form.insertAfter($('.thread'));
44
44
45 $(CLOSE_BUTTON).hide();
45 $(CLOSE_BUTTON).hide();
46 $(REPLY_TO_MSG).hide();
46 $(REPLY_TO_MSG).hide();
47 }
47 }
48
48
49 function showFormAfter(blockToInsertAfter) {
49 function showFormAfter(blockToInsertAfter) {
50 var form = getForm();
50 var form = getForm();
51 form.insertAfter(blockToInsertAfter);
51 form.insertAfter(blockToInsertAfter);
52
52
53 $(CLOSE_BUTTON).show();
53 $(CLOSE_BUTTON).show();
54 form.show();
54 form.show();
55 $(REPLY_TO_MSG_ID).text(blockToInsertAfter.attr('id'));
55 $(REPLY_TO_MSG_ID).text(blockToInsertAfter.attr('id'));
56 $(REPLY_TO_MSG).show();
56 $(REPLY_TO_MSG).show();
57 }
57 }
58
58
59 function addQuickReply(postId) {
59 function addQuickReply(postId) {
60 var blockToInsert = null;
60 var blockToInsert = null;
61 var textAreaJq = getPostTextarea();
61 var textAreaJq = getPostTextarea();
62 var postLinkRaw = '[post]' + postId + '[/post]'
62 var postLinkRaw = '[post]' + postId + '[/post]'
63 var textToAdd = '';
63 var textToAdd = '';
64
64
65 if (postId != null) {
65 if (postId != null) {
66 var post = $('#' + postId);
66 var post = $('#' + postId);
67
67
68 // If this is not OP, add reflink to the post. If there already is
68 // If this is not OP, add reflink to the post. If there already is
69 // the same reflink, don't add it again.
69 // the same reflink, don't add it again.
70 var postText = textAreaJq.val();
70 var postText = textAreaJq.val();
71 if (!post.is(':first-child') && postText.indexOf(postLinkRaw) < 0) {
71 if (!post.is(':first-child') && postText.indexOf(postLinkRaw) < 0) {
72 // Insert line break if none is present.
72 // Insert line break if none is present.
73 if (postText.length > 0 && !postText.endsWith('\n') && !postText.endsWith('\r')) {
73 if (postText.length > 0 && !postText.endsWith('\n') && !postText.endsWith('\r')) {
74 textToAdd += '\n';
74 textToAdd += '\n';
75 }
75 }
76 textToAdd += postLinkRaw + '\n';
76 textToAdd += postLinkRaw + '\n';
77 }
77 }
78
78
79 textAreaJq.val(textAreaJq.val()+ textToAdd);
79 textAreaJq.val(textAreaJq.val()+ textToAdd);
80 blockToInsert = post;
80 blockToInsert = post;
81 } else {
81 } else {
82 blockToInsert = $('.thread');
82 blockToInsert = $('.thread');
83 }
83 }
84 showFormAfter(blockToInsert);
84 showFormAfter(blockToInsert);
85
85
86 textAreaJq.focus();
86 textAreaJq.focus();
87
87
88 moveCaretToEnd(textAreaJq);
88 moveCaretToEnd(textAreaJq);
89 }
89 }
90
90
91 function addQuickQuote() {
91 function addQuickQuote() {
92 var textAreaJq = getPostTextarea();
92 var textAreaJq = getPostTextarea();
93
93
94 var quoteButton = $("#quote-button");
94 var quoteButton = $("#quote-button");
95 var postId = quoteButton.attr('data-post-id');
95 var postId = quoteButton.attr('data-post-id');
96 if (postId != null) {
96 if (postId != null) {
97 addQuickReply(postId);
97 addQuickReply(postId);
98 }
98 }
99
99
100 var textToAdd = '';
100 var textToAdd = '';
101 var selection = window.getSelection().toString();
101 var selection = window.getSelection().toString();
102 if (selection.length == 0) {
102 if (selection.length == 0) {
103 selection = quoteButton.attr('data-text');
103 selection = quoteButton.attr('data-text');
104 }
104 }
105 if (selection.length > 0) {
105 if (selection.length > 0) {
106 textToAdd += '[quote]' + selection + '[/quote]\n';
106 textToAdd += '[quote]' + selection + '[/quote]\n';
107 }
107 }
108
108
109 textAreaJq.val(textAreaJq.val() + textToAdd);
109 textAreaJq.val(textAreaJq.val() + textToAdd);
110
110
111 textAreaJq.focus();
111 textAreaJq.focus();
112
112
113 moveCaretToEnd(textAreaJq);
113 moveCaretToEnd(textAreaJq);
114 }
114 }
115
115
116 function scrollToBottom() {
116 function scrollToBottom() {
117 $html.animate({scrollTop: $html.height()}, "fast");
117 $html.animate({scrollTop: $html.height()}, "fast");
118 }
118 }
119
119
120 function showQuoteButton() {
120 function showQuoteButton() {
121 var selection = window.getSelection().getRangeAt(0).getBoundingClientRect();
121 var windowSelection = window.getSelection();
122 var selection = windowSelection.rangeCount && windowSelection.getRangeAt(0).getBoundingClientRect();
122 var quoteButton = $("#quote-button");
123 var quoteButton = $("#quote-button");
123 if (selection.width > 0) {
124 if (selection.width > 0) {
124 // quoteButton.offset({ top: selection.top - selection.height, left: selection.left });
125 // quoteButton.offset({ top: selection.top - selection.height, left: selection.left });
125 quoteButton.css({top: selection.top + $(window).scrollTop() - 30, left: selection.left});
126 quoteButton.css({top: selection.top + $(window).scrollTop() - 30, left: selection.left});
126 quoteButton.show();
127 quoteButton.show();
127
128
128 var text = window.getSelection().toString();
129 var text = window.getSelection().toString();
129 quoteButton.attr('data-text', text);
130 quoteButton.attr('data-text', text);
130
131
131 var rect = window.getSelection().getRangeAt(0).getBoundingClientRect();
132 var rect = window.getSelection().getRangeAt(0).getBoundingClientRect();
132 var element = $(document.elementFromPoint(rect.x, rect.y));
133 var element = $(document.elementFromPoint(rect.x, rect.y));
133 var postId = null;
134 var postId = null;
134 if (element.hasClass('post')) {
135 if (element.hasClass('post')) {
135 postId = element.attr('id');
136 postId = element.attr('id');
136 } else {
137 } else {
137 var postParent = element.parents('.post');
138 var postParent = element.parents('.post');
138 if (postParent.length > 0) {
139 if (postParent.length > 0) {
139 postId = postParent.attr('id');
140 postId = postParent.attr('id');
140 }
141 }
141 }
142 }
142 quoteButton.attr('data-post-id', postId);
143 quoteButton.attr('data-post-id', postId);
143 } else {
144 } else {
144 quoteButton.hide();
145 quoteButton.hide();
145 }
146 }
146 }
147 }
147
148
148 $(document).ready(function() {
149 $(document).ready(function() {
149 $('body').on('mouseup', function() {
150 $('body').on('mouseup', function() {
150 showQuoteButton();
151 showQuoteButton();
151 });
152 });
152 $("#quote-button").click(function() {
153 $("#quote-button").click(function() {
153 addQuickQuote();
154 addQuickQuote();
154 })
155 })
155 });
156 });
@@ -1,399 +1,403 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013-2014 neko259
6 Copyright (C) 2013-2014 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var CLASS_POST = '.post';
26 var CLASS_POST = '.post';
27
27
28 var POST_ADDED = 0;
28 var POST_ADDED = 0;
29 var POST_UPDATED = 1;
29 var POST_UPDATED = 1;
30
30
31 // TODO These need to be syncronized with board settings.
31 // TODO These need to be syncronized with board settings.
32 var JS_AUTOUPDATE_PERIOD = 20000;
32 var JS_AUTOUPDATE_PERIOD = 20000;
33 // TODO This needs to be the same for attachment download time limit.
33 // TODO This needs to be the same for attachment download time limit.
34 var POST_AJAX_TIMEOUT = 30000;
34 var POST_AJAX_TIMEOUT = 30000;
35 var BLINK_SPEED = 500;
35 var BLINK_SPEED = 500;
36
36
37 var ALLOWED_FOR_PARTIAL_UPDATE = [
37 var ALLOWED_FOR_PARTIAL_UPDATE = [
38 'refmap',
38 'refmap',
39 'post-info'
39 'post-info'
40 ];
40 ];
41
41
42 var ATTR_CLASS = 'class';
42 var ATTR_CLASS = 'class';
43 var ATTR_UID = 'data-uid';
43 var ATTR_UID = 'data-uid';
44
44
45 var unreadPosts = 0;
45 var unreadPosts = 0;
46 var documentOriginalTitle = '';
46 var documentOriginalTitle = '';
47
47
48 // Thread ID does not change, can be stored one time
48 // Thread ID does not change, can be stored one time
49 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
49 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
50 var blinkColor = $('<div class="post-blink"></div>').css('background-color');
50 var blinkColor = $('<div class="post-blink"></div>').css('background-color');
51
51
52 /**
52 /**
53 * Get diff of the posts from the current thread timestamp.
53 * Get diff of the posts from the current thread timestamp.
54 * This is required if the browser was closed and some post updates were
54 * This is required if the browser was closed and some post updates were
55 * missed.
55 * missed.
56 */
56 */
57 function getThreadDiff() {
57 function getThreadDiff() {
58 var all_posts = $('.post');
58 var all_posts = $('.post');
59
59
60 var uids = '';
60 var uids = '';
61 var posts = all_posts;
61 var posts = all_posts;
62 for (var i = 0; i < posts.length; i++) {
62 for (var i = 0; i < posts.length; i++) {
63 uids += posts[i].getAttribute('data-uid') + ' ';
63 uids += posts[i].getAttribute('data-uid') + ' ';
64 }
64 }
65
65
66 var data = {
66 var data = {
67 uids: uids,
67 uids: uids,
68 thread: threadId
68 thread: threadId
69 };
69 };
70
70
71 var diffUrl = '/api/diff_thread/';
71 var diffUrl = '/api/diff_thread/';
72
72
73 $.post(diffUrl,
73 $.post(diffUrl,
74 data,
74 data,
75 function(data) {
75 function(data) {
76 var updatedPosts = data.updated;
76 var updatedPosts = data.updated;
77 var addedPostCount = 0;
77 var addedPostCount = 0;
78
78
79 for (var i = 0; i < updatedPosts.length; i++) {
79 for (var i = 0; i < updatedPosts.length; i++) {
80 var postText = updatedPosts[i];
80 var postText = updatedPosts[i];
81 var post = $(postText);
81 var post = $(postText);
82
82
83 if (updatePost(post) == POST_ADDED) {
83 if (updatePost(post) == POST_ADDED) {
84 addedPostCount++;
84 addedPostCount++;
85 }
85 }
86 }
86 }
87
87
88 var hasMetaUpdates = updatedPosts.length > 0;
88 var hasMetaUpdates = updatedPosts.length > 0;
89 if (hasMetaUpdates) {
89 if (hasMetaUpdates) {
90 updateMetadataPanel();
90 updateMetadataPanel();
91 }
91 }
92
92
93 if (addedPostCount > 0) {
93 if (addedPostCount > 0) {
94 updateBumplimitProgress(addedPostCount);
94 updateBumplimitProgress(addedPostCount);
95 }
95 }
96
96
97 if (updatedPosts.length > 0) {
97 if (updatedPosts.length > 0) {
98 showNewPostsTitle(addedPostCount);
98 showNewPostsTitle(addedPostCount);
99 }
99 }
100
100
101 // TODO Process removed posts if any
101 // TODO Process removed posts if any
102 $('.metapanel').attr('data-last-update', data.last_update);
102 $('.metapanel').attr('data-last-update', data.last_update);
103
103
104 if (data.subscribed == 'True') {
104 if (data.subscribed == 'True') {
105 var favButton = $('#thread-fav-button .not_fav');
105 var favButton = $('#thread-fav-button .not_fav');
106
106
107 if (favButton.length > 0) {
107 if (favButton.length > 0) {
108 favButton.attr('value', 'unsubscribe');
108 favButton.attr('value', 'unsubscribe');
109 favButton.removeClass('not_fav');
109 favButton.removeClass('not_fav');
110 favButton.addClass('fav');
110 favButton.addClass('fav');
111 }
111 }
112 }
112 }
113 },
113 },
114 'json'
114 'json'
115 )
115 )
116 }
116 }
117
117
118 /**
118 /**
119 * Add or update the post on html page.
119 * Add or update the post on html page.
120 */
120 */
121 function updatePost(postHtml) {
121 function updatePost(postHtml) {
122 // This needs to be set on start because the page is scrolled after posts
122 // This needs to be set on start because the page is scrolled after posts
123 // are added or updated
123 // are added or updated
124 var bottom = isPageBottom();
124 var bottom = isPageBottom();
125
125
126 var post = $(postHtml);
126 var post = $(postHtml);
127
127
128 var threadBlock = $('div.thread');
128 var threadBlock = $('div.thread');
129
129
130 var postId = post.attr('id');
130 var postId = post.attr('id');
131
131
132 // If the post already exists, replace it. Otherwise add as a new one.
132 // If the post already exists, replace it. Otherwise add as a new one.
133 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
133 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
134
134
135 var type;
135 var type;
136
136
137 if (existingPosts.size() > 0) {
137 if (existingPosts.size() > 0) {
138 replacePartial(existingPosts.first(), post, false);
138 replacePartial(existingPosts.first(), post, false);
139 post = existingPosts.first();
139 post = existingPosts.first();
140
140
141 type = POST_UPDATED;
141 type = POST_UPDATED;
142 } else {
142 } else {
143 post.appendTo(threadBlock);
143 post.appendTo(threadBlock);
144
144
145 if (bottom) {
145 if (bottom) {
146 scrollToBottom();
146 scrollToBottom();
147 }
147 }
148
148
149 type = POST_ADDED;
149 type = POST_ADDED;
150 }
150 }
151
151
152 processNewPost(post);
152 processNewPost(post);
153
153
154 setTimeout(function() {
155 ambiplayer_init_hook && ambiplayer_init_hook();
156 }, 0);
157
154 return type;
158 return type;
155 }
159 }
156
160
157 /**
161 /**
158 * Initiate a blinking animation on a node to show it was updated.
162 * Initiate a blinking animation on a node to show it was updated.
159 */
163 */
160 function blink(node) {
164 function blink(node) {
161 node.effect('highlight', { color: blinkColor }, BLINK_SPEED);
165 node.effect('highlight', { color: blinkColor }, BLINK_SPEED);
162 }
166 }
163
167
164 function isPageBottom() {
168 function isPageBottom() {
165 var scroll = $(window).scrollTop() / ($(document).height()
169 var scroll = $(window).scrollTop() / ($(document).height()
166 - $(window).height());
170 - $(window).height());
167
171
168 return scroll == 1
172 return scroll == 1
169 }
173 }
170
174
171 function enableJsUpdate() {
175 function enableJsUpdate() {
172 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
176 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
173 return true;
177 return true;
174 }
178 }
175
179
176 function initAutoupdate() {
180 function initAutoupdate() {
177 return enableJsUpdate();
181 return enableJsUpdate();
178 }
182 }
179
183
180 function getReplyCount() {
184 function getReplyCount() {
181 return $('.thread').children(CLASS_POST).length
185 return $('.thread').children(CLASS_POST).length
182 }
186 }
183
187
184 function getImageCount() {
188 function getImageCount() {
185 return $('.thread').find('img').length
189 return $('.thread').find('img').length
186 }
190 }
187
191
188 /**
192 /**
189 * Update post count, images count and last update time in the metadata
193 * Update post count, images count and last update time in the metadata
190 * panel.
194 * panel.
191 */
195 */
192 function updateMetadataPanel() {
196 function updateMetadataPanel() {
193 var replyCountField = $('#reply-count');
197 var replyCountField = $('#reply-count');
194 var imageCountField = $('#image-count');
198 var imageCountField = $('#image-count');
195
199
196 var replyCount = getReplyCount();
200 var replyCount = getReplyCount();
197 replyCountField.text(replyCount);
201 replyCountField.text(replyCount);
198 var imageCount = getImageCount();
202 var imageCount = getImageCount();
199 imageCountField.text(imageCount);
203 imageCountField.text(imageCount);
200
204
201 var lastUpdate = $('.post:last').children('.post-info').first()
205 var lastUpdate = $('.post:last').children('.post-info').first()
202 .children('.pub_time').first().html();
206 .children('.pub_time').first().html();
203 if (lastUpdate !== '') {
207 if (lastUpdate !== '') {
204 var lastUpdateField = $('#last-update');
208 var lastUpdateField = $('#last-update');
205 lastUpdateField.html(lastUpdate);
209 lastUpdateField.html(lastUpdate);
206 blink(lastUpdateField);
210 blink(lastUpdateField);
207 }
211 }
208
212
209 blink(replyCountField);
213 blink(replyCountField);
210 blink(imageCountField);
214 blink(imageCountField);
211 }
215 }
212
216
213 /**
217 /**
214 * Update bumplimit progress bar
218 * Update bumplimit progress bar
215 */
219 */
216 function updateBumplimitProgress(postDelta) {
220 function updateBumplimitProgress(postDelta) {
217 var progressBar = $('#bumplimit_progress');
221 var progressBar = $('#bumplimit_progress');
218 if (progressBar) {
222 if (progressBar) {
219 var postsToLimitElement = $('#left_to_limit');
223 var postsToLimitElement = $('#left_to_limit');
220
224
221 var oldPostsToLimit = parseInt(postsToLimitElement.text());
225 var oldPostsToLimit = parseInt(postsToLimitElement.text());
222 var postCount = getReplyCount();
226 var postCount = getReplyCount();
223 var bumplimit = postCount - postDelta + oldPostsToLimit;
227 var bumplimit = postCount - postDelta + oldPostsToLimit;
224
228
225 var newPostsToLimit = bumplimit - postCount;
229 var newPostsToLimit = bumplimit - postCount;
226 if (newPostsToLimit <= 0) {
230 if (newPostsToLimit <= 0) {
227 $('.bar-bg').remove();
231 $('.bar-bg').remove();
228 } else {
232 } else {
229 postsToLimitElement.text(newPostsToLimit);
233 postsToLimitElement.text(newPostsToLimit);
230 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
234 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
231 }
235 }
232 }
236 }
233 }
237 }
234
238
235 /**
239 /**
236 * Show 'new posts' text in the title if the document is not visible to a user
240 * Show 'new posts' text in the title if the document is not visible to a user
237 */
241 */
238 function showNewPostsTitle(newPostCount) {
242 function showNewPostsTitle(newPostCount) {
239 if (document.hidden) {
243 if (document.hidden) {
240 if (documentOriginalTitle === '') {
244 if (documentOriginalTitle === '') {
241 documentOriginalTitle = document.title;
245 documentOriginalTitle = document.title;
242 }
246 }
243 unreadPosts = unreadPosts + newPostCount;
247 unreadPosts = unreadPosts + newPostCount;
244
248
245 var newTitle = null;
249 var newTitle = null;
246 if (unreadPosts > 0) {
250 if (unreadPosts > 0) {
247 newTitle = '[' + unreadPosts + '] ';
251 newTitle = '[' + unreadPosts + '] ';
248 } else {
252 } else {
249 newTitle = '* ';
253 newTitle = '* ';
250 }
254 }
251 newTitle += documentOriginalTitle;
255 newTitle += documentOriginalTitle;
252
256
253 document.title = newTitle;
257 document.title = newTitle;
254
258
255 document.addEventListener('visibilitychange', function() {
259 document.addEventListener('visibilitychange', function() {
256 if (documentOriginalTitle !== '') {
260 if (documentOriginalTitle !== '') {
257 document.title = documentOriginalTitle;
261 document.title = documentOriginalTitle;
258 documentOriginalTitle = '';
262 documentOriginalTitle = '';
259 unreadPosts = 0;
263 unreadPosts = 0;
260 }
264 }
261
265
262 document.removeEventListener('visibilitychange', null);
266 document.removeEventListener('visibilitychange', null);
263 });
267 });
264 }
268 }
265 }
269 }
266
270
267 /**
271 /**
268 * Clear all entered values in the form fields
272 * Clear all entered values in the form fields
269 */
273 */
270 function resetForm(form) {
274 function resetForm(form) {
271 form.find('input:text, input:password, input:file, select, textarea').val('');
275 form.find('input:text, input:password, input:file, select, textarea').val('');
272 form.find('input:radio, input:checkbox')
276 form.find('input:radio, input:checkbox')
273 .removeAttr('checked').removeAttr('selected');
277 .removeAttr('checked').removeAttr('selected');
274 $('.file_wrap').find('.file-thumb').remove();
278 $('.file_wrap').find('.file-thumb').remove();
275 $('#preview-text').hide();
279 $('#preview-text').hide();
276 }
280 }
277
281
278 /**
282 /**
279 * When the form is posted, this method will be run as a callback
283 * When the form is posted, this method will be run as a callback
280 */
284 */
281 function updateOnPost(response, statusText, xhr, form) {
285 function updateOnPost(response, statusText, xhr, form) {
282 var json = $.parseJSON(response);
286 var json = $.parseJSON(response);
283 var status = json.status;
287 var status = json.status;
284
288
285 showAsErrors(form, '');
289 showAsErrors(form, '');
286 $('.post-form-w').unblock();
290 $('.post-form-w').unblock();
287
291
288 if (status === 'ok') {
292 if (status === 'ok') {
289 resetFormPosition();
293 resetFormPosition();
290 resetForm(form);
294 resetForm(form);
291 getThreadDiff();
295 getThreadDiff();
292 scrollToBottom();
296 scrollToBottom();
293 } else {
297 } else {
294 var errors = json.errors;
298 var errors = json.errors;
295 for (var i = 0; i < errors.length; i++) {
299 for (var i = 0; i < errors.length; i++) {
296 var fieldErrors = errors[i];
300 var fieldErrors = errors[i];
297
301
298 var error = fieldErrors.errors;
302 var error = fieldErrors.errors;
299
303
300 showAsErrors(form, error);
304 showAsErrors(form, error);
301 }
305 }
302 }
306 }
303 }
307 }
304
308
305
309
306 /**
310 /**
307 * Run js methods that are usually run on the document, on the new post
311 * Run js methods that are usually run on the document, on the new post
308 */
312 */
309 function processNewPost(post) {
313 function processNewPost(post) {
310 addScriptsToPost(post);
314 addScriptsToPost(post);
311 blink(post);
315 blink(post);
312 }
316 }
313
317
314 function replacePartial(oldNode, newNode, recursive) {
318 function replacePartial(oldNode, newNode, recursive) {
315 if (!equalNodes(oldNode, newNode)) {
319 if (!equalNodes(oldNode, newNode)) {
316 // Update parent node attributes
320 // Update parent node attributes
317 updateNodeAttr(oldNode, newNode, ATTR_CLASS);
321 updateNodeAttr(oldNode, newNode, ATTR_CLASS);
318 updateNodeAttr(oldNode, newNode, ATTR_UID);
322 updateNodeAttr(oldNode, newNode, ATTR_UID);
319
323
320 // Replace children
324 // Replace children
321 var children = oldNode.children();
325 var children = oldNode.children();
322 if (children.length == 0) {
326 if (children.length == 0) {
323 oldNode.replaceWith(newNode);
327 oldNode.replaceWith(newNode);
324 } else {
328 } else {
325 var newChildren = newNode.children();
329 var newChildren = newNode.children();
326 newChildren.each(function(i) {
330 newChildren.each(function(i) {
327 var newChild = newChildren.eq(i);
331 var newChild = newChildren.eq(i);
328 var newChildClass = newChild.attr(ATTR_CLASS);
332 var newChildClass = newChild.attr(ATTR_CLASS);
329
333
330 // Update only certain allowed blocks (e.g. not images)
334 // Update only certain allowed blocks (e.g. not images)
331 if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) {
335 if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) {
332 var oldChild = oldNode.children('.' + newChildClass);
336 var oldChild = oldNode.children('.' + newChildClass);
333
337
334 if (oldChild.length == 0) {
338 if (oldChild.length == 0) {
335 oldNode.append(newChild);
339 oldNode.append(newChild);
336 } else {
340 } else {
337 if (!equalNodes(oldChild, newChild)) {
341 if (!equalNodes(oldChild, newChild)) {
338 if (recursive) {
342 if (recursive) {
339 replacePartial(oldChild, newChild, false);
343 replacePartial(oldChild, newChild, false);
340 } else {
344 } else {
341 oldChild.replaceWith(newChild);
345 oldChild.replaceWith(newChild);
342 }
346 }
343 }
347 }
344 }
348 }
345 }
349 }
346 });
350 });
347 }
351 }
348 }
352 }
349 }
353 }
350
354
351 /**
355 /**
352 * Compare nodes by content
356 * Compare nodes by content
353 */
357 */
354 function equalNodes(node1, node2) {
358 function equalNodes(node1, node2) {
355 return node1[0].outerHTML == node2[0].outerHTML;
359 return node1[0].outerHTML == node2[0].outerHTML;
356 }
360 }
357
361
358 /**
362 /**
359 * Update attribute of a node if it has changed
363 * Update attribute of a node if it has changed
360 */
364 */
361 function updateNodeAttr(oldNode, newNode, attrName) {
365 function updateNodeAttr(oldNode, newNode, attrName) {
362 var oldAttr = oldNode.attr(attrName);
366 var oldAttr = oldNode.attr(attrName);
363 var newAttr = newNode.attr(attrName);
367 var newAttr = newNode.attr(attrName);
364 if (oldAttr != newAttr) {
368 if (oldAttr != newAttr) {
365 oldNode.attr(attrName, newAttr);
369 oldNode.attr(attrName, newAttr);
366 }
370 }
367 }
371 }
368
372
369 $(document).ready(function() {
373 $(document).ready(function() {
370 if (initAutoupdate()) {
374 if (initAutoupdate()) {
371 // Post form data over AJAX
375 // Post form data over AJAX
372 var threadId = $('div.thread').children('.post').first().attr('id');
376 var threadId = $('div.thread').children('.post').first().attr('id');
373
377
374 var form = $('#form');
378 var form = $('#form');
375
379
376 if (form.length > 0) {
380 if (form.length > 0) {
377 var options = {
381 var options = {
378 beforeSubmit: function(arr, form, options) {
382 beforeSubmit: function(arr, form, options) {
379 $('.post-form-w').block({ message: gettext('Sending message...') });
383 $('.post-form-w').block({ message: gettext('Sending message...') });
380 },
384 },
381 success: updateOnPost,
385 success: updateOnPost,
382 error: function(xhr, textStatus, errorString) {
386 error: function(xhr, textStatus, errorString) {
383 var errorText = gettext('Server error: ') + textStatus;
387 var errorText = gettext('Server error: ') + textStatus;
384 if (errorString) {
388 if (errorString) {
385 errorText += ' / ' + errorString;
389 errorText += ' / ' + errorString;
386 }
390 }
387 showAsErrors(form, errorText);
391 showAsErrors(form, errorText);
388 $('.post-form-w').unblock();
392 $('.post-form-w').unblock();
389 },
393 },
390 url: '/api/add_post/' + threadId + '/',
394 url: '/api/add_post/' + threadId + '/',
391 timeout: POST_AJAX_TIMEOUT
395 timeout: POST_AJAX_TIMEOUT
392 };
396 };
393
397
394 form.ajaxForm(options);
398 form.ajaxForm(options);
395
399
396 resetForm(form);
400 resetForm(form);
397 }
401 }
398 }
402 }
399 });
403 });
@@ -1,97 +1,99 b''
1 {% load staticfiles %}
1 {% load staticfiles %}
2 {% load i18n %}
2 {% load i18n %}
3 {% load l10n %}
3 {% load l10n %}
4 {% load static from staticfiles %}
4 {% load static from staticfiles %}
5
5
6 <!DOCTYPE html>
6 <!DOCTYPE html>
7 <html>
7 <html>
8 <head>
8 <head>
9 <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/>
9 <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/>
10 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/>
10 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/>
11 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/jquery-ui.min.css' %}" media="all"/>
11 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/jquery-ui.min.css' %}" media="all"/>
12 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/jquery.contextMenu.min.css' %}" media="all"/>
12 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/jquery.contextMenu.min.css' %}" media="all"/>
13 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/ambiplayer.css' %}" media="all"/>
13 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
14 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
14
15
15 {% if rss_url %}
16 {% if rss_url %}
16 <link rel="alternate" type="application/rss+xml" href="{{ rss_url }}" title="{% trans 'Feed' %}"/>
17 <link rel="alternate" type="application/rss+xml" href="{{ rss_url }}" title="{% trans 'Feed' %}"/>
17 {% endif %}
18 {% endif %}
18
19
19 <link rel="icon" type="image/png"
20 <link rel="icon" type="image/png"
20 href="{% static 'favicon.png' %}">
21 href="{% static 'favicon.png' %}">
21
22
22 <meta name="viewport" content="width=device-width, initial-scale=1"/>
23 <meta name="viewport" content="width=device-width, initial-scale=1"/>
23 <meta charset="utf-8"/>
24 <meta charset="utf-8"/>
24
25
25 {% block head %}{% endblock %}
26 {% block head %}{% endblock %}
26 </head>
27 </head>
27 <body data-image-viewer="{{ image_viewer }}"
28 <body data-image-viewer="{{ image_viewer }}"
28 data-pow-difficulty="{{ pow_difficulty }}"
29 data-pow-difficulty="{{ pow_difficulty }}"
29 data-update-script="{% static 'js/updates.js' %}">
30 data-update-script="{% static 'js/updates.js' %}">
30 <script src="{% static 'js/jquery-2.2.0.min.js' %}"></script>
31 <script src="{% static 'js/jquery-2.2.0.min.js' %}"></script>
31
32
32 <header class="navigation_panel">
33 <header class="navigation_panel">
33 <a class="link" href="{% url 'landing' %}">{{ site_name }}</a>
34 <a class="link" href="{% url 'landing' %}">{{ site_name }}</a>
34 <a href="{% url 'index' %}" title="{% trans "All threads" %}">~~~</a>,
35 <a href="{% url 'index' %}" title="{% trans "All threads" %}">~~~</a>,
35 {% if tags_str %}
36 {% if tags_str %}
36 <form action="{% url 'index' %}" method="post" class="post-button-form">{% csrf_token %}
37 <form action="{% url 'index' %}" method="post" class="post-button-form">{% csrf_token %}
37 {% if only_favorites %}
38 {% if only_favorites %}
38 <button name="method" value="toggle_fav" class="fav">β˜…</button>,
39 <button name="method" value="toggle_fav" class="fav">β˜…</button>,
39 {% else %}
40 {% else %}
40 <button name="method" value="toggle_fav" class="not_fav">β˜…</button>,
41 <button name="method" value="toggle_fav" class="not_fav">β˜…</button>,
41 {% endif %}
42 {% endif %}
42 </form>
43 </form>
43 {{ tags_str|safe }},
44 {{ tags_str|safe }},
44 {% endif %}
45 {% endif %}
45 <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>,
46 <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}">{% trans "tags" %}</a>,
46 <a href="{% url 'search' %}" title="{% trans 'Search' %}">{% trans 'search' %}</a>,
47 <a href="{% url 'search' %}" title="{% trans 'Search' %}">{% trans 'search' %}</a>,
47 <a href="{% url 'feed' %}" title="{% trans 'Feed' %}">{% trans 'feed' %}</a>,
48 <a href="{% url 'feed' %}" title="{% trans 'Feed' %}">{% trans 'feed' %}</a>,
48 <a href="{% url 'random' %}" title="{% trans 'Random images' %}">{% trans 'images' %}</a>{% if has_fav_threads %},
49 <a href="{% url 'random' %}" title="{% trans 'Random images' %}">{% trans 'images' %}</a>{% if has_fav_threads %},
49
50
50 <a href="{% url 'feed' %}?favorites" id="fav-panel-btn">{% trans 'favorites' %} <span id="new-fav-post-count" {% if not new_post_count %}style="display: none" {% endif %}>{{ new_post_count }}</span></a>
51 <a href="{% url 'feed' %}?favorites" id="fav-panel-btn">{% trans 'favorites' %} <span id="new-fav-post-count" {% if not new_post_count %}style="display: none" {% endif %}>{{ new_post_count }}</span></a>
51 {% endif %}
52 {% endif %}
52
53
53 {% if usernames %}
54 {% if usernames %}
54 <a class="right-link link" href="{% url 'notifications' %}" title="{% trans 'Notifications' %}">
55 <a class="right-link link" href="{% url 'notifications' %}" title="{% trans 'Notifications' %}">
55 {% trans 'Notifications' %}
56 {% trans 'Notifications' %}
56 {% ifnotequal new_notifications_count 0 %}
57 {% ifnotequal new_notifications_count 0 %}
57 (<b>{{ new_notifications_count }}</b>)
58 (<b>{{ new_notifications_count }}</b>)
58 {% endifnotequal %}
59 {% endifnotequal %}
59 </a>
60 </a>
60 {% endif %}
61 {% endif %}
61
62
62 <a class="right-link link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
63 <a class="right-link link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
63 </header>
64 </header>
64
65
65 <div id="fav-panel"><div class="post">{% trans "Loading..." %}</div></div>
66 <div id="fav-panel"><div class="post">{% trans "Loading..." %}</div></div>
66
67
67 <script src="{% static 'js/3party/jquery-ui.min.js' %}"></script>
68 <script src="{% static 'js/3party/jquery-ui.min.js' %}"></script>
68 <script src="{% static 'js/3party/jquery.contextMenu.min.js' %}"></script>
69 <script src="{% static 'js/3party/jquery.contextMenu.min.js' %}"></script>
69
70
70 {% block content %}{% endblock %}
71 {% block content %}{% endblock %}
71
72
72 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
73 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
73 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
74 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
75 <script src="{% static 'js/3party/ambiplayer.js' %}"></script>
74
76
75 <script src="{% url 'js_info_dict' %}"></script>
77 <script src="{% url 'js_info_dict' %}"></script>
76
78
77 <script src="{% static 'js/popup.js' %}"></script>
79 <script src="{% static 'js/popup.js' %}"></script>
78 <script src="{% static 'js/image.js' %}"></script>
80 <script src="{% static 'js/image.js' %}"></script>
79 <script src="{% static 'js/refpopup.js' %}"></script>
81 <script src="{% static 'js/refpopup.js' %}"></script>
80 <script src="{% static 'js/main.js' %}"></script>
82 <script src="{% static 'js/main.js' %}"></script>
81
83
82 <footer class="navigation_panel">
84 <footer class="navigation_panel">
83 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
85 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
84 {% block metapanel %}{% endblock %}
86 {% block metapanel %}{% endblock %}
85 {% if rss_url %}
87 {% if rss_url %}
86 [<a href="{{ rss_url }}">RSS</a>]
88 [<a href="{{ rss_url }}">RSS</a>]
87 {% endif %}
89 {% endif %}
88 [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>]
90 [<a href="{% url 'admin:index' %}">{% trans 'Admin' %}</a>]
89 [<a href="{% url 'index' %}?order=pub">{% trans 'New threads' %}</a>]
91 [<a href="{% url 'index' %}?order=pub">{% trans 'New threads' %}</a>]
90 {% with ppd=posts_per_day|floatformat:2 %}
92 {% with ppd=posts_per_day|floatformat:2 %}
91 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
93 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
92 {% endwith %}
94 {% endwith %}
93 <a class="link" href="#top" id="up">{% trans 'Up' %}</a>
95 <a class="link" href="#top" id="up">{% trans 'Up' %}</a>
94 </footer>
96 </footer>
95
97
96 </body>
98 </body>
97 </html>
99 </html>
General Comments 0
You need to be logged in to leave comments. Login now