##// END OF EJS Templates
Added max width to tag description in MD style. Recognize bmp extension from...
neko259 -
r1454:90cde611 default
parent child Browse files
Show More
@@ -1,443 +1,445 b''
1 1 import hashlib
2 2 import re
3 3 import time
4 4 import logging
5 5
6 6 import pytz
7 7
8 8 from django import forms
9 9 from django.core.files.uploadedfile import SimpleUploadedFile
10 10 from django.core.exceptions import ObjectDoesNotExist
11 11 from django.forms.util import ErrorList
12 12 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
13 13 from django.utils import timezone
14 14
15 15 from boards.mdx_neboard import formatters
16 16 from boards.models.attachment.downloaders import Downloader
17 17 from boards.models.post import TITLE_MAX_LENGTH
18 18 from boards.models import Tag, Post
19 19 from boards.utils import validate_file_size, get_file_mimetype, \
20 20 FILE_EXTENSION_DELIMITER
21 21 from neboard import settings
22 22 import boards.settings as board_settings
23 23 import neboard
24 24
25 25 POW_HASH_LENGTH = 16
26 26 POW_LIFE_MINUTES = 5
27 27
28 28 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
29 29 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
30 30
31 31 VETERAN_POSTING_DELAY = 5
32 32
33 33 ATTRIBUTE_PLACEHOLDER = 'placeholder'
34 34 ATTRIBUTE_ROWS = 'rows'
35 35
36 36 LAST_POST_TIME = 'last_post_time'
37 37 LAST_LOGIN_TIME = 'last_login_time'
38 38 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
39 39 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
40 40
41 41 LABEL_TITLE = _('Title')
42 42 LABEL_TEXT = _('Text')
43 43 LABEL_TAG = _('Tag')
44 44 LABEL_SEARCH = _('Search')
45 45
46 46 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
47 47 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
48 48
49 49 TAG_MAX_LENGTH = 20
50 50
51 51 TEXTAREA_ROWS = 4
52 52
53 53 TRIPCODE_DELIM = '#'
54 54
55 55 # TODO Maybe this may be converted into the database table?
56 56 MIMETYPE_EXTENSIONS = {
57 57 'image/jpeg': 'jpeg',
58 58 'image/png': 'png',
59 59 'image/gif': 'gif',
60 60 'video/webm': 'webm',
61 61 'application/pdf': 'pdf',
62 62 'x-diff': 'diff',
63 63 'image/svg+xml': 'svg',
64 64 'application/x-shockwave-flash': 'swf',
65 'image/x-ms-bmp': 'bmp',
66 'image/bmp': 'bmp',
65 67 }
66 68
67 69
68 70 def get_timezones():
69 71 timezones = []
70 72 for tz in pytz.common_timezones:
71 73 timezones.append((tz, tz),)
72 74 return timezones
73 75
74 76
75 77 class FormatPanel(forms.Textarea):
76 78 """
77 79 Panel for text formatting. Consists of buttons to add different tags to the
78 80 form text area.
79 81 """
80 82
81 83 def render(self, name, value, attrs=None):
82 84 output = '<div id="mark-panel">'
83 85 for formatter in formatters:
84 86 output += '<span class="mark_btn"' + \
85 87 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
86 88 '\', \'' + formatter.format_right + '\')">' + \
87 89 formatter.preview_left + formatter.name + \
88 90 formatter.preview_right + '</span>'
89 91
90 92 output += '</div>'
91 93 output += super(FormatPanel, self).render(name, value, attrs=attrs)
92 94
93 95 return output
94 96
95 97
96 98 class PlainErrorList(ErrorList):
97 99 def __unicode__(self):
98 100 return self.as_text()
99 101
100 102 def as_text(self):
101 103 return ''.join(['(!) %s ' % e for e in self])
102 104
103 105
104 106 class NeboardForm(forms.Form):
105 107 """
106 108 Form with neboard-specific formatting.
107 109 """
108 110
109 111 def as_div(self):
110 112 """
111 113 Returns this form rendered as HTML <as_div>s.
112 114 """
113 115
114 116 return self._html_output(
115 117 # TODO Do not show hidden rows in the list here
116 118 normal_row='<div class="form-row">'
117 119 '<div class="form-label">'
118 120 '%(label)s'
119 121 '</div>'
120 122 '<div class="form-input">'
121 123 '%(field)s'
122 124 '</div>'
123 125 '</div>'
124 126 '<div class="form-row">'
125 127 '%(help_text)s'
126 128 '</div>',
127 129 error_row='<div class="form-row">'
128 130 '<div class="form-label"></div>'
129 131 '<div class="form-errors">%s</div>'
130 132 '</div>',
131 133 row_ender='</div>',
132 134 help_text_html='%s',
133 135 errors_on_separate_row=True)
134 136
135 137 def as_json_errors(self):
136 138 errors = []
137 139
138 140 for name, field in list(self.fields.items()):
139 141 if self[name].errors:
140 142 errors.append({
141 143 'field': name,
142 144 'errors': self[name].errors.as_text(),
143 145 })
144 146
145 147 return errors
146 148
147 149
148 150 class PostForm(NeboardForm):
149 151
150 152 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
151 153 label=LABEL_TITLE,
152 154 widget=forms.TextInput(
153 155 attrs={ATTRIBUTE_PLACEHOLDER:
154 156 'test#tripcode'}))
155 157 text = forms.CharField(
156 158 widget=FormatPanel(attrs={
157 159 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
158 160 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
159 161 }),
160 162 required=False, label=LABEL_TEXT)
161 163 file = forms.FileField(required=False, label=_('File'),
162 164 widget=forms.ClearableFileInput(
163 165 attrs={'accept': 'file/*'}))
164 166 file_url = forms.CharField(required=False, label=_('File URL'),
165 167 widget=forms.TextInput(
166 168 attrs={ATTRIBUTE_PLACEHOLDER:
167 169 'http://example.com/image.png'}))
168 170
169 171 # This field is for spam prevention only
170 172 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
171 173 widget=forms.TextInput(attrs={
172 174 'class': 'form-email'}))
173 175 threads = forms.CharField(required=False, label=_('Additional threads'),
174 176 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
175 177 '123 456 789'}))
176 178
177 179 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
178 180 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
179 181 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
180 182
181 183 session = None
182 184 need_to_ban = False
183 185
184 186 def _update_file_extension(self, file):
185 187 if file:
186 188 mimetype = get_file_mimetype(file)
187 189 extension = MIMETYPE_EXTENSIONS.get(mimetype)
188 190 if extension:
189 191 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
190 192 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
191 193
192 194 file.name = new_filename
193 195 else:
194 196 logger = logging.getLogger('boards.forms.extension')
195 197
196 198 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
197 199
198 200 def clean_title(self):
199 201 title = self.cleaned_data['title']
200 202 if title:
201 203 if len(title) > TITLE_MAX_LENGTH:
202 204 raise forms.ValidationError(_('Title must have less than %s '
203 205 'characters') %
204 206 str(TITLE_MAX_LENGTH))
205 207 return title
206 208
207 209 def clean_text(self):
208 210 text = self.cleaned_data['text'].strip()
209 211 if text:
210 212 max_length = board_settings.get_int('Forms', 'MaxTextLength')
211 213 if len(text) > max_length:
212 214 raise forms.ValidationError(_('Text must have less than %s '
213 215 'characters') % str(max_length))
214 216 return text
215 217
216 218 def clean_file(self):
217 219 file = self.cleaned_data['file']
218 220
219 221 if file:
220 222 validate_file_size(file.size)
221 223 self._update_file_extension(file)
222 224
223 225 return file
224 226
225 227 def clean_file_url(self):
226 228 url = self.cleaned_data['file_url']
227 229
228 230 file = None
229 231 if url:
230 232 file = self._get_file_from_url(url)
231 233
232 234 if not file:
233 235 raise forms.ValidationError(_('Invalid URL'))
234 236 else:
235 237 validate_file_size(file.size)
236 238 self._update_file_extension(file)
237 239
238 240 return file
239 241
240 242 def clean_threads(self):
241 243 threads_str = self.cleaned_data['threads']
242 244
243 245 if len(threads_str) > 0:
244 246 threads_id_list = threads_str.split(' ')
245 247
246 248 threads = list()
247 249
248 250 for thread_id in threads_id_list:
249 251 try:
250 252 thread = Post.objects.get(id=int(thread_id))
251 253 if not thread.is_opening() or thread.get_thread().is_archived():
252 254 raise ObjectDoesNotExist()
253 255 threads.append(thread)
254 256 except (ObjectDoesNotExist, ValueError):
255 257 raise forms.ValidationError(_('Invalid additional thread list'))
256 258
257 259 return threads
258 260
259 261 def clean(self):
260 262 cleaned_data = super(PostForm, self).clean()
261 263
262 264 if cleaned_data['email']:
263 265 self.need_to_ban = True
264 266 raise forms.ValidationError('A human cannot enter a hidden field')
265 267
266 268 if not self.errors:
267 269 self._clean_text_file()
268 270
269 271 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
270 272 if not self.errors and limit_speed:
271 273 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
272 274 if pow_difficulty > 0 and cleaned_data['timestamp'] and cleaned_data['iteration'] and cleaned_data['guess']:
273 275 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
274 276 else:
275 277 self._validate_posting_speed()
276 278
277 279 return cleaned_data
278 280
279 281 def get_file(self):
280 282 """
281 283 Gets file from form or URL.
282 284 """
283 285
284 286 file = self.cleaned_data['file']
285 287 return file or self.cleaned_data['file_url']
286 288
287 289 def get_tripcode(self):
288 290 title = self.cleaned_data['title']
289 291 if title is not None and TRIPCODE_DELIM in title:
290 292 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
291 293 tripcode = hashlib.md5(code.encode()).hexdigest()
292 294 else:
293 295 tripcode = ''
294 296 return tripcode
295 297
296 298 def get_title(self):
297 299 title = self.cleaned_data['title']
298 300 if title is not None and TRIPCODE_DELIM in title:
299 301 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
300 302 else:
301 303 return title
302 304
303 305 def _clean_text_file(self):
304 306 text = self.cleaned_data.get('text')
305 307 file = self.get_file()
306 308
307 309 if (not text) and (not file):
308 310 error_message = _('Either text or file must be entered.')
309 311 self._errors['text'] = self.error_class([error_message])
310 312
311 313 def _validate_posting_speed(self):
312 314 can_post = True
313 315
314 316 posting_delay = settings.POSTING_DELAY
315 317
316 318 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
317 319 now = time.time()
318 320
319 321 current_delay = 0
320 322
321 323 if LAST_POST_TIME not in self.session:
322 324 self.session[LAST_POST_TIME] = now
323 325
324 326 need_delay = True
325 327 else:
326 328 last_post_time = self.session.get(LAST_POST_TIME)
327 329 current_delay = int(now - last_post_time)
328 330
329 331 need_delay = current_delay < posting_delay
330 332
331 333 if need_delay:
332 334 delay = posting_delay - current_delay
333 335 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
334 336 delay) % {'delay': delay}
335 337 self._errors['text'] = self.error_class([error_message])
336 338
337 339 can_post = False
338 340
339 341 if can_post:
340 342 self.session[LAST_POST_TIME] = now
341 343
342 344 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
343 345 """
344 346 Gets an file file from URL.
345 347 """
346 348
347 349 img_temp = None
348 350
349 351 try:
350 352 for downloader in Downloader.__subclasses__():
351 353 if downloader.handles(url):
352 354 return downloader.download(url)
353 355 # If nobody of the specific downloaders handles this, use generic
354 356 # one
355 357 return Downloader.download(url)
356 358 except forms.ValidationError as e:
357 359 raise e
358 360 except Exception as e:
359 361 raise forms.ValidationError(e)
360 362
361 363 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
362 364 post_time = timezone.datetime.fromtimestamp(
363 365 int(timestamp[:-3]), tz=timezone.get_current_timezone())
364 366 timedelta = (timezone.now() - post_time).seconds / 60
365 367 if timedelta > POW_LIFE_MINUTES:
366 368 self._errors['text'] = self.error_class([_('Stale PoW.')])
367 369
368 370 payload = timestamp + message.replace('\r\n', '\n')
369 371 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
370 372 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
371 373 if len(target) < POW_HASH_LENGTH:
372 374 target = '0' * (POW_HASH_LENGTH - len(target)) + target
373 375
374 376 computed_guess = hashlib.sha256((payload + iteration).encode())\
375 377 .hexdigest()[0:POW_HASH_LENGTH]
376 378 if guess != computed_guess or guess > target:
377 379 self._errors['text'] = self.error_class(
378 380 [_('Invalid PoW.')])
379 381
380 382
381 383 class ThreadForm(PostForm):
382 384
383 385 tags = forms.CharField(
384 386 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
385 387 max_length=100, label=_('Tags'), required=True)
386 388 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
387 389
388 390 def clean_tags(self):
389 391 tags = self.cleaned_data['tags'].strip()
390 392
391 393 if not tags or not REGEX_TAGS.match(tags):
392 394 raise forms.ValidationError(
393 395 _('Inappropriate characters in tags.'))
394 396
395 397 required_tag_exists = False
396 398 tag_set = set()
397 399 for tag_string in tags.split():
398 400 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
399 401 tag_set.add(tag)
400 402
401 403 # If this is a new tag, don't check for its parents because nobody
402 404 # added them yet
403 405 if not created:
404 406 tag_set |= set(tag.get_all_parents())
405 407
406 408 for tag in tag_set:
407 409 if tag.required:
408 410 required_tag_exists = True
409 411 break
410 412
411 413 if not required_tag_exists:
412 414 raise forms.ValidationError(
413 415 _('Need at least one section.'))
414 416
415 417 return tag_set
416 418
417 419 def clean(self):
418 420 cleaned_data = super(ThreadForm, self).clean()
419 421
420 422 return cleaned_data
421 423
422 424 def is_monochrome(self):
423 425 return self.cleaned_data['monochrome']
424 426
425 427
426 428 class SettingsForm(NeboardForm):
427 429
428 430 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
429 431 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
430 432 username = forms.CharField(label=_('User name'), required=False)
431 433 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
432 434
433 435 def clean_username(self):
434 436 username = self.cleaned_data['username']
435 437
436 438 if username and not REGEX_USERNAMES.match(username):
437 439 raise forms.ValidationError(_('Inappropriate characters.'))
438 440
439 441 return username
440 442
441 443
442 444 class SearchForm(NeboardForm):
443 445 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,568 +1,569 b''
1 1 * {
2 2 text-decoration: none;
3 3 font-weight: inherit;
4 4 }
5 5
6 6 b, strong {
7 7 font-weight: bold;
8 8 }
9 9
10 10 html {
11 11 background: #555;
12 12 color: #ffffff;
13 13 }
14 14
15 15 body {
16 16 margin: 0;
17 17 }
18 18
19 19 #admin_panel {
20 20 background: #FF0000;
21 21 color: #00FF00
22 22 }
23 23
24 24 .input_field_error {
25 25 color: #FF0000;
26 26 }
27 27
28 28 .title {
29 29 font-weight: bold;
30 30 color: #ffcc00;
31 31 }
32 32
33 33 .link, a {
34 34 color: #afdcec;
35 35 }
36 36
37 37 .block {
38 38 display: inline-block;
39 39 vertical-align: top;
40 40 }
41 41
42 42 .tag {
43 43 color: #FFD37D;
44 44 }
45 45
46 46 .post_id {
47 47 color: #fff380;
48 48 }
49 49
50 50 .post, .dead_post, .archive_post, #posts-table {
51 51 background: #333;
52 52 padding: 10px;
53 53 clear: left;
54 54 word-wrap: break-word;
55 55 border-top: 1px solid #777;
56 56 border-bottom: 1px solid #777;
57 57 }
58 58
59 59 .post + .post {
60 60 border-top: none;
61 61 }
62 62
63 63 .dead_post + .dead_post {
64 64 border-top: none;
65 65 }
66 66
67 67 .archive_post + .archive_post {
68 68 border-top: none;
69 69 }
70 70
71 71 .metadata {
72 72 padding-top: 5px;
73 73 margin-top: 10px;
74 74 border-top: solid 1px #666;
75 75 color: #ddd;
76 76 }
77 77
78 78 .navigation_panel, .tag_info {
79 79 background: #222;
80 80 margin-bottom: 5px;
81 81 margin-top: 5px;
82 82 padding: 10px;
83 83 border-bottom: solid 1px #888;
84 84 border-top: solid 1px #888;
85 85 color: #eee;
86 86 }
87 87
88 88 .navigation_panel .link:first-child {
89 89 border-right: 1px solid #fff;
90 90 font-weight: bold;
91 91 margin-right: 1ex;
92 92 padding-right: 1ex;
93 93 }
94 94
95 95 .navigation_panel .right-link {
96 96 border-left: 1px solid #fff;
97 97 border-right: none;
98 98 float: right;
99 99 margin-left: 1ex;
100 100 margin-right: 0;
101 101 padding-left: 1ex;
102 102 padding-right: 0;
103 103 }
104 104
105 105 .navigation_panel .link {
106 106 font-weight: bold;
107 107 }
108 108
109 109 .navigation_panel::after, .post::after {
110 110 clear: both;
111 111 content: ".";
112 112 display: block;
113 113 height: 0;
114 114 line-height: 0;
115 115 visibility: hidden;
116 116 }
117 117
118 118 .tag_info {
119 119 text-align: center;
120 120 }
121 121
122 122 .tag_info > .tag-text-data {
123 123 text-align: left;
124 max-width: 30em;
124 125 }
125 126
126 127 .header {
127 128 border-bottom: solid 2px #ccc;
128 129 margin-bottom: 5px;
129 130 border-top: none;
130 131 margin-top: 0;
131 132 }
132 133
133 134 .footer {
134 135 border-top: solid 2px #ccc;
135 136 margin-top: 5px;
136 137 border-bottom: none;
137 138 margin-bottom: 0;
138 139 }
139 140
140 141 p, .br {
141 142 margin-top: .5em;
142 143 margin-bottom: .5em;
143 144 }
144 145
145 146 .post-form-w {
146 147 background: #333344;
147 148 border-top: solid 1px #888;
148 149 border-bottom: solid 1px #888;
149 150 color: #fff;
150 151 padding: 10px;
151 152 margin-bottom: 5px;
152 153 margin-top: 5px;
153 154 }
154 155
155 156 .form-row {
156 157 width: 100%;
157 158 display: table-row;
158 159 }
159 160
160 161 .form-label {
161 162 padding: .25em 1ex .25em 0;
162 163 vertical-align: top;
163 164 display: table-cell;
164 165 }
165 166
166 167 .form-input {
167 168 padding: .25em 0;
168 169 width: 100%;
169 170 display: table-cell;
170 171 }
171 172
172 173 .form-errors {
173 174 font-weight: bolder;
174 175 vertical-align: middle;
175 176 display: table-cell;
176 177 }
177 178
178 179 .post-form input:not([name="image"]):not([type="checkbox"]):not([type="submit"]), .post-form textarea, .post-form select {
179 180 background: #333;
180 181 color: #fff;
181 182 border: solid 1px;
182 183 padding: 0;
183 184 font: medium sans-serif;
184 185 width: 100%;
185 186 }
186 187
187 188 .post-form textarea {
188 189 resize: vertical;
189 190 }
190 191
191 192 .form-submit {
192 193 display: table;
193 194 margin-bottom: 1ex;
194 195 margin-top: 1ex;
195 196 }
196 197
197 198 .form-title {
198 199 font-weight: bold;
199 200 font-size: 2ex;
200 201 margin-bottom: 0.5ex;
201 202 }
202 203
203 204 input[type="submit"], button {
204 205 background: #222;
205 206 border: solid 2px #fff;
206 207 color: #fff;
207 208 padding: 0.5ex;
208 209 margin-right: 0.5ex;
209 210 }
210 211
211 212 input[type="submit"]:hover {
212 213 background: #060;
213 214 }
214 215
215 216 .form-submit > button:hover {
216 217 background: #006;
217 218 }
218 219
219 220 blockquote {
220 221 border-left: solid 2px;
221 222 padding-left: 5px;
222 223 color: #B1FB17;
223 224 margin: 0;
224 225 }
225 226
226 227 .post > .image {
227 228 float: left;
228 229 margin: 0 1ex .5ex 0;
229 230 min-width: 1px;
230 231 text-align: center;
231 232 display: table-row;
232 233 }
233 234
234 235 .post > .metadata {
235 236 clear: left;
236 237 }
237 238
238 239 .get {
239 240 font-weight: bold;
240 241 color: #d55;
241 242 }
242 243
243 244 * {
244 245 text-decoration: none;
245 246 }
246 247
247 248 .dead_post > .post-info {
248 249 font-style: italic;
249 250 }
250 251
251 252 .archive_post > .post-info {
252 253 text-decoration: line-through;
253 254 }
254 255
255 256 .mark_btn {
256 257 border: 1px solid;
257 258 padding: 2px 2ex;
258 259 display: inline-block;
259 260 margin: 0 5px 4px 0;
260 261 }
261 262
262 263 .mark_btn:hover {
263 264 background: #555;
264 265 }
265 266
266 267 .quote {
267 268 color: #92cf38;
268 269 font-style: italic;
269 270 }
270 271
271 272 .multiquote {
272 273 padding: 3px;
273 274 display: inline-block;
274 275 background: #222;
275 276 border-style: solid;
276 277 border-width: 1px 1px 1px 4px;
277 278 font-size: 0.9em;
278 279 }
279 280
280 281 .spoiler {
281 282 background: black;
282 283 color: black;
283 284 padding: 0 1ex 0 1ex;
284 285 }
285 286
286 287 .spoiler:hover {
287 288 color: #ddd;
288 289 }
289 290
290 291 .comment {
291 292 color: #eb2;
292 293 }
293 294
294 295 a:hover {
295 296 text-decoration: underline;
296 297 }
297 298
298 299 .last-replies {
299 300 margin-left: 3ex;
300 301 margin-right: 3ex;
301 302 border-left: solid 1px #777;
302 303 border-right: solid 1px #777;
303 304 }
304 305
305 306 .last-replies > .post:first-child {
306 307 border-top: none;
307 308 }
308 309
309 310 .thread {
310 311 margin-bottom: 3ex;
311 312 margin-top: 1ex;
312 313 }
313 314
314 315 .post:target {
315 316 border: solid 2px white;
316 317 }
317 318
318 319 pre{
319 320 white-space:pre-wrap
320 321 }
321 322
322 323 li {
323 324 list-style-position: inside;
324 325 }
325 326
326 327 .fancybox-skin {
327 328 position: relative;
328 329 background-color: #fff;
329 330 color: #ddd;
330 331 text-shadow: none;
331 332 }
332 333
333 334 .fancybox-image {
334 335 border: 1px solid black;
335 336 }
336 337
337 338 .image-mode-tab {
338 339 background: #444;
339 340 color: #eee;
340 341 margin-top: 5px;
341 342 padding: 5px;
342 343 border-top: 1px solid #888;
343 344 border-bottom: 1px solid #888;
344 345 }
345 346
346 347 .image-mode-tab > label {
347 348 margin: 0 1ex;
348 349 }
349 350
350 351 .image-mode-tab > label > input {
351 352 margin-right: .5ex;
352 353 }
353 354
354 355 #posts-table {
355 356 margin-top: 5px;
356 357 margin-bottom: 5px;
357 358 }
358 359
359 360 .tag_info > h2 {
360 361 margin: 0;
361 362 }
362 363
363 364 .post-info {
364 365 color: #ddd;
365 366 margin-bottom: 1ex;
366 367 }
367 368
368 369 .moderator_info {
369 370 color: #e99d41;
370 371 opacity: 0.4;
371 372 }
372 373
373 374 .moderator_info:hover {
374 375 opacity: 1;
375 376 }
376 377
377 378 .refmap {
378 379 font-size: 0.9em;
379 380 color: #ccc;
380 381 margin-top: 1em;
381 382 }
382 383
383 384 .fav {
384 385 color: yellow;
385 386 }
386 387
387 388 .not_fav {
388 389 color: #ccc;
389 390 }
390 391
391 392 .form-email {
392 393 display: none;
393 394 }
394 395
395 396 .bar-value {
396 397 background: rgba(50, 55, 164, 0.45);
397 398 font-size: 0.9em;
398 399 height: 1.5em;
399 400 }
400 401
401 402 .bar-bg {
402 403 position: relative;
403 404 border-top: solid 1px #888;
404 405 border-bottom: solid 1px #888;
405 406 margin-top: 5px;
406 407 overflow: hidden;
407 408 }
408 409
409 410 .bar-text {
410 411 padding: 2px;
411 412 position: absolute;
412 413 left: 0;
413 414 top: 0;
414 415 }
415 416
416 417 .page_link {
417 418 background: #444;
418 419 border-top: solid 1px #888;
419 420 border-bottom: solid 1px #888;
420 421 padding: 5px;
421 422 color: #eee;
422 423 font-size: 2ex;
423 424 margin-top: .5ex;
424 425 margin-bottom: .5ex;
425 426 }
426 427
427 428 .skipped_replies {
428 429 padding: 5px;
429 430 margin-left: 3ex;
430 431 margin-right: 3ex;
431 432 border-left: solid 1px #888;
432 433 border-right: solid 1px #888;
433 434 border-bottom: solid 1px #888;
434 435 background: #000;
435 436 }
436 437
437 438 .current_page {
438 439 padding: 2px;
439 440 background-color: #afdcec;
440 441 color: #000;
441 442 }
442 443
443 444 .current_mode {
444 445 font-weight: bold;
445 446 }
446 447
447 448 .gallery_image {
448 449 border: solid 1px;
449 450 margin: 0.5ex;
450 451 text-align: center;
451 452 padding: 1ex;
452 453 }
453 454
454 455 code {
455 456 border: dashed 1px #ccc;
456 457 background: #111;
457 458 padding: 2px;
458 459 font-size: 1.2em;
459 460 display: inline-block;
460 461 }
461 462
462 463 pre {
463 464 overflow: auto;
464 465 }
465 466
466 467 .img-full {
467 468 background: #222;
468 469 border: solid 1px white;
469 470 }
470 471
471 472 .tag_item {
472 473 display: inline-block;
473 474 }
474 475
475 476 #id_models li {
476 477 list-style: none;
477 478 }
478 479
479 480 #id_q {
480 481 margin-left: 1ex;
481 482 }
482 483
483 484 ul {
484 485 padding-left: 0px;
485 486 }
486 487
487 488 .quote-header {
488 489 border-bottom: 2px solid #ddd;
489 490 margin-bottom: 1ex;
490 491 padding-bottom: .5ex;
491 492 color: #ddd;
492 493 font-size: 1.2em;
493 494 }
494 495
495 496 /* Reflink preview */
496 497 .post_preview {
497 498 border-left: 1px solid #777;
498 499 border-right: 1px solid #777;
499 500 max-width: 600px;
500 501 }
501 502
502 503 /* Code highlighter */
503 504 .hljs {
504 505 color: #fff;
505 506 background: #000;
506 507 display: inline-block;
507 508 }
508 509
509 510 .hljs, .hljs-subst, .hljs-tag .hljs-title, .lisp .hljs-title, .clojure .hljs-built_in, .nginx .hljs-title {
510 511 color: #fff;
511 512 }
512 513
513 514 #up {
514 515 position: fixed;
515 516 bottom: 5px;
516 517 right: 5px;
517 518 border: 1px solid #777;
518 519 background: #000;
519 520 padding: 4px;
520 521 opacity: 0.3;
521 522 }
522 523
523 524 #up:hover {
524 525 opacity: 1;
525 526 }
526 527
527 528 .user-cast {
528 529 border: solid #ffffff 1px;
529 530 padding: .2ex;
530 531 background: #152154;
531 532 color: #fff;
532 533 }
533 534
534 535 .highlight {
535 536 background: #222;
536 537 }
537 538
538 539 .post-button-form > button:hover {
539 540 text-decoration: underline;
540 541 }
541 542
542 543 .tree_reply > .post {
543 544 margin-top: 1ex;
544 545 border-left: solid 1px #777;
545 546 padding-right: 0;
546 547 }
547 548
548 549 #preview-text {
549 550 border: solid 1px white;
550 551 margin: 1ex 0 1ex 0;
551 552 padding: 1ex;
552 553 }
553 554
554 555 .image-metadata {
555 556 font-size: 0.9em;
556 557 }
557 558
558 559 .tripcode {
559 560 color: white;
560 561 }
561 562
562 563 #fav-panel {
563 564 border: 1px solid white;
564 565 }
565 566
566 567 .post-blink {
567 568 background-color: #000;
568 569 }
General Comments 0
You need to be logged in to leave comments. Login now