This commit is contained in:
Sync1211 2022-07-24 15:05:55 +02:00
commit cf6a77e8a0
No known key found for this signature in database
GPG Key ID: B8878699435E69EC
39 changed files with 966 additions and 467 deletions

2
.gitignore vendored
View File

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

View File

@ -1,4 +1,5 @@
{
"js/ts.implicitProjectConfig.strictNullChecks": false,
"js/ts.implicitProjectConfig.checkJs": false
"js/ts.implicitProjectConfig.checkJs": false,
"js/ts.implicitProjectConfig.experimentalDecorators": true
}

8
0-prepare.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
cd www/lang/
echo "opencc zh-CN to zh-TW..."
./0-opencc.sh
cd ..
echo "tsc bundle scripts..."
./0-transpile.sh
cd ..

View File

@ -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!
[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)
[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7)](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 up a terminal and run:
@ -75,6 +76,7 @@ You can get the "pure" release, extract it, fire up a terminal and run:
python3 server.py
```
It is recommended to set the scan time to 2 seconds.
On Arch Linux based distros you may first install `bluez`, as it may not be installled by default
```bash
sudo pacman -S bluez bluez-utils
@ -130,12 +132,15 @@ All other parts, except which have special statements, are in Public Domain (`CC
--------
## Development
## Contribution
You may interested in language support, anyway. See the translation files in directory `www/lang` and `readme.i18n`!
Note: you can correct some mistakes in them, if there are any. Also feel free to make it (truly) better!
You may interested in language support, anyway. There are translation files in directory `www/lang` and `readme.i18n/`!
Also interested in code development? See [development.md](development.md)!
You can correct mistakes here/there, if there are any. Also feel free to make it (truly) better!
Also interested in code development? See [CONTRIBUTING.md](CONTRIBUTING.md) and [development.md](development.md)!
After that, give yourself a credit in `www/about.html`, if you prefer.
### Credits

4
TODO
View File

@ -8,16 +8,16 @@ Note: not ordered. do whatever I/you want
+ Even better CUPS/IPP support
+ Even better frontend, language-friendly text printing
+ Tcl/Tk frontend. More in dev-diary.txt, July 7th.
+ Re-style frontend to make it usable in e.g. Otter Browser
+ Make a build guide for android:
Summary the hacks to p4a, bleak p4a recipe, p4a webview bootstrap, and AdvancedWebView
+ Try to implement enough without more dependencies
+ More funny i18n
+ More funny "languages" (now there's lolcat; I think the next is 文言)
+ Arch Linux package / AUR, package for other distros
+ Service for other init systems (a systemd unit file is there)
+ ...
? Optimize PF2 text printing? It seems a bit slow (in algorithm).
? Use Go as part of backend? It can boost things up, and build some (essential) image manipulation in, quicker. And strip some way-too-big Python libs away (for smaller Windows/Android dist).
? Built-in PostScript (Even if very basic)
? Data compression for GB03. Optional
? Plugin, for including community features (that involves usefulness but also bloatness)

View File

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

View File

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

View File

@ -1,7 +1,7 @@
'''
Cat-Printer Commander, way to communicate with cat printers via bluetooth
Copyright © 2021-2022 NaitLee Soft. No rights reserved.
No rights reserved.
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
'''
@ -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)) )

View File

@ -1,7 +1,7 @@
'''
Minimal internationalization
Copyright © 2021-2022 NaitLee Soft. No rights reserved.
No rights reserved.
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
'''

View File

@ -1,7 +1,7 @@
'''
Provide *very* basic CUPS/IPP support
Copyright © 2021-2022 NaitLee Soft. No rights reserved.
No rights reserved.
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
'''

View File

@ -1,7 +1,7 @@
'''
Python lib for reading PF2 font files: http://grub.gibibit.com/New_font_format
Copyright © 2021-2022 NaitLee Soft. No rights reserved.
No rights reserved.
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
Don't forget to see how it's used in `text_print.py`

View File

@ -3,7 +3,7 @@
🐱🖨 Ein Projekt, das Unterstützung für einige Bluetooth-"Cat Printer"-Modelle auf *vielen* Plattformen bietet!
[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)
[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7)](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7)
## Unterstützte Geräte

View File

@ -3,17 +3,14 @@
🐱🖨 猫咪打印机:此应用*跨平台地*对一些蓝牙“喵喵机”提供支持!
[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)](https://repository-images.githubusercontent.com/403563361/ad018f6e-3a6e-4028-84b2-205f7d35c22b)
[![cat-printer-poster](https://repository-images.githubusercontent.com/403563361/93e32942-856c-4552-a8b0-b03c0976a3a7)](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
@ -130,14 +133,18 @@ Copyright © 2021-2022 NaitLee Soft. 保留一些权利。
## 开发
您可能对翻译工作感兴趣。可于目录 `www/lang``readme.i18n` 中查看翻译文件!
您可能对翻译工作感兴趣。可于目录 `www/lang``readme.i18n/` 中查看翻译文件!
注:
1. 通常英语与简体中文同时更新。请考虑其他,如繁体中文(需注意在繁体中与简体的用字、技术术语差别)。
2. 如果(真的)有能力,您也可以纠正/改善某些翻译!
2. 目前使用 [OpenCC](https://github.com/BYVoid/OpenCC) 转换简体到繁体(臺灣正體)。若有不当之处,请指出。
当前仅转换程序界面语言、暂不转换文档。
3. 如果(真的)有能力,您也可以纠正/改善某些翻译!
还想写代码?看看 [development.md](development.md)!(英文)
那之后,可以将您的贡献概括添加至 `www/about.html`
### 鸣谢
- 当然不能没有 Python 和 Web 技术!

View File

@ -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 != '':

View File

@ -1 +1 @@
0.6.0.1
0.6.0.2

6
wasm/0-compile.sh Executable file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,12 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
],
"compilerOptions": {
"experimentalDecorators": true,
"outDir": "../www/",
"module": "None",
"declaration": true
}
}

View File

@ -1,4 +1,2 @@
#!/bin/sh
cd www
npx tsc $@ --allowJs --outFile main.comp.js $(cat all_js.txt)
cd ..

View File

@ -43,6 +43,7 @@
<a target="_blank" href="https://github.com/sync1211">sync1211</a>
</dt>
<dd data-i18n="developer">Developer</dd>
<dd data-i18n="translator">Translator</dd>
</dl>
<dl>
<dt data-i18n="all-users-and-developers">All testers & users</dt>

View File

@ -1,5 +1,5 @@
`
Copyright © 2021-2022 NaitLee Soft. No rights reserved.
No rights reserved.
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
`;

View File

@ -1,5 +1,5 @@
`
Copyright © 2021-2022 NaitLee Soft. No rights reserved.
No rights reserved.
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
`;

View File

@ -1,5 +1,5 @@
`
Copyright © 2021-2022 NaitLee Soft. No rights reserved.
No rights reserved.
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
`;

9
www/image.d.ts vendored Normal file
View 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;

View File

@ -1,117 +1,125 @@
/**
* 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
"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) {
/**
* 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;

View File

@ -7,7 +7,7 @@
<link rel="stylesheet" href="main.css" />
<link rel="icon" href="icon.svg" />
</head>
<body>
<body class="hard-animation">
<main class="hard-hidden">
<div class="menu-side">
<h1 id="title" data-i18n="cat-printer">Cat Printer</h1>
@ -19,7 +19,7 @@
<label for="device-options" data-i18n="device-">Device:</label>
<select id="device-options" data-key>
</select>
<button id="device-refresh" data-i18n="refresh" data-key>Refresh</button>
<button id="device-refresh" data-i18n="scan" data-key>Scan</button>
<hr />
</div>
<div class="input-group">
@ -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>

3
www/lang/0-opencc.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
# convert with OpenCC: https://github.com/BYVoid/OpenCC
sed 's/中文(简体)/中文(臺灣正體)/' < zh-CN.json | opencc -c s2twp.json > zh-TW.json

View File

@ -154,5 +154,8 @@
"serif": "Serif",
"sans-serif": "Sans Serif",
"monospace": "Monospace",
"rotate-image": "Rotate Image"
"rotate-image": "Rotate Image",
"test-unknown-device": "Test Unknown Device",
"scan": "Scan",
"now-will-scan-for-all-bluetooth-devices-nearby": "Now will scan for all bluetooth devices nearby."
}

View File

@ -1,6 +1,7 @@
{
"en-US": "English (US)",
"zh-CN": "中文(简体)",
"zh-TW": "中文(臺灣正體)",
"de-DE": "Deutsch",
"lolcat": "LOLCAT"
}

View File

@ -132,5 +132,8 @@
"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",
"scan": "FIND"
}

View File

@ -124,9 +124,28 @@
"text-size": "大小",
"enter-text": "在此处输入文本",
"wrap-words-by-spaces": "空格处换行(不建议用于汉语)",
"minor-tweaks": "小优化",
"minor-tweaks": "细节调整",
"serif": "衬线字体",
"sans-serif": "无衬线字体",
"monospace": "等宽字体",
"rotate-image": "旋转图像"
"rotate-image": "旋转图像",
"test-unknown-device": "测试未知设备",
"now-will-scan-for-all-bluetooth-devices-nearby": "现在将搜索附近所有设备。",
"scan": "扫描",
"you-can-see-all-javascript-programs-used": "您可以看到此程序使用的 JavaScript 脚本均为自由软件。",
"javascript-resource": "资源",
"javascript-license": "许可",
"javascript-source": "源",
"javascript-description": "详述",
"javascript-everyjs-description": "所有开发脚本的动态联接",
"javascript-maincompjs-description": "经过转译的所有以下开发脚本包,起兼容作用",
"javascript-loaderjs-description": "动态加载其余脚本,并在出现问题时回退",
"javascript-polyfilljs-description": "添加不被旧浏览器支持的功能",
"javascript-i18nextjs-description": "国际化 (i18n)“扩展”",
"javascript-i18njs-description": "用于国际化(语言支持)",
"javascript-imagejs-description": "用于画布 (canvas) 上的图像处理",
"javascript-accessibilityjs-description": "一些无障碍功能",
"javascript-catprinter-description": "猫咪打印机 (Cat-Printer) 主脚本",
"free-software": "自由软件",
"free-software-description": "尊重您计算自由的软件。"
}

151
www/lang/zh-TW.json Normal file
View File

@ -0,0 +1,151 @@
{
"$language": "中文(臺灣正體)",
"cat-printer": "貓咪印表機",
"printer": "印表機",
"device-": "裝置:",
"refresh": "重新整理",
"mode-": "模式:",
"canvas": "畫布",
"document": "文件",
"insert-picture": "插入圖片",
"insert-text": "輸入文字",
"help": "幫助",
"javascript-license-information": "JavaScript 許可證資訊",
"settings": "設定",
"image": "影象",
"threshold-": "閾值:",
"transparent-as-white": "透明為白色",
"misc": "雜項",
"system": "系統",
"disable-animation": "禁用動畫",
"exit": "退出",
"error-message": "錯誤訊息",
"preview": "預覽",
"print": "列印",
"expand": "擴大",
"crop": "裁減",
"scanning-for-devices": "正在掃描裝置……",
"scan-time-": "掃描時間:",
"-seconds": "秒",
"no-available-devices-found": "未發現可用裝置",
"found-0-available-devices": "發現 {0} 個可用裝置",
"please-check-if-the-printer-is-down": "請檢查印表機是否已關閉",
"printing": "列印中……",
"finished": "完成",
"coming-soon": "即將到來……",
"dry-run": "幹執行",
"dry-run-test-print-process-only": "幹執行:僅測試列印流程",
"you-can-close-this-page-manually": "您可手動關閉此頁面",
"please-enable-bluetooth": "請啟用藍芽",
"error-happened-please-check-error-message": "發生錯誤,請檢查錯誤訊息",
"you-can-seek-for-help-with-detailed-info-below": "您可以使用以下詳細資訊尋求幫助",
"or-try-to-scan-longer": "或者嘗試延長掃描時間",
"print-to-cat-printer": "列印到貓咪印表機。",
"supported-models-": "支援的型號:",
"path-to-input-file-dash-for-stdin": "輸入檔案的位置。使用 '-' 作為標準輸入",
"please-install-pyobjc-via-pip": "請從 pip 安裝 `pyobjc`",
"please-install-bleak-via-pip": "請從 pip 安裝 `bleak`",
"folder-printer_lib-is-incomplete-or-missing-please-check": "資料夾 `printer_lib` 不完整或丟失,請檢查",
"input-is-not-pbm-image": "輸入不是 PBM 影象",
"unsuitable-image-width-expected-0-got-1": "不適合的影象寬度,需要 {0}, 輸入為 {1}",
"broken-pbm-image": "損壞的 PBM 影象",
"input-is-not-text-file": "輸入不是文字檔案",
"match-printer-with-this-name-or-address": "使用符合此名稱或地址的印表機",
"virtual-run-on-specified-model": "在指定的型號模擬執行",
"font-size-0": "字型大小 {0}",
"stopping": "停止中",
"connecting": "正在連線",
"model-0-is-not-supported-yet": "型號 '{0}' 仍未支援",
"invalid-address-0": "無效的地址:'{0}'",
"will-listen-on-all-addresses": "將接受所有地址的連線",
"serving-at-0": "伺服器在 {0}",
"disconnecting-from-printer": "正在從印表機斷開連線",
"flip-horizontally": "水平翻轉",
"flip-vertically": "垂直翻轉",
"dump-traffic": "轉儲資料",
"right-to-left-text-order": "從右到左的文字順序",
"auto-wrap-line": "自動折行",
"process-as-": "處理方式:",
"text": "文字",
"picture": "照片",
"pattern": "圖案",
"large-font": "大字型",
"accessibility": "無障礙",
"language": "語言",
"layout": "佈局",
"ok": "確定",
"cancel": "取消",
"yes": "是",
"no": "否",
"about": "關於",
"home-page-": "主頁:",
"contributors": "貢獻者",
"developer": "開發者",
"translator": "翻譯",
"all-users-and-developers": "每位使用者及開發者",
"everyone-is-awesome": "每個人都是好樣的!",
"license": "許可證",
"exiting": "退出中……",
"dark-theme": "深色主題",
"high-contrast": "高對比度",
"welcome": "歡迎!",
"copyright-and-license": "版權與許可",
"some-rights-reserved": "保留一些權利。",
"ENTER": "回車",
"SPACE": "空格",
"ESCAPE": "ESC",
"TAB": "Tab",
"COMMA": "逗號",
"DOT": "句號",
"to-enter-keyboard-mode-press-tab": "要進入鍵盤模式,請按 Tab",
"usage-": "用法:",
"positional-arguments-": "引數:",
"options-": "選項:",
"show-this-help-message": "顯示此幫助資訊",
"do-nothing": "什麼也不做",
"scan-for-a-printer": "掃描印表機",
"text-printing-mode-with-options": "啟用文字列印並指定選項",
"image-printing-options": "圖片列印選項",
"convert-input-image-with-imagemagick": "使用 ImageMagick 轉換輸入圖片",
"reset-configuration-": "要重置配置嗎?",
"brightness-": "亮度:",
"text-printing-mode": "文字列印模式",
"internal-error-please-see-terminal": "內部錯誤,請檢查終端",
"control-printer-thermal-strength": "控制列印力度",
"strength-": "力度:",
"or-drag-file-to-below": "或拖拽檔案至下方",
"reset": "重置",
"cat-face-toward": "貓臉朝上",
"quality-": "質量:",
"print-quality": "列印質量",
"show-more-options": "顯示更多選項",
"text-font": "字型",
"text-size": "大小",
"enter-text": "在此處輸入文字",
"wrap-words-by-spaces": "空格處換行(不建議用於漢語)",
"minor-tweaks": "細節調整",
"serif": "襯線字型",
"sans-serif": "無襯線字型",
"monospace": "等寬字型",
"rotate-image": "旋轉影象",
"test-unknown-device": "測試未知裝置",
"now-will-scan-for-all-bluetooth-devices-nearby": "現在將搜尋附近所有裝置。",
"scan": "掃描",
"you-can-see-all-javascript-programs-used": "您可以看到此程式使用的 JavaScript 指令碼均為自由軟體。",
"javascript-resource": "資源",
"javascript-license": "許可",
"javascript-source": "源",
"javascript-description": "詳述",
"javascript-everyjs-description": "所有開發指令碼的動態聯接",
"javascript-maincompjs-description": "經過轉譯的所有以下開發指令碼包,起相容作用",
"javascript-loaderjs-description": "動態載入其餘指令碼,並在出現問題時回退",
"javascript-polyfilljs-description": "新增不被舊瀏覽器支援的功能",
"javascript-i18nextjs-description": "國際化 (i18n)“擴充套件”",
"javascript-i18njs-description": "用於國際化(語言支援)",
"javascript-imagejs-description": "用於畫布 (canvas) 上的影象處理",
"javascript-accessibilityjs-description": "一些無障礙功能",
"javascript-catprinter-description": "貓咪印表機 (Cat-Printer) 主指令碼",
"free-software": "自由軟體",
"free-software-description": "尊重您計算自由的軟體。"
}

View File

@ -1,9 +1,11 @@
`
Copyright © 2021-2022 NaitLee Soft. No rights reserved.
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
*/

View File

@ -15,7 +15,6 @@
--fore-color: #111;
--back-color: #eee;
--canvas-back: #fff;
--curve: cubic-bezier(.08,.82,.17,1);
--panel-height: 20em;
--target-color: rgba(0, 255, 255, 0.2);
--notice-wait: rgba(0, 128, 255, 0.2);
@ -24,6 +23,19 @@
--notice-error: rgba(255, 0, 0, 0.2);
--shade: rgba(238, 238, 238, 0.5);
}
@media (prefers-color-scheme: dark) {
:root { --fore-color: #eee; --back-color: #333; --shade: rgba(51, 51, 51, 0.5); }
a:link, a:visited { color: #66f; }
a:hover, a:active { color: #77f; }
.canvas-group, .logo { opacity: 0.6; }
#control-overlay { background-color: var(--shade); }
}
/* so silly... */
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 { opacity: 0.6; }
body.dark-theme #control-overlay { background-color: var(--shade); }
body {
border: none;
@ -36,12 +48,26 @@ body {
margin: 1em 0;
user-select: none;
}
* {
box-sizing: border-box;
}
body.android .hide-on-android {
display: none;
}
* {
box-sizing: border-box;
transition-property: background-color, transform,/* box-shadow,*/ flex-grow, opacity;
transition-delay: 0s;
transition-duration: var(--anim-time);
transition-timing-function: ease-out;
}
button, input[type="number"], input[type="text"], select,
#dialog>.shade, #dialog>.content, #canvas {
transition-timing-function: cubic-bezier(.08,.82,.17,1);
}
#dialog>.shade {
transition-duration: calc(var(--anim-time) / 2);
}
a {
transition: color var(--anim-time) ease-out;
}
.selectable {
user-select: all;
}
@ -83,7 +109,6 @@ button, input[type="number"], input[type="text"], select {
margin: var(--span-half) var(--span);
border: var(--border) solid var(--fore-color);
padding: var(--span-half) var(--span);
transition: all var(--anim-time) var(--curve);
cursor: pointer;
min-width: 6em;
line-height: calc(var(--font-size) + var(--span));
@ -179,7 +204,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 +229,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;
@ -211,9 +243,7 @@ main, header, footer {
background-color: var(--back-color);
}
.compact-button {
width: max-content;
height: var(--compact-menu-height);
flex-grow: 1;
line-height: var(--compact-menu-height);
text-align: center;
cursor: pointer;
@ -222,12 +252,11 @@ main, header, footer {
border-bottom: var(--border) solid transparent;
padding: 0;
margin: 0;
flex-grow: 1;
}
.compact-button:hover {
transform: unset;
margin: 0;
padding: 0 var(--span) calc(var(--span-double));
min-width: 6em;
flex-grow: 1.2;
}
.compact-button.active {
border: var(--border) solid var(--fore-color);
@ -245,7 +274,6 @@ main, header, footer {
#canvas {
position: absolute;
opacity: 0;
transition: opacity var(--anim-time) var(--curve);
z-index: 1;
}
#canvas:hover {
@ -300,6 +328,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%;
@ -373,15 +404,13 @@ iframe#frame {
text-align: center;
z-index: 2;
opacity: 1;
transition: opacity var(--anim-time) var(--curve);
}
#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);
transform-origin: center 33%;
}
#dialog.hidden {
@ -391,18 +420,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 +441,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 +458,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); }
@ -452,7 +482,7 @@ iframe#frame {
text-align: center;
z-index: 3;
opacity: 1;
transition: opacity var(--anim-time) var(--curve);
transition-duration: 0.2s;
}
.logo {
background-image: url('icon.svg');
@ -507,9 +537,6 @@ iframe#frame {
transform: translate(-1em, calc(var(--font-size) * -1));
}
a {
transition: all var(--anim-time) ease-out;
}
@keyframes delay-scrollable {
from { overflow: hidden; }
to { overflow: auto; }
@ -542,14 +569,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 +610,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;
@ -598,46 +618,26 @@ a {
}
}
/* @media (min-resolution: 120dpi) {
:root { --font-size: calc(1.2rem * var(--dpi-zoom)); }
}
@media (min-resolution: 144dpi) {
:root { --font-size: calc(1.2rem * var(--dpi-zoom) * var(--dpi-zoom)); }
} */
@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); }
@media (prefers-reduced-motion) {
body *,
body *::before,
body *::after {
transition-duration: 0ms !important;
transition: none;
animation-timing-function: steps(1);
animation-duration: 0ms !important;
transition-duration: 0s !important;
animation-duration: 0s !important;
transition-timing-function: steps(1) !important;
animation-timing-function: steps(1) !important;
}
}
/* but i have no way... */
body.no-animation,
body.hard-animation,
body.no-animation *,
body.hard-animation *:not(#loading-screen, #loading-screen *),
body.no-animation *::before,
body.no-animation *::after {
transition-duration: 0ms !important;
transition: none;
animation-timing-function: steps(1);
animation-duration: 0ms !important;
transition-duration: 0s !important;
animation-duration: 0s !important;
transition-timing-function: steps(1) !important;
animation-timing-function: steps(1) !important;
}
body.large-font,
@ -667,7 +667,7 @@ body.high-contrast {
}
body.high-contrast .shade { transition-duration: 0s; opacity: 1; }
/* body.high-contrast * { background-color: var(--back-color); } */
body.high-contrast .logo, body.high-contrast .canvas-group { filter: unset !important; }
body.high-contrast .logo, body.high-contrast .canvas-group { opacity: 1 !important; }
body.high-contrast #notice * { border: var(--border) dashed var(--fore-color); }
body.high-contrast a:any-link { color: #00f; }
body.high-contrast #control-overlay { background-color: var(--shade); }

View File

@ -1,3 +1,4 @@
`
Cat-Printer: Web Frontend
@ -205,62 +206,89 @@ async function callApi(path, body, errorPreHandler) {
}
});
}
/**
* 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;
const Ev = (function() {
/** @type {Record<string, NodeListOf<HTMLElement>>} */
let map = {};
return {
/**
* 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);
callback.call(thisArg || e, event);
});
if (element.hasAttribute('data-default') || element.checked) {
element.dispatchEvent(new Event(type));
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,72 @@ 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(0);
/** @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 = ''; // trigger some dumb browser
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 +616,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,16 +627,20 @@ 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;
}
}
/** Global variable indicating current language */
var language = navigator.language;
/** @param {Document} doc */
function applyI18nToDom(doc) {
doc = doc || document;
let elements = doc.querySelectorAll('*[data-i18n]');
let i18n_data, translated_string;
doc.querySelector('html').lang = language;
elements.forEach(element => {
i18n_data = element.getAttribute('data-i18n');
translated_string = i18n(i18n_data);
@ -643,7 +657,8 @@ async function initI18n(current_language) {
/** @type {{ [code: string]: string }} */
let list = await fetch('/lang/list.json').then(r => r.json());
let use_language = async (value) => {
i18n.useLanguage(value);
language = value;
i18n.useLanguage(language);
i18n.add(value, await fetch(`/lang/${value}.json`).then(r => r.json()), true);
applyI18nToDom();
}
@ -698,10 +713,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 +724,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 +744,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 +826,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';
@ -796,6 +834,7 @@ class Main {
}
if (typeof initKeyboardShortcuts === 'function') initKeyboardShortcuts();
// this.searchDevices();
document.body.classList.remove('hard-animation');
document.querySelector('main').classList.remove('hard-hidden');
document.getElementById('loading-screen').classList.add('hidden');
resolve();
@ -824,30 +863,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 +875,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 +937,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 +950,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() {

View File

@ -1,5 +1,5 @@
`
Copyright © 2021-2022 NaitLee Soft. No rights reserved.
No rights reserved.
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
`;