summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoremkael <emkael@tlen.pl>2019-07-08 22:45:38 +0200
committeremkael <emkael@tlen.pl>2019-07-08 22:45:38 +0200
commita0644da194f61f627535e6b72c7a6e761986498d (patch)
treec9fe545dc585320c90f547dfe70c4bb0bd8e2ca7
parent3a136318f40f607e21069afc4141a5cbcba9ccb8 (diff)
parente3e5c088687d73dc492199be071cc12d786ba00d (diff)
Merge branch 'master' of github.com:emkael/deal-convert
-rw-r--r--README.txt2
-rw-r--r--_cron/clear_cache1
-rw-r--r--cache/.gitignore1
-rw-r--r--dealconvert/__init__.py9
-rw-r--r--dealconvert/formats/__init__.py2
-rw-r--r--dealconvert/formats/ber.py2
-rw-r--r--dealconvert/formats/bhg.py4
-rw-r--r--dealconvert/formats/bri.py2
-rw-r--r--dealconvert/formats/dlm.py4
-rw-r--r--dealconvert/formats/rzd.py4
-rw-r--r--http/api/.htaccess6
-rw-r--r--http/api/api.py181
-rw-r--r--http/dealconvert.js143
-rw-r--r--http/img/e-ew.pngbin0 -> 5657 bytes
-rw-r--r--http/img/e-ns-ew.pngbin0 -> 5952 bytes
-rw-r--r--http/img/e-ns.pngbin0 -> 5674 bytes
-rw-r--r--http/img/e.pngbin0 -> 5267 bytes
-rw-r--r--http/img/n-ew.pngbin0 -> 5577 bytes
-rw-r--r--http/img/n-ns-ew.pngbin0 -> 5955 bytes
-rw-r--r--http/img/n-ns.pngbin0 -> 5731 bytes
-rw-r--r--http/img/n.pngbin0 -> 5247 bytes
-rw-r--r--http/img/s-ew.pngbin0 -> 5558 bytes
-rw-r--r--http/img/s-ns-ew.pngbin0 -> 5936 bytes
-rw-r--r--http/img/s-ns.pngbin0 -> 5688 bytes
-rw-r--r--http/img/s.pngbin0 -> 5250 bytes
-rw-r--r--http/img/w-ew.pngbin0 -> 5595 bytes
-rw-r--r--http/img/w-ns-ew.pngbin0 -> 5901 bytes
-rw-r--r--http/img/w-ns.pngbin0 -> 5602 bytes
-rw-r--r--http/img/w.pngbin0 -> 5207 bytes
-rw-r--r--http/index.html254
30 files changed, 604 insertions, 11 deletions
diff --git a/README.txt b/README.txt
index 24002b2..753d94b 100644
--- a/README.txt
+++ b/README.txt
@@ -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
new file mode 100644
index 0000000..bbf899f
--- /dev/null
+++ b/http/img/e-ew.png
Binary files differ
diff --git a/http/img/e-ns-ew.png b/http/img/e-ns-ew.png
new file mode 100644
index 0000000..05a3b6c
--- /dev/null
+++ b/http/img/e-ns-ew.png
Binary files differ
diff --git a/http/img/e-ns.png b/http/img/e-ns.png
new file mode 100644
index 0000000..2e94563
--- /dev/null
+++ b/http/img/e-ns.png
Binary files differ
diff --git a/http/img/e.png b/http/img/e.png
new file mode 100644
index 0000000..0d9304b
--- /dev/null
+++ b/http/img/e.png
Binary files differ
diff --git a/http/img/n-ew.png b/http/img/n-ew.png
new file mode 100644
index 0000000..8cc1807
--- /dev/null
+++ b/http/img/n-ew.png
Binary files differ
diff --git a/http/img/n-ns-ew.png b/http/img/n-ns-ew.png
new file mode 100644
index 0000000..d45ef7e
--- /dev/null
+++ b/http/img/n-ns-ew.png
Binary files differ
diff --git a/http/img/n-ns.png b/http/img/n-ns.png
new file mode 100644
index 0000000..a94eb42
--- /dev/null
+++ b/http/img/n-ns.png
Binary files differ
diff --git a/http/img/n.png b/http/img/n.png
new file mode 100644
index 0000000..cb079c4
--- /dev/null
+++ b/http/img/n.png
Binary files differ
diff --git a/http/img/s-ew.png b/http/img/s-ew.png
new file mode 100644
index 0000000..44c3e71
--- /dev/null
+++ b/http/img/s-ew.png
Binary files differ
diff --git a/http/img/s-ns-ew.png b/http/img/s-ns-ew.png
new file mode 100644
index 0000000..8cff6f8
--- /dev/null
+++ b/http/img/s-ns-ew.png
Binary files differ
diff --git a/http/img/s-ns.png b/http/img/s-ns.png
new file mode 100644
index 0000000..36fe441
--- /dev/null
+++ b/http/img/s-ns.png
Binary files differ
diff --git a/http/img/s.png b/http/img/s.png
new file mode 100644
index 0000000..a0611ac
--- /dev/null
+++ b/http/img/s.png
Binary files differ
diff --git a/http/img/w-ew.png b/http/img/w-ew.png
new file mode 100644
index 0000000..b66f4f9
--- /dev/null
+++ b/http/img/w-ew.png
Binary files differ
diff --git a/http/img/w-ns-ew.png b/http/img/w-ns-ew.png
new file mode 100644
index 0000000..448abf2
--- /dev/null
+++ b/http/img/w-ns-ew.png
Binary files differ
diff --git a/http/img/w-ns.png b/http/img/w-ns.png
new file mode 100644
index 0000000..623a641
--- /dev/null
+++ b/http/img/w-ns.png
Binary files differ
diff --git a/http/img/w.png b/http/img/w.png
new file mode 100644
index 0000000..6a995ad
--- /dev/null
+++ b/http/img/w.png
Binary files differ
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>