mirror of
https://github.com/NaitLee/Cat-Printer.git
synced 2025-05-15 23:00:15 -07:00
Major code clean up; new features:
Test unknown devices (scan everything) Rework of Web FE Event & Dispatch Fix major performance problem Better image processing algorithm New Halftone-like dither algo., namingly "Pattern" Some fixes to CSS Fix potential misc problems by manual testing
This commit is contained in:
parent
502a572183
commit
500971ae95
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,6 +7,8 @@ www/main.comp.js
|
||||
www/vconsole.js
|
||||
# https://github.com/delight-im/Android-AdvancedWebView
|
||||
build-android/advancedwebview
|
||||
# cd wasm && npm install
|
||||
wasm/node_modules
|
||||
# python bytecode
|
||||
*.pyc
|
||||
# releases
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,4 +1,5 @@
|
||||
{
|
||||
"js/ts.implicitProjectConfig.strictNullChecks": false,
|
||||
"js/ts.implicitProjectConfig.checkJs": false
|
||||
"js/ts.implicitProjectConfig.checkJs": false,
|
||||
"js/ts.implicitProjectConfig.experimentalDecorators": true
|
||||
}
|
17
README.md
17
README.md
@ -4,17 +4,14 @@ English | [Deutsch](./readme.i18n/README.de_DE.md) | [简体中文](./readme.i18
|
||||
|
||||
🐱🖨 A project that provides support to some Bluetooth "Cat Printer" models, on *many* platforms!
|
||||
|
||||
[](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)
|
||||
[](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7)
|
||||
|
||||
## Models
|
||||
|
||||
Currently:
|
||||
Known to support: `GB0X, GT01, YT01` (`X` represents any digit)
|
||||
|
||||
| | |
|
||||
|----|----|
|
||||
| Known to support | `GB0X, GT01, YT01` |
|
||||
|
||||
\* `X` represents any digit
|
||||
You can test other models with the Web UI, in `Settings -> Test Unknown Device`
|
||||
It may work!
|
||||
|
||||
## Features
|
||||
|
||||
@ -63,11 +60,15 @@ Get the newest apk release and install, then well done!
|
||||
It may ask for background location permission, you can deny it safely.
|
||||
(Foreground) Location permission is required for scanning Bluetooth devices in newer Android system.
|
||||
|
||||
Recommend to set scan time to 1 second.
|
||||
|
||||
### Windows
|
||||
|
||||
Get the newest release archive with "windows" in the file name,
|
||||
extract to somewhere and run `start.bat`
|
||||
|
||||
Windows typically needs longer scan time. Defaults to 4 seconds, try to find your case.
|
||||
|
||||
### GNU/Linux
|
||||
|
||||
You can get the "pure" release, extract it, fire a terminal inside and run:
|
||||
@ -75,6 +76,8 @@ You can get the "pure" release, extract it, fire a terminal inside and run:
|
||||
python3 server.py
|
||||
```
|
||||
|
||||
Recommend to set scan time to 2 seconds.
|
||||
|
||||
On Arch Linux based distros you may first install `bluez`, as it's often missing
|
||||
```bash
|
||||
sudo pacman -S bluez bluez-utils
|
||||
|
@ -184,3 +184,15 @@ Tried to "fix" it, used at least 4 hours, finally found it's a matter of didn't
|
||||
So the Internet JavaScript memes are damned true.
|
||||
https://programmerhumor.io/javascript-memes/why-is-it-like-this-2/
|
||||
https://programmerhumor.io/javascript-memes/sorry-dad-_-2/
|
||||
|
||||
14th
|
||||
|
||||
... How silly is the above approach. This time I simply changed to Uint32Array. That became much more trivial.
|
||||
|
||||
So, I've re-written the image processing "lib". I wanted to go for WebAssembly (with AssemblyScript), so made it separate (in dir `wasm`).
|
||||
After finish, it really worked -- but it's ~100% slower than the equivalent JavaScript (`asc` versus `tsc`)
|
||||
And that may involve unacceptable change to scripting structure (ESModule etc.), thus Wasm was given up.
|
||||
But hey, in this rewrite some algorithm practial overhead was removed, thus much more efficient! Enjoy the blazing speed!
|
||||
|
||||
In main.js the event handler was reworked too. No more double event dispatches.
|
||||
Thanks to this, another image processing performance problem is fixed.
|
||||
|
20
printer.py
20
printer.py
@ -270,7 +270,7 @@ class PrinterDriver(Commander):
|
||||
model: Model = None
|
||||
'The printer model'
|
||||
|
||||
scan_timeout: float = 4.0
|
||||
scan_time: float = 4.0
|
||||
|
||||
connection_timeout : float = 5.0
|
||||
|
||||
@ -351,15 +351,19 @@ class PrinterDriver(Commander):
|
||||
self.device.start_notify(self.rx_characteristic, notify)
|
||||
)
|
||||
|
||||
def scan(self, identifier: str=None, *, use_result=False):
|
||||
def scan(self, identifier: str=None, *, use_result=False, everything=False):
|
||||
''' Scan for supported devices, optionally filter with `identifier`,
|
||||
which can be device model (bluetooth name), and optionally MAC address, after a comma.
|
||||
If `use_result` is True, connect to the first available device to driver instantly.
|
||||
If `everything` is True, return all bluetooth devices found.
|
||||
Note: MAC address doesn't work on Apple MacOS. In place with it,
|
||||
You need an UUID of BLE device dynamically given by MacOS.
|
||||
'''
|
||||
if self.fake:
|
||||
return
|
||||
return []
|
||||
if everything:
|
||||
devices = self.loop(BleakScanner.discover(self.scan_time))
|
||||
return devices
|
||||
if identifier:
|
||||
if identifier.find(',') != -1:
|
||||
name, address = identifier.split(',')
|
||||
@ -370,12 +374,12 @@ class PrinterDriver(Commander):
|
||||
if use_result:
|
||||
self.connect(name, address)
|
||||
return [BLEDevice(address, name)]
|
||||
elif (identifier not in Models and
|
||||
if (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()
|
||||
devices = [x for x in self.loop(
|
||||
BleakScanner.discover(self.scan_timeout)
|
||||
BleakScanner.discover(self.scan_time)
|
||||
) if x.name in Models]
|
||||
if identifier:
|
||||
if identifier in Models:
|
||||
@ -397,7 +401,7 @@ class PrinterDriver(Commander):
|
||||
self.scan(identifier, use_result=True)
|
||||
if self.device is None and not self.fake:
|
||||
error('no-available-devices-found', exception=PrinterError)
|
||||
if mode == 'pbm' or mode == 'default':
|
||||
if mode in ('pbm', 'default'):
|
||||
printer_data = PrinterData(self.model.paper_width, file)
|
||||
self._print_bitmap(printer_data)
|
||||
elif mode == 'text':
|
||||
@ -443,7 +447,7 @@ class PrinterDriver(Commander):
|
||||
if self.quality: # well, slower makes stable heating
|
||||
self.set_speed(self.quality)
|
||||
if self.energy is not None:
|
||||
self.set_energy(self.energy * 0xff)
|
||||
self.set_energy(self.energy * 0x100)
|
||||
self.apply_energy()
|
||||
self.update_device()
|
||||
self.flush()
|
||||
@ -648,7 +652,7 @@ def _main():
|
||||
printer = PrinterDriver()
|
||||
|
||||
scan_param = args.scan.split(',')
|
||||
printer.scan_timeout = float(scan_param[0])
|
||||
printer.scan_time = float(scan_param[0])
|
||||
identifier = ','.join(scan_param[1:])
|
||||
if args.energy is not None:
|
||||
printer.energy = int(args.energy * 0xff)
|
||||
|
@ -137,7 +137,7 @@ class Commander(metaclass=ABCMeta):
|
||||
|
||||
def set_energy(self, amount: int):
|
||||
''' Set thermal energy, max to `0xffff`
|
||||
By default, it's seems around `0x3000`, aka 1 / 5.
|
||||
By default, it's seems around `0x3000` (1 / 5)
|
||||
'''
|
||||
self.send( self.make_command(0xaf, int_to_bytes(amount)) )
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
🐱🖨 Ein Projekt, das Unterstützung für einige Bluetooth-"Cat Printer"-Modelle auf *vielen* Plattformen bietet!
|
||||
|
||||
[](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)
|
||||
[](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7)
|
||||
|
||||
## unterstützte Geräte
|
||||
|
||||
|
@ -3,17 +3,14 @@
|
||||
|
||||
🐱🖨 猫咪打印机:此应用*跨平台地*对一些蓝牙“喵喵机”提供支持!
|
||||
|
||||
[](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)
|
||||
[](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7)
|
||||
|
||||
## 型号
|
||||
|
||||
目前有:
|
||||
已知支持:`GB0X, GT01, YT01` (`X` 表示任意数字)
|
||||
|
||||
| | |
|
||||
|----|----|
|
||||
| 已知支持 | `GB0X, GT01, YT01` |
|
||||
|
||||
\* `X` 表示任意数字
|
||||
可在 Web 界面测试未列出的型号。在 `设置 -> 测试未知设备`
|
||||
有概率成功!
|
||||
|
||||
## 特性
|
||||
|
||||
@ -60,11 +57,15 @@
|
||||
应用可能请求“后台位置”权限,您可以拒绝它。
|
||||
(前台)位置权限是较新版安卓系统扫描蓝牙设备所需要的。
|
||||
|
||||
建议将扫描时间设为 1 秒。
|
||||
|
||||
### Windows
|
||||
|
||||
获取名称中有 "windows" 的版本,
|
||||
解压并运行 `start.bat`
|
||||
|
||||
Windows 通常需要较长的扫描时间。默认为 4 秒,可按需调整。
|
||||
|
||||
### GNU/Linux
|
||||
|
||||
您可以获取“纯净(pure)”版,解压、在其中打开终端并输入:
|
||||
@ -72,6 +73,8 @@
|
||||
python3 server.py
|
||||
```
|
||||
|
||||
建议将扫描时间设为 2 秒。
|
||||
|
||||
在 Arch Linux 等发行版您可能需要首先安装 `bluez`
|
||||
```bash
|
||||
sudo pacman -S bluez bluez-utils
|
||||
|
16
server.py
16
server.py
@ -49,6 +49,7 @@ mime_type = {
|
||||
'json': 'application/json;charset=utf-8',
|
||||
'png': 'image/png',
|
||||
'svg': 'image/svg+xml;charset=utf-8',
|
||||
'wasm': 'application/wasm',
|
||||
'octet-stream': 'application/octet-stream'
|
||||
}
|
||||
def mime(url: str):
|
||||
@ -72,10 +73,10 @@ class PrinterServerHandler(BaseHTTPRequestHandler):
|
||||
|
||||
settings = DictAsObject({
|
||||
'config_path': 'config.json',
|
||||
'version': 2,
|
||||
'version': 3,
|
||||
'first_run': True,
|
||||
'is_android': False,
|
||||
'scan_timeout': 4.0,
|
||||
'scan_time': 4.0,
|
||||
'dry_run': False,
|
||||
'energy': 0.2
|
||||
})
|
||||
@ -193,11 +194,13 @@ class PrinterServerHandler(BaseHTTPRequestHandler):
|
||||
def update_printer(self):
|
||||
'Update `PrinterDriver` state/config'
|
||||
self.printer.dry_run = self.settings.dry_run
|
||||
self.printer.scan_timeout = self.settings.scan_timeout
|
||||
self.printer.scan_time = self.settings.scan_time
|
||||
self.printer.fake = self.settings.fake
|
||||
self.printer.dump = self.settings.dump
|
||||
self.printer.energy = self.settings.energy
|
||||
self.printer.quality = self.settings.quality
|
||||
if self.settings.energy is not None:
|
||||
self.printer.energy = int(self.settings.energy)
|
||||
if self.settings.quality is not None:
|
||||
self.printer.quality = int(self.settings.quality)
|
||||
self.printer.flip_h = self.settings.flip_h or self.settings.flip
|
||||
self.printer.flip_v = self.settings.flip_v or self.settings.flip
|
||||
self.printer.rtl = self.settings.force_rtl
|
||||
@ -218,7 +221,7 @@ class PrinterServerHandler(BaseHTTPRequestHandler):
|
||||
devices_list = [{
|
||||
'name': device.name,
|
||||
'address': device.address
|
||||
} for device in self.printer.scan()]
|
||||
} for device in self.printer.scan(everything=data.get('everything'))]
|
||||
self.api_success({
|
||||
'devices': devices_list
|
||||
})
|
||||
@ -313,6 +316,7 @@ class PrinterServer(HTTPServer):
|
||||
def finish_request(self, request, client_address):
|
||||
if self.handler is None:
|
||||
self.handler = self.handler_class(request, client_address, self)
|
||||
self.handler.load_config()
|
||||
with open(os.path.join('www', 'all_js.txt'), 'r', encoding='utf-8') as file:
|
||||
for path in file.read().split('\n'):
|
||||
if path != '':
|
||||
|
6
wasm/0-compile.sh
Executable file
6
wasm/0-compile.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
if [[ $1 == 1 ]]; then
|
||||
npm run asbuild:release;
|
||||
else
|
||||
npm run asbuild:debug;
|
||||
fi
|
24
wasm/asconfig.json
Normal file
24
wasm/asconfig.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"targets": {
|
||||
"debug": {
|
||||
"outFile": "../www/image-wasm.wasm",
|
||||
"textFile": "../www/image-wasm.wat",
|
||||
"sourceMap": true,
|
||||
"debug": true
|
||||
},
|
||||
"release": {
|
||||
"outFile": "../www/image-wasm.wasm",
|
||||
"textFile": "../www/image-wasm.wat",
|
||||
"sourceMap": true,
|
||||
"optimizeLevel": 3,
|
||||
"shrinkLevel": 0,
|
||||
"converge": false,
|
||||
"noAssert": false
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"exportRuntime": true,
|
||||
"baseDir": ".",
|
||||
"bindings": "esm"
|
||||
}
|
||||
}
|
115
wasm/image.ts
Normal file
115
wasm/image.ts
Normal file
@ -0,0 +1,115 @@
|
||||
|
||||
export function monoGrayscale(rgba: Uint32Array, brightness: i32, alpha_as_white: bool): Uint8ClampedArray {
|
||||
let mono = new Uint8ClampedArray(rgba.length);
|
||||
let r: f32 = 0.0, g: f32 = 0.0, b: f32 = 0.0, a: f32 = 0.0, m: f32 = 0.0, n: i32 = 0;
|
||||
for (let i: i32 = 0; i < mono.length; ++i) {
|
||||
n = rgba[i];
|
||||
// little endian
|
||||
r = <f32>(n & 0xff), g = <f32>(n >> 8 & 0xff), b = <f32>(n >> 16 & 0xff);
|
||||
a = <f32>(n >> 24 & 0xff) / 0xff;
|
||||
if (a < 1 && alpha_as_white) {
|
||||
a = 1 - a;
|
||||
r += (0xff - r) * a;
|
||||
g += (0xff - g) * a;
|
||||
b += (0xff - b) * a;
|
||||
} else { r *= a; g *= a; b *= a; }
|
||||
m = r * 0.2125 + g * 0.7154 + b * 0.0721;
|
||||
m += <f32>(brightness - 0x80) * (<f32>1 - m / 0xff) * (m / 0xff) * 2;
|
||||
mono[i] = <u8>m;
|
||||
}
|
||||
return mono;
|
||||
}
|
||||
|
||||
/** Note: returns a `Uint32Array` */
|
||||
export function monoToRgba(mono: Uint8ClampedArray): Uint32Array {
|
||||
let rgba = new Uint32Array(mono.length);
|
||||
for (let i: i32 = 0; i < mono.length; ++i) {
|
||||
// little endian
|
||||
rgba[i] = 0xff000000 | (mono[i] << 16) | (mono[i] << 8) | mono[i];
|
||||
}
|
||||
return rgba;
|
||||
}
|
||||
|
||||
export function monoDirect(mono: Uint8ClampedArray, w: i32, h:i32): Uint8ClampedArray {
|
||||
for (let i: i32 = 0; i < mono.length; ++i) {
|
||||
mono[i] = mono[i] > 0x80 ? 0xff : 0x00;
|
||||
}
|
||||
return mono;
|
||||
}
|
||||
|
||||
export function monoSteinberg(mono: Uint8ClampedArray, w: i32, h: i32): Uint8ClampedArray {
|
||||
let p: i32 = 0, m: i32, n: i32, o: i32;
|
||||
for (let j: i32 = 0; j < h; ++j) {
|
||||
for (let i: i32 = 0; i < w; ++i) {
|
||||
m = mono[p];
|
||||
n = mono[p] > 0x80 ? 0xff : 0x00;
|
||||
o = m - n;
|
||||
mono[p] = n;
|
||||
if (i >= 0 && i < w - 1 && j >= 0 && j < h)
|
||||
mono[p + 1] += <u8>(o * 7 / 16);
|
||||
if (i >= 1 && i < w && j >= 0 && j < h - 1)
|
||||
mono[p + w - 1] += <u8>(o * 3 / 16);
|
||||
if (i >= 0 && i < w && j >= 0 && j < h - 1)
|
||||
mono[p + w] += <u8>(o * 5 / 16);
|
||||
if (i >= 0 && i < w - 1 && j >= 0 && j < h - 1)
|
||||
mono[p + w + 1] += <u8>(o * 1 / 16);
|
||||
++p;
|
||||
}
|
||||
}
|
||||
return mono;
|
||||
}
|
||||
|
||||
export function monoHalftone(mono: Uint8ClampedArray, w: i32, h: i32): Uint8ClampedArray {
|
||||
const spot: i32 = 4;
|
||||
const spot_h: i32 = spot / 2 + 1;
|
||||
const spot_d: i32 = spot * 2;
|
||||
const spot_s: i32 = spot * spot;
|
||||
let i: i32, j: i32, x: i32, y: i32, o: f64 = 0.0;
|
||||
for (j = 0; j < h - spot; j += spot) {
|
||||
for (i = 0; i < w - spot; i += spot) {
|
||||
for (x = 0; x < spot; ++x)
|
||||
for (y = 0; y < spot; ++y)
|
||||
o += mono[(j + y) * w + i + x];
|
||||
o = (1 - o / spot_s / 0xff) * spot;
|
||||
for (x = 0; x < spot; ++x)
|
||||
for (y = 0; y < spot; ++y) {
|
||||
mono[(j + y) * w + i + x] = Math.abs(x - spot_h) >= o || Math.abs(y - spot_h) >= o ? 0xff : 0x00;
|
||||
// mono[(j + y) * w + i + x] = Math.abs(x - spot_h) + Math.abs(y - spot_h) >= o ? 0xff : 0x00;
|
||||
}
|
||||
}
|
||||
for (; i < w; ++i) mono[j * w + i] = 0xff;
|
||||
}
|
||||
for (; j < h; ++j)
|
||||
for (i = 0; i < w; ++i) mono[j * w + i] = 0xff;
|
||||
return mono;
|
||||
}
|
||||
|
||||
export function monoToPbm(data: Uint8ClampedArray): Uint8ClampedArray {
|
||||
let length: i32 = (data.length / 8) | 0;
|
||||
let result = new Uint8ClampedArray(length);
|
||||
for (let i: i32 = 0, p: i32 = 0; i < data.length; ++p) {
|
||||
result[p] = 0;
|
||||
for (let d: u8 = 0; d < 8; ++i, ++d)
|
||||
result[p] |= data[i] & (0b10000000 >> d);
|
||||
result[p] ^= 0b11111111;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Note: takes & gives `Uint32Array` */
|
||||
export function rotateRgba(before: Uint32Array, w: i32, h: i32): Uint32Array {
|
||||
/**
|
||||
* w h
|
||||
* o------+ +---o
|
||||
* h | | | | w
|
||||
* +------+ | | after
|
||||
* before +---+
|
||||
*/
|
||||
let after = new Uint32Array(before.length);
|
||||
for (let j: i32 = 0; j < h; j++) {
|
||||
for (let i: i32 = 0; i < w; i++) {
|
||||
after[j * w + i] = before[(w - i - 1) * h + j];
|
||||
}
|
||||
}
|
||||
return after;
|
||||
}
|
64
wasm/package-lock.json
generated
Normal file
64
wasm/package-lock.json
generated
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "wasm",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"assemblyscript": "^0.20.13"
|
||||
}
|
||||
},
|
||||
"node_modules/assemblyscript": {
|
||||
"version": "0.20.13",
|
||||
"resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.20.13.tgz",
|
||||
"integrity": "sha512-F4ACXdBdXCBnPEzRCl/ovFFPGmKXQPvWW4cM6U21eRezaCoREqxA0hjSUOYTz7txY3MJclyBfjwMSEJ5e9HKsw==",
|
||||
"dependencies": {
|
||||
"binaryen": "108.0.0-nightly.20220528",
|
||||
"long": "^5.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"asc": "bin/asc.js",
|
||||
"asinit": "bin/asinit.js"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/assemblyscript"
|
||||
}
|
||||
},
|
||||
"node_modules/binaryen": {
|
||||
"version": "108.0.0-nightly.20220528",
|
||||
"resolved": "https://registry.npmjs.org/binaryen/-/binaryen-108.0.0-nightly.20220528.tgz",
|
||||
"integrity": "sha512-9biG357fx3NXmJNotIuY9agZBcCNHP7d1mgOGaTlPYVHZE7/61lt1IyHCXAL+W5jUOYgmFZ260PR4IbD19RKuA==",
|
||||
"bin": {
|
||||
"wasm-opt": "bin/wasm-opt",
|
||||
"wasm2js": "bin/wasm2js"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
|
||||
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"assemblyscript": {
|
||||
"version": "0.20.13",
|
||||
"resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.20.13.tgz",
|
||||
"integrity": "sha512-F4ACXdBdXCBnPEzRCl/ovFFPGmKXQPvWW4cM6U21eRezaCoREqxA0hjSUOYTz7txY3MJclyBfjwMSEJ5e9HKsw==",
|
||||
"requires": {
|
||||
"binaryen": "108.0.0-nightly.20220528",
|
||||
"long": "^5.2.0"
|
||||
}
|
||||
},
|
||||
"binaryen": {
|
||||
"version": "108.0.0-nightly.20220528",
|
||||
"resolved": "https://registry.npmjs.org/binaryen/-/binaryen-108.0.0-nightly.20220528.tgz",
|
||||
"integrity": "sha512-9biG357fx3NXmJNotIuY9agZBcCNHP7d1mgOGaTlPYVHZE7/61lt1IyHCXAL+W5jUOYgmFZ260PR4IbD19RKuA=="
|
||||
},
|
||||
"long": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz",
|
||||
"integrity": "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="
|
||||
}
|
||||
}
|
||||
}
|
10
wasm/package.json
Normal file
10
wasm/package.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"assemblyscript": "^0.20.13"
|
||||
},
|
||||
"scripts": {
|
||||
"asbuild:debug": "asc image.ts --target debug && tsc",
|
||||
"asbuild:release": "asc image.ts --target release && tsc",
|
||||
"asbuild": "npm run asbuild:release"
|
||||
}
|
||||
}
|
12
wasm/tsconfig.json
Normal file
12
wasm/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "assemblyscript/std/assembly.json",
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"outDir": "../www/",
|
||||
"module": "None",
|
||||
"declaration": true
|
||||
}
|
||||
}
|
9
www/image.d.ts
vendored
Normal file
9
www/image.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
export declare function monoGrayscale(rgba: Uint32Array, brightness: i32, alpha_as_white: bool): Uint8ClampedArray;
|
||||
/** Note: returns a `Uint32Array` */
|
||||
export declare function monoToRgba(mono: Uint8ClampedArray): Uint32Array;
|
||||
export declare function monoDirect(mono: Uint8ClampedArray, w: i32, h: i32): Uint8ClampedArray;
|
||||
export declare function monoSteinberg(mono: Uint8ClampedArray, w: i32, h: i32): Uint8ClampedArray;
|
||||
export declare function monoHalftone(mono: Uint8ClampedArray, w: i32, h: i32): Uint8ClampedArray;
|
||||
export declare function monoToPbm(data: Uint8ClampedArray): Uint8ClampedArray;
|
||||
/** Note: takes & gives `Uint32Array` */
|
||||
export declare function rotateRgba(before: Uint32Array, w: i32, h: i32): Uint32Array;
|
228
www/image.js
228
www/image.js
@ -1,117 +1,125 @@
|
||||
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.rotateRgba = exports.monoToPbm = exports.monoHalftone = exports.monoSteinberg = exports.monoDirect = exports.monoToRgba = exports.monoGrayscale = void 0;
|
||||
function monoGrayscale(rgba, brightness, alpha_as_white) {
|
||||
let mono = new Uint8ClampedArray(rgba.length);
|
||||
let r = 0.0, g = 0.0, b = 0.0, a = 0.0, m = 0.0, n = 0;
|
||||
for (let i = 0; i < mono.length; ++i) {
|
||||
n = rgba[i];
|
||||
// little endian
|
||||
r = (n & 0xff), g = (n >> 8 & 0xff), b = (n >> 16 & 0xff);
|
||||
a = (n >> 24 & 0xff) / 0xff;
|
||||
if (a < 1 && alpha_as_white) {
|
||||
a = 1 - a;
|
||||
r += (0xff - r) * a;
|
||||
g += (0xff - g) * a;
|
||||
b += (0xff - b) * a;
|
||||
}
|
||||
else {
|
||||
r *= a;
|
||||
g *= a;
|
||||
b *= a;
|
||||
}
|
||||
m = r * 0.2125 + g * 0.7154 + b * 0.0721;
|
||||
m += (brightness - 0x80) * (1 - m / 0xff) * (m / 0xff) * 2;
|
||||
mono[i] = m;
|
||||
}
|
||||
return mono;
|
||||
}
|
||||
exports.monoGrayscale = monoGrayscale;
|
||||
/** Note: returns a `Uint32Array` */
|
||||
function monoToRgba(mono) {
|
||||
let rgba = new Uint32Array(mono.length);
|
||||
for (let i = 0; i < mono.length; ++i) {
|
||||
// little endian
|
||||
rgba[i] = 0xff000000 | (mono[i] << 16) | (mono[i] << 8) | mono[i];
|
||||
}
|
||||
return rgba;
|
||||
}
|
||||
exports.monoToRgba = monoToRgba;
|
||||
function monoDirect(mono, w, h) {
|
||||
for (let i = 0; i < mono.length; ++i) {
|
||||
mono[i] = mono[i] > 0x80 ? 0xff : 0x00;
|
||||
}
|
||||
return mono;
|
||||
}
|
||||
exports.monoDirect = monoDirect;
|
||||
function monoSteinberg(mono, w, h) {
|
||||
let p = 0, m, n, o;
|
||||
for (let j = 0; j < h; ++j) {
|
||||
for (let i = 0; i < w; ++i) {
|
||||
m = mono[p];
|
||||
n = mono[p] > 0x80 ? 0xff : 0x00;
|
||||
o = m - n;
|
||||
mono[p] = n;
|
||||
if (i >= 0 && i < w - 1 && j >= 0 && j < h)
|
||||
mono[p + 1] += (o * 7 / 16);
|
||||
if (i >= 1 && i < w && j >= 0 && j < h - 1)
|
||||
mono[p + w - 1] += (o * 3 / 16);
|
||||
if (i >= 0 && i < w && j >= 0 && j < h - 1)
|
||||
mono[p + w] += (o * 5 / 16);
|
||||
if (i >= 0 && i < w - 1 && j >= 0 && j < h - 1)
|
||||
mono[p + w + 1] += (o * 1 / 16);
|
||||
++p;
|
||||
}
|
||||
}
|
||||
return mono;
|
||||
}
|
||||
exports.monoSteinberg = monoSteinberg;
|
||||
function monoHalftone(mono, w, h) {
|
||||
const spot = 4;
|
||||
const spot_h = spot / 2 + 1;
|
||||
const spot_d = spot * 2;
|
||||
const spot_s = spot * spot;
|
||||
let i, j, x, y, o = 0.0;
|
||||
for (j = 0; j < h - spot; j += spot) {
|
||||
for (i = 0; i < w - spot; i += spot) {
|
||||
for (x = 0; x < spot; ++x)
|
||||
for (y = 0; y < spot; ++y)
|
||||
o += mono[(j + y) * w + i + x];
|
||||
o = (1 - o / spot_s / 0xff) * spot;
|
||||
for (x = 0; x < spot; ++x)
|
||||
for (y = 0; y < spot; ++y) {
|
||||
mono[(j + y) * w + i + x] = Math.abs(x - spot_h) >= o || Math.abs(y - spot_h) >= o ? 0xff : 0x00;
|
||||
// mono[(j + y) * w + i + x] = Math.abs(x - spot_h) + Math.abs(y - spot_h) >= o ? 0xff : 0x00;
|
||||
}
|
||||
}
|
||||
for (; i < w; ++i)
|
||||
mono[j * w + i] = 0xff;
|
||||
}
|
||||
for (; j < h; ++j)
|
||||
for (i = 0; i < w; ++i)
|
||||
mono[j * w + i] = 0xff;
|
||||
return mono;
|
||||
}
|
||||
exports.monoHalftone = monoHalftone;
|
||||
function monoToPbm(data) {
|
||||
let length = (data.length / 8) | 0;
|
||||
let result = new Uint8ClampedArray(length);
|
||||
for (let i = 0, p = 0; i < data.length; ++p) {
|
||||
result[p] = 0;
|
||||
for (let d = 0; d < 8; ++i, ++d)
|
||||
result[p] |= data[i] & (0b10000000 >> d);
|
||||
result[p] ^= 0b11111111;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
exports.monoToPbm = monoToPbm;
|
||||
/** Note: takes & gives `Uint32Array` */
|
||||
function rotateRgba(before, w, h) {
|
||||
/**
|
||||
* Convert colored image to grayscale.
|
||||
* @param {Uint8ClampedArray} image_data `data` property of an ImageData instance,
|
||||
* i.e. `canvas.getContext('2d').getImageData(...).data`
|
||||
* @param {Uint8ClampedArray} mono_data an `Uint8ClampedArray` that have the size `w * h`
|
||||
* i.e. `image_data.length / 4`
|
||||
* The result data will be here, as a 8-bit grayscale image data.
|
||||
* @param {number} w width of image
|
||||
* @param {number} h height of image
|
||||
* @param {number} t brightness, historically "threshold"
|
||||
* @param {boolean} transparencyAsWhite whether render opacity as white rather than black
|
||||
* w h
|
||||
* o------+ +---o
|
||||
* h | | | | w
|
||||
* +------+ | | after
|
||||
* before +---+
|
||||
*/
|
||||
function monoGrayscale(image_data, mono_data, w, h, t, transparencyAsWhite) {
|
||||
let p, q, r, g, b, a, m;
|
||||
let after = new Uint32Array(before.length);
|
||||
for (let j = 0; j < h; j++) {
|
||||
for (let i = 0; i < w; i++) {
|
||||
p = j * w + i;
|
||||
q = p * 4;
|
||||
[r, g, b, a] = image_data.slice(q, q + 4);
|
||||
a /= 255;
|
||||
if (a < 1 && transparencyAsWhite) {
|
||||
a = 1 - a;
|
||||
r += (255 - r) * a;
|
||||
g += (255 - g) * a;
|
||||
b += (255 - b) * a;
|
||||
}
|
||||
else { r *= a; g *= a; b *= a; }
|
||||
m = Math.floor(r * 0.2125 + g * 0.7154 + b * 0.0721);
|
||||
m += (t - 128) * (1 - m / 255) * (m / 255) * 2;
|
||||
mono_data[p] = m;
|
||||
after[j * w + i] = before[(w - i - 1) * h + j];
|
||||
}
|
||||
}
|
||||
return after;
|
||||
}
|
||||
|
||||
/**
|
||||
* The most simple monochrome algorithm, any value bigger than threshold is white, otherwise black.
|
||||
* @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place**
|
||||
* @param {number} w width of image
|
||||
* @param {number} h height of image
|
||||
*/
|
||||
function monoDirect(data, w, h) {
|
||||
let p, i, j;
|
||||
for (j = 0; j < h; j++) {
|
||||
for (i = 0; i < w; i++) {
|
||||
p = j * w + i;
|
||||
data[p] = data[p] > 128 ? 255 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The widely used Floyd Steinberg algorithm, the most "natual" one.
|
||||
* @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place**
|
||||
* @param {number} w width of image
|
||||
* @param {number} h height of image
|
||||
*/
|
||||
function monoSteinberg(data, w, h) {
|
||||
let p, m, n, o, i, j;
|
||||
function adjust(x, y, delta) {
|
||||
if (
|
||||
x < 0 || x >= w ||
|
||||
y < 0 || y >= h
|
||||
) return;
|
||||
p = y * w + x;
|
||||
data[p] += delta;
|
||||
}
|
||||
for (j = 0; j < h; j++) {
|
||||
for (i = 0; i < w; i++) {
|
||||
p = j * w + i;
|
||||
m = data[p];
|
||||
n = m > 128 ? 255 : 0;
|
||||
o = m - n;
|
||||
data[p] = n;
|
||||
adjust(i + 1, j , o * 7 / 16);
|
||||
adjust(i - 1, j + 1, o * 3 / 16);
|
||||
adjust(i , j + 1, o * 5 / 16);
|
||||
adjust(i + 1, j + 1, o * 1 / 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (Work in Progress...)
|
||||
*/
|
||||
function monoHalftone(data, w, h, t) {}
|
||||
|
||||
/**
|
||||
* Convert a monochrome image data to PBM mono image file data.
|
||||
* Returns a Blob containing the file data.
|
||||
* @param {Uint8ClampedArray} data the data that have a size of `w * h`
|
||||
* @param {number} w width of image
|
||||
* @param {number} h height of image
|
||||
* @returns {Blob}
|
||||
*/
|
||||
function mono2pbm(data, w, h) {
|
||||
let result = new Uint8ClampedArray(data.length / 8);
|
||||
let slice, p, i;
|
||||
for (i = 0; i < result.length; i++) {
|
||||
p = i * 8;
|
||||
slice = data.slice(p, p + 8);
|
||||
// Merge 8 bytes to 1 byte, and negate the bits
|
||||
// assuming there's only 255 (0b11111111) or 0 (0b00000000) in the data
|
||||
result[i] = (
|
||||
slice[0] & 0b10000000 |
|
||||
slice[1] & 0b01000000 |
|
||||
slice[2] & 0b00100000 |
|
||||
slice[3] & 0b00010000 |
|
||||
slice[4] & 0b00001000 |
|
||||
slice[5] & 0b00000100 |
|
||||
slice[6] & 0b00000010 |
|
||||
slice[7] & 0b00000001
|
||||
) ^ 0b11111111;
|
||||
}
|
||||
let pbm_data = new Blob([`P4\n${w} ${h}\n`, result]);
|
||||
return pbm_data;
|
||||
}
|
||||
exports.rotateRgba = rotateRgba;
|
||||
|
@ -30,6 +30,10 @@
|
||||
<input type="radio" name="algo" value="algo-steinberg" data-key checked />
|
||||
<span data-i18n="picture">Picture</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="algo" value="algo-halftone" data-key />
|
||||
<span data-i18n="pattern">Pattern</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="algo" value="algo-direct" data-key />
|
||||
<span data-i18n="text">Text</span>
|
||||
@ -99,6 +103,7 @@
|
||||
<span>🌎</span>
|
||||
<span data-i18n="accessibility">Accessibility</span>
|
||||
</button>
|
||||
<button id="test-unknown-device" data-i18n="test-unknown-device" data-key>Test Unknown Device</button>
|
||||
<button class="hidden" data-panel="panel-error" data-i18n="error-message" data-key>Error Message</button>
|
||||
<div class="center">
|
||||
<button id="button-exit" data-i18n="exit" data-key>Exit</button>
|
||||
@ -156,6 +161,7 @@
|
||||
<br />
|
||||
<span id="hint-tab-control" class="hide-on-android"
|
||||
data-i18n="to-enter-keyboard-mode-press-tab">To enter Keyboard Mode, press Tab</span>
|
||||
<br />
|
||||
</div>
|
||||
<div>
|
||||
<h2 data-i18n="layout">Layout</h2>
|
||||
|
@ -137,5 +137,7 @@
|
||||
"serif": "Serif",
|
||||
"sans-serif": "Sans Serif",
|
||||
"monospace": "Monospace",
|
||||
"rotate-image": "Rotate Image"
|
||||
"rotate-image": "Rotate Image",
|
||||
"test-unknown-device": "Test Unknown Device",
|
||||
"now-will-scan-for-all-bluetooth-devices-nearby": "Now will scan for all bluetooth devices nearby."
|
||||
}
|
@ -132,5 +132,7 @@
|
||||
"serif": "SHARP PAW",
|
||||
"sans-serif": "SOFT PAW",
|
||||
"monospace": "H4CKY PAW",
|
||||
"rotate-image": "ROLL PIC"
|
||||
"rotate-image": "ROLL PIC",
|
||||
"test-unknown-device": "I HAV STRENGE KITTE",
|
||||
"now-will-scan-for-all-bluetooth-devices-nearby": "WIL FIND ALL THINY KITTE OR NOT"
|
||||
}
|
||||
|
@ -128,5 +128,7 @@
|
||||
"serif": "衬线字体",
|
||||
"sans-serif": "无衬线字体",
|
||||
"monospace": "等宽字体",
|
||||
"rotate-image": "旋转图像"
|
||||
"rotate-image": "旋转图像",
|
||||
"test-unknown-device": "测试未知设备",
|
||||
"now-will-scan-for-all-bluetooth-devices-nearby": "现在将搜索附近所有设备。"
|
||||
}
|
@ -4,6 +4,8 @@ Copyright © 2021-2022 NaitLee Soft. No rights reserved.
|
||||
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
|
||||
`;
|
||||
|
||||
window.exports = {};
|
||||
|
||||
/**
|
||||
* Satisfy both development and old-old webView need
|
||||
*/
|
||||
|
57
www/main.css
57
www/main.css
@ -36,6 +36,7 @@ body {
|
||||
margin: 1em 0;
|
||||
user-select: none;
|
||||
}
|
||||
body, .shade { transition: background-color calc(var(--anim-time) * 2) ease-in; }
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@ -179,7 +180,7 @@ main, header, footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
overflow-x: hidden;
|
||||
/* overflow-x: hidden; */ /* this causes sticky position not work */
|
||||
}
|
||||
.canvas-side {
|
||||
flex-grow: 0;
|
||||
@ -204,6 +205,13 @@ main, header, footer {
|
||||
border-bottom: none;
|
||||
margin-top: var(--span);
|
||||
}
|
||||
.canvas-side>.buttons {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: var(--span) 0;
|
||||
background-color: var(--back-color);
|
||||
z-index: 1;
|
||||
}
|
||||
.compact-menu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -300,6 +308,9 @@ input[type="range"] {
|
||||
#hidden, .hard-hidden {
|
||||
display: none;
|
||||
}
|
||||
#hint-tab-control {
|
||||
position: relative;
|
||||
}
|
||||
#error-record {
|
||||
font-family: 'DejaVu Sans Mono', 'Consolas', monospace;
|
||||
width: 100%;
|
||||
@ -378,7 +389,7 @@ iframe#frame {
|
||||
#dialog>.content {
|
||||
max-width: 100%;
|
||||
width: 42em;
|
||||
max-height: 80vh;
|
||||
max-height: 100vh;
|
||||
margin: 12vh auto;
|
||||
border: var(--border) solid var(--fore-color);
|
||||
transition: transform var(--anim-time) var(--curve);
|
||||
@ -391,18 +402,18 @@ iframe#frame {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
#dialog-content {
|
||||
max-height: 60vh;
|
||||
margin: auto;
|
||||
padding: var(--span-double);
|
||||
padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
max-height: calc(76vh - 1em);
|
||||
overflow-y: auto;
|
||||
}
|
||||
#dialog-choices {
|
||||
margin: auto;
|
||||
padding: var(--span);
|
||||
padding-top: 0;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
#choice-input {
|
||||
max-width: 100%;
|
||||
@ -412,13 +423,12 @@ iframe#frame {
|
||||
text-align: initial;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
#accessibility>*:nth-child(1) {
|
||||
flex-grow: 1;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#select-language {
|
||||
/* width: calc(100% - var(--span-double)); */
|
||||
width: 12em;
|
||||
width: 100%;
|
||||
height: 8em;
|
||||
border: var(--border) solid var(--fore-color);
|
||||
padding: var(--span);
|
||||
@ -430,9 +440,11 @@ iframe#frame {
|
||||
#select-language option:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
#accessibility>*:nth-child(2) {
|
||||
flex-grow: 1;
|
||||
#accessibility>* {
|
||||
flex-grow: 0;
|
||||
min-width: 16em;
|
||||
white-space: nowrap;
|
||||
margin: 1em;
|
||||
}
|
||||
@keyframes jump {
|
||||
0% { transform: translateY(0); }
|
||||
@ -542,14 +554,7 @@ a {
|
||||
}
|
||||
.canvas-side>.buttons,
|
||||
.menu-side>.buttons {
|
||||
position: sticky;
|
||||
bottom: var(--span);
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.canvas-side>.buttons button,
|
||||
.menu-side>.buttons button {
|
||||
background-color: var(--back-color);
|
||||
}
|
||||
#control-overlay {
|
||||
width: 100%;
|
||||
@ -590,7 +595,7 @@ a {
|
||||
height: var(--compact-menu-height);
|
||||
}
|
||||
}
|
||||
@media (max-width: 384px) {
|
||||
@media (max-width: 385px) {
|
||||
#preview, #canvas, #control-overlay, .canvas-side>* {
|
||||
width: 100%;
|
||||
border: none;
|
||||
@ -607,19 +612,17 @@ a {
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --fore-color: #eee; --back-color: #333; --shade: rgba(51, 51, 51, 0.5); }
|
||||
body, .shade { transition: background-color calc(var(--anim-time) * 2) ease-in; }
|
||||
a:link, a:visited { color: #66f; }
|
||||
a:hover, a:active { color: #77f; }
|
||||
.canvas-group, .logo { filter: brightness(0.6); }
|
||||
#control-overlay { background-color: var(--shade); }
|
||||
}
|
||||
/* so silly... */
|
||||
body.dark { --fore-color: #eee; --back-color: #333; --shade: rgba(51, 51, 51, 0.5); }
|
||||
body.dark, .shade { transition: background-color calc(var(--anim-time) * 2) ease-in; }
|
||||
body.dark a:link, body.dark a:visited { color: #66f; }
|
||||
body.dark a:hover, body.dark a:active { color: #77f; }
|
||||
body.dark .canvas-group, body.dark .logo { filter: brightness(0.6); }
|
||||
body.dark #control-overlay { background-color: var(--shade); }
|
||||
body.dark-theme { --fore-color: #eee; --back-color: #333; --shade: rgba(51, 51, 51, 0.5); }
|
||||
body.dark-theme a:link, body.dark-theme a:visited { color: #66f; }
|
||||
body.dark-theme a:hover, body.dark-theme a:active { color: #77f; }
|
||||
body.dark-theme .canvas-group, body.dark-theme .logo { filter: brightness(0.6); }
|
||||
body.dark-theme #control-overlay { background-color: var(--shade); }
|
||||
@media (prefers-reduced-motion) {
|
||||
body *,
|
||||
body *::before,
|
||||
|
390
www/main.js
390
www/main.js
@ -1,3 +1,4 @@
|
||||
|
||||
`
|
||||
Cat-Printer: Web Frontend
|
||||
|
||||
@ -205,62 +206,89 @@ async function callApi(path, body, errorPreHandler) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const Ev = (function() {
|
||||
/** @type {Record<string, NodeListOf<HTMLElement>>} */
|
||||
let map = {};
|
||||
return {
|
||||
/**
|
||||
* call addEventListener on all selected elements by `seletor`,
|
||||
* with each element itself as `this` unless specifyed `thisArg`,
|
||||
* with type `type` and a function `callback`.
|
||||
* If an element have attribute `data-default` or `checked`, dispatch event immediately on it.
|
||||
* You can of course assign resulting object to a variable for futher use.
|
||||
*/
|
||||
class EventPutter {
|
||||
elements;
|
||||
callback;
|
||||
/**
|
||||
* Attach event & callback to elements selected by selector.
|
||||
* @param {string} selector
|
||||
* @param {string} type
|
||||
* @param {(event?: Event) => void} callback
|
||||
* @param {any} thisArg
|
||||
*/
|
||||
constructor(selector, type, callback, thisArg) {
|
||||
let elements = this.elements = document.querySelectorAll(selector);
|
||||
if (elements.length === 0) return;
|
||||
this.callback = callback;
|
||||
elements.forEach(element => {
|
||||
element.addEventListener(type, function(event) {
|
||||
put: function(selector, type, callback, thisArg) {
|
||||
let elements = document.querySelectorAll(selector);
|
||||
map[selector] = elements;
|
||||
for (let e of elements) {
|
||||
e.addEventListener(type, function(event) {
|
||||
event.stopPropagation();
|
||||
event.cancelBubble = true;
|
||||
callback.call(thisArg || element, event);
|
||||
});
|
||||
if (element.hasAttribute('data-default') || element.checked) {
|
||||
element.dispatchEvent(new Event(type));
|
||||
}
|
||||
callback.call(thisArg || e, event);
|
||||
});
|
||||
if (e.hasAttribute('data-default') || e.checked) {
|
||||
e.dispatchEvent(new Event(type));
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Dispatch event to elements that are selected before with the same selector.
|
||||
* Optionally set a value.
|
||||
* @param {string} selector
|
||||
* @param {string} type
|
||||
* @param {(event?: Event) => void} callback
|
||||
* @param {any} thisArg
|
||||
* @param {{
|
||||
* event?: Event,
|
||||
* value?: string | number | boolean
|
||||
* }} args
|
||||
*/
|
||||
function putEvent(selector, type, callback, thisArg) {
|
||||
return new EventPutter(selector, type, callback, thisArg);
|
||||
dispatch: function(selector, type, { event, value } = {}) {
|
||||
if (map[selector] === undefined) return;
|
||||
for (let e of map[selector]) {
|
||||
if (value !== undefined)
|
||||
switch (e.type) {
|
||||
case 'checkbox':
|
||||
if (e.checked === !value) e.click();
|
||||
break;
|
||||
case 'radio':
|
||||
if (e.value === value) e.click();
|
||||
break;
|
||||
case 'text':
|
||||
case 'number':
|
||||
case 'range':
|
||||
e.value = value;
|
||||
break;
|
||||
default:
|
||||
if (e.value === value) e.click();
|
||||
}
|
||||
else e.dispatchEvent(event || new Event(type));
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(function() {
|
||||
/**
|
||||
* Open a panel
|
||||
* @type {(id: string) => void}
|
||||
*/
|
||||
const Panel = (function() {
|
||||
let panels = document.querySelectorAll('.panel');
|
||||
let buttons = document.querySelectorAll('*[data-panel]');
|
||||
panels.forEach(panel => {
|
||||
let map = {};
|
||||
for (let panel of panels) {
|
||||
let button = document.querySelector(`*[data-panel="${panel.id}"]`);
|
||||
if (button) button.addEventListener('click', event => {
|
||||
if (!button) continue;
|
||||
button.addEventListener('click', event => {
|
||||
event.stopPropagation();
|
||||
panels.forEach(p => p.classList.remove('active'));
|
||||
buttons.forEach(b => b.classList.remove('active'));
|
||||
panel.classList.add('active');
|
||||
button.classList.add('active');
|
||||
});
|
||||
map[panel.id] = button;
|
||||
if (panel.hasAttribute('data-default')) button.click();
|
||||
});
|
||||
}
|
||||
return id => map[id]?.click();
|
||||
})();
|
||||
|
||||
/**
|
||||
@ -273,33 +301,33 @@ class CanvasController {
|
||||
/** @type {HTMLCanvasElement} */
|
||||
canvas;
|
||||
imageUrl;
|
||||
isImageNew;
|
||||
// this costs most of the effort. cache it
|
||||
/** @type {Uint8ClampedArray} */
|
||||
grayscaleCache;
|
||||
algorithm;
|
||||
algoElements;
|
||||
textFont;
|
||||
textSize;
|
||||
textArea;
|
||||
transparentAsWhite;
|
||||
previewData;
|
||||
previewPbm;
|
||||
rotate;
|
||||
#height;
|
||||
#threshold;
|
||||
#energy;
|
||||
#thresholdRange;
|
||||
#energyRange;
|
||||
#rotateCheck;
|
||||
static defaultHeight = 384;
|
||||
static defaultThreshold = 256 / 3;
|
||||
get threshold() {
|
||||
return this.#threshold;
|
||||
}
|
||||
set threshold(value) {
|
||||
this.#threshold = this.#thresholdRange.value = value;
|
||||
Ev.dispatch('#threshold', 'change', { value: this.#threshold = value });
|
||||
}
|
||||
get energy() {
|
||||
return this.#energy;
|
||||
}
|
||||
set energy(value) {
|
||||
this.#energy = this.#energyRange.value = value;
|
||||
Ev.dispatch('#energy', 'change', { value: this.#energy = value });
|
||||
}
|
||||
get height() {
|
||||
return this.#height;
|
||||
@ -316,11 +344,11 @@ class CanvasController {
|
||||
this.textArea = document.getElementById("insert-text-area");
|
||||
this.wrapBySpace = document.querySelector('input[name="wrap-words-by-spaces"]');
|
||||
this.height = CanvasController.defaultHeight;
|
||||
this.#thresholdRange = document.querySelector('[name="threshold"]');
|
||||
this.#energyRange = document.querySelector('[name="energy"]');
|
||||
this.imageUrl = null;
|
||||
this.textAlign = "left";
|
||||
this.rotate = false;
|
||||
this.isImageNew = true;
|
||||
this.grayscaleCache = null;
|
||||
|
||||
for (let elem of document.querySelectorAll("input[name=text-align]")){
|
||||
if (elem.checked) { this.textAlign = elem.value; }
|
||||
@ -351,47 +379,44 @@ class CanvasController {
|
||||
this.textArea.style["font-family"] = this.textFont.value;
|
||||
this.textArea.style["word-break"] = this.wrapBySpace.checked ? "break-word" : "break-all";
|
||||
|
||||
this.algoElements = document.querySelectorAll('input[name="algo"]');
|
||||
|
||||
putEvent('input[name="algo"]', 'change', (event) => this.useAlgorithm(event.currentTarget.value), this);
|
||||
putEvent('#insert-picture' , 'click', () => this.useFiles(), this);
|
||||
putEvent('#insert-text' , 'click', () => Dialog.alert("#text-input", () => this.insertText(this.textArea.value)));
|
||||
putEvent('#text-size' , 'change', () => this.textArea.style["font-size"] = this.textSize.value + "px");
|
||||
putEvent('#text-font' , 'change', () => this.textArea.style["font-family"] = this.textFont.value);
|
||||
putEvent('input[name="text-align"]', 'change', (event) => {
|
||||
this.textAlign = event.currentTarget.value
|
||||
Ev.put('[name="algo"]' , 'change', (event) => this.useAlgorithm(event.currentTarget.value), this);
|
||||
Ev.put('#insert-picture', 'click' , () => this.useFiles(), this);
|
||||
Ev.put('#insert-text' , 'click' , () => Dialog.alert("#text-input", () => this.insertText(this.textArea.value)));
|
||||
Ev.put('#text-size' , 'change', () => this.textArea.style["font-size"] = this.textSize.value + "px");
|
||||
Ev.put('#text-font' , 'change', () => this.textArea.style["font-family"] = this.textFont.value);
|
||||
Ev.put('input[name="text-align"]', 'change', (event) => {
|
||||
this.textAlign = event.currentTarget.value;
|
||||
this.textArea.style["text-align"] = this.textAlign;
|
||||
}, this);
|
||||
putEvent('input[name="wrap-words-by-spaces"]' , 'change', () => this.textArea.style["word-break"] = this.wrapBySpace.checked ? "break-word" : "break-all");
|
||||
putEvent('#button-preview' , 'click', this.activatePreview , this);
|
||||
putEvent('#button-reset' , 'click', this.reset , this);
|
||||
putEvent('#canvas-expand' , 'click', this.expand , this);
|
||||
putEvent('#canvas-crop' , 'click', this.crop , this);
|
||||
putEvent('[name="rotate"]' , 'change', e => this.setRotate(e.currentTarget.checked), this);
|
||||
this.#rotateCheck = document.querySelector('[name="rotate"]');
|
||||
Ev.put('input[name="wrap-words-by-spaces"]', 'change',
|
||||
() => this.textArea.style["word-break"] = this.wrapBySpace.checked ? "break-word" : "break-all");
|
||||
Ev.put('#button-preview' , 'click', this.activatePreview , this);
|
||||
Ev.put('#button-reset' , 'click', this.reset , this);
|
||||
Ev.put('#canvas-expand' , 'click', this.expand , this);
|
||||
Ev.put('#canvas-crop' , 'click', this.crop , this);
|
||||
Ev.put('[name="rotate"]' , 'change', e => this.setRotate(e.currentTarget.checked), this);
|
||||
|
||||
putEvent('[name="threshold"]', 'change', (event) => {
|
||||
Ev.put('[name="threshold"]', 'change', (event) => {
|
||||
this.threshold = parseInt(event.currentTarget.value);
|
||||
// it's really new
|
||||
this.isImageNew = true;
|
||||
this.activatePreview();
|
||||
}, this);
|
||||
putEvent('[name="energy"]', 'change', (event) => {
|
||||
Ev.put('[name="energy"]', 'change', (event) => {
|
||||
this.energy = parseInt(event.currentTarget.value);
|
||||
this.visualEnergy(this.energy);
|
||||
}, this);
|
||||
putEvent('[name="transparent-as-white"]', 'change', (event) => {
|
||||
Ev.put('[name="transparent-as-white"]', 'change', (event) => {
|
||||
this.transparentAsWhite = event.currentTarget.checked;
|
||||
this.isImageNew = true;
|
||||
this.activatePreview();
|
||||
}, this);
|
||||
}
|
||||
useAlgorithm(name) {
|
||||
for (let e of this.algoElements) {
|
||||
e.checked = e.value === name;
|
||||
}
|
||||
this.algorithm = name;
|
||||
this.threshold = CanvasController.defaultThreshold;
|
||||
this.#thresholdRange.dispatchEvent(new Event('change'));
|
||||
this.energy = name == 'algo-direct' ? 96 : 64;
|
||||
this.#energyRange.dispatchEvent(new Event('change'));
|
||||
// Ev.dispatch('[name="algo"]', 'change', { value: name });
|
||||
Ev.dispatch('[name="threshold"]', 'change', { value: CanvasController.defaultThreshold });
|
||||
Ev.dispatch('[name="energy"]', 'change', { value: (name == 'algo-direct' ? 96 : 64) });
|
||||
this.activatePreview();
|
||||
}
|
||||
expand(length = CanvasController.defaultHeight) {
|
||||
@ -401,7 +426,6 @@ class CanvasController {
|
||||
// STUB
|
||||
}
|
||||
setRotate(value) {
|
||||
this.#rotateCheck.checked = value;
|
||||
this.rotate = value;
|
||||
if (this.imageUrl !== null) this.putImage(this.imageUrl);
|
||||
}
|
||||
@ -414,87 +438,71 @@ class CanvasController {
|
||||
activatePreview() {
|
||||
if (!this.imageUrl) return;
|
||||
let preview = this.preview;
|
||||
let t = Math.min(this.threshold, 255);
|
||||
let threshold = Math.min(this.threshold, 255);
|
||||
let canvas = this.canvas;
|
||||
let w = canvas.width, h = canvas.height;
|
||||
preview.width = w; preview.height = h;
|
||||
let context_c = canvas.getContext('2d');
|
||||
let context_p = preview.getContext('2d');
|
||||
let data = context_c.getImageData(0, 0, w, h);
|
||||
let mono_data = new Uint8ClampedArray(w * h);
|
||||
monoGrayscale(data.data, mono_data, w, h, t, this.transparentAsWhite);
|
||||
let rgba_data = context_c.getImageData(0, 0, w, h);
|
||||
let gray_data = (this.grayscaleCache =
|
||||
this.isImageNew || !this.grayscaleCache
|
||||
? monoGrayscale(
|
||||
new Uint32Array(rgba_data.data.buffer),
|
||||
threshold,
|
||||
this.transparentAsWhite
|
||||
)
|
||||
: this.grayscaleCache).slice();
|
||||
/** @type {Uint8ClampedArray} */
|
||||
let result;
|
||||
switch (this.algorithm) {
|
||||
case 'algo-direct':
|
||||
monoDirect(mono_data, w, h, t);
|
||||
result = monoDirect(gray_data, w, h);
|
||||
break;
|
||||
case 'algo-steinberg':
|
||||
monoSteinberg(mono_data, w, h, Math.floor(t / 2 - 64));
|
||||
result = monoSteinberg(gray_data, w, h);
|
||||
break;
|
||||
case 'algo-halftone':
|
||||
// monoHalftone(mono_data, w, h, t);
|
||||
// Sorry, do it later
|
||||
break;
|
||||
case 'algo-new':
|
||||
monoNew(mono_data, w, h, t);
|
||||
break;
|
||||
case 'algo-new-h':
|
||||
monoNewH(mono_data, w, h, Math.floor(t / 2 - 64));
|
||||
break;
|
||||
case 'algo-new-v':
|
||||
monoNewV(mono_data, w, h, t);
|
||||
break;
|
||||
case 'algo-legacy':
|
||||
monoLegacy(mono_data, w, h, t);
|
||||
result = monoHalftone(gray_data, w, h);
|
||||
break;
|
||||
}
|
||||
let new_data = context_p.createImageData(w, h);
|
||||
let p;
|
||||
for (let i = 0; i < mono_data.length; i++) {
|
||||
p = i * 4;
|
||||
new_data.data.fill(mono_data[i], p, p + 3);
|
||||
new_data.data[p + 3] = 255;
|
||||
}
|
||||
this.previewData = mono_data;
|
||||
context_p.putImageData(new_data, 0, 0);
|
||||
this.previewPbm = monoToPbm(result);
|
||||
let rgba = new Uint8ClampedArray(monoToRgba(result).buffer);
|
||||
context_p.putImageData(new ImageData(rgba, w, h), 0, 0);
|
||||
this.isImageNew = false;
|
||||
}
|
||||
putImage(url) {
|
||||
let before = document.createElement('canvas');
|
||||
let b_ctx = before.getContext('2d');
|
||||
let img = document.getElementById('img');
|
||||
img.src = url;
|
||||
img.addEventListener('load', () => {
|
||||
let canvas = this.canvas;
|
||||
let ctx = canvas.getContext('2d');
|
||||
if (this.rotate) {
|
||||
let intermediate_canvas = document.createElement('canvas');
|
||||
/**
|
||||
* w h
|
||||
* +------+ +---+
|
||||
* h | | | | w
|
||||
* +------+ | | intermediate_canvas
|
||||
* canvas +---+
|
||||
*/
|
||||
let w = canvas.width;
|
||||
let h = this.height = Math.floor(canvas.width * img.width / img.height);
|
||||
intermediate_canvas.width = h;
|
||||
intermediate_canvas.height = w;
|
||||
let i_ctx = intermediate_canvas.getContext('2d');
|
||||
i_ctx.drawImage(img, 0, 0, h, w);
|
||||
let i_data = i_ctx.getImageData(0, 0, h, w);
|
||||
let data = ctx.createImageData(w, h);
|
||||
for (let j = 0; j < h; j++) {
|
||||
for (let i = 0; i < w; i++) {
|
||||
for (let d = 0; d < 4; d++)
|
||||
data.data[(i * 4 + d) + (j * w * 4)] = i_data.data[(j * 4 + d) + ((w - i) * 4 * h)];
|
||||
}
|
||||
}
|
||||
before.width = h, before.height = w;
|
||||
b_ctx.drawImage(img, 0, 0, h, w);
|
||||
let data = new ImageData(
|
||||
new Uint8ClampedArray(
|
||||
rotateRgba(
|
||||
new Uint32Array(
|
||||
b_ctx.getImageData(0, 0, h, w).data.buffer
|
||||
), w, h
|
||||
).buffer
|
||||
), w, h
|
||||
);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
} else {
|
||||
this.height = Math.floor(canvas.width * img.height / img.width);
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
this.crop();
|
||||
this.isImageNew = true;
|
||||
this.activatePreview();
|
||||
hint('#button-print');
|
||||
});
|
||||
}, { once: true });
|
||||
}
|
||||
useFiles(files) {
|
||||
const use_files = (files) => {
|
||||
@ -607,6 +615,7 @@ class CanvasController {
|
||||
let canvas = this.canvas;
|
||||
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
|
||||
canvas.height = CanvasController.defaultHeight;
|
||||
this.isImageNew = true;
|
||||
this.activatePreview();
|
||||
this.imageUrl = null;
|
||||
this.controls.classList.remove('hidden');
|
||||
@ -617,7 +626,7 @@ class CanvasController {
|
||||
}
|
||||
}
|
||||
makePbm() {
|
||||
let blob = mono2pbm(this.previewData, this.preview.width, this.preview.height);
|
||||
let blob = new Blob([`P4\n${this.canvas.width} ${this.canvas.height}\n`, this.previewPbm]);
|
||||
return blob;
|
||||
}
|
||||
}
|
||||
@ -698,10 +707,10 @@ class Main {
|
||||
/** @type {CanvasController} */
|
||||
canvasController;
|
||||
deviceOptions;
|
||||
testUnknownDevice;
|
||||
/** An object containing configuration, fetched from server */
|
||||
settings;
|
||||
/** @type {{ [key: string]: EventPutter }} */
|
||||
setters;
|
||||
selectorMap;
|
||||
/**
|
||||
* There are race conditions in initialization query/set,
|
||||
* use this flag to avoid
|
||||
@ -709,11 +718,11 @@ class Main {
|
||||
allowSet;
|
||||
constructor() {
|
||||
this.allowSet = false;
|
||||
this.testUnknownDevice = false;
|
||||
this.deviceOptions = document.getElementById('device-options');
|
||||
this.settings = {};
|
||||
this.setters = {};
|
||||
this.selectorMap = {};
|
||||
// window.addEventListener('unload', () => this.exit());
|
||||
this.promise = new Promise(async (resolve, reject) => {
|
||||
/** @type {HTMLIFrameElement} */
|
||||
let iframe = document.getElementById('frame');
|
||||
iframe.addEventListener('load', () => {
|
||||
@ -729,58 +738,80 @@ class Main {
|
||||
});
|
||||
applyI18nToDom(iframe.contentDocument);
|
||||
});
|
||||
function apply_class(class_name, value) {
|
||||
[document, iframe.contentDocument].forEach(d => value ?
|
||||
d.body.classList.add(class_name) :
|
||||
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-print' , 'click', this.print, this);
|
||||
putEvent('#device-refresh' , 'click', this.searchDevices, this);
|
||||
putEvent('#button-exit' , 'contextmenu', (event) => (event.preventDefault(), this.exit(true)), this);
|
||||
putEvent('#set-accessibility', 'click', () => Dialog.alert('#accessibility'));
|
||||
this.attachSetter('#device-options', 'input', 'printer',
|
||||
|
||||
Ev.put('#button-exit' , 'click', () => this.exit(false), this);
|
||||
Ev.put('#button-print' , 'click', this.print, this);
|
||||
Ev.put('#device-refresh' , 'click', this.searchDevices, this);
|
||||
Ev.put('#button-exit' , 'contextmenu', (event) => (event.preventDefault(), this.exit(true)), this);
|
||||
Ev.put('#set-accessibility' , 'click', () => Dialog.alert('#accessibility'));
|
||||
Ev.put('a[target="frame"]', 'click', () => Dialog.alert('#frame'));
|
||||
Ev.put('#test-unknown-device' , 'click', () => {
|
||||
Dialog.alert(i18n('now-will-scan-for-all-bluetooth-devices-nearby'), null, true);
|
||||
this.testUnknownDevice = true;
|
||||
Panel('panel-print');
|
||||
Hider.show('print');
|
||||
this.searchDevices();
|
||||
});
|
||||
|
||||
this.conf('#device-options', 'change', 'printer',
|
||||
(value) => callApi('/connect', { device: value })
|
||||
);
|
||||
putEvent('a[target="frame"]', 'click', () => Dialog.alert('#frame'));
|
||||
this.attachSetter('[name="scan-time"]' , 'change', 'scan_timeout');
|
||||
this.attachSetter('[name="rotate"]' , 'change', 'rotate');
|
||||
this.attachSetter('input[name="algo"]' , 'change', 'mono_algorithm',
|
||||
this.conf('[name="algo"]' , 'change', 'mono_algorithm',
|
||||
(value) => this.settings['text_mode'] = (value === 'algo-direct')
|
||||
);
|
||||
this.attachSetter('[name="transparent-as-white"]', 'change', 'transparent_as_white');
|
||||
this.attachSetter('[name="wrap-words-by-spaces"]', 'change', 'wrap_by_space');
|
||||
this.attachSetter('[name="dry-run"]', 'change', 'dry_run',
|
||||
this.conf('[name="dry-run"]', 'change', 'dry_run',
|
||||
(checked) => checked && Notice.note('dry-run-test-print-process-only')
|
||||
);
|
||||
this.attachSetter('[name="no-animation"]', 'change', 'no_animation',
|
||||
(checked) => apply_class('no-animation', checked)
|
||||
|
||||
const apply_class = (class_name, value) => {
|
||||
for (let d of [document, iframe.contentDocument])
|
||||
value ? d.body.classList.add(class_name)
|
||||
: d.body.classList.remove(class_name);
|
||||
};
|
||||
// const toggle_class = (class_name) => (value) => apply_class(class_name, value);
|
||||
const conf = (...keys) => {
|
||||
for (let key of keys)
|
||||
this.conf(
|
||||
'[name="' + key + '"]', 'change',
|
||||
key.replace(/-/g, '_')
|
||||
);
|
||||
this.attachSetter('[name="large-font"]', 'change', 'large_font',
|
||||
(checked) => apply_class('large-font', checked)
|
||||
};
|
||||
const conf_class = (...keys) => {
|
||||
for (let key of keys)
|
||||
this.conf(
|
||||
'[name="' + key + '"]', 'change',
|
||||
key.replace(/-/g, '_'),
|
||||
value => apply_class(key, value)
|
||||
);
|
||||
this.attachSetter('[name="force-rtl"]', 'change', 'force_rtl',
|
||||
(checked) => apply_class('force-rtl', checked)
|
||||
};
|
||||
|
||||
conf(
|
||||
'scan-time',
|
||||
'rotate',
|
||||
'transparent-as-white',
|
||||
'wrap-words-by-spaces',
|
||||
'threshold',
|
||||
'energy',
|
||||
'quality',
|
||||
'flip'
|
||||
);
|
||||
this.attachSetter('[name="dark-theme"]', 'change', 'dark_theme',
|
||||
(checked) => apply_class('dark', checked)
|
||||
conf_class(
|
||||
'no-animation',
|
||||
'large-font',
|
||||
'force-rtl',
|
||||
'dark-theme',
|
||||
'high-contrast'
|
||||
);
|
||||
this.attachSetter('[name="high-contrast"]', 'change', 'high_contrast',
|
||||
(checked) => apply_class('high-contrast', checked)
|
||||
);
|
||||
this.attachSetter('[name="threshold"]' , 'change', 'threshold');
|
||||
this.attachSetter('[name="energy"]' , 'change', 'energy');
|
||||
this.attachSetter('[name="quality"]' , 'change', 'quality');
|
||||
this.attachSetter('[name="flip"]' , 'change', 'flip');
|
||||
|
||||
this.promise = new Promise(async (resolve, reject) => {
|
||||
await this.loadConfig();
|
||||
await initI18n(this.settings['language']);
|
||||
await this.activateConfig();
|
||||
|
||||
// one exception
|
||||
this.attachSetter('#select-language', 'change', 'language');
|
||||
this.conf('#select-language', 'change', 'language');
|
||||
|
||||
if (this.settings['is_android']) {
|
||||
document.body.classList.add('android');
|
||||
@ -789,6 +820,7 @@ class Main {
|
||||
let select = document.getElementById('select-language');
|
||||
Array.from(select.children).forEach(e => {
|
||||
e.selected = false;
|
||||
e.addEventListener('click', () => this.set({ language: e.value }));
|
||||
div.appendChild(e);
|
||||
});
|
||||
div.id = 'select-language';
|
||||
@ -824,30 +856,9 @@ class Main {
|
||||
if (this.settings['first_run'])
|
||||
Dialog.alert('#accessibility', () => this.set({ first_run: false }));
|
||||
for (let key in this.settings) {
|
||||
if (this.selectorMap[key] === undefined) continue;
|
||||
let value = this.settings[key];
|
||||
if (this.setters[key] === undefined) continue;
|
||||
// Set the *reasonable* value
|
||||
this.setters[key].elements.forEach(element => {
|
||||
switch (element.type) {
|
||||
case 'checkbox':
|
||||
element.checked = value;
|
||||
break;
|
||||
case 'radio':
|
||||
// Only dispatch on the selected one
|
||||
if (element.value !== value) return;
|
||||
element.checked = value;
|
||||
break;
|
||||
case 'text':
|
||||
case 'number':
|
||||
case 'range':
|
||||
element.value = value;
|
||||
break;
|
||||
default:
|
||||
if (element.value === value)
|
||||
element.click();
|
||||
}
|
||||
element.dispatchEvent(new Event('change'));
|
||||
});
|
||||
Ev.dispatch(this.selectorMap[key], 'change', { value: value });
|
||||
}
|
||||
this.allowSet = true;
|
||||
await this.set(this.settings);
|
||||
@ -857,8 +868,9 @@ class Main {
|
||||
* @param {string} attribute The setting to change, i.e. `this.setting[attribute] = value;`
|
||||
* @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 => {
|
||||
conf(selector, type, attribute, callback) {
|
||||
this.selectorMap[attribute] = selector;
|
||||
Ev.put(selector, type, event => {
|
||||
event.stopPropagation();
|
||||
event.cancelBubble = true;
|
||||
let input = event.currentTarget;
|
||||
@ -918,10 +930,12 @@ class Main {
|
||||
}
|
||||
async searchDevices() {
|
||||
Notice.wait('scanning-for-devices');
|
||||
let search_result = await callApi('/devices', null, this.handleBluetoothProblem);
|
||||
let search_result = await callApi('/devices', {
|
||||
everything: this.testUnknownDevice
|
||||
}, this.handleBluetoothProblem);
|
||||
if (search_result === null) return false;
|
||||
let devices = search_result.devices;
|
||||
Array.from(this.deviceOptions.children).forEach(e => e.remove());
|
||||
for (let e of this.deviceOptions.children) e.remove();
|
||||
if (devices.length === 0) {
|
||||
Notice.note('no-available-devices-found');
|
||||
hint('#device-refresh');
|
||||
@ -929,13 +943,13 @@ class Main {
|
||||
}
|
||||
Notice.note('found-0-available-devices', [devices.length]);
|
||||
hint('#insert-picture');
|
||||
devices.forEach(device => {
|
||||
for (let device of devices) {
|
||||
let option = document.createElement('option');
|
||||
option.value = `${device.name},${device.address}`;
|
||||
option.innerText = `${device.name}-${device.address.slice(3, 5)}${device.address.slice(0, 2)}`;
|
||||
this.deviceOptions.appendChild(option);
|
||||
});
|
||||
this.deviceOptions.dispatchEvent(new Event('input'));
|
||||
}
|
||||
Ev.dispatch('#device-options', 'change');
|
||||
return true;
|
||||
}
|
||||
async print() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user