diff options
author | emkael <emkael@tlen.pl> | 2019-07-08 22:45:38 +0200 |
---|---|---|
committer | emkael <emkael@tlen.pl> | 2019-07-08 22:45:38 +0200 |
commit | a0644da194f61f627535e6b72c7a6e761986498d (patch) | |
tree | c9fe545dc585320c90f547dfe70c4bb0bd8e2ca7 | |
parent | 3a136318f40f607e21069afc4141a5cbcba9ccb8 (diff) | |
parent | e3e5c088687d73dc492199be071cc12d786ba00d (diff) |
Merge branch 'master' of github.com:emkael/deal-convert
-rw-r--r-- | README.txt | 2 | ||||
-rw-r--r-- | _cron/clear_cache | 1 | ||||
-rw-r--r-- | cache/.gitignore | 1 | ||||
-rw-r--r-- | dealconvert/__init__.py | 9 | ||||
-rw-r--r-- | dealconvert/formats/__init__.py | 2 | ||||
-rw-r--r-- | dealconvert/formats/ber.py | 2 | ||||
-rw-r--r-- | dealconvert/formats/bhg.py | 4 | ||||
-rw-r--r-- | dealconvert/formats/bri.py | 2 | ||||
-rw-r--r-- | dealconvert/formats/dlm.py | 4 | ||||
-rw-r--r-- | dealconvert/formats/rzd.py | 4 | ||||
-rw-r--r-- | http/api/.htaccess | 6 | ||||
-rw-r--r-- | http/api/api.py | 181 | ||||
-rw-r--r-- | http/dealconvert.js | 143 | ||||
-rw-r--r-- | http/img/e-ew.png | bin | 0 -> 5657 bytes | |||
-rw-r--r-- | http/img/e-ns-ew.png | bin | 0 -> 5952 bytes | |||
-rw-r--r-- | http/img/e-ns.png | bin | 0 -> 5674 bytes | |||
-rw-r--r-- | http/img/e.png | bin | 0 -> 5267 bytes | |||
-rw-r--r-- | http/img/n-ew.png | bin | 0 -> 5577 bytes | |||
-rw-r--r-- | http/img/n-ns-ew.png | bin | 0 -> 5955 bytes | |||
-rw-r--r-- | http/img/n-ns.png | bin | 0 -> 5731 bytes | |||
-rw-r--r-- | http/img/n.png | bin | 0 -> 5247 bytes | |||
-rw-r--r-- | http/img/s-ew.png | bin | 0 -> 5558 bytes | |||
-rw-r--r-- | http/img/s-ns-ew.png | bin | 0 -> 5936 bytes | |||
-rw-r--r-- | http/img/s-ns.png | bin | 0 -> 5688 bytes | |||
-rw-r--r-- | http/img/s.png | bin | 0 -> 5250 bytes | |||
-rw-r--r-- | http/img/w-ew.png | bin | 0 -> 5595 bytes | |||
-rw-r--r-- | http/img/w-ns-ew.png | bin | 0 -> 5901 bytes | |||
-rw-r--r-- | http/img/w-ns.png | bin | 0 -> 5602 bytes | |||
-rw-r--r-- | http/img/w.png | bin | 0 -> 5207 bytes | |||
-rw-r--r-- | http/index.html | 254 |
30 files changed, 604 insertions, 11 deletions
@@ -12,3 +12,5 @@ optional arguments: Supported formats: BER BHG BRI CDS CSV DGE DLM DUP PBN RZD. Formats are auto-detected based on file extension. To display deals on STDOUT, provide "-" as an output file name. + +Web interface available: https://deal.emkael.info/ diff --git a/_cron/clear_cache b/_cron/clear_cache new file mode 100644 index 0000000..ca29bcb --- /dev/null +++ b/_cron/clear_cache @@ -0,0 +1 @@ +* * * * * find $SITEPATH/cache -type f -mmin +15 -not -name .gitignore -delete diff --git a/cache/.gitignore b/cache/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/cache/.gitignore @@ -0,0 +1 @@ +* diff --git a/dealconvert/__init__.py b/dealconvert/__init__.py index 1779976..3b5a8ba 100644 --- a/dealconvert/__init__.py +++ b/dealconvert/__init__.py @@ -1,17 +1,18 @@ from .formats import * class DealConverter(object): - def __init__(self, input_file): + def __init__(self, input_file=None): self.input = input_file self.formats = {} - self.parser = self._detect_format(self.input) + if input_file is not None: + self.parser = self._detect_format(self.input) def output(self, output_files): deal_set = sorted(self.parser.parse(self.input), key=lambda d:d.number) for output in output_files: self._detect_format(output).output(output, deal_set) - def _detect_format(self, filename): + def detect_format(self, filename): for deal_format in globals()['formats'].__all__: if deal_format not in self.formats: self.formats[deal_format] = getattr( @@ -19,4 +20,4 @@ class DealConverter(object): deal_format.upper() + 'Format')() if self.formats[deal_format].match_file(filename): return self.formats[deal_format] - raise ValueError('Unrecognized file extension: %s' % filename) + raise RuntimeError('Unrecognized file extension: %s' % filename) diff --git a/dealconvert/formats/__init__.py b/dealconvert/formats/__init__.py index eea9fc6..9c37cf9 100644 --- a/dealconvert/formats/__init__.py +++ b/dealconvert/formats/__init__.py @@ -8,6 +8,8 @@ class DealFormat(object): return self.parse_content(content) def output(self, output_file, deal): + if not len(deal): + raise RuntimeError('Dealset is empty') with open(output_file, 'wb') as out_file: return self.output_content(out_file, deal) diff --git a/dealconvert/formats/ber.py b/dealconvert/formats/ber.py index 54ffd37..1ddd6c8 100644 --- a/dealconvert/formats/ber.py +++ b/dealconvert/formats/ber.py @@ -50,7 +50,7 @@ class BERFormat(DealFormat): deal_str[j*13 + self.cards.index(card)] = str(i + 1) except ValueError: raise RuntimeError( - 'invalid card character: %s' % (card)) + 'invalid card character: %s in board %d' % (card, board.number)) if ' ' in deal_str: warnings.warn('not all cards present in board %d' % ( board.number)) diff --git a/dealconvert/formats/bhg.py b/dealconvert/formats/bhg.py index 23a1673..6964ea3 100644 --- a/dealconvert/formats/bhg.py +++ b/dealconvert/formats/bhg.py @@ -40,7 +40,7 @@ class BHGFormat(DealFormat): return deals def output_content(self, out_file, dealset): - lines = [''] * (max([board.number for board in dealset])+2) + lines = [''] * (max([board.number for board in dealset])+2) if len(dealset) else [] for deal in dealset: line = '' for hand in range(0, 4): @@ -51,7 +51,7 @@ class BHGFormat(DealFormat): line += chr((65 if card < 26 else 71)+card) except ValueError: raise RuntimeError( - 'invalid suit %s in board #%d' % ( + 'invalid suit %s in board %d' % ( ''.join(suit), deal.number)) lines[deal.number] = line out_file.write('\r\n'.join(lines)) diff --git a/dealconvert/formats/bri.py b/dealconvert/formats/bri.py index 942c4e4..1a4cc0f 100644 --- a/dealconvert/formats/bri.py +++ b/dealconvert/formats/bri.py @@ -67,5 +67,5 @@ class BRIFormat(DealFormat): deal_str += '%02d' % (self.cards.index(card) + 13*i + 1) except ValueError: raise RuntimeError( - 'invalid card character: %s' % (card)) + 'invalid card character: %s in board %d' % (card, deal.number)) return deal_str diff --git a/dealconvert/formats/dlm.py b/dealconvert/formats/dlm.py index db9ac79..56f6e5d 100644 --- a/dealconvert/formats/dlm.py +++ b/dealconvert/formats/dlm.py @@ -66,7 +66,7 @@ class DLMFormat(DealFormat): def output_content(self, out_file, dealset): dealset = dealset[0:99] board_numbers = [deal.number for deal in dealset] - first_board = min(board_numbers) + first_board = min(board_numbers) if len(board_numbers) else 1 board_count = len(dealset) for board in range(first_board, first_board+board_count): if board not in board_numbers: @@ -98,7 +98,7 @@ class DLMFormat(DealFormat): try: values[suit*13+self.cards.index(card)] = i except ValueError: - raise RuntimeError('invalid card: %s' % (card)) + raise RuntimeError('invalid card: %s in board %d' % (card, board)) line = 'Board %02d=' % (board) checksum = board for i in range(0, 26): diff --git a/dealconvert/formats/rzd.py b/dealconvert/formats/rzd.py index 8ca1e97..93dbc45 100644 --- a/dealconvert/formats/rzd.py +++ b/dealconvert/formats/rzd.py @@ -51,11 +51,13 @@ class RZDFormat(DealFormat): try: idx = self.cards.index(card) except ValueError: - raise RuntimeError('invalid card: %s' % (card)) + raise RuntimeError('invalid card: %s in board %d' % (card, deal.number)) values[idx*4+suit] = (i + offset)%4 for i in range(0, 13): byte = 0 for j in range(0, 4): + if values[4*i+j] is None: + raise RuntimeError('missing card: %s%s in board %d' % ('SHDC'[j], self.cards[i], deal.number)) byte *= 4 byte += values[4*i+j] value += chr(byte) diff --git a/http/api/.htaccess b/http/api/.htaccess new file mode 100644 index 0000000..998bfca --- /dev/null +++ b/http/api/.htaccess @@ -0,0 +1,6 @@ +RewriteEngine On +RewriteRule ^.*$ api.py [QSA,L] + +AddHandler mod_python .py +PythonHandler api +PythonDebug On diff --git a/http/api/api.py b/http/api/api.py new file mode 100644 index 0000000..be2635f --- /dev/null +++ b/http/api/api.py @@ -0,0 +1,181 @@ +# coding=utf-8 + +import base64, copy, json, os, random, sys, warnings +from StringIO import StringIO + +from mod_python import apache, Session + +OLDPATH = copy.copy(sys.path) +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) +from dealconvert import DealConverter +sys.path = OLDPATH + +CACHEPATH = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + '../../cache')) + +def _get_rand_string(length=30): + return ('%0' + str(length) + 'x') % (random.randrange(16**length)) + +def _get_file_id(): + while True: + output_id = _get_rand_string() + output_path = os.path.join(CACHEPATH, output_id) + if not os.path.exists(output_path): + return output_id, output_path + +def _print_response(response, obj): + response.write(json.dumps(obj)) + +def handle_upload(response, request): + if request.method != 'POST': + response.status = apache.HTTP_METHOD_NOT_ALLOWED + return + try: + params = json.load(request) + except ValueError as e: + response.write(str(e)) + response.status = apache.HTTP_BAD_REQUEST + return + + session = Session.Session(response) + if 'tokens' not in session: + session['tokens'] = {} + + response.content_type = 'application/json' + return_obj = { + 'name': None, + 'warnings': [], + 'error': None, + 'files': [] + } + warnings.simplefilter('always') + warnings.showwarning = lambda msg, *args: return_obj['warnings'].append( + unicode(msg)) + + try: + return_obj['name'] = params['name'] + converter = DealConverter() + parser = converter.detect_format(params['name']) + input_file = StringIO(base64.b64decode(params['content'])) + dealset = parser.parse_content(input_file) + input_file.close() + if not len(dealset): + raise RuntimeError('Dealset is empty') + if params['display_deals']: + preview_obj = [] + for board in dealset: + deal_preview = { + 'number': board.number, + 'conditions': 'nesw'[board.dealer], + 'hands': [] + } + for pair in ['ns', 'ew']: + if board.vulnerable[pair.upper()]: + deal_preview['conditions'] += '-' + pair + deal_preview['hands'] = board.hands + preview_obj.append(deal_preview) + return_obj['preview'] = preview_obj + else: + return_obj['preview'] = None + except RuntimeError as e: + return_obj['error'] = unicode(e) + return _print_response(response, return_obj) + + for output_type in params['output']: + output_return = { + 'name': None, + 'link': None, + 'warnings': [], + 'error': None + } + warnings.showwarning = lambda msg, *args: output_return['warnings'].append( + unicode(msg)) + try: + output_name = '.'.join(params['name'].split('.')[:-1] + [output_type]) + output_return['name'] = output_name + output = converter.detect_format(output_name) + output_id, output_path = _get_file_id() + token = _get_rand_string(16) + output_buffer = StringIO() + output.output_content(output_buffer, dealset) + with file(output_path, 'w') as output_file: + json.dump({ + 'token': token, + 'name': output_name, + 'content': base64.b64encode(output_buffer.getvalue()) + }, output_file) + output_buffer.close() + session['tokens'][output_id] = token + output_return['link'] = 'download/%s' % (output_id) + except RuntimeError as e: + output_return['error'] = unicode(e) + return_obj['files'].append(output_return) + session.save() + _print_response(response, return_obj) + + +def handle_download(response, request, uri_parts=[]): + if not len(uri_parts): + response.status = apache.HTTP_BAD_REQUEST + return + if request.method != 'GET': + response.status = apache.HTTP_METHOD_NOT_ALLOWED + return + + session = Session.Session(response) + + if 'tokens' not in session: + response.status = apache.HTTP_NOT_FOUND + return + + output_id = uri_parts[0] + output_path = os.path.join(CACHEPATH, output_id) + if not os.path.exists(output_path): + response.status = apache.HTTP_NOT_FOUND + return + if output_id not in session['tokens']: + response.status = apache.HTTP_NOT_FOUND + return + with file(output_path) as output_file: + output = json.load(output_file) + if output['token'] != session['tokens'][output_id]: + response.status = apache.HTTP_NOT_FOUND + return + content = base64.b64decode(output['content']) + response.content_type = 'application/octet-stream' + response.headers_out.add( + 'Content-Disposition', 'attachment; filename=%s' % (output['name'])) + response.write(content) + +def handler(req): + # MIME type fix for error messages + req.content_type = 'text/plain' + + # we need to recover original request path, from before rewrite + orig_req = req + while True: + if orig_req.prev: + orig_req = orig_req.prev + else: + break + + uri_parts = [part for part in orig_req.uri.split('/') if part.strip()] + uri_parts = uri_parts[uri_parts.index('api')+1:] + + if not len(uri_parts): + req.status = apache.HTTP_BAD_REQUEST + else: + try: + if uri_parts[0] == 'upload': + handle_upload(req, orig_req) + elif uri_parts[0] == 'download': + handle_download(req, orig_req, uri_parts[1:]) + else: + req.status = apache.HTTP_BAD_REQUEST + except Exception as e: + req.status = apache.HTTP_INTERNAL_SERVER_ERROR + req.write(str(e)) + + return apache.OK diff --git a/http/dealconvert.js b/http/dealconvert.js new file mode 100644 index 0000000..2478fe9 --- /dev/null +++ b/http/dealconvert.js @@ -0,0 +1,143 @@ +$(document).ready(function() { + $('a.faq-btn').popover(); + $('input[name="output"]').change(function() { + if ($('input[name="output"]:checked').length > 0) { + $('#submit-btn').removeAttr('disabled'); + } else { + $('#submit-btn').attr('disabled', 'disabled'); + } + }); + $('#input-files').change(function() { + $('#submit-panel, #output-formats').collapse(this.files.length ? 'show' : 'hide'); + }); + $('#converter-input').submit(function() { + var that = $(this); + var output = []; + that.find('input[name="output"]:checked').each(function() { + output.push(this.value); + }); + var display = that.find('input[name="display"]').is(':checked'); + var files = {}; + var formFiles = this['input-files'].files; + if (formFiles.length) { + $('.output-group').remove(); + that.css('opacity', 0.3); + that.find('input, button').attr('disabled', 'disabled'); + } + var completed = 0; + for (var i = 0; i < formFiles.length; i++) { + var currentFile = formFiles[i]; + files[currentFile.name] = null; + var reader = new FileReader(); + reader.file = currentFile; + reader.onload = function(e) { + files[e.target.file.name] = btoa(e.target.result); + for (var file in files) { + if (files[file] == null) { + return; + } + } + for (var file in files) { + var paramObj = { + 'name': file, + 'content': files[file], + 'output': output, + 'display_deals': display + }; + $.ajax( + 'api/upload/', + { + data: JSON.stringify(paramObj), + dataType: 'json', + method: 'POST', + success: function(data, status, xhr) { + var outputGroup = $('template#file-output-group').clone().contents().unwrap(); + var warningTemplate = $('template#file-output-warning'); + var errorTemplate = $('template#file-output-error'); + var fileTemplate = $('template#file-output'); + var inputHeader = outputGroup.find('.file-header'); + inputHeader.text(data.name); + var groupBody = outputGroup.find('.file-body'); + if (data.error) { + inputHeader.addClass('bg-danger'); + groupBody.append(errorTemplate.clone().contents().unwrap().text(data.error)); + } else { + inputHeader.addClass('bg-success'); + if (data.warnings.length) { + inputHeader.removeClass('bg-success'); + inputHeader.addClass('bg-warning'); + for (var w = 0; w < data.warnings.length; w++) { + groupBody.append(warningTemplate.clone().contents().unwrap().text(data.warnings[w])); + } + } + for (var f = 0; f < data.files.length; f++) { + var fileContent = fileTemplate.clone().contents().unwrap(); + groupBody.append(fileContent); + fileContent.find('.file-name').text(data.files[f].name); + fileContent.find('.file-status').popover(); + if (data.files[f].error) { + fileContent.find('.file-link').remove(); + fileContent.find('.file-status').addClass('btn-danger').text('⚠️').attr('data-content', data.files[f].error); + fileContent.find('.file-name').addClass('btn-danger'); + inputHeader.removeClass('bg-success'); + inputHeader.addClass('bg-warning'); + } else { + if (data.files[f].warnings.length) { + fileContent.find('.file-status').addClass('btn-warning').text('⚠️').attr( + 'data-content', data.files[f].warnings.join("<br />")); + fileContent.find('.file-name').addClass('btn-warning'); + inputHeader.removeClass('bg-success'); + inputHeader.addClass('bg-warning'); + } else { + fileContent.find('.file-status').addClass('btn-success').text('✔️'); + fileContent.find('.file-name').addClass('btn-success'); + } + fileContent.find('.file-link').attr( + 'href', 'api/' + data.files[f].link + ); + } + } + if (data.preview) { + var boardTemplate = $('#board-preview'); + var hands = ['north', 'east', 'south', 'west']; + var suits = ['spades', 'hearts', 'diamonds', 'clubs']; + for (var b = 0; b < data.preview.length; b++) { + var board = boardTemplate.clone().contents().unwrap(); + board.find('.board-number').text(data.preview[b].number); + board.find('.board-conditions').attr('src', 'img/' + data.preview[b].conditions + '.png'); + for (var h = 0; h < hands.length; h++) { + for (var s = 0; s < suits.length; s++) { + board.find('.board-' + hands[h] + '-' + suits[s]).text(data.preview[b].hands[h][s].join('')); + } + } + outputGroup.find('.file-boards-panel .board-body').append(board); + } + } else { + outputGroup.find('.file-boards-panel').remove(); + } + } + $('body').append(outputGroup); + completed += 1; + if (completed >= formFiles.length) { + that.css('opacity', ''); + that.find('input, button').removeAttr('disabled'); + } + }, + error: function(xhr, status, error) { + var errorBox = $('<div class="container output-group"></div>'); + errorBox.append($('template#file-output-error').clone().contents().unwrap().text( + 'Nie udało się wykonać konwersji: ' + xhr.responseText + )); + $('body').append(errorBox); + that.css('opacity', ''); + that.find('input, button').removeAttr('disabled'); + } + } + ); + } + } + reader.readAsBinaryString(currentFile); + } + return false; + }); +}); diff --git a/http/img/e-ew.png b/http/img/e-ew.png Binary files differnew file mode 100644 index 0000000..bbf899f --- /dev/null +++ b/http/img/e-ew.png diff --git a/http/img/e-ns-ew.png b/http/img/e-ns-ew.png Binary files differnew file mode 100644 index 0000000..05a3b6c --- /dev/null +++ b/http/img/e-ns-ew.png diff --git a/http/img/e-ns.png b/http/img/e-ns.png Binary files differnew file mode 100644 index 0000000..2e94563 --- /dev/null +++ b/http/img/e-ns.png diff --git a/http/img/e.png b/http/img/e.png Binary files differnew file mode 100644 index 0000000..0d9304b --- /dev/null +++ b/http/img/e.png diff --git a/http/img/n-ew.png b/http/img/n-ew.png Binary files differnew file mode 100644 index 0000000..8cc1807 --- /dev/null +++ b/http/img/n-ew.png diff --git a/http/img/n-ns-ew.png b/http/img/n-ns-ew.png Binary files differnew file mode 100644 index 0000000..d45ef7e --- /dev/null +++ b/http/img/n-ns-ew.png diff --git a/http/img/n-ns.png b/http/img/n-ns.png Binary files differnew file mode 100644 index 0000000..a94eb42 --- /dev/null +++ b/http/img/n-ns.png diff --git a/http/img/n.png b/http/img/n.png Binary files differnew file mode 100644 index 0000000..cb079c4 --- /dev/null +++ b/http/img/n.png diff --git a/http/img/s-ew.png b/http/img/s-ew.png Binary files differnew file mode 100644 index 0000000..44c3e71 --- /dev/null +++ b/http/img/s-ew.png diff --git a/http/img/s-ns-ew.png b/http/img/s-ns-ew.png Binary files differnew file mode 100644 index 0000000..8cff6f8 --- /dev/null +++ b/http/img/s-ns-ew.png diff --git a/http/img/s-ns.png b/http/img/s-ns.png Binary files differnew file mode 100644 index 0000000..36fe441 --- /dev/null +++ b/http/img/s-ns.png diff --git a/http/img/s.png b/http/img/s.png Binary files differnew file mode 100644 index 0000000..a0611ac --- /dev/null +++ b/http/img/s.png diff --git a/http/img/w-ew.png b/http/img/w-ew.png Binary files differnew file mode 100644 index 0000000..b66f4f9 --- /dev/null +++ b/http/img/w-ew.png diff --git a/http/img/w-ns-ew.png b/http/img/w-ns-ew.png Binary files differnew file mode 100644 index 0000000..448abf2 --- /dev/null +++ b/http/img/w-ns-ew.png diff --git a/http/img/w-ns.png b/http/img/w-ns.png Binary files differnew file mode 100644 index 0000000..623a641 --- /dev/null +++ b/http/img/w-ns.png diff --git a/http/img/w.png b/http/img/w.png Binary files differnew file mode 100644 index 0000000..6a995ad --- /dev/null +++ b/http/img/w.png diff --git a/http/index.html b/http/index.html new file mode 100644 index 0000000..9f40cf1 --- /dev/null +++ b/http/index.html @@ -0,0 +1,254 @@ +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <title>DealConvert by mkl</title> + <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> + <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> + <script src="dealconvert.js"></script> + </head> + <body style="overflow-y: scroll"> + <div class="container"> + <div class="page-header"> + <a href="#" class="faq-btn btn btn-info float-right" data-toggle="collapse" data-trigger="hover" data-content="FAQ" data-placement="bottom" data-target="#faq">ⓘ</a> + <h1>Konwerter rozdań</h1> + <p><a href="https://github.com/emkael/deal-convert/">Deal-convert</a>, autor: <a href="https://emkael.info/">Michał Klichowicz</a></p> + </div> + </div> + <div class="container collapse mb-3" id="faq"> + <div class="card"> + <div class="card-header bg-info text-white"> + Pytania i odpowiedzi + </div> + <div class="card-body"> + <dl> + <dt>Czemu konwerter mi nie zadziałał?</dt> + <dd> + Nie wiem, prześlij mi problemowy plik z opisem błędu <a href="mailto:klichowicz.michal@gmail.com">mailem</a> albo od razu <a href="https://github.com/emkael/deal-convert/issues/">do systemu śledzenia błędów</a>.<br /> + Pliki wszystkich formatów wygenerowane poprawnie i z ciągłą numeracją rozdań od 1 (np. wygenerowane przez BigDeala) powinny konwertować się we wszystkie strony bez problemu. Nie wszystkie formaty plików są udokumentowane w sposób pozwalający obsłużyć je kompleksowo, na przykład formaty <code>DUP</code> i <code>DLM</code> (stary i nowy format Duplimate) zawierają dużo opcji, które konwerter ignoruje, a czasami wręcz wprost przyznaje, że nie wie, jak się zachować. Jeśli masz jakieś informacje, które mogłyby usprawnić wsparcie takich formatów, pisz jak wyżej. + </dd> + <dt>Czemu konwerter nie wspiera formatu X?</dt> + <dd> + Bo nie obsługiwał go BigDeal w momencie, kiedy konwerter powstawał.<br /> + Wyjątkiem są "ślepe" formaty Duplimate, które były wyodrębione w BigDeal, ale nie są obsługiwane, a różnią się od "jawnych" formatów Duplimate tylko określonymi opcjami. Oba te formaty są prawidłowo importowane, konwerter zapisuje jednak tylko do plików "jawnych".<br /> + W przyszłości rozważane jest wsparcie dla formatu <code>LIN</code>. Jeśli chcesz obsługi jakiegoś innego formatu, pisz jak wyżej. + </dd> + <dt>Czemu w pliku PBN nie ma analizy w widne?</dt> + <dd>Nie ma, pracuję nad tym. Możesz ją sobie łatwo (i szybko!) wykonać przy pomocy <a href="https://github.com/emkael/bcdd/">BCDD</a>.</dd> + <dt>Czemu dostaję mnóstwo ostrzeżeń <kbd>.xxx file format assumes consequent deal numbers from 1</kbd>?</dt> + <dd>Niektóre formaty nie przechowują numerów rozdań. W takich sytuacjach, zarówno przy imporcie, jak i eksporcie, konwerter zakłada, że rozdania mają kolejne numery, od 1. Może to doprowadzić do zmiany numeracji rozdań, jeśli oryginalny plik nie zaczynał się od rozdania nr 1 albo numeracja zawierała dziury.</dd> + <dt>Czemu mój PBN nie chce się otworzyć w BigDealu?</dt> + <dd> + BigDeal zakłada, że numeracja rozdań w pliku PBN zaczyna się od 1. Możliwy jest eksport do PBN plików, które nie spełniają tego warunku (np. formaty Duplimate robią tak powszechnie).<br /> + Jest łatwy sposób, aby poradzić sobie z tym problemem: + <ol> + <li>Wyeksportować rozkłady do plik <code>DLM</code>.</li> + <li>Odnaleźć w pliku <code>DLM</code> linię <kbd>From board=X</kbd>.</li> + <li>Zmienić ją na <kbd>From board=1</kbd>.</li> + <li>Tak spreparowany plik skonwertować do <code>PBN</code>.</li> + <li>Prawdopodobnie konwerter zgłosi błąd sumy kontrolnej, można go ignorować, jeśli nie chce się używać pośredniego pliku <code>DLM</code>.</li> + <li>Wynikowy plik <code>PBN</code> rozpoczyna się od rozdania nr 1 i jest uzupełniony rozdaniami z 13-kartowymi kolorami w każdej z rąk.</li> + </ol> + <dt>Czy to bezpieczne, tak wysyłać rozkłady do Internetu?</dt> + <dd> + Doskonałe pytanie!<br /> + Nie jestem oczywiście w stanie zapewnić, że przesyłane w ten sposób rozkłady są w 100% odporne na wyciek, ale ze strony konwertera: + <ul> + <li>zapewniam szyfrowane połączenie HTTPS</li> + <li>udostępniam <a href="https://github.com/emkael/deal-convert/">kod źródłowy całości narzędzia</a></li> + <li>nie przechowuję oryginalnych plików wejściowych na serwerze</li> + <li>pliki wynikowe przechowuję przez <strong>15 minut</strong> od momentu wygenerowania</li> + <li>pliki wynikowe dostępne są tylko w tej samej sesji przeglądarki, w której zostały wygenerowane</li> + </ul> + Ogólnie, jeśli zachować elementarne środki ostrożności, tj. niekorzystanie z konwertera w niezaufanych sieciach oraz z niezaufanych komputerów, konwerter niesie dużo mniejsze ryzyko niż np. zostawienie rozkładów na pamięci przenośnej na sali gry czy wysyłanie ich pocztą elektroniczną. + </dd> + </dl> + </div> + </div> + </div> + <div class="container"> + <form id="converter-input"> + <div class="form-group"> + <label for="input-files">Wrzuć pliki wejściowe:</label> + <input type="file" multiple="multiple" class="form-control-file" id="input-files" name="input-files"> + </div> + <div class="form-group collapse" id="output-formats"> + <label>Wybierz formaty wyjściowe:</label> + <div class="row"> + <div class="col-md-6 col-lg-4"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="output" value="ber" id="output-ber" /> + <label class="form-check-label" for="output-ber">Bernasconi (BER)</label> + </div> + </div> + <div class="col-md-6 col-lg-4"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="output" value="bhg" id="output-bhg" /> + <label class="form-check-label" for="output-bhg">Borel Hand Generator (BHG)</label> + </div> + </div> + <div class="col-md-6 col-lg-4"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="output" value="bri" id="output-bri" /> + <label class="form-check-label" for="output-bri">BRI</label> + </div> + </div> + <div class="col-md-6 col-lg-4"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="output" value="cds" id="output-cds" /> + <label class="form-check-label" for="output-cds">CDS-2000 (CDS)</label> + </div> + </div> + <div class="col-md-6 col-lg-4"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="output" value="csv" id="output-csv" /> + <label class="form-check-label" for="output-csv">CSV</label> + </div> + </div> + <div class="col-md-6 col-lg-4"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="output" value="dge" id="output-dge" /> + <label class="form-check-label" for="output-dge">DGE</label> + </div> + </div> + <div class="col-md-6 col-lg-4"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="output" value="dlm" id="output-dlm" /> + <label class="form-check-label" for="output-dlm">New Duplimate (DLM)</label> + </div> + </div> + <div class="col-md-6 col-lg-4"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="output" value="dup" id="output-dup" /> + <label class="form-check-label" for="output-dup">Old Duplimate (DUP)</label> + </div> + </div> + <div class="col-md-6 col-lg-4"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="output" value="pbn" id="output-pbn" /> + <label class="form-check-label" for="output-pbn">Portable Bridge Notation (PBN)</label> + </div> + </div> + <div class="col-md-6 col-lg-4"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" name="output" value="rzd" id="output-rzd" /> + <label class="form-check-label" for="output-rzd">KoPS (RZD)</label> + </div> + </div> + </div> + </div> + <div class="form-group collapse" id="submit-panel"> + <button type="submit" class="btn btn-primary" id="submit-btn" disabled="disabled">Konwertuj</button> + <div class="form-check-inline"> + <input class="form-check-input" type="checkbox" name="display" value="yes" id="display-boards" /> + <label class="form-check-label" for="display-boards">Wyświetl podgląd rozkładów</label> + </div> + </div> + </form> + </div> + <template id="file-output-group"> + <div class="container output-group mb-1"> + <div class="card"> + <div class="card-header file-header"></div> + <div class="card-body file-body"> + <div class="card file-boards-panel mb-2"> + <button class="btn card-header bg-primary text-white text-left" data-toggle="collapse" role="button" data-target="#boards-panel-body"> + Podgląd rozkładów + </button> + <div id="boards-panel-body" class="collapse collapsed card-body"> + <div class="board-body d-flex" style="flex-flow: row wrap; align-content: flex-start; justify-content: space-around"></div> + </div> + </div> + </div> + </div> + </div> + </template> + <template id="file-output"> + <div class="d-flex btn-group m-1"> + <a class="btn file-status" data-toggle="poppler" data-trigger="click hover" data-placement="bottom" data-html="true"></a> + <a class="btn w-100 text-left p-2 file-name"></a> + <a class="btn ml-auto btn-primary file-link">Pobierz</a> + </div> + </template> + <template id="file-output-warning"> + <div class="alert alert-warning"></div> + </template> + <template id="file-output-error"> + <div class="alert alert-danger"></div> + </template> + <template id="board-preview"> + <div class="card m-2" style="width: 20rem;"> + <div class="card-header"> + <h5 class="card-title">Rozdanie <span class="board-number"></span></h5> + </div> + <div class="card-body"> + <table class="table table-sm table-borderless m-auto"> + <tr> + <td rowspan="4" colspan="2"></td> + <td><b>♠</b></td> + <td class="board-north-spades"></td> + <td rowspan="4" colspan="2"></td> + </tr> + <tr> + <td><b class="text-danger">♥</b></td> + <td class="board-north-hearts"></td> + </tr> + <tr> + <td><b class="text-danger">♦</b></td> + <td class="board-north-diamonds"></td> + </tr> + <tr> + <td><b>♣</b></td> + <td class="board-north-clubs"></td> + </tr> + <tr> + <td><b>♠</b></td> + <td class="board-west-spades"></td> + <td rowspan="4" colspan="2" class="text-center align-middle"><img class="board-conditions" /></td> + <td><b>♠</b></td> + <td class="board-east-spades"></td> + </tr> + <tr> + <td><b class="text-danger">♥</b></td> + <td class="board-west-hearts"></td> + <td><b class="text-danger">♥</b></td> + <td class="board-east-hearts"></td> + </tr> + <tr> + <td><b class="text-danger">♦</b></td> + <td class="board-west-diamonds"></td> + <td><b class="text-danger">♦</b></td> + <td class="board-east-diamonds"></td> + </tr> + <tr> + <td><b>♣</b></td> + <td class="board-west-clubs"></td> + <td><b>♣</b></td> + <td class="board-east-clubs"></td> + </tr> + <tr> + <td rowspan="4" colspan="2"></td> + <td><b>♠</b></td> + <td class="board-south-spades"></td> + <td rowspan="4" colspan="2"></td> + </tr> + <tr> + <td><b class="text-danger">♥</b></td> + <td class="board-south-hearts"></td> + </tr> + <tr> + <td><b class="text-danger">♦</b></td> + <td class="board-south-diamonds"></td> + </tr> + <tr> + <td><b>♣</b></td> + <td class="board-south-clubs"></td> + </tr> + </table> + </div> + </div> + </template> + </body> +</html> |