mirror of
https://github.com/NaitLee/Cat-Printer.git
synced 2025-05-18 08:10:24 -07:00
Merge branch 'main' of https://github.com/NaitLee/Cat-printer into main
This commit is contained in:
commit
cf6a77e8a0
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,6 +7,8 @@ www/main.comp.js
|
|||||||
www/vconsole.js
|
www/vconsole.js
|
||||||
# https://github.com/delight-im/Android-AdvancedWebView
|
# https://github.com/delight-im/Android-AdvancedWebView
|
||||||
build-android/advancedwebview
|
build-android/advancedwebview
|
||||||
|
# cd wasm && npm install
|
||||||
|
wasm/node_modules
|
||||||
# python bytecode
|
# python bytecode
|
||||||
*.pyc
|
*.pyc
|
||||||
# releases
|
# releases
|
||||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"js/ts.implicitProjectConfig.strictNullChecks": false,
|
"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
8
0-prepare.sh
Executable 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 ..
|
27
README.md
27
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!
|
🐱🖨 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
|
## Models
|
||||||
|
|
||||||
Currently:
|
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!
|
||||||
| Known to support | `GB0X, GT01, YT01` |
|
|
||||||
|
|
||||||
\* `X` represents any digit
|
|
||||||
|
|
||||||
## Features
|
## 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.
|
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.
|
(Foreground) Location permission is required for scanning Bluetooth devices in newer Android system.
|
||||||
|
|
||||||
|
Recommend to set scan time to 1 second.
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
Get the newest release archive with "windows" in the file name,
|
Get the newest release archive with "windows" in the file name,
|
||||||
extract to somewhere and run `start.bat`
|
extract to somewhere and run `start.bat`
|
||||||
|
|
||||||
|
Windows typically needs longer scan time. Defaults to 4 seconds, try to find your case.
|
||||||
|
|
||||||
### GNU/Linux
|
### GNU/Linux
|
||||||
|
|
||||||
You can get the "pure" release, extract it, fire up a terminal and run:
|
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
|
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
|
On Arch Linux based distros you may first install `bluez`, as it may not be installled by default
|
||||||
```bash
|
```bash
|
||||||
sudo pacman -S bluez bluez-utils
|
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`!
|
You may interested in language support, anyway. There are 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!
|
|
||||||
|
|
||||||
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
|
### Credits
|
||||||
|
|
||||||
|
4
TODO
4
TODO
@ -8,16 +8,16 @@ Note: not ordered. do whatever I/you want
|
|||||||
+ Even better CUPS/IPP support
|
+ Even better CUPS/IPP support
|
||||||
+ Even better frontend, language-friendly text printing
|
+ Even better frontend, language-friendly text printing
|
||||||
+ Tcl/Tk frontend. More in dev-diary.txt, July 7th.
|
+ 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:
|
+ Make a build guide for android:
|
||||||
Summary the hacks to p4a, bleak p4a recipe, p4a webview bootstrap, and AdvancedWebView
|
Summary the hacks to p4a, bleak p4a recipe, p4a webview bootstrap, and AdvancedWebView
|
||||||
+ Try to implement enough without more dependencies
|
+ 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
|
+ Arch Linux package / AUR, package for other distros
|
||||||
+ Service for other init systems (a systemd unit file is there)
|
+ Service for other init systems (a systemd unit file is there)
|
||||||
+ ...
|
+ ...
|
||||||
|
|
||||||
? Optimize PF2 text printing? It seems a bit slow (in algorithm).
|
? 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)
|
? Built-in PostScript (Even if very basic)
|
||||||
? Data compression for GB03. Optional
|
? Data compression for GB03. Optional
|
||||||
? Plugin, for including community features (that involves usefulness but also bloatness)
|
? Plugin, for including community features (that involves usefulness but also bloatness)
|
||||||
|
@ -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.
|
So the Internet JavaScript memes are damned true.
|
||||||
https://programmerhumor.io/javascript-memes/why-is-it-like-this-2/
|
https://programmerhumor.io/javascript-memes/why-is-it-like-this-2/
|
||||||
https://programmerhumor.io/javascript-memes/sorry-dad-_-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
|
model: Model = None
|
||||||
'The printer model'
|
'The printer model'
|
||||||
|
|
||||||
scan_timeout: float = 4.0
|
scan_time: float = 4.0
|
||||||
|
|
||||||
connection_timeout : float = 5.0
|
connection_timeout : float = 5.0
|
||||||
|
|
||||||
@ -351,15 +351,19 @@ class PrinterDriver(Commander):
|
|||||||
self.device.start_notify(self.rx_characteristic, notify)
|
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`,
|
''' Scan for supported devices, optionally filter with `identifier`,
|
||||||
which can be device model (bluetooth name), and optionally MAC address, after a comma.
|
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 `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,
|
Note: MAC address doesn't work on Apple MacOS. In place with it,
|
||||||
You need an UUID of BLE device dynamically given by MacOS.
|
You need an UUID of BLE device dynamically given by MacOS.
|
||||||
'''
|
'''
|
||||||
if self.fake:
|
if self.fake:
|
||||||
return
|
return []
|
||||||
|
if everything:
|
||||||
|
devices = self.loop(BleakScanner.discover(self.scan_time))
|
||||||
|
return devices
|
||||||
if identifier:
|
if identifier:
|
||||||
if identifier.find(',') != -1:
|
if identifier.find(',') != -1:
|
||||||
name, address = identifier.split(',')
|
name, address = identifier.split(',')
|
||||||
@ -370,12 +374,12 @@ class PrinterDriver(Commander):
|
|||||||
if use_result:
|
if use_result:
|
||||||
self.connect(name, address)
|
self.connect(name, address)
|
||||||
return [BLEDevice(address, name)]
|
return [BLEDevice(address, name)]
|
||||||
elif (identifier not in Models and
|
if (identifier not in Models and
|
||||||
identifier[2::3] != ':::::' and len(identifier.replace('-', '')) != 32):
|
identifier[2::3] != ':::::' and len(identifier.replace('-', '')) != 32):
|
||||||
error('model-0-is-not-supported-yet', identifier, exception=PrinterError)
|
error('model-0-is-not-supported-yet', identifier, exception=PrinterError)
|
||||||
# scanner = BleakScanner()
|
# scanner = BleakScanner()
|
||||||
devices = [x for x in self.loop(
|
devices = [x for x in self.loop(
|
||||||
BleakScanner.discover(self.scan_timeout)
|
BleakScanner.discover(self.scan_time)
|
||||||
) if x.name in Models]
|
) if x.name in Models]
|
||||||
if identifier:
|
if identifier:
|
||||||
if identifier in Models:
|
if identifier in Models:
|
||||||
@ -397,7 +401,7 @@ class PrinterDriver(Commander):
|
|||||||
self.scan(identifier, use_result=True)
|
self.scan(identifier, use_result=True)
|
||||||
if self.device is None and not self.fake:
|
if self.device is None and not self.fake:
|
||||||
error('no-available-devices-found', exception=PrinterError)
|
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)
|
printer_data = PrinterData(self.model.paper_width, file)
|
||||||
self._print_bitmap(printer_data)
|
self._print_bitmap(printer_data)
|
||||||
elif mode == 'text':
|
elif mode == 'text':
|
||||||
@ -443,7 +447,7 @@ class PrinterDriver(Commander):
|
|||||||
if self.quality: # well, slower makes stable heating
|
if self.quality: # well, slower makes stable heating
|
||||||
self.set_speed(self.quality)
|
self.set_speed(self.quality)
|
||||||
if self.energy is not None:
|
if self.energy is not None:
|
||||||
self.set_energy(self.energy * 0xff)
|
self.set_energy(self.energy * 0x100)
|
||||||
self.apply_energy()
|
self.apply_energy()
|
||||||
self.update_device()
|
self.update_device()
|
||||||
self.flush()
|
self.flush()
|
||||||
@ -648,7 +652,7 @@ def _main():
|
|||||||
printer = PrinterDriver()
|
printer = PrinterDriver()
|
||||||
|
|
||||||
scan_param = args.scan.split(',')
|
scan_param = args.scan.split(',')
|
||||||
printer.scan_timeout = float(scan_param[0])
|
printer.scan_time = float(scan_param[0])
|
||||||
identifier = ','.join(scan_param[1:])
|
identifier = ','.join(scan_param[1:])
|
||||||
if args.energy is not None:
|
if args.energy is not None:
|
||||||
printer.energy = int(args.energy * 0xff)
|
printer.energy = int(args.energy * 0xff)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'''
|
'''
|
||||||
Cat-Printer Commander, way to communicate with cat printers via bluetooth
|
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
|
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):
|
def set_energy(self, amount: int):
|
||||||
''' Set thermal energy, max to `0xffff`
|
''' 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)) )
|
self.send( self.make_command(0xaf, int_to_bytes(amount)) )
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'''
|
'''
|
||||||
Minimal internationalization
|
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
|
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'''
|
'''
|
||||||
Provide *very* basic CUPS/IPP support
|
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
|
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'''
|
'''
|
||||||
Python lib for reading PF2 font files: http://grub.gibibit.com/New_font_format
|
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
|
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`
|
Don't forget to see how it's used in `text_print.py`
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
🐱🖨 Ein Projekt, das Unterstützung für einige Bluetooth-"Cat Printer"-Modelle auf *vielen* Plattformen bietet!
|
🐱🖨 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
|
## 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` 表示任意数字)
|
||||||
|
|
||||||
| | |
|
可在 Web 界面测试未列出的型号。在 `设置 -> 测试未知设备`
|
||||||
|----|----|
|
有概率成功!
|
||||||
| 已知支持 | `GB0X, GT01, YT01` |
|
|
||||||
|
|
||||||
\* `X` 表示任意数字
|
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
@ -60,11 +57,15 @@
|
|||||||
应用可能请求“后台位置”权限,您可以拒绝它。
|
应用可能请求“后台位置”权限,您可以拒绝它。
|
||||||
(前台)位置权限是较新版安卓系统扫描蓝牙设备所需要的。
|
(前台)位置权限是较新版安卓系统扫描蓝牙设备所需要的。
|
||||||
|
|
||||||
|
建议将扫描时间设为 1 秒。
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
获取名称中有 "windows" 的版本,
|
获取名称中有 "windows" 的版本,
|
||||||
解压并运行 `start.bat`
|
解压并运行 `start.bat`
|
||||||
|
|
||||||
|
Windows 通常需要较长的扫描时间。默认为 4 秒,可按需调整。
|
||||||
|
|
||||||
### GNU/Linux
|
### GNU/Linux
|
||||||
|
|
||||||
您可以获取“纯净(pure)”版,解压、在其中打开终端并输入:
|
您可以获取“纯净(pure)”版,解压、在其中打开终端并输入:
|
||||||
@ -72,6 +73,8 @@
|
|||||||
python3 server.py
|
python3 server.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
建议将扫描时间设为 2 秒。
|
||||||
|
|
||||||
在 Arch Linux 等发行版您可能需要首先安装 `bluez`
|
在 Arch Linux 等发行版您可能需要首先安装 `bluez`
|
||||||
```bash
|
```bash
|
||||||
sudo pacman -S bluez bluez-utils
|
sudo pacman -S bluez bluez-utils
|
||||||
@ -130,14 +133,18 @@ Copyright © 2021-2022 NaitLee Soft. 保留一些权利。
|
|||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
您可能对翻译工作感兴趣。可于目录 `www/lang` 和 `readme.i18n` 中查看翻译文件!
|
您可能对翻译工作感兴趣。可于目录 `www/lang` 和 `readme.i18n/` 中查看翻译文件!
|
||||||
|
|
||||||
注:
|
注:
|
||||||
1. 通常英语与简体中文同时更新。请考虑其他,如繁体中文(需注意在繁体中与简体的用字、技术术语差别)。
|
1. 通常英语与简体中文同时更新。请考虑其他,如繁体中文(需注意在繁体中与简体的用字、技术术语差别)。
|
||||||
2. 如果(真的)有能力,您也可以纠正/改善某些翻译!
|
2. 目前使用 [OpenCC](https://github.com/BYVoid/OpenCC) 转换简体到繁体(臺灣正體)。若有不当之处,请指出。
|
||||||
|
当前仅转换程序界面语言、暂不转换文档。
|
||||||
|
3. 如果(真的)有能力,您也可以纠正/改善某些翻译!
|
||||||
|
|
||||||
还想写代码?看看 [development.md](development.md)!(英文)
|
还想写代码?看看 [development.md](development.md)!(英文)
|
||||||
|
|
||||||
|
那之后,可以将您的贡献概括添加至 `www/about.html`
|
||||||
|
|
||||||
### 鸣谢
|
### 鸣谢
|
||||||
|
|
||||||
- 当然不能没有 Python 和 Web 技术!
|
- 当然不能没有 Python 和 Web 技术!
|
||||||
|
16
server.py
16
server.py
@ -49,6 +49,7 @@ mime_type = {
|
|||||||
'json': 'application/json;charset=utf-8',
|
'json': 'application/json;charset=utf-8',
|
||||||
'png': 'image/png',
|
'png': 'image/png',
|
||||||
'svg': 'image/svg+xml;charset=utf-8',
|
'svg': 'image/svg+xml;charset=utf-8',
|
||||||
|
'wasm': 'application/wasm',
|
||||||
'octet-stream': 'application/octet-stream'
|
'octet-stream': 'application/octet-stream'
|
||||||
}
|
}
|
||||||
def mime(url: str):
|
def mime(url: str):
|
||||||
@ -72,10 +73,10 @@ class PrinterServerHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
settings = DictAsObject({
|
settings = DictAsObject({
|
||||||
'config_path': 'config.json',
|
'config_path': 'config.json',
|
||||||
'version': 2,
|
'version': 3,
|
||||||
'first_run': True,
|
'first_run': True,
|
||||||
'is_android': False,
|
'is_android': False,
|
||||||
'scan_timeout': 4.0,
|
'scan_time': 4.0,
|
||||||
'dry_run': False,
|
'dry_run': False,
|
||||||
'energy': 0.2
|
'energy': 0.2
|
||||||
})
|
})
|
||||||
@ -193,11 +194,13 @@ class PrinterServerHandler(BaseHTTPRequestHandler):
|
|||||||
def update_printer(self):
|
def update_printer(self):
|
||||||
'Update `PrinterDriver` state/config'
|
'Update `PrinterDriver` state/config'
|
||||||
self.printer.dry_run = self.settings.dry_run
|
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.fake = self.settings.fake
|
||||||
self.printer.dump = self.settings.dump
|
self.printer.dump = self.settings.dump
|
||||||
self.printer.energy = self.settings.energy
|
if self.settings.energy is not None:
|
||||||
self.printer.quality = self.settings.quality
|
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_h = self.settings.flip_h or self.settings.flip
|
||||||
self.printer.flip_v = self.settings.flip_v or self.settings.flip
|
self.printer.flip_v = self.settings.flip_v or self.settings.flip
|
||||||
self.printer.rtl = self.settings.force_rtl
|
self.printer.rtl = self.settings.force_rtl
|
||||||
@ -218,7 +221,7 @@ class PrinterServerHandler(BaseHTTPRequestHandler):
|
|||||||
devices_list = [{
|
devices_list = [{
|
||||||
'name': device.name,
|
'name': device.name,
|
||||||
'address': device.address
|
'address': device.address
|
||||||
} for device in self.printer.scan()]
|
} for device in self.printer.scan(everything=data.get('everything'))]
|
||||||
self.api_success({
|
self.api_success({
|
||||||
'devices': devices_list
|
'devices': devices_list
|
||||||
})
|
})
|
||||||
@ -313,6 +316,7 @@ class PrinterServer(HTTPServer):
|
|||||||
def finish_request(self, request, client_address):
|
def finish_request(self, request, client_address):
|
||||||
if self.handler is None:
|
if self.handler is None:
|
||||||
self.handler = self.handler_class(request, client_address, self)
|
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:
|
with open(os.path.join('www', 'all_js.txt'), 'r', encoding='utf-8') as file:
|
||||||
for path in file.read().split('\n'):
|
for path in file.read().split('\n'):
|
||||||
if path != '':
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,2 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
cd www
|
|
||||||
npx tsc $@ --allowJs --outFile main.comp.js $(cat all_js.txt)
|
npx tsc $@ --allowJs --outFile main.comp.js $(cat all_js.txt)
|
||||||
cd ..
|
|
@ -43,6 +43,7 @@
|
|||||||
<a target="_blank" href="https://github.com/sync1211">sync1211</a>
|
<a target="_blank" href="https://github.com/sync1211">sync1211</a>
|
||||||
</dt>
|
</dt>
|
||||||
<dd data-i18n="developer">Developer</dd>
|
<dd data-i18n="developer">Developer</dd>
|
||||||
|
<dd data-i18n="translator">Translator</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<dl>
|
<dl>
|
||||||
<dt data-i18n="all-users-and-developers">All testers & users</dt>
|
<dt data-i18n="all-users-and-developers">All testers & users</dt>
|
||||||
|
@ -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
|
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
|
||||||
`;
|
`;
|
||||||
|
@ -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
|
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
|
||||||
`;
|
`;
|
||||||
|
@ -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
|
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
|
||||||
`;
|
`;
|
||||||
|
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;
|
230
www/image.js
230
www/image.js
@ -1,117 +1,125 @@
|
|||||||
|
"use strict";
|
||||||
/**
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
* Convert colored image to grayscale.
|
exports.rotateRgba = exports.monoToPbm = exports.monoHalftone = exports.monoSteinberg = exports.monoDirect = exports.monoToRgba = exports.monoGrayscale = void 0;
|
||||||
* @param {Uint8ClampedArray} image_data `data` property of an ImageData instance,
|
function monoGrayscale(rgba, brightness, alpha_as_white) {
|
||||||
* i.e. `canvas.getContext('2d').getImageData(...).data`
|
let mono = new Uint8ClampedArray(rgba.length);
|
||||||
* @param {Uint8ClampedArray} mono_data an `Uint8ClampedArray` that have the size `w * h`
|
let r = 0.0, g = 0.0, b = 0.0, a = 0.0, m = 0.0, n = 0;
|
||||||
* i.e. `image_data.length / 4`
|
for (let i = 0; i < mono.length; ++i) {
|
||||||
* The result data will be here, as a 8-bit grayscale image data.
|
n = rgba[i];
|
||||||
* @param {number} w width of image
|
// little endian
|
||||||
* @param {number} h height of image
|
r = (n & 0xff), g = (n >> 8 & 0xff), b = (n >> 16 & 0xff);
|
||||||
* @param {number} t brightness, historically "threshold"
|
a = (n >> 24 & 0xff) / 0xff;
|
||||||
* @param {boolean} transparencyAsWhite whether render opacity as white rather than black
|
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 after = new Uint32Array(before.length);
|
||||||
let p, q, r, g, b, a, m;
|
|
||||||
for (let j = 0; j < h; j++) {
|
for (let j = 0; j < h; j++) {
|
||||||
for (let i = 0; i < w; i++) {
|
for (let i = 0; i < w; i++) {
|
||||||
p = j * w + i;
|
after[j * w + i] = before[(w - i - 1) * h + j];
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return after;
|
||||||
}
|
}
|
||||||
|
exports.rotateRgba = rotateRgba;
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<link rel="stylesheet" href="main.css" />
|
<link rel="stylesheet" href="main.css" />
|
||||||
<link rel="icon" href="icon.svg" />
|
<link rel="icon" href="icon.svg" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="hard-animation">
|
||||||
<main class="hard-hidden">
|
<main class="hard-hidden">
|
||||||
<div class="menu-side">
|
<div class="menu-side">
|
||||||
<h1 id="title" data-i18n="cat-printer">Cat Printer</h1>
|
<h1 id="title" data-i18n="cat-printer">Cat Printer</h1>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
<label for="device-options" data-i18n="device-">Device:</label>
|
<label for="device-options" data-i18n="device-">Device:</label>
|
||||||
<select id="device-options" data-key>
|
<select id="device-options" data-key>
|
||||||
</select>
|
</select>
|
||||||
<button id="device-refresh" data-i18n="refresh" data-key>Refresh</button>
|
<button id="device-refresh" data-i18n="scan" data-key>Scan</button>
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@ -30,6 +30,10 @@
|
|||||||
<input type="radio" name="algo" value="algo-steinberg" data-key checked />
|
<input type="radio" name="algo" value="algo-steinberg" data-key checked />
|
||||||
<span data-i18n="picture">Picture</span>
|
<span data-i18n="picture">Picture</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="algo" value="algo-halftone" data-key />
|
||||||
|
<span data-i18n="pattern">Pattern</span>
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="algo" value="algo-direct" data-key />
|
<input type="radio" name="algo" value="algo-direct" data-key />
|
||||||
<span data-i18n="text">Text</span>
|
<span data-i18n="text">Text</span>
|
||||||
@ -99,6 +103,7 @@
|
|||||||
<span>🌎</span>
|
<span>🌎</span>
|
||||||
<span data-i18n="accessibility">Accessibility</span>
|
<span data-i18n="accessibility">Accessibility</span>
|
||||||
</button>
|
</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>
|
<button class="hidden" data-panel="panel-error" data-i18n="error-message" data-key>Error Message</button>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<button id="button-exit" data-i18n="exit" data-key>Exit</button>
|
<button id="button-exit" data-i18n="exit" data-key>Exit</button>
|
||||||
@ -156,6 +161,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<span id="hint-tab-control" class="hide-on-android"
|
<span id="hint-tab-control" class="hide-on-android"
|
||||||
data-i18n="to-enter-keyboard-mode-press-tab">To enter Keyboard Mode, press Tab</span>
|
data-i18n="to-enter-keyboard-mode-press-tab">To enter Keyboard Mode, press Tab</span>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 data-i18n="layout">Layout</h2>
|
<h2 data-i18n="layout">Layout</h2>
|
||||||
|
3
www/lang/0-opencc.sh
Executable file
3
www/lang/0-opencc.sh
Executable 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
|
@ -154,5 +154,8 @@
|
|||||||
"serif": "Serif",
|
"serif": "Serif",
|
||||||
"sans-serif": "Sans Serif",
|
"sans-serif": "Sans Serif",
|
||||||
"monospace": "Monospace",
|
"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."
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"en-US": "English (US)",
|
"en-US": "English (US)",
|
||||||
"zh-CN": "中文(简体)",
|
"zh-CN": "中文(简体)",
|
||||||
|
"zh-TW": "中文(臺灣正體)",
|
||||||
"de-DE": "Deutsch",
|
"de-DE": "Deutsch",
|
||||||
"lolcat": "LOLCAT"
|
"lolcat": "LOLCAT"
|
||||||
}
|
}
|
@ -132,5 +132,8 @@
|
|||||||
"serif": "SHARP PAW",
|
"serif": "SHARP PAW",
|
||||||
"sans-serif": "SOFT PAW",
|
"sans-serif": "SOFT PAW",
|
||||||
"monospace": "H4CKY 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"
|
||||||
}
|
}
|
||||||
|
@ -124,9 +124,28 @@
|
|||||||
"text-size": "大小",
|
"text-size": "大小",
|
||||||
"enter-text": "在此处输入文本",
|
"enter-text": "在此处输入文本",
|
||||||
"wrap-words-by-spaces": "空格处换行(不建议用于汉语)",
|
"wrap-words-by-spaces": "空格处换行(不建议用于汉语)",
|
||||||
"minor-tweaks": "小优化",
|
"minor-tweaks": "细节调整",
|
||||||
"serif": "衬线字体",
|
"serif": "衬线字体",
|
||||||
"sans-serif": "无衬线字体",
|
"sans-serif": "无衬线字体",
|
||||||
"monospace": "等宽字体",
|
"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
151
www/lang/zh-TW.json
Normal 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": "尊重您計算自由的軟體。"
|
||||||
|
}
|
@ -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
|
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
window.exports = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Satisfy both development and old-old webView need
|
* Satisfy both development and old-old webView need
|
||||||
*/
|
*/
|
||||||
|
138
www/main.css
138
www/main.css
@ -15,7 +15,6 @@
|
|||||||
--fore-color: #111;
|
--fore-color: #111;
|
||||||
--back-color: #eee;
|
--back-color: #eee;
|
||||||
--canvas-back: #fff;
|
--canvas-back: #fff;
|
||||||
--curve: cubic-bezier(.08,.82,.17,1);
|
|
||||||
--panel-height: 20em;
|
--panel-height: 20em;
|
||||||
--target-color: rgba(0, 255, 255, 0.2);
|
--target-color: rgba(0, 255, 255, 0.2);
|
||||||
--notice-wait: rgba(0, 128, 255, 0.2);
|
--notice-wait: rgba(0, 128, 255, 0.2);
|
||||||
@ -24,6 +23,19 @@
|
|||||||
--notice-error: rgba(255, 0, 0, 0.2);
|
--notice-error: rgba(255, 0, 0, 0.2);
|
||||||
--shade: rgba(238, 238, 238, 0.5);
|
--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 {
|
body {
|
||||||
border: none;
|
border: none;
|
||||||
@ -36,12 +48,26 @@ body {
|
|||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body.android .hide-on-android {
|
body.android .hide-on-android {
|
||||||
display: none;
|
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 {
|
.selectable {
|
||||||
user-select: all;
|
user-select: all;
|
||||||
}
|
}
|
||||||
@ -83,7 +109,6 @@ button, input[type="number"], input[type="text"], select {
|
|||||||
margin: var(--span-half) var(--span);
|
margin: var(--span-half) var(--span);
|
||||||
border: var(--border) solid var(--fore-color);
|
border: var(--border) solid var(--fore-color);
|
||||||
padding: var(--span-half) var(--span);
|
padding: var(--span-half) var(--span);
|
||||||
transition: all var(--anim-time) var(--curve);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-width: 6em;
|
min-width: 6em;
|
||||||
line-height: calc(var(--font-size) + var(--span));
|
line-height: calc(var(--font-size) + var(--span));
|
||||||
@ -179,7 +204,7 @@ main, header, footer {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
overflow-x: hidden;
|
/* overflow-x: hidden; */ /* this causes sticky position not work */
|
||||||
}
|
}
|
||||||
.canvas-side {
|
.canvas-side {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
@ -204,6 +229,13 @@ main, header, footer {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
margin-top: var(--span);
|
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 {
|
.compact-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -211,9 +243,7 @@ main, header, footer {
|
|||||||
background-color: var(--back-color);
|
background-color: var(--back-color);
|
||||||
}
|
}
|
||||||
.compact-button {
|
.compact-button {
|
||||||
width: max-content;
|
|
||||||
height: var(--compact-menu-height);
|
height: var(--compact-menu-height);
|
||||||
flex-grow: 1;
|
|
||||||
line-height: var(--compact-menu-height);
|
line-height: var(--compact-menu-height);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -222,12 +252,11 @@ main, header, footer {
|
|||||||
border-bottom: var(--border) solid transparent;
|
border-bottom: var(--border) solid transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
.compact-button:hover {
|
.compact-button:hover {
|
||||||
transform: unset;
|
transform: unset;
|
||||||
margin: 0;
|
flex-grow: 1.2;
|
||||||
padding: 0 var(--span) calc(var(--span-double));
|
|
||||||
min-width: 6em;
|
|
||||||
}
|
}
|
||||||
.compact-button.active {
|
.compact-button.active {
|
||||||
border: var(--border) solid var(--fore-color);
|
border: var(--border) solid var(--fore-color);
|
||||||
@ -245,7 +274,6 @@ main, header, footer {
|
|||||||
#canvas {
|
#canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity var(--anim-time) var(--curve);
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
#canvas:hover {
|
#canvas:hover {
|
||||||
@ -300,6 +328,9 @@ input[type="range"] {
|
|||||||
#hidden, .hard-hidden {
|
#hidden, .hard-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
#hint-tab-control {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
#error-record {
|
#error-record {
|
||||||
font-family: 'DejaVu Sans Mono', 'Consolas', monospace;
|
font-family: 'DejaVu Sans Mono', 'Consolas', monospace;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -373,15 +404,13 @@ iframe#frame {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity var(--anim-time) var(--curve);
|
|
||||||
}
|
}
|
||||||
#dialog>.content {
|
#dialog>.content {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 42em;
|
width: 42em;
|
||||||
max-height: 80vh;
|
max-height: 100vh;
|
||||||
margin: 12vh auto;
|
margin: 12vh auto;
|
||||||
border: var(--border) solid var(--fore-color);
|
border: var(--border) solid var(--fore-color);
|
||||||
transition: transform var(--anim-time) var(--curve);
|
|
||||||
transform-origin: center 33%;
|
transform-origin: center 33%;
|
||||||
}
|
}
|
||||||
#dialog.hidden {
|
#dialog.hidden {
|
||||||
@ -391,18 +420,18 @@ iframe#frame {
|
|||||||
transform: scaleY(0);
|
transform: scaleY(0);
|
||||||
}
|
}
|
||||||
#dialog-content {
|
#dialog-content {
|
||||||
max-height: 60vh;
|
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: var(--span-double);
|
padding: var(--span-double);
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
display: flex;
|
max-height: calc(76vh - 1em);
|
||||||
flex-direction: column;
|
overflow-y: auto;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
#dialog-choices {
|
#dialog-choices {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: var(--span);
|
padding: var(--span);
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
}
|
}
|
||||||
#choice-input {
|
#choice-input {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@ -412,13 +441,12 @@ iframe#frame {
|
|||||||
text-align: initial;
|
text-align: initial;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
justify-content: space-around;
|
||||||
#accessibility>*:nth-child(1) {
|
flex-wrap: wrap;
|
||||||
flex-grow: 1;
|
|
||||||
}
|
}
|
||||||
#select-language {
|
#select-language {
|
||||||
/* width: calc(100% - var(--span-double)); */
|
/* width: calc(100% - var(--span-double)); */
|
||||||
width: 12em;
|
width: 100%;
|
||||||
height: 8em;
|
height: 8em;
|
||||||
border: var(--border) solid var(--fore-color);
|
border: var(--border) solid var(--fore-color);
|
||||||
padding: var(--span);
|
padding: var(--span);
|
||||||
@ -430,9 +458,11 @@ iframe#frame {
|
|||||||
#select-language option:hover {
|
#select-language option:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
#accessibility>*:nth-child(2) {
|
#accessibility>* {
|
||||||
flex-grow: 1;
|
flex-grow: 0;
|
||||||
|
min-width: 16em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
margin: 1em;
|
||||||
}
|
}
|
||||||
@keyframes jump {
|
@keyframes jump {
|
||||||
0% { transform: translateY(0); }
|
0% { transform: translateY(0); }
|
||||||
@ -452,7 +482,7 @@ iframe#frame {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity var(--anim-time) var(--curve);
|
transition-duration: 0.2s;
|
||||||
}
|
}
|
||||||
.logo {
|
.logo {
|
||||||
background-image: url('icon.svg');
|
background-image: url('icon.svg');
|
||||||
@ -507,9 +537,6 @@ iframe#frame {
|
|||||||
transform: translate(-1em, calc(var(--font-size) * -1));
|
transform: translate(-1em, calc(var(--font-size) * -1));
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
transition: all var(--anim-time) ease-out;
|
|
||||||
}
|
|
||||||
@keyframes delay-scrollable {
|
@keyframes delay-scrollable {
|
||||||
from { overflow: hidden; }
|
from { overflow: hidden; }
|
||||||
to { overflow: auto; }
|
to { overflow: auto; }
|
||||||
@ -542,14 +569,7 @@ a {
|
|||||||
}
|
}
|
||||||
.canvas-side>.buttons,
|
.canvas-side>.buttons,
|
||||||
.menu-side>.buttons {
|
.menu-side>.buttons {
|
||||||
position: sticky;
|
|
||||||
bottom: var(--span);
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.canvas-side>.buttons button,
|
|
||||||
.menu-side>.buttons button {
|
|
||||||
background-color: var(--back-color);
|
|
||||||
}
|
}
|
||||||
#control-overlay {
|
#control-overlay {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -590,7 +610,7 @@ a {
|
|||||||
height: var(--compact-menu-height);
|
height: var(--compact-menu-height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 384px) {
|
@media (max-width: 385px) {
|
||||||
#preview, #canvas, #control-overlay, .canvas-side>* {
|
#preview, #canvas, #control-overlay, .canvas-side>* {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
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) {
|
@media (prefers-reduced-motion) {
|
||||||
body *,
|
body *,
|
||||||
body *::before,
|
body *::before,
|
||||||
body *::after {
|
body *::after {
|
||||||
transition-duration: 0ms !important;
|
transition-duration: 0s !important;
|
||||||
transition: none;
|
animation-duration: 0s !important;
|
||||||
animation-timing-function: steps(1);
|
transition-timing-function: steps(1) !important;
|
||||||
animation-duration: 0ms !important;
|
animation-timing-function: steps(1) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* but i have no way... */
|
body.no-animation,
|
||||||
|
body.hard-animation,
|
||||||
body.no-animation *,
|
body.no-animation *,
|
||||||
|
body.hard-animation *:not(#loading-screen, #loading-screen *),
|
||||||
body.no-animation *::before,
|
body.no-animation *::before,
|
||||||
body.no-animation *::after {
|
body.no-animation *::after {
|
||||||
transition-duration: 0ms !important;
|
transition-duration: 0s !important;
|
||||||
transition: none;
|
animation-duration: 0s !important;
|
||||||
animation-timing-function: steps(1);
|
transition-timing-function: steps(1) !important;
|
||||||
animation-duration: 0ms !important;
|
animation-timing-function: steps(1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.large-font,
|
body.large-font,
|
||||||
@ -667,7 +667,7 @@ body.high-contrast {
|
|||||||
}
|
}
|
||||||
body.high-contrast .shade { transition-duration: 0s; opacity: 1; }
|
body.high-contrast .shade { transition-duration: 0s; opacity: 1; }
|
||||||
/* body.high-contrast * { background-color: var(--back-color); } */
|
/* 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 #notice * { border: var(--border) dashed var(--fore-color); }
|
||||||
body.high-contrast a:any-link { color: #00f; }
|
body.high-contrast a:any-link { color: #00f; }
|
||||||
body.high-contrast #control-overlay { background-color: var(--shade); }
|
body.high-contrast #control-overlay { background-color: var(--shade); }
|
||||||
|
403
www/main.js
403
www/main.js
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
`
|
`
|
||||||
Cat-Printer: Web Frontend
|
Cat-Printer: Web Frontend
|
||||||
|
|
||||||
@ -205,62 +206,89 @@ async function callApi(path, body, errorPreHandler) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* call addEventListener on all selected elements by `seletor`,
|
const Ev = (function() {
|
||||||
* with each element itself as `this` unless specifyed `thisArg`,
|
/** @type {Record<string, NodeListOf<HTMLElement>>} */
|
||||||
* with type `type` and a function `callback`.
|
let map = {};
|
||||||
* If an element have attribute `data-default` or `checked`, dispatch event immediately on it.
|
return {
|
||||||
* 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} selector
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
* @param {(event?: Event) => void} callback
|
* @param {(event?: Event) => void} callback
|
||||||
* @param {any} thisArg
|
* @param {any} thisArg
|
||||||
*/
|
*/
|
||||||
constructor(selector, type, callback, thisArg) {
|
put: function(selector, type, callback, thisArg) {
|
||||||
let elements = this.elements = document.querySelectorAll(selector);
|
let elements = document.querySelectorAll(selector);
|
||||||
if (elements.length === 0) return;
|
map[selector] = elements;
|
||||||
this.callback = callback;
|
for (let e of elements) {
|
||||||
elements.forEach(element => {
|
e.addEventListener(type, function(event) {
|
||||||
element.addEventListener(type, function(event) {
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.cancelBubble = true;
|
event.cancelBubble = true;
|
||||||
callback.call(thisArg || element, event);
|
callback.call(thisArg || e, event);
|
||||||
});
|
});
|
||||||
if (element.hasAttribute('data-default') || element.checked) {
|
if (e.hasAttribute('data-default') || e.checked) {
|
||||||
element.dispatchEvent(new Event(type));
|
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} selector
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
* @param {(event?: Event) => void} callback
|
* @param {{
|
||||||
* @param {any} thisArg
|
* event?: Event,
|
||||||
|
* value?: string | number | boolean
|
||||||
|
* }} args
|
||||||
*/
|
*/
|
||||||
function putEvent(selector, type, callback, thisArg) {
|
dispatch: function(selector, type, { event, value } = {}) {
|
||||||
return new EventPutter(selector, type, callback, thisArg);
|
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 panels = document.querySelectorAll('.panel');
|
||||||
let buttons = document.querySelectorAll('*[data-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}"]`);
|
let button = document.querySelector(`*[data-panel="${panel.id}"]`);
|
||||||
if (button) button.addEventListener('click', event => {
|
if (!button) continue;
|
||||||
|
button.addEventListener('click', event => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
panels.forEach(p => p.classList.remove('active'));
|
panels.forEach(p => p.classList.remove('active'));
|
||||||
buttons.forEach(b => b.classList.remove('active'));
|
buttons.forEach(b => b.classList.remove('active'));
|
||||||
panel.classList.add('active');
|
panel.classList.add('active');
|
||||||
button.classList.add('active');
|
button.classList.add('active');
|
||||||
});
|
});
|
||||||
|
map[panel.id] = button;
|
||||||
if (panel.hasAttribute('data-default')) button.click();
|
if (panel.hasAttribute('data-default')) button.click();
|
||||||
});
|
}
|
||||||
|
return id => map[id]?.click();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -273,33 +301,33 @@ class CanvasController {
|
|||||||
/** @type {HTMLCanvasElement} */
|
/** @type {HTMLCanvasElement} */
|
||||||
canvas;
|
canvas;
|
||||||
imageUrl;
|
imageUrl;
|
||||||
|
isImageNew;
|
||||||
|
// this costs most of the effort. cache it
|
||||||
|
/** @type {Uint8ClampedArray} */
|
||||||
|
grayscaleCache;
|
||||||
algorithm;
|
algorithm;
|
||||||
algoElements;
|
|
||||||
textFont;
|
textFont;
|
||||||
textSize;
|
textSize;
|
||||||
textArea;
|
textArea;
|
||||||
transparentAsWhite;
|
transparentAsWhite;
|
||||||
previewData;
|
previewPbm;
|
||||||
rotate;
|
rotate;
|
||||||
#height;
|
#height;
|
||||||
#threshold;
|
#threshold;
|
||||||
#energy;
|
#energy;
|
||||||
#thresholdRange;
|
|
||||||
#energyRange;
|
|
||||||
#rotateCheck;
|
|
||||||
static defaultHeight = 384;
|
static defaultHeight = 384;
|
||||||
static defaultThreshold = 256 / 3;
|
static defaultThreshold = 256 / 3;
|
||||||
get threshold() {
|
get threshold() {
|
||||||
return this.#threshold;
|
return this.#threshold;
|
||||||
}
|
}
|
||||||
set threshold(value) {
|
set threshold(value) {
|
||||||
this.#threshold = this.#thresholdRange.value = value;
|
Ev.dispatch('#threshold', 'change', { value: this.#threshold = value });
|
||||||
}
|
}
|
||||||
get energy() {
|
get energy() {
|
||||||
return this.#energy;
|
return this.#energy;
|
||||||
}
|
}
|
||||||
set energy(value) {
|
set energy(value) {
|
||||||
this.#energy = this.#energyRange.value = value;
|
Ev.dispatch('#energy', 'change', { value: this.#energy = value });
|
||||||
}
|
}
|
||||||
get height() {
|
get height() {
|
||||||
return this.#height;
|
return this.#height;
|
||||||
@ -316,11 +344,11 @@ class CanvasController {
|
|||||||
this.textArea = document.getElementById("insert-text-area");
|
this.textArea = document.getElementById("insert-text-area");
|
||||||
this.wrapBySpace = document.querySelector('input[name="wrap-words-by-spaces"]');
|
this.wrapBySpace = document.querySelector('input[name="wrap-words-by-spaces"]');
|
||||||
this.height = CanvasController.defaultHeight;
|
this.height = CanvasController.defaultHeight;
|
||||||
this.#thresholdRange = document.querySelector('[name="threshold"]');
|
|
||||||
this.#energyRange = document.querySelector('[name="energy"]');
|
|
||||||
this.imageUrl = null;
|
this.imageUrl = null;
|
||||||
this.textAlign = "left";
|
this.textAlign = "left";
|
||||||
this.rotate = false;
|
this.rotate = false;
|
||||||
|
this.isImageNew = true;
|
||||||
|
this.grayscaleCache = null;
|
||||||
|
|
||||||
for (let elem of document.querySelectorAll("input[name=text-align]")){
|
for (let elem of document.querySelectorAll("input[name=text-align]")){
|
||||||
if (elem.checked) { this.textAlign = elem.value; }
|
if (elem.checked) { this.textAlign = elem.value; }
|
||||||
@ -351,47 +379,44 @@ class CanvasController {
|
|||||||
this.textArea.style["font-family"] = this.textFont.value;
|
this.textArea.style["font-family"] = this.textFont.value;
|
||||||
this.textArea.style["word-break"] = this.wrapBySpace.checked ? "break-word" : "break-all";
|
this.textArea.style["word-break"] = this.wrapBySpace.checked ? "break-word" : "break-all";
|
||||||
|
|
||||||
this.algoElements = document.querySelectorAll('input[name="algo"]');
|
Ev.put('[name="algo"]' , 'change', (event) => this.useAlgorithm(event.currentTarget.value), this);
|
||||||
|
Ev.put('#insert-picture', 'click' , () => this.useFiles(), this);
|
||||||
putEvent('input[name="algo"]', 'change', (event) => this.useAlgorithm(event.currentTarget.value), this);
|
Ev.put('#insert-text' , 'click' , () => Dialog.alert("#text-input", () => this.insertText(this.textArea.value)));
|
||||||
putEvent('#insert-picture' , 'click', () => this.useFiles(), this);
|
Ev.put('#text-size' , 'change', () => this.textArea.style["font-size"] = this.textSize.value + "px");
|
||||||
putEvent('#insert-text' , 'click', () => Dialog.alert("#text-input", () => this.insertText(this.textArea.value)));
|
Ev.put('#text-font' , 'change', () => this.textArea.style["font-family"] = this.textFont.value);
|
||||||
putEvent('#text-size' , 'change', () => this.textArea.style["font-size"] = this.textSize.value + "px");
|
Ev.put('input[name="text-align"]', 'change', (event) => {
|
||||||
putEvent('#text-font' , 'change', () => this.textArea.style["font-family"] = this.textFont.value);
|
this.textAlign = event.currentTarget.value;
|
||||||
putEvent('input[name="text-align"]', 'change', (event) => {
|
|
||||||
this.textAlign = event.currentTarget.value
|
|
||||||
this.textArea.style["text-align"] = this.textAlign;
|
this.textArea.style["text-align"] = this.textAlign;
|
||||||
}, this);
|
}, this);
|
||||||
putEvent('input[name="wrap-words-by-spaces"]' , 'change', () => this.textArea.style["word-break"] = this.wrapBySpace.checked ? "break-word" : "break-all");
|
Ev.put('input[name="wrap-words-by-spaces"]', 'change',
|
||||||
putEvent('#button-preview' , 'click', this.activatePreview , this);
|
() => this.textArea.style["word-break"] = this.wrapBySpace.checked ? "break-word" : "break-all");
|
||||||
putEvent('#button-reset' , 'click', this.reset , this);
|
Ev.put('#button-preview' , 'click', this.activatePreview , this);
|
||||||
putEvent('#canvas-expand' , 'click', this.expand , this);
|
Ev.put('#button-reset' , 'click', this.reset , this);
|
||||||
putEvent('#canvas-crop' , 'click', this.crop , this);
|
Ev.put('#canvas-expand' , 'click', this.expand , this);
|
||||||
putEvent('[name="rotate"]' , 'change', e => this.setRotate(e.currentTarget.checked), this);
|
Ev.put('#canvas-crop' , 'click', this.crop , this);
|
||||||
this.#rotateCheck = document.querySelector('[name="rotate"]');
|
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);
|
this.threshold = parseInt(event.currentTarget.value);
|
||||||
|
// it's really new
|
||||||
|
this.isImageNew = true;
|
||||||
this.activatePreview();
|
this.activatePreview();
|
||||||
}, this);
|
}, this);
|
||||||
putEvent('[name="energy"]', 'change', (event) => {
|
Ev.put('[name="energy"]', 'change', (event) => {
|
||||||
this.energy = parseInt(event.currentTarget.value);
|
this.energy = parseInt(event.currentTarget.value);
|
||||||
this.visualEnergy(this.energy);
|
this.visualEnergy(this.energy);
|
||||||
}, this);
|
}, this);
|
||||||
putEvent('[name="transparent-as-white"]', 'change', (event) => {
|
Ev.put('[name="transparent-as-white"]', 'change', (event) => {
|
||||||
this.transparentAsWhite = event.currentTarget.checked;
|
this.transparentAsWhite = event.currentTarget.checked;
|
||||||
|
this.isImageNew = true;
|
||||||
this.activatePreview();
|
this.activatePreview();
|
||||||
}, this);
|
}, this);
|
||||||
}
|
}
|
||||||
useAlgorithm(name) {
|
useAlgorithm(name) {
|
||||||
for (let e of this.algoElements) {
|
|
||||||
e.checked = e.value === name;
|
|
||||||
}
|
|
||||||
this.algorithm = name;
|
this.algorithm = name;
|
||||||
this.threshold = CanvasController.defaultThreshold;
|
// Ev.dispatch('[name="algo"]', 'change', { value: name });
|
||||||
this.#thresholdRange.dispatchEvent(new Event('change'));
|
Ev.dispatch('[name="threshold"]', 'change', { value: CanvasController.defaultThreshold });
|
||||||
this.energy = name == 'algo-direct' ? 96 : 64;
|
Ev.dispatch('[name="energy"]', 'change', { value: (name == 'algo-direct' ? 96 : 64) });
|
||||||
this.#energyRange.dispatchEvent(new Event('change'));
|
|
||||||
this.activatePreview();
|
this.activatePreview();
|
||||||
}
|
}
|
||||||
expand(length = CanvasController.defaultHeight) {
|
expand(length = CanvasController.defaultHeight) {
|
||||||
@ -401,7 +426,6 @@ class CanvasController {
|
|||||||
// STUB
|
// STUB
|
||||||
}
|
}
|
||||||
setRotate(value) {
|
setRotate(value) {
|
||||||
this.#rotateCheck.checked = value;
|
|
||||||
this.rotate = value;
|
this.rotate = value;
|
||||||
if (this.imageUrl !== null) this.putImage(this.imageUrl);
|
if (this.imageUrl !== null) this.putImage(this.imageUrl);
|
||||||
}
|
}
|
||||||
@ -414,87 +438,72 @@ class CanvasController {
|
|||||||
activatePreview() {
|
activatePreview() {
|
||||||
if (!this.imageUrl) return;
|
if (!this.imageUrl) return;
|
||||||
let preview = this.preview;
|
let preview = this.preview;
|
||||||
let t = Math.min(this.threshold, 255);
|
let threshold = Math.min(this.threshold, 255);
|
||||||
let canvas = this.canvas;
|
let canvas = this.canvas;
|
||||||
let w = canvas.width, h = canvas.height;
|
let w = canvas.width, h = canvas.height;
|
||||||
preview.width = w; preview.height = h;
|
preview.width = w; preview.height = h;
|
||||||
let context_c = canvas.getContext('2d');
|
let context_c = canvas.getContext('2d');
|
||||||
let context_p = preview.getContext('2d');
|
let context_p = preview.getContext('2d');
|
||||||
let data = context_c.getImageData(0, 0, w, h);
|
let rgba_data = context_c.getImageData(0, 0, w, h);
|
||||||
let mono_data = new Uint8ClampedArray(w * h);
|
let gray_data = (this.grayscaleCache =
|
||||||
monoGrayscale(data.data, mono_data, w, h, t, this.transparentAsWhite);
|
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) {
|
switch (this.algorithm) {
|
||||||
case 'algo-direct':
|
case 'algo-direct':
|
||||||
monoDirect(mono_data, w, h, t);
|
result = monoDirect(gray_data, w, h);
|
||||||
break;
|
break;
|
||||||
case 'algo-steinberg':
|
case 'algo-steinberg':
|
||||||
monoSteinberg(mono_data, w, h, Math.floor(t / 2 - 64));
|
result = monoSteinberg(gray_data, w, h);
|
||||||
break;
|
break;
|
||||||
case 'algo-halftone':
|
case 'algo-halftone':
|
||||||
// monoHalftone(mono_data, w, h, t);
|
result = monoHalftone(gray_data, w, h);
|
||||||
// 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);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let new_data = context_p.createImageData(w, h);
|
this.previewPbm = monoToPbm(result);
|
||||||
let p;
|
let rgba = new Uint8ClampedArray(monoToRgba(result).buffer);
|
||||||
for (let i = 0; i < mono_data.length; i++) {
|
context_p.putImageData(new ImageData(rgba, w, h), 0, 0);
|
||||||
p = i * 4;
|
this.isImageNew = false;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
putImage(url) {
|
putImage(url) {
|
||||||
|
let before = document.createElement('canvas');
|
||||||
|
let b_ctx = before.getContext('2d');
|
||||||
let img = document.getElementById('img');
|
let img = document.getElementById('img');
|
||||||
|
img.src = ''; // trigger some dumb browser
|
||||||
img.src = url;
|
img.src = url;
|
||||||
img.addEventListener('load', () => {
|
img.addEventListener('load', () => {
|
||||||
let canvas = this.canvas;
|
let canvas = this.canvas;
|
||||||
let ctx = canvas.getContext('2d');
|
let ctx = canvas.getContext('2d');
|
||||||
if (this.rotate) {
|
if (this.rotate) {
|
||||||
let intermediate_canvas = document.createElement('canvas');
|
|
||||||
/**
|
|
||||||
* w h
|
|
||||||
* +------+ +---+
|
|
||||||
* h | | | | w
|
|
||||||
* +------+ | | intermediate_canvas
|
|
||||||
* canvas +---+
|
|
||||||
*/
|
|
||||||
let w = canvas.width;
|
let w = canvas.width;
|
||||||
let h = this.height = Math.floor(canvas.width * img.width / img.height);
|
let h = this.height = Math.floor(canvas.width * img.width / img.height);
|
||||||
intermediate_canvas.width = h;
|
before.width = h, before.height = w;
|
||||||
intermediate_canvas.height = w;
|
b_ctx.drawImage(img, 0, 0, h, w);
|
||||||
let i_ctx = intermediate_canvas.getContext('2d');
|
let data = new ImageData(
|
||||||
i_ctx.drawImage(img, 0, 0, h, w);
|
new Uint8ClampedArray(
|
||||||
let i_data = i_ctx.getImageData(0, 0, h, w);
|
rotateRgba(
|
||||||
let data = ctx.createImageData(w, h);
|
new Uint32Array(
|
||||||
for (let j = 0; j < h; j++) {
|
b_ctx.getImageData(0, 0, h, w).data.buffer
|
||||||
for (let i = 0; i < w; i++) {
|
), w, h
|
||||||
for (let d = 0; d < 4; d++)
|
).buffer
|
||||||
data.data[(i * 4 + d) + (j * w * 4)] = i_data.data[(j * 4 + d) + ((w - i) * 4 * h)];
|
), w, h
|
||||||
}
|
);
|
||||||
}
|
|
||||||
ctx.putImageData(data, 0, 0);
|
ctx.putImageData(data, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
this.height = Math.floor(canvas.width * img.height / img.width);
|
this.height = Math.floor(canvas.width * img.height / img.width);
|
||||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
}
|
}
|
||||||
this.crop();
|
this.crop();
|
||||||
|
this.isImageNew = true;
|
||||||
this.activatePreview();
|
this.activatePreview();
|
||||||
hint('#button-print');
|
hint('#button-print');
|
||||||
});
|
}, { once: true });
|
||||||
}
|
}
|
||||||
useFiles(files) {
|
useFiles(files) {
|
||||||
const use_files = (files) => {
|
const use_files = (files) => {
|
||||||
@ -607,6 +616,7 @@ class CanvasController {
|
|||||||
let canvas = this.canvas;
|
let canvas = this.canvas;
|
||||||
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
|
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
|
||||||
canvas.height = CanvasController.defaultHeight;
|
canvas.height = CanvasController.defaultHeight;
|
||||||
|
this.isImageNew = true;
|
||||||
this.activatePreview();
|
this.activatePreview();
|
||||||
this.imageUrl = null;
|
this.imageUrl = null;
|
||||||
this.controls.classList.remove('hidden');
|
this.controls.classList.remove('hidden');
|
||||||
@ -617,16 +627,20 @@ class CanvasController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
makePbm() {
|
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;
|
return blob;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Global variable indicating current language */
|
||||||
|
var language = navigator.language;
|
||||||
|
|
||||||
/** @param {Document} doc */
|
/** @param {Document} doc */
|
||||||
function applyI18nToDom(doc) {
|
function applyI18nToDom(doc) {
|
||||||
doc = doc || document;
|
doc = doc || document;
|
||||||
let elements = doc.querySelectorAll('*[data-i18n]');
|
let elements = doc.querySelectorAll('*[data-i18n]');
|
||||||
let i18n_data, translated_string;
|
let i18n_data, translated_string;
|
||||||
|
doc.querySelector('html').lang = language;
|
||||||
elements.forEach(element => {
|
elements.forEach(element => {
|
||||||
i18n_data = element.getAttribute('data-i18n');
|
i18n_data = element.getAttribute('data-i18n');
|
||||||
translated_string = i18n(i18n_data);
|
translated_string = i18n(i18n_data);
|
||||||
@ -643,7 +657,8 @@ async function initI18n(current_language) {
|
|||||||
/** @type {{ [code: string]: string }} */
|
/** @type {{ [code: string]: string }} */
|
||||||
let list = await fetch('/lang/list.json').then(r => r.json());
|
let list = await fetch('/lang/list.json').then(r => r.json());
|
||||||
let use_language = async (value) => {
|
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);
|
i18n.add(value, await fetch(`/lang/${value}.json`).then(r => r.json()), true);
|
||||||
applyI18nToDom();
|
applyI18nToDom();
|
||||||
}
|
}
|
||||||
@ -698,10 +713,10 @@ class Main {
|
|||||||
/** @type {CanvasController} */
|
/** @type {CanvasController} */
|
||||||
canvasController;
|
canvasController;
|
||||||
deviceOptions;
|
deviceOptions;
|
||||||
|
testUnknownDevice;
|
||||||
/** An object containing configuration, fetched from server */
|
/** An object containing configuration, fetched from server */
|
||||||
settings;
|
settings;
|
||||||
/** @type {{ [key: string]: EventPutter }} */
|
selectorMap;
|
||||||
setters;
|
|
||||||
/**
|
/**
|
||||||
* There are race conditions in initialization query/set,
|
* There are race conditions in initialization query/set,
|
||||||
* use this flag to avoid
|
* use this flag to avoid
|
||||||
@ -709,11 +724,11 @@ class Main {
|
|||||||
allowSet;
|
allowSet;
|
||||||
constructor() {
|
constructor() {
|
||||||
this.allowSet = false;
|
this.allowSet = false;
|
||||||
|
this.testUnknownDevice = false;
|
||||||
this.deviceOptions = document.getElementById('device-options');
|
this.deviceOptions = document.getElementById('device-options');
|
||||||
this.settings = {};
|
this.settings = {};
|
||||||
this.setters = {};
|
this.selectorMap = {};
|
||||||
// window.addEventListener('unload', () => this.exit());
|
// window.addEventListener('unload', () => this.exit());
|
||||||
this.promise = new Promise(async (resolve, reject) => {
|
|
||||||
/** @type {HTMLIFrameElement} */
|
/** @type {HTMLIFrameElement} */
|
||||||
let iframe = document.getElementById('frame');
|
let iframe = document.getElementById('frame');
|
||||||
iframe.addEventListener('load', () => {
|
iframe.addEventListener('load', () => {
|
||||||
@ -729,58 +744,80 @@ class Main {
|
|||||||
});
|
});
|
||||||
applyI18nToDom(iframe.contentDocument);
|
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();
|
this.canvasController = new CanvasController();
|
||||||
putEvent('#button-exit' , 'click', () => this.exit(false), this);
|
|
||||||
putEvent('#button-print' , 'click', this.print, this);
|
Ev.put('#button-exit' , 'click', () => this.exit(false), this);
|
||||||
putEvent('#device-refresh' , 'click', this.searchDevices, this);
|
Ev.put('#button-print' , 'click', this.print, this);
|
||||||
putEvent('#button-exit' , 'contextmenu', (event) => (event.preventDefault(), this.exit(true)), this);
|
Ev.put('#device-refresh' , 'click', this.searchDevices, this);
|
||||||
putEvent('#set-accessibility', 'click', () => Dialog.alert('#accessibility'));
|
Ev.put('#button-exit' , 'contextmenu', (event) => (event.preventDefault(), this.exit(true)), this);
|
||||||
this.attachSetter('#device-options', 'input', 'printer',
|
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 })
|
(value) => callApi('/connect', { device: value })
|
||||||
);
|
);
|
||||||
putEvent('a[target="frame"]', 'click', () => Dialog.alert('#frame'));
|
this.conf('[name="algo"]' , 'change', 'mono_algorithm',
|
||||||
this.attachSetter('[name="scan-time"]' , 'change', 'scan_timeout');
|
|
||||||
this.attachSetter('[name="rotate"]' , 'change', 'rotate');
|
|
||||||
this.attachSetter('input[name="algo"]' , 'change', 'mono_algorithm',
|
|
||||||
(value) => this.settings['text_mode'] = (value === 'algo-direct')
|
(value) => this.settings['text_mode'] = (value === 'algo-direct')
|
||||||
);
|
);
|
||||||
this.attachSetter('[name="transparent-as-white"]', 'change', 'transparent_as_white');
|
this.conf('[name="dry-run"]', 'change', 'dry_run',
|
||||||
this.attachSetter('[name="wrap-words-by-spaces"]', 'change', 'wrap_by_space');
|
|
||||||
this.attachSetter('[name="dry-run"]', 'change', 'dry_run',
|
|
||||||
(checked) => checked && Notice.note('dry-run-test-print-process-only')
|
(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',
|
conf_class(
|
||||||
(checked) => apply_class('dark', checked)
|
'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.promise = new Promise(async (resolve, reject) => {
|
||||||
);
|
await this.loadConfig();
|
||||||
this.attachSetter('[name="threshold"]' , 'change', 'threshold');
|
await initI18n(this.settings['language']);
|
||||||
this.attachSetter('[name="energy"]' , 'change', 'energy');
|
|
||||||
this.attachSetter('[name="quality"]' , 'change', 'quality');
|
|
||||||
this.attachSetter('[name="flip"]' , 'change', 'flip');
|
|
||||||
await this.activateConfig();
|
await this.activateConfig();
|
||||||
|
|
||||||
// one exception
|
// one exception
|
||||||
this.attachSetter('#select-language', 'change', 'language');
|
this.conf('#select-language', 'change', 'language');
|
||||||
|
|
||||||
if (this.settings['is_android']) {
|
if (this.settings['is_android']) {
|
||||||
document.body.classList.add('android');
|
document.body.classList.add('android');
|
||||||
@ -789,6 +826,7 @@ class Main {
|
|||||||
let select = document.getElementById('select-language');
|
let select = document.getElementById('select-language');
|
||||||
Array.from(select.children).forEach(e => {
|
Array.from(select.children).forEach(e => {
|
||||||
e.selected = false;
|
e.selected = false;
|
||||||
|
e.addEventListener('click', () => this.set({ language: e.value }));
|
||||||
div.appendChild(e);
|
div.appendChild(e);
|
||||||
});
|
});
|
||||||
div.id = 'select-language';
|
div.id = 'select-language';
|
||||||
@ -796,6 +834,7 @@ class Main {
|
|||||||
}
|
}
|
||||||
if (typeof initKeyboardShortcuts === 'function') initKeyboardShortcuts();
|
if (typeof initKeyboardShortcuts === 'function') initKeyboardShortcuts();
|
||||||
// this.searchDevices();
|
// this.searchDevices();
|
||||||
|
document.body.classList.remove('hard-animation');
|
||||||
document.querySelector('main').classList.remove('hard-hidden');
|
document.querySelector('main').classList.remove('hard-hidden');
|
||||||
document.getElementById('loading-screen').classList.add('hidden');
|
document.getElementById('loading-screen').classList.add('hidden');
|
||||||
resolve();
|
resolve();
|
||||||
@ -824,30 +863,9 @@ class Main {
|
|||||||
if (this.settings['first_run'])
|
if (this.settings['first_run'])
|
||||||
Dialog.alert('#accessibility', () => this.set({ first_run: false }));
|
Dialog.alert('#accessibility', () => this.set({ first_run: false }));
|
||||||
for (let key in this.settings) {
|
for (let key in this.settings) {
|
||||||
|
if (this.selectorMap[key] === undefined) continue;
|
||||||
let value = this.settings[key];
|
let value = this.settings[key];
|
||||||
if (this.setters[key] === undefined) continue;
|
Ev.dispatch(this.selectorMap[key], 'change', { value: value });
|
||||||
// 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'));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this.allowSet = true;
|
this.allowSet = true;
|
||||||
await this.set(this.settings);
|
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 {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
|
* @param {(value: any) => any} callback Optional additinal post-procedure to call, with a *reasonable* value as parameter
|
||||||
*/
|
*/
|
||||||
attachSetter(selector, type, attribute, callback) {
|
conf(selector, type, attribute, callback) {
|
||||||
this.setters[attribute] = putEvent(selector, type, event => {
|
this.selectorMap[attribute] = selector;
|
||||||
|
Ev.put(selector, type, event => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.cancelBubble = true;
|
event.cancelBubble = true;
|
||||||
let input = event.currentTarget;
|
let input = event.currentTarget;
|
||||||
@ -918,10 +937,12 @@ class Main {
|
|||||||
}
|
}
|
||||||
async searchDevices() {
|
async searchDevices() {
|
||||||
Notice.wait('scanning-for-devices');
|
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;
|
if (search_result === null) return false;
|
||||||
let devices = search_result.devices;
|
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) {
|
if (devices.length === 0) {
|
||||||
Notice.note('no-available-devices-found');
|
Notice.note('no-available-devices-found');
|
||||||
hint('#device-refresh');
|
hint('#device-refresh');
|
||||||
@ -929,13 +950,13 @@ class Main {
|
|||||||
}
|
}
|
||||||
Notice.note('found-0-available-devices', [devices.length]);
|
Notice.note('found-0-available-devices', [devices.length]);
|
||||||
hint('#insert-picture');
|
hint('#insert-picture');
|
||||||
devices.forEach(device => {
|
for (let device of devices) {
|
||||||
let option = document.createElement('option');
|
let option = document.createElement('option');
|
||||||
option.value = `${device.name},${device.address}`;
|
option.value = `${device.name},${device.address}`;
|
||||||
option.innerText = `${device.name}-${device.address.slice(3, 5)}${device.address.slice(0, 2)}`;
|
option.innerText = `${device.name}-${device.address.slice(3, 5)}${device.address.slice(0, 2)}`;
|
||||||
this.deviceOptions.appendChild(option);
|
this.deviceOptions.appendChild(option);
|
||||||
});
|
}
|
||||||
this.deviceOptions.dispatchEvent(new Event('input'));
|
Ev.dispatch('#device-options', 'change');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async print() {
|
async print() {
|
||||||
|
@ -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
|
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
|
||||||
`;
|
`;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user