diff --git a/0-transpile.sh b/0-transpile.sh index 4af87b6..bfe388f 100755 --- a/0-transpile.sh +++ b/0-transpile.sh @@ -1,4 +1,4 @@ #!/bin/sh cd www -npx tsc --allowJs --outFile main.comp.js polyfill.js i18n-ext.js i18n.js image.js accessibility.js main.js +npx tsc $@ --allowJs --outFile main.comp.js $(cat all_js.txt) cd .. diff --git a/printer.py b/printer.py index 57f92ac..145ef4e 100644 --- a/printer.py +++ b/printer.py @@ -364,11 +364,10 @@ class PrinterDriver(Commander): elif (identifier not in Models and identifier[2::3] != ':::::' and len(identifier.replace('-', '')) != 32): error('model-0-is-not-supported-yet', identifier, exception=PrinterError) - scanner = BleakScanner() + # scanner = BleakScanner() devices = self.loop( - scanner.discover(self.scan_timeout) + BleakScanner.discover(self.scan_timeout) ) - devices = [dev for dev in devices if dev.name in Models] if identifier is not None: if identifier in Models: devices = [dev for dev in devices if dev.name == identifier] @@ -694,7 +693,7 @@ def main(): 'Run the `_main` routine while catching exceptions' try: _main() - except (BleakError, AttributeError) as e: + except BleakError as e: error_message = str(e) if ( ('not turned on' in error_message) or # windows or android @@ -702,15 +701,15 @@ def main(): getattr(e, 'dbus_error') == 'org.bluez.Error.NotReady') ): fatal(I18n['please-enable-bluetooth'], code=ExitCodes.GeneralError) - elif ( - (isinstance(e, AttributeError) and # macos, possibly? - 'CentralManagerDelegate' in error_message) - ): - fatal(I18n['please-enable-bluetooth-or-try-to-reboot'], code=ExitCodes.GeneralError) else: raise except PrinterError as e: fatal(e.message_localized, code=ExitCodes.PrinterError) + except RuntimeError as e: + if 'no running event loop' in str(e): + pass # non-sense + else: + raise if __name__ == '__main__': main() diff --git a/server.py b/server.py index 5bc5863..e1a6efa 100644 --- a/server.py +++ b/server.py @@ -46,6 +46,13 @@ def mime(url: str): 'Get pre-defined MIME type of a certain url by extension name' return mime_type.get(url.rsplit('.', 1)[-1], mime_type['octet-stream']) +def concat_files(*paths, prefix_format='', buffer=4 * 1024 * 1024): + for path in paths: + yield prefix_format.format(path).encode('utf-8') + with open(path, 'rb') as file: + while data := file.read(buffer): + yield data + class PrinterServerHandler(BaseHTTPRequestHandler): '(Local) server handler for Cat Printer Web interface' @@ -64,6 +71,7 @@ class PrinterServerHandler(BaseHTTPRequestHandler): _settings_blacklist = ( 'printer', 'is_android' ) + all_js: list = [] printer: PrinterDriver = PrinterDriver() @@ -84,17 +92,31 @@ class PrinterServerHandler(BaseHTTPRequestHandler): pass def do_GET(self): - 'Called when server get a GET http request' - path = 'www' + self.path - if self.path == '/': - path += 'index.html' - if '/..' in path: + 'Called when server got a GET http request' + # prepare + path, _, _args = self.path.partition('?') + if '/..' in path or '../' in path: return + if path == '/': + path += 'index.html' + # special + if path.startswith('/~'): + action = path[2:] + if action == 'every.js': + self.send_response(200) + self.send_header('Content-Type', mime(path)) + self.end_headers() + for data in concat_files(*(self.all_js), prefix_format='\n// {0}\n'): + self.wfile.write(data) + return + path = 'www' + path + # not found if not os.path.isfile(path): self.send_response(404) self.send_header('Content-Type', mime('txt')) self.end_headers() return + # static self.send_response(200) self.send_header('Content-Type', mime(path)) # self.send_header('Content-Size', str(os.stat(path).st_size)) @@ -214,7 +236,7 @@ class PrinterServerHandler(BaseHTTPRequestHandler): sys.exit(0) def do_POST(self): - 'Called when server get a POST http request' + 'Called when server got a POST http request' content_length = int(self.headers.get('Content-Length', -1)) if (content_length < -1 or content_length > self.max_payload @@ -239,16 +261,22 @@ class PrinterServerHandler(BaseHTTPRequestHandler): 'name': 'BleakError', 'details': str(e) }) + except EOFError as e: + # mostly, device disconnected but not by this program + self.api_fail({ + 'name': 'EOFError', + 'details': '' + }) + except RuntimeError as e: + self.api_fail({ + 'name': 'RuntimeError', + 'details': str(e) + }) except PrinterError as e: self.api_fail({ 'name': e.message, 'details': e.message_localized }) - except EOFError as e: - self.api_fail({ - 'name': 'EOFError', - 'details': '' - }) except Exception as e: self.api_fail({ 'name': 'Exception', @@ -272,6 +300,10 @@ class PrinterServer(HTTPServer): def finish_request(self, request, client_address): if self.handler is None: self.handler = self.handler_class(request, client_address, self) + with open(os.path.join('www', 'all_js.txt'), 'r', encoding='utf-8') as file: + for path in file.read().split('\n'): + if path != '': + self.handler.all_js.append(os.path.join('www', path)) return self.handler.__init__(request, client_address, self) diff --git a/version b/version index 60a2d3e..44bb5d1 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.4.0 \ No newline at end of file +0.4.1 \ No newline at end of file diff --git a/www/accessibility.js b/www/accessibility.js index 7a152fd..01b9327 100644 --- a/www/accessibility.js +++ b/www/accessibility.js @@ -39,22 +39,30 @@ function keyToLetter(key) { return map[key] || key; } +function keyFromCode(code) { + const map = { + 9: 'Tab' + }; + return map[code] || String.fromCharCode(code); +} + function initKeyboardShortcuts() { const layer = document.getElementById('keyboard-shortcuts-layer'); const dialog = document.getElementById('dialog'); const keys = 'qwertyuiopasdfghjklzxcvbnm'; let focusing = false, started = false; let shortcuts = {}; - let key, focus, inputs; + let focus, inputs; const mark_keys = () => { - let index; + document.querySelectorAll(':focus').forEach(e => e.isSameNode(focus) || e.blur()); + let index, key; if (dialog.classList.contains('hidden')) inputs = Array.from(document.querySelectorAll('*[data-key]')); else inputs = Array.from(document.querySelectorAll('#dialog *[data-key]')); /** @type {{ [key: string]: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement }} */ let keys2 = keys.split(''); shortcuts = {}; - if (focusing) shortcuts = { ESC: focus }; + if (focusing) shortcuts = { 'ESC': focus }; else for (let input of inputs) { if (isHidden(input)) continue; @@ -69,14 +77,14 @@ function initKeyboardShortcuts() { layer.appendChild(span); } index = 0; - for (let key in shortcuts) { + for (key in shortcuts) { let span = layer.children[index++]; let input = shortcuts[key]; let position = input.getBoundingClientRect(); let text = i18n(keyToLetter(key.toUpperCase())); if (span.innerText !== text) span.innerText = text; - span.style.top = position.y + 'px'; - span.style.left = position.x + 'px'; + span.style.top = (position.y || position.top) + 'px'; + span.style.left = (position.x || position.left) + 'px'; span.style.display = ''; } for (let i = index; i < layer.children.length; i++) { @@ -86,17 +94,15 @@ function initKeyboardShortcuts() { const start = () => setInterval(mark_keys, 1000); const types_to_click = ['submit', 'file', 'checkbox', 'radio', 'A']; document.body.addEventListener('keyup', (event) => { - key = event.key; + let key = event.key || keyFromCode(event.keyCode); if (!started) { if (key !== 'Tab') return; mark_keys(); start(); started = true; } - document.body.addEventListener('keyup', () => - requestAnimationFrame(mark_keys) - , { once: true }); - let input = shortcuts[key.toLocaleLowerCase()]; + requestAnimationFrame(mark_keys) + let input = shortcuts[key]; if (input) { if (types_to_click.includes(input.type || input.tagName)) input.dispatchEvent(new MouseEvent(event.shiftKey ? 'contextmenu' : 'click')); @@ -105,7 +111,7 @@ function initKeyboardShortcuts() { focusing = true; } focus = input; - } else if (key === 'Escape' && focus) { + } else if ((key === 'Escape' || !event.isTrusted) && focus) { focus.blur(); focusing = !focusing; } diff --git a/www/all_js.txt b/www/all_js.txt new file mode 100644 index 0000000..4c34244 --- /dev/null +++ b/www/all_js.txt @@ -0,0 +1,6 @@ +polyfill.js +i18n-ext.js +i18n.js +image.js +accessibility.js +main.js diff --git a/www/index.html b/www/index.html index 1d81dcf..c9dc057 100644 --- a/www/index.html +++ b/www/index.html @@ -46,7 +46,7 @@ -->
- +
diff --git a/www/jslicense.html b/www/jslicense.html index b6e8c8a..d75caa5 100644 --- a/www/jslicense.html +++ b/www/jslicense.html @@ -26,6 +26,18 @@ + + ~every.js + GPL-3.0 + main.js + Dynamic concatenation of all development scripts + + + main.comp.js + GPL-3.0 + main.js + All following development scripts, transpiled for compatibility. + loader.js CC0-1.0 @@ -33,10 +45,16 @@ For dynamically loading other scripts, and fallback if there are problems. - image.js + polyfill.js CC0-1.0 - image.js - Contains functions for image manipulation and public algorithms of image monochrome filters. + polyfill.js + Put features that are not supported by old browsers. + + + i18n-ext.js + CC0-1.0 + i18n-ext.js + I18n "extensions" i18n.js @@ -45,10 +63,10 @@ For internationalization (language support) - i18n-ext.js + image.js CC0-1.0 - i18n-ext.js - I18n "extensions" + image.js + For canvas image manipulation accessibility.js @@ -62,18 +80,6 @@ main.js The main script for Cat-Printer - - polyfill.js - CC0-1.0 - polyfill.js - Put features that are not supported by old browsers. - - - main.comp.js - GPL-3.0 - main.js - A bundle of transpiled scripts (polyfill.js, image.js, i18n.js, main.js), for compatibility to old browsers. - diff --git a/www/lang/en-US.json b/www/lang/en-US.json index f78279c..82c3566 100644 --- a/www/lang/en-US.json +++ b/www/lang/en-US.json @@ -119,5 +119,5 @@ "reset-configuration-": "Reset configuration?", "brightness-": "Brightness:", "text-printing-mode": "Text Printing Mode", - "please-enable-bluetooth-or-try-to-reboot": "Please enable bluetooth or try to reboot" + "internal-error-please-see-terminal": "Internal error, please see terminal" } \ No newline at end of file diff --git a/www/lang/zh-CN.json b/www/lang/zh-CN.json index 9e26846..f83ecb7 100644 --- a/www/lang/zh-CN.json +++ b/www/lang/zh-CN.json @@ -114,5 +114,5 @@ "reset-configuration-": "要重置配置吗?", "brightness-": "亮度:", "text-printing-mode": "文字打印模式", - "please-enable-bluetooth-or-try-to-reboot": "请启用蓝牙或尝试重启" + "internal-error-please-see-terminal": "内部错误,请检查终端" } \ No newline at end of file diff --git a/www/loader.js b/www/loader.js index eb6e641..ed8c8a8 100644 --- a/www/loader.js +++ b/www/loader.js @@ -4,12 +4,11 @@ */ (function() { - var fallbacks = [ - // main scripts, which we will directly modify - 'i18n-ext.js', 'i18n.js', 'image.js', 'accessibility.js', 'main.js', - // "compatibility" script, produced with eg. typescript tsc - 'main.comp.js' - ]; + var fallbacks; + if (location.href.indexOf('?debug') !== -1) + fallbacks = ['i18n-ext.js', 'i18n.js', 'image.js', 'accessibility.js', 'main.js']; + else + fallbacks = ['~every.js', 'main.comp.js']; var trial_count = 0; /** * Try to load next "fallback" script, @@ -21,6 +20,7 @@ var script = document.createElement('script'); script.addEventListener('load', function() { if (typeof main === 'undefined') { + // the script can't be 'unrun', though script.remove(); try_load(); } else { diff --git a/www/main.css b/www/main.css index 2319d63..1bbb10f 100644 --- a/www/main.css +++ b/www/main.css @@ -82,7 +82,7 @@ input[type="number"], input[type="text"] { } button:hover { margin: 0; - padding: var(--span) calc(var(--span-double)); + padding: var(--span) var(--span-double); min-width: calc(6em + var(--span-double)); } button:active { diff --git a/www/main.js b/www/main.js index 3269b01..40485c5 100644 --- a/www/main.js +++ b/www/main.js @@ -418,7 +418,7 @@ function applyI18nToDom(doc) { element.firstChild.textContent = translated_string; }); } -async function initI18n() { +async function initI18n(current_language) { if (typeof i18n === 'undefined') return; /** @type {HTMLSelectElement} */ let language_options = document.getElementById('select-language'); @@ -429,9 +429,6 @@ async function initI18n() { i18n.add(value, await fetch(`/lang/${value}.json`).then(r => r.json()), true); applyI18nToDom(); } - language_options.addEventListener('change', () => { - language_options.selectedOptions.item(0).click(); - }); for (let code in list) { let option = document.createElement('option'); option.value = code; @@ -440,35 +437,28 @@ async function initI18n() { /** @type {HTMLOptionElement} */ let option = event.currentTarget; let value = option.value; - // option.selected = true; + option.selected = true; language_options.selectedIndex = option.index; use_language(value); Notice.note('welcome'); }); language_options.appendChild(option); } - apply_default: - for (let code of navigator.languages) { - if (list[code]) { - for (let option of language_options.children) { - if (option.value === code) { - // option.setAttribute('data-default', ''); - option.setAttribute('data-default', ''); - option.click(); - i18n.useLanguage(navigator.languages[0]); - for (let language of navigator.languages) { - if (!list[language]) return; - let data = await fetch(`/lang/${language}.json`) - .then(response => response.ok ? response.json() : null); - if (data !== null) { - i18n.add(language, data); - } - } - break apply_default; - } - } - } + if (!navigator.languages) { + if (!navigator.language) return; + else navigator.languages = [navigator.language, 'en-US']; } + if (current_language) { + for (let option of language_options.children) + if (option.value === current_language) + option.click(); + } else for (let code of navigator.languages) + if (list[code]) for (let option of language_options.children) + if (option.value === code) { + option.setAttribute('data-default', ''); + if (!current_language) option.click(); + return; + } } async function testI18n(lang) { @@ -500,14 +490,18 @@ class Main { this.setters = {}; // window.addEventListener('unload', () => this.exit()); this.promise = new Promise(async (resolve, reject) => { - await initI18n(); /** @type {HTMLIFrameElement} */ let iframe = document.getElementById('frame'); iframe.addEventListener('load', () => { + if (!iframe.contentWindow.NodeList.prototype.forEach) + iframe.contentWindow.NodeList.prototype.forEach = NodeList.prototype.forEach; iframe.contentDocument.body.classList.value = document.body.classList.value; iframe.contentDocument.body.addEventListener('keyup', (event) => { - if (event.key === 'Escape') - document.body.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' })); + if (event.key === 'Escape' || event.keyCode === 27) { + document.body.dispatchEvent( + new KeyboardEvent('keyup', { key: 'Escape', keyCode: 27 }) + ); + } }); applyI18nToDom(iframe.contentDocument); }); @@ -517,6 +511,10 @@ class Main { d.body.classList.remove(class_name) ); } + + await this.loadConfig(); + await initI18n(this.settings['language']); + this.canvasController = new CanvasController(); putEvent('#button-exit', 'click', () => this.exit(false), this); putEvent('#button-exit', 'contextmenu', @@ -531,7 +529,6 @@ class Main { (value) => this.settings['text_mode'] = (value === 'algo-direct') ); this.attachSetter('#transparent-as-white', 'change', 'transparent_as_white'); - this.attachSetter('#select-language option', 'click', 'language'); this.attachSetter('#dry-run', 'change', 'dry_run', (checked) => checked && Notice.note('dry-run-test-print-process-only') ); @@ -556,7 +553,10 @@ class Main { this.attachSetter('#flip-h', 'change', 'flip_h'); this.attachSetter('#flip-v', 'change', 'flip_v'); this.attachSetter('#dump', 'change', 'dump'); - await this.loadConfig(); + await this.activateConfig(); + // one exception + this.attachSetter('#select-language option', 'click', 'language'); + if (this.settings['is_android']) { // Android doesn't work well with select[multiple] let div = document.createElement('div'); @@ -583,13 +583,19 @@ class Main { else return null; } /** - * Load saved config from server, and activate all setters with corresponding values in settings. - * Please do `attachSetter` on all desired elements/inputs before calling. - * After the load, will save config to server again in order to sync default values. - * Then, if permitted, every single change will sync to server instantly + * Load saved config from server */ async loadConfig() { this.settings = await callApi('/query'); + } + /** + * Activate all setters with corresponding values in settings. + * Before calling, please first loadConfig & do `attachSetter` on all desired elements/inputs. + * After the load, will save config to server again in order to sync default values. + * Then, if permitted, every single change will sync to server instantly + */ + async activateConfig() { + this.allowSet = false; if (this.settings['first_run']) Dialog.alert('#accessibility', () => this.set({ first_run: false })); for (let key in this.settings) { @@ -627,7 +633,7 @@ class Main { * @param {(value: any) => any} callback Optional additinal post-procedure to call, with a *reasonable* value as parameter */ attachSetter(selector, type, attribute, callback) { - this.setters[attribute] = putEvent(selector, type, (event => { + this.setters[attribute] = putEvent(selector, type, event => { event.stopPropagation(); event.cancelBubble = true; let input = event.currentTarget; @@ -647,7 +653,7 @@ class Main { this.settings[attribute] = value; this.set({ [attribute]: value }); return callback ? callback(value) : undefined; - }).bind(this), this); + }, this); } async exit(reset) { Notice.wait('exiting'); @@ -674,6 +680,9 @@ class Main { error_details.details.includes('not turned on') || error_details.details.includes('WinError -2147020577') ) Notice.warn('please-enable-bluetooth'); + else if ( + error_details.details.includes('no running event loop') + ) Notice.error('internal-error-please-see-terminal'); else throw new Error('Unknown Bluetooth Problem'); return null; }