clean js routine; concat script; try fix macos

This commit is contained in:
NaitLee 2022-05-04 12:07:14 +08:00
parent 002072b586
commit 1b4402c6d2
13 changed files with 157 additions and 99 deletions

View File

@ -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 ..

View File

@ -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()

View File

@ -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)

View File

@ -1 +1 @@
0.4.0
0.4.1

View File

@ -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;
}

6
www/all_js.txt Normal file
View File

@ -0,0 +1,6 @@
polyfill.js
i18n-ext.js
i18n.js
image.js
accessibility.js
main.js

View File

@ -46,7 +46,7 @@
</label> --><br />
<!-- "brightness" is historically "threshold" -->
<label for="threshold" data-i18n="brightness-">Brightness:</label>
<input type="range" min="0" max="256" value="86" step="1" id="threshold" data-key data-default />
<input type="range" min="0" max="256" value="86" step="16" id="threshold" data-key data-default />
<br />
<input type="checkbox" name="transparent-as-white" id="transparent-as-white" data-key checked />
<label for="transparent-as-white" data-i18n="transparent-as-white">Transparent as White</label>

View File

@ -26,6 +26,18 @@
</thead>
<tbody>
<tr>
<td><a href="~every.js">~every.js</a></td>
<td><a href="https://www.gnu.org/licenses/gpl-3.0.html">GPL-3.0</a></td>
<td><a href="main.js">main.js</a></td>
<td>Dynamic concatenation of all development scripts</td>
</tr>
<tr>
<td><a href="main.comp.js">main.comp.js</a></td>
<td><a href="https://www.gnu.org/licenses/gpl-3.0.html">GPL-3.0</a></td>
<td><a href="main.js">main.js</a></td>
<td>All following development scripts, transpiled for compatibility.</td>
</tr>
<tr>
<td><a href="loader.js">loader.js</a></td>
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0</a></td>
@ -33,10 +45,16 @@
<td>For dynamically loading other scripts, and fallback if there are problems.</td>
</tr>
<tr>
<td><a href="image.js">image.js</a></td>
<td><a href="polyfill.js">polyfill.js</a></td>
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0</a></td>
<td><a href="image.js">image.js</a></td>
<td>Contains functions for image manipulation and public algorithms of image monochrome filters.</td>
<td><a href="polyfill.js">polyfill.js</a></td>
<td>Put features that are not supported by old browsers.</td>
</tr>
<tr>
<td><a href="i18n-ext.js">i18n-ext.js</a></td>
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0</a></td>
<td><a href="i18n-ext.js">i18n-ext.js</a></td>
<td>I18n "extensions"</td>
</tr>
<tr>
<td><a href="i18n.js">i18n.js</a></td>
@ -45,10 +63,10 @@
<td>For internationalization (language support)</td>
</tr>
<tr>
<td><a href="i18n-ext.js">i18n-ext.js</a></td>
<td><a href="image.js">image.js</a></td>
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0</a></td>
<td><a href="i18n-ext.js">i18n-ext.js</a></td>
<td>I18n "extensions"</td>
<td><a href="image.js">image.js</a></td>
<td>For canvas image manipulation</td>
</tr>
<tr>
<td><a href="accessibility.js">accessibility.js</a></td>
@ -62,18 +80,6 @@
<td><a href="main.js">main.js</a></td>
<td>The main script for Cat-Printer</td>
</tr>
<tr>
<td><a href="polyfill.js">polyfill.js</a></td>
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0</a></td>
<td><a href="polyfill.js">polyfill.js</a></td>
<td>Put features that are not supported by old browsers.</td>
</tr>
<tr>
<td><a href="main.comp.js">main.comp.js</a></td>
<td><a href="https://www.gnu.org/licenses/gpl-3.0.html">GPL-3.0</a></td>
<td><a href="main.js">main.js</a></td>
<td>A bundle of transpiled scripts (polyfill.js, image.js, i18n.js, main.js), for compatibility to old browsers.</td>
</tr>
</tbody>
</table>

View File

@ -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"
}

View File

@ -114,5 +114,5 @@
"reset-configuration-": "要重置配置吗?",
"brightness-": "亮度:",
"text-printing-mode": "文字打印模式",
"please-enable-bluetooth-or-try-to-reboot": "请启用蓝牙或尝试重启"
"internal-error-please-see-terminal": "内部错误,请检查终端"
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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;
}