mirror of
https://github.com/NaitLee/Cat-Printer.git
synced 2025-05-16 07:10:30 -07:00
Backend Big Update (document later)
This commit is contained in:
parent
1f3c16cf16
commit
205f5ad5ca
6
.gitignore
vendored
6
.gitignore
vendored
@ -3,9 +3,6 @@ __pycache__
|
|||||||
# Compatibility version of script, for old-old webView,
|
# Compatibility version of script, for old-old webView,
|
||||||
# generated by typescript tsc
|
# generated by typescript tsc
|
||||||
www/main.comp.js
|
www/main.comp.js
|
||||||
# https://github.com/roddeh/i18njs
|
|
||||||
www/i18n.js
|
|
||||||
www/i18n.d.ts
|
|
||||||
# https://www.npmjs.com/package/vconsole
|
# https://www.npmjs.com/package/vconsole
|
||||||
www/vconsole.js
|
www/vconsole.js
|
||||||
# https://github.com/delight-im/Android-AdvancedWebView
|
# https://github.com/delight-im/Android-AdvancedWebView
|
||||||
@ -15,6 +12,7 @@ build-android/advancedwebview
|
|||||||
# releases
|
# releases
|
||||||
build-android/dist
|
build-android/dist
|
||||||
*.apk
|
*.apk
|
||||||
|
*.apk.*
|
||||||
cat-printer*.zip
|
cat-printer*.zip
|
||||||
# bleak, the bare pip package as a folder
|
# bleak, the bare pip package as a folder
|
||||||
build-common/bleak
|
build-common/bleak
|
||||||
@ -22,6 +20,8 @@ build-common/bleak
|
|||||||
build-common/python-win32*
|
build-common/python-win32*
|
||||||
# dev config
|
# dev config
|
||||||
config.json
|
config.json
|
||||||
|
# dev backup
|
||||||
|
*.bak
|
||||||
# test files
|
# test files
|
||||||
*.dump
|
*.dump
|
||||||
*.pf2
|
*.pf2
|
||||||
|
@ -7,15 +7,15 @@
|
|||||||
jobs=4
|
jobs=4
|
||||||
|
|
||||||
[BASIC]
|
[BASIC]
|
||||||
class-const-naming-style=snake_case
|
class-const-naming-style=PascalCase
|
||||||
const-naming-style=snake_case
|
const-naming-style=PascalCase
|
||||||
|
|
||||||
[MESSAGES CONTROL]
|
[MESSAGES CONTROL]
|
||||||
disable=broad-except,
|
disable=broad-except,
|
||||||
global-statement,
|
global-statement,
|
||||||
fixme,
|
fixme, too-few-public-methods,
|
||||||
import-outside-toplevel
|
import-outside-toplevel
|
||||||
|
|
||||||
[BASIC]
|
[BASIC]
|
||||||
good-names=i, j, k, ex, x, y, _, e,
|
good-names=i, j, k, ex, x, y, _, e, b, u, s,
|
||||||
Run, do_GET, do_POST, do_HEAD, do_PUT
|
Run, do_GET, do_POST, do_HEAD, do_PUT
|
||||||
|
13
.vscode/launch.json
vendored
13
.vscode/launch.json
vendored
@ -4,13 +4,24 @@
|
|||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python: Bitmap Print",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "printer.py",
|
||||||
|
"args": [
|
||||||
|
"-m", "-s", "1", "dump.pbm"
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Python: Text Print",
|
"name": "Python: Text Print",
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "printer.py",
|
"program": "printer.py",
|
||||||
"args": [
|
"args": [
|
||||||
"-m", "-t", "-d", "-s", "1", "-"
|
"-mt", "-f", "GB02", "COPYING"
|
||||||
],
|
],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"justMyCode": true
|
"justMyCode": true
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
cd www
|
cd www
|
||||||
npx tsc --allowJs --outFile main.comp.js polyfill.js i18n.js image.js main.js
|
npx tsc --allowJs --outFile main.comp.js polyfill.js i18n-ext.js i18n.js image.js main.js
|
||||||
cd ..
|
cd ..
|
||||||
|
54
README.md
54
README.md
@ -10,35 +10,36 @@ English | [Deutsch](./readme.i18n/README.de_DE.md) | [简体中文](./readme.i18
|
|||||||
|
|
||||||
Currently:
|
Currently:
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|-------------|-------------------|
|
|----|----|
|
||||||
| Supported | GB01, GB02, GT01 |
|
| Supported | GB01, GB02, GT01, GB03 |
|
||||||
| Maybe | GB03 |
|
<!-- | Maybe | N/A | -->
|
||||||
| Planned | N/A |
|
<!-- | Planned | N/A | -->
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
*Currently it's in development. More will be here soon!*
|
*Currently it's in development. More will be here soon!*
|
||||||
|
|
||||||
| Available | Partial | Planned |
|
|
||||||
|-----------------|-----------|---------------|
|
|
||||||
| Web Interface | CUPS/IPP* | Visual Editor |
|
|
||||||
| Print a Picture | | Help/Manual |
|
|
||||||
| Command-line | | Text Printing |
|
|
||||||
|
|
||||||
<!-- May comment the line below if there are no * -->
|
|
||||||
\* In development code. Will be released in a short period.
|
|
||||||
|
|
||||||
*Along with...*
|
|
||||||
|
|
||||||
- Simple!
|
- Simple!
|
||||||
- Operate via a Web UI just in browser,
|
- Operate via Web UI just in browser,
|
||||||
- or get the Android release!
|
- or get the Android release!
|
||||||
|
- Even no problem with command line hackers!
|
||||||
|
|
||||||
- Friendly!
|
- Friendly!
|
||||||
- Language support! You can participate in translation!
|
- Language support! You can participate in translation!
|
||||||
- Good user interface, adaptive to PC/mobile and light/dark theme!
|
- Good user interface, adaptive to PC/mobile and light/dark theme!
|
||||||
|
|
||||||
|
- Feature-rich!
|
||||||
|
- Web UI, for most people!
|
||||||
|
- Take full control of printer config
|
||||||
|
- Print picture, or just test if it works
|
||||||
|
- Command line, for geeks & hackers!
|
||||||
|
- Control printer with a few parameters
|
||||||
|
- Simplified text printing
|
||||||
|
- Make use of every part of the program
|
||||||
|
- Some other goodies!
|
||||||
|
- Server program is also CUPS/IPP capable
|
||||||
|
|
||||||
- Cross platform!
|
- Cross platform!
|
||||||
- Newer Windows 10 and above
|
- Newer Windows 10 and above
|
||||||
- GNU/Linux
|
- GNU/Linux
|
||||||
@ -78,33 +79,36 @@ On Arch Linux based distros you may first install `bluez`, as it's often missing
|
|||||||
sudo pacman -S bluez bluez-utils
|
sudo pacman -S bluez bluez-utils
|
||||||
```
|
```
|
||||||
|
|
||||||
|
*Packaging is also on the way!*
|
||||||
|
|
||||||
### MacOS
|
### MacOS
|
||||||
|
|
||||||
For MacOS please install [Python 3](https://www.python.org/).
|
For MacOS please install [Python 3](https://www.python.org/),
|
||||||
|
then install `pyobjc` and `bleak` via `pip` in terminal:
|
||||||
|
```bash
|
||||||
|
pip3 install pyobjc bleak
|
||||||
|
```
|
||||||
|
|
||||||
Fetch a "pure" release and do the same in a shell:
|
After that, fetch a "bare" release and do the same in a shell:
|
||||||
```bash
|
```bash
|
||||||
python3 server.py
|
python3 server.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Currently in Mac the browser will not pop up automatically. Please run manually and go to `http://127.0.0.1:8095`, or just click [here](http://127.0.0.1:8095).
|
Currently in Mac the browser will not pop up automatically. Please run manually and go to `http://127.0.0.1:8095`, or just click [here](http://127.0.0.1:8095).
|
||||||
|
|
||||||
|
|
||||||
### Worth to Note
|
### Worth to Note
|
||||||
|
|
||||||
For all supported platforms,
|
For all supported platforms,
|
||||||
You can also use "pure" edition once you have [Python 3](https://www.python.org/) installed,
|
You can also use "pure" edition once you have [Python 3](https://www.python.org/) installed,
|
||||||
or "bare" edition if you also managed to install `bleak` via `pip`.
|
or "bare" edition if you also managed to install `bleak` via `pip`.
|
||||||
|
|
||||||
Command line hackers? Just use `printer.py`!
|
|
||||||
|
|
||||||
See the [releases](https://github.com/NaitLee/Cat-Printer/releases) now!
|
See the [releases](https://github.com/NaitLee/Cat-Printer/releases) now!
|
||||||
|
|
||||||
## Problems?
|
## Problems?
|
||||||
|
|
||||||
Please open an issue if there's something in your mind!
|
Please talk in Discussion if there's something in your mind!
|
||||||
|
|
||||||
Of course PRs are welcome if you can handle them!
|
Of course Pull Requests are welcome if you can handle them!
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@ -125,7 +129,7 @@ Also interested in code development? See [development.md](development.md)!
|
|||||||
|
|
||||||
- Of course, Python & the Web!
|
- Of course, Python & the Web!
|
||||||
- [Bleak](https://bleak.readthedocs.io/en/latest/) BLE lib! The overall Hero!
|
- [Bleak](https://bleak.readthedocs.io/en/latest/) BLE lib! The overall Hero!
|
||||||
- [roddeh-i18n](https://github.com/roddeh/i18njs), good work!
|
- [roddeh-i18n](https://github.com/roddeh/i18njs), the current built-in i18n is inspired by this
|
||||||
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/), though there are some painful troubles
|
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/), though there are some painful troubles
|
||||||
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) for saving my life from Java
|
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) for saving my life from Java
|
||||||
- Stack Overflow & the whole Internet, you let me know Android `Activity` all from beginning
|
- Stack Overflow & the whole Internet, you let me know Android `Activity` all from beginning
|
||||||
|
31
TODO
31
TODO
@ -1,26 +1,23 @@
|
|||||||
|
|
||||||
Note: not ordered. do whatever I/you want
|
Note: not ordered. do whatever I/you want
|
||||||
|
|
||||||
+ Check GB03, again
|
+ Cookbook of basic things
|
||||||
+ Hacky text printing, typewriter-like, with PF2 font
|
|
||||||
ok I won't forget frontend, but it's different
|
|
||||||
+ Better CLI, e.g. invoke imagemagick if input is not PBM
|
|
||||||
+ Consider better MTU adaption
|
|
||||||
+ Consider better BLE traffic regulation, with BLE notification
|
|
||||||
+ Consider the printer 'text' mode, it can make things faster
|
|
||||||
+ Consider more control to something like 'energy'
|
|
||||||
+ Consider keeping server backend (more) secure to be used by IPP
|
|
||||||
+ Clean up CUPS/IPP code
|
|
||||||
+ Make a build guide for android:
|
|
||||||
Summary the hacks to p4a, bleak p4a recipe, p4a webview bootstrap, and AdvancedWebView
|
|
||||||
+ More frontend usability, more functions
|
|
||||||
+ A better layout for mobile?
|
|
||||||
+ Better Canvas mode, (re-)consider fabric.js
|
|
||||||
+ Implement Document mode, (re-)consider html2canvas.js
|
|
||||||
+ Write good help/manual
|
+ Write good help/manual
|
||||||
+ Make error notice short while let users see detailed help/manual for what-to-do
|
+ Make error notice short while let users see detailed help/manual for what-to-do
|
||||||
|
+ Even Better CLI, e.g. invoke imagemagick if input is not PBM
|
||||||
|
+ Even better CUPS/IPP support
|
||||||
|
+ Even better frontend usability, more functions
|
||||||
|
+ A better layout for mobile?
|
||||||
|
+ Make a build guide for android:
|
||||||
|
Summary the hacks to p4a, bleak p4a recipe, p4a webview bootstrap, and AdvancedWebView
|
||||||
|
+ Try to implement enough without more dependencies
|
||||||
|
+ Better Canvas mode, (re-)consider fabric.js
|
||||||
|
+ Implement Document mode, (re-)consider html2canvas.js
|
||||||
+ ...
|
+ ...
|
||||||
|
|
||||||
? Compression for GB03 data
|
? Consider more control to something like 'energy'
|
||||||
|
This have no effect on my GB02
|
||||||
|
? Data compression for GB03. Optional
|
||||||
? Put Android APP on F-Droid? But it needs automatic build system...
|
? Put Android APP on F-Droid? But it needs automatic build system...
|
||||||
Android guys can help this!
|
Android guys can help this!
|
||||||
|
? ... Or put to APKPure?
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
'Minimal internationalization lib'
|
|
||||||
|
|
||||||
import os
|
|
||||||
import math
|
|
||||||
import json
|
|
||||||
import locale
|
|
||||||
|
|
||||||
class I18n():
|
|
||||||
''' Minimal implementation of current frontend i18n in Python
|
|
||||||
Not Complete (yet)!
|
|
||||||
'''
|
|
||||||
|
|
||||||
lang: str
|
|
||||||
fallback: str
|
|
||||||
data: dict = {
|
|
||||||
'values': {},
|
|
||||||
'contexts': []
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, search_path='lang', lang=None, fallback=None):
|
|
||||||
self.lang = lang or locale.getdefaultlocale()[0]
|
|
||||||
self.fallback = fallback or 'en_US'
|
|
||||||
self.load_file(os.path.join(search_path, self.fallback.replace('_', '-') + '.json'))
|
|
||||||
for name in os.listdir(search_path):
|
|
||||||
if name == self.lang.replace('_', '-') + '.json':
|
|
||||||
self.load_file(os.path.join(search_path, name))
|
|
||||||
|
|
||||||
def load_file(self, name):
|
|
||||||
'Load an i18n json file'
|
|
||||||
with open(name, 'r', encoding='utf-8') as file:
|
|
||||||
self.load_data(file.read())
|
|
||||||
|
|
||||||
def load_data(self, raw_json):
|
|
||||||
'Load i18n json data (from str)'
|
|
||||||
data = json.loads(raw_json)
|
|
||||||
for key in data['values']:
|
|
||||||
self.data['values'][key] = data['values'][key]
|
|
||||||
if data.get('contexts') is not None:
|
|
||||||
self.data['contexts'] = data['contexts']
|
|
||||||
|
|
||||||
def __getitem__(self, keys):
|
|
||||||
if not isinstance(keys, tuple):
|
|
||||||
keys = (keys, )
|
|
||||||
data = self.data['values'].get(keys[0], keys[0])
|
|
||||||
string = data[0][2] if isinstance(data, list) else data
|
|
||||||
for i in keys:
|
|
||||||
if isinstance(i, (int, float)):
|
|
||||||
if string is None:
|
|
||||||
string = data
|
|
||||||
if isinstance(data, list):
|
|
||||||
for j in data:
|
|
||||||
if j[0] is None:
|
|
||||||
j[0] = -math.inf
|
|
||||||
if j[1] is None:
|
|
||||||
j[1] = math.inf
|
|
||||||
if j[0] < i < j[1]:
|
|
||||||
template = j[2]
|
|
||||||
break
|
|
||||||
string = template.replace('%%n', i).replace('-%%n', -i)
|
|
||||||
elif isinstance(i, dict):
|
|
||||||
# not verified if would work
|
|
||||||
if string is None:
|
|
||||||
string = data
|
|
||||||
for j in i:
|
|
||||||
string = string.replace(f'%%{j}', i[j])
|
|
||||||
return string
|
|
@ -1,6 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
p4a apk --private .. --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \
|
p4a apk --private .. --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \
|
||||||
--icon=icon.png --version="0.1.1" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
|
--icon=icon.png --version="0.2.0" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
|
||||||
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a --blacklist="blacklist.txt" \
|
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a --blacklist="blacklist.txt" \
|
||||||
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
|
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
|
||||||
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \
|
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \
|
||||||
|
@ -4,7 +4,7 @@ unzip -q "../cat-printer-bare-$1.zip"
|
|||||||
mv "cat-printer" "dist"
|
mv "cat-printer" "dist"
|
||||||
p4a apk --private "dist" --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \
|
p4a apk --private "dist" --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \
|
||||||
--icon=icon.png --version="$1" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
|
--icon=icon.png --version="$1" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
|
||||||
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a \
|
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a --release \
|
||||||
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
|
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
|
||||||
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \
|
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \
|
||||||
--permission=BLUETOOTH_ADMIN --permission=ACCESS_FINE_LOCATION --permission=ACCESS_COARSE_LOCATION
|
--permission=BLUETOOTH_ADMIN --permission=ACCESS_FINE_LOCATION --permission=ACCESS_COARSE_LOCATION
|
||||||
|
@ -22,23 +22,20 @@ if not sys.argv[-1].startswith('-'):
|
|||||||
bundle_name %= (edition, version)
|
bundle_name %= (edition, version)
|
||||||
|
|
||||||
ignore_whitelist = (
|
ignore_whitelist = (
|
||||||
'www/i18n.js',
|
|
||||||
'www/main.comp.js'
|
'www/main.comp.js'
|
||||||
)
|
)
|
||||||
|
|
||||||
additional_ignore = (
|
additional_ignore = (
|
||||||
# prevent recurse
|
# prevent recurse
|
||||||
bundle_name,
|
bundle_name,
|
||||||
|
# non-production (yet)
|
||||||
|
'PKGBUILD', 'systemd',
|
||||||
# build helpers
|
# build helpers
|
||||||
'build-*',
|
'build-*', '?-*.sh',
|
||||||
'?-*.sh',
|
|
||||||
# no need
|
# no need
|
||||||
'.git',
|
'.git', '.gitignore',
|
||||||
'.vscode',
|
'.vscode', '.pylintrc',
|
||||||
'.pylintrc',
|
'dev-diary.txt', 'TODO',
|
||||||
'.gitignore',
|
|
||||||
'dev-diary.txt',
|
|
||||||
'TODO',
|
|
||||||
# cache
|
# cache
|
||||||
'*.pyc',
|
'*.pyc',
|
||||||
# other
|
# other
|
||||||
|
@ -55,3 +55,40 @@ It's finally ready...
|
|||||||
Documentation.
|
Documentation.
|
||||||
|
|
||||||
What else? First Release!
|
What else? First Release!
|
||||||
|
|
||||||
|
... ...
|
||||||
|
|
||||||
|
(some day)
|
||||||
|
|
||||||
|
Determined to make backend better,
|
||||||
|
but resulting in full rewrite.
|
||||||
|
|
||||||
|
... ... (worked hard)
|
||||||
|
|
||||||
|
15th
|
||||||
|
|
||||||
|
Feeling it's there.
|
||||||
|
|
||||||
|
Oh, asyncio always quits like a mad cat, throwing bleak there
|
||||||
|
and just ends everything, uncleanly.
|
||||||
|
|
||||||
|
16th
|
||||||
|
|
||||||
|
Solved many things left yesterday.
|
||||||
|
|
||||||
|
Thought i18n needs be universal across there, so made one by myself.
|
||||||
|
|
||||||
|
Tried to implement flip, rtl and wrap in text printing,
|
||||||
|
wasted some time, but didn't regret.
|
||||||
|
|
||||||
|
Well, slightly update document and try compiling,
|
||||||
|
give everyone a surprise.
|
||||||
|
|
||||||
|
Try --release on p4a build. It worked. 5.9MiB apk, satisfied now?
|
||||||
|
Don't forget -Djava.net.useSystemProxies=true on gradle anymore,
|
||||||
|
when a proxy to google is needed.
|
||||||
|
|
||||||
|
Phone says a release apk should be signed to be installed.
|
||||||
|
Satisfy it. https://stackoverflow.com/questions/4853011/how-to-sign-an-android-apk-file
|
||||||
|
|
||||||
|
Okay, it's 17th 3 a.m. publish it and sleep.
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
|
**Note: Some maybe outdated at the moment**
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This application have a Client/Server module, but it's just locally.
|
This application have a Client/Server module, but it's just locally.
|
||||||
@ -9,7 +11,6 @@ The backend is in Python 3, aiming to have fewest dependencies, and in fact curr
|
|||||||
This can ensure the simplicity of the core part.
|
This can ensure the simplicity of the core part.
|
||||||
|
|
||||||
And the frontend is in a "old good" way, that use no "framework".
|
And the frontend is in a "old good" way, that use no "framework".
|
||||||
It needs [roddeh-i18n](https://www.npmjs.com/package/roddeh-i18n) lib for localization, and optionally [vConsole](https://www.npmjs.com/package/vconsole) for debugging on mobile.
|
|
||||||
|
|
||||||
My workspace stack is Linux/GNU/Artix/KDE/VSCodium, if you're interested.
|
My workspace stack is Linux/GNU/Artix/KDE/VSCodium, if you're interested.
|
||||||
For Android, GNU/Linux is required, though.
|
For Android, GNU/Linux is required, though.
|
||||||
@ -27,12 +28,11 @@ Just clone this repo first!
|
|||||||
|
|
||||||
1. Get Bleak BLE lib:
|
1. Get Bleak BLE lib:
|
||||||
`pip install bleak`
|
`pip install bleak`
|
||||||
2. Grab i18n.js [here](https://github.com/roddeh/i18njs/tree/master/dist), put to `www` as `i18n.js`
|
|
||||||
|
|
||||||
You are already well done for basic development. See [files](#files) section for what all the files do.
|
Alright, you are already well done for basic development. See [files](#files) section for what all the files do.
|
||||||
For more, read on...
|
For more, read on...
|
||||||
|
|
||||||
### Additional
|
### Optional
|
||||||
|
|
||||||
Sorry, I'm not a dev package manager enthusiast.
|
Sorry, I'm not a dev package manager enthusiast.
|
||||||
|
|
||||||
@ -48,19 +48,13 @@ If there are something better to organize these, feel free to discuss in issue.
|
|||||||
- Get an Windows 64-bit embeddable Python, extract to `build-common/python-win32-amd64-embed`
|
- Get an Windows 64-bit embeddable Python, extract to `build-common/python-win32-amd64-embed`
|
||||||
- You may remove the "bloated" parts inside, notably `libssl`, `libcrypto`, `sqlite3` and `pydoc`, of both `dll`/`pyd` files and in `python<version>.zip`, if have any.
|
- You may remove the "bloated" parts inside, notably `libssl`, `libcrypto`, `sqlite3` and `pydoc`, of both `dll`/`pyd` files and in `python<version>.zip`, if have any.
|
||||||
- Now you're able to bundle a "windows" edition, via `python3 bundle.py -w`
|
- Now you're able to bundle a "windows" edition, via `python3 bundle.py -w`
|
||||||
- Grab i18n.js typings `index.d.ts` from [here](https://github.com/roddeh/i18njs/tree/master/typings), put to `www` as `i18n.d.ts`
|
|
||||||
In the file, replace the last line:
|
|
||||||
`export = roddeh_i18n;`
|
|
||||||
with:
|
|
||||||
`declare var i18n = roddeh_i18n;`
|
|
||||||
Now you are ready to do more with i18n lib with the typing hint
|
|
||||||
- Get a [vConsole](https://www.npmjs.com/package/vconsole) script, put to `www` as `vconsole.js`
|
- Get a [vConsole](https://www.npmjs.com/package/vconsole) script, put to `www` as `vconsole.js`
|
||||||
Now you're ready to debug in browsers without a dev panel, by double-tapping "Cat Printer" title in the UI
|
Now you're ready to debug in browsers without a dev panel, by double-tapping "Cat Printer" title in the UI
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
- `server.py` - A Web server that:
|
- `server.py` - A Web server that:
|
||||||
- Is single threaded, to work with Android/pyjnius
|
- Is single threaded & with static handler, for some reasons
|
||||||
- Serves static Web files, that are in folder `www`
|
- Serves static Web files, that are in folder `www`
|
||||||
- Opens a Web browser once launched, unless specify the `-s` command-line parameter
|
- Opens a Web browser once launched, unless specify the `-s` command-line parameter
|
||||||
- Only listen to localhost, unless specify the `-a` command-line parameter
|
- Only listen to localhost, unless specify the `-a` command-line parameter
|
||||||
@ -83,6 +77,10 @@ If there are something better to organize these, feel free to discuss in issue.
|
|||||||
- Transpiled with TypeScript, for fallback on old browsers
|
- Transpiled with TypeScript, for fallback on old browsers
|
||||||
- Bundled all required scripts, see file `0-transpile.sh`
|
- Bundled all required scripts, see file `0-transpile.sh`
|
||||||
- Is not there by default. Transpile it yourself
|
- Is not there by default. Transpile it yourself
|
||||||
|
- `www/i18n*` - Scripts about I18n:
|
||||||
|
- TODO. In fact it worth a dedicated document to describe it
|
||||||
|
- (Mostly) Depends on "extensions" to work in the correct way,
|
||||||
|
feel free to extend, as it's *your* turn
|
||||||
- `www/*.js` - Other scripts:
|
- `www/*.js` - Other scripts:
|
||||||
- Small but useful, just look at them directly
|
- Small but useful, just look at them directly
|
||||||
- `www/jslicense.html` - Dedicated JavaScript License information
|
- `www/jslicense.html` - Dedicated JavaScript License information
|
||||||
@ -93,6 +91,7 @@ If there are something better to organize these, feel free to discuss in issue.
|
|||||||
- Quickly invoke with `./N<tab><enter>`
|
- Quickly invoke with `./N<tab><enter>`
|
||||||
- `build-common/bundle.py` - Bundler for "windows", "pure" and "bare" editions
|
- `build-common/bundle.py` - Bundler for "windows", "pure" and "bare" editions
|
||||||
- You can define what to include or not in this script, just modify directly, while trying to not alter other
|
- You can define what to include or not in this script, just modify directly, while trying to not alter other
|
||||||
|
- Adviced to transpile scripts before bundling
|
||||||
- To do the builds you should be in the build dir: `cd build-common`
|
- To do the builds you should be in the build dir: `cd build-common`
|
||||||
- With `bleak` there you're able to bundle a "pure" edition via just `python3 bundle.py`
|
- With `bleak` there you're able to bundle a "pure" edition via just `python3 bundle.py`
|
||||||
- In any case you're able to bundle a "bare" edition, via `python3 bundle.py -b`
|
- In any case you're able to bundle a "bare" edition, via `python3 bundle.py -b`
|
||||||
|
965
printer.py
965
printer.py
File diff suppressed because it is too large
Load Diff
133
printer_lib/commander.py
Normal file
133
printer_lib/commander.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
'Printer Commander'
|
||||||
|
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
|
crc8_table = [
|
||||||
|
0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, 0x38, 0x3f, 0x36, 0x31,
|
||||||
|
0x24, 0x23, 0x2a, 0x2d, 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65,
|
||||||
|
0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, 0xe0, 0xe7, 0xee, 0xe9,
|
||||||
|
0xfc, 0xfb, 0xf2, 0xf5, 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd,
|
||||||
|
0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, 0xa8, 0xaf, 0xa6, 0xa1,
|
||||||
|
0xb4, 0xb3, 0xba, 0xbd, 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2,
|
||||||
|
0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, 0xb7, 0xb0, 0xb9, 0xbe,
|
||||||
|
0xab, 0xac, 0xa5, 0xa2, 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a,
|
||||||
|
0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, 0x1f, 0x18, 0x11, 0x16,
|
||||||
|
0x03, 0x04, 0x0d, 0x0a, 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42,
|
||||||
|
0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, 0x89, 0x8e, 0x87, 0x80,
|
||||||
|
0x95, 0x92, 0x9b, 0x9c, 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4,
|
||||||
|
0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, 0xc1, 0xc6, 0xcf, 0xc8,
|
||||||
|
0xdd, 0xda, 0xd3, 0xd4, 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c,
|
||||||
|
0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, 0x19, 0x1e, 0x17, 0x10,
|
||||||
|
0x05, 0x02, 0x0b, 0x0c, 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34,
|
||||||
|
0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, 0x76, 0x71, 0x78, 0x7f,
|
||||||
|
0x6a, 0x6d, 0x64, 0x63, 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b,
|
||||||
|
0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, 0xae, 0xa9, 0xa0, 0xa7,
|
||||||
|
0xb2, 0xb5, 0xbc, 0xbb, 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8d, 0x84, 0x83,
|
||||||
|
0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, 0xe6, 0xe1, 0xe8, 0xef,
|
||||||
|
0xfa, 0xfd, 0xf4, 0xf3
|
||||||
|
]
|
||||||
|
|
||||||
|
def crc8(data):
|
||||||
|
'crc8 checksum'
|
||||||
|
crc = 0
|
||||||
|
for byte in data:
|
||||||
|
crc = crc8_table[(crc ^ byte) & 0xff]
|
||||||
|
return crc & 0xff
|
||||||
|
|
||||||
|
def reverse_bits(i: int):
|
||||||
|
'Reverse the bits of this byte (as `int`)'
|
||||||
|
return (
|
||||||
|
(i & 0b10000000) >> 7 |
|
||||||
|
(i & 0b01000000) >> 5 |
|
||||||
|
(i & 0b00100000) >> 3 |
|
||||||
|
(i & 0b00010000) >> 1 |
|
||||||
|
(i & 0b00001000) << 1 |
|
||||||
|
(i & 0b00000100) << 3 |
|
||||||
|
(i & 0b00000010) << 5 |
|
||||||
|
(i & 0b00000001) << 7
|
||||||
|
)
|
||||||
|
|
||||||
|
def int_to_bytes(i: int, big_endian=False):
|
||||||
|
''' Turn `int` into `bytearray`, that have
|
||||||
|
least bytes possible to represent the int
|
||||||
|
'''
|
||||||
|
result = bytearray()
|
||||||
|
while i != 0:
|
||||||
|
result.append(i & 0xff)
|
||||||
|
i >>= 8
|
||||||
|
if big_endian:
|
||||||
|
result.reverse()
|
||||||
|
return result
|
||||||
|
|
||||||
|
class Commander(metaclass=ABCMeta):
|
||||||
|
'Semi-abstract class, to be inherited by `PrinterDriver`'
|
||||||
|
|
||||||
|
dry_run: bool = False
|
||||||
|
|
||||||
|
def make_command(self, command_bit, payload: bytearray, *,
|
||||||
|
prefix=bytearray(), suffix=bytearray()):
|
||||||
|
'Make bytes that to be used to control printer'
|
||||||
|
payload_size = len(payload)
|
||||||
|
if payload_size > 0xff:
|
||||||
|
raise ValueError(f'Command payload too big ({payload_size} > 255)')
|
||||||
|
return prefix + bytearray(
|
||||||
|
[ 0x51, 0x78, command_bit, 0x00, payload_size, 0x00 ]
|
||||||
|
) + payload + bytearray( [ crc8(payload), 0xff ] ) + suffix
|
||||||
|
|
||||||
|
def start_printing(self):
|
||||||
|
'Start printing'
|
||||||
|
self.send( bytearray([0x51, 0x78, 0xa3, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff]) )
|
||||||
|
|
||||||
|
def start_printing_new(self):
|
||||||
|
'Start printing on newer printers'
|
||||||
|
self.send( bytearray([0x12, 0x51, 0x78, 0xa3, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff]) )
|
||||||
|
|
||||||
|
def image_mode(self):
|
||||||
|
'Enable Image Mode on the printer. Lighter, slower.'
|
||||||
|
self.send( self.make_command(0xbe, int_to_bytes(0x00)) )
|
||||||
|
|
||||||
|
def text_mode(self):
|
||||||
|
'Enable Text Mode on the printer. Darker, faster.'
|
||||||
|
self.send( self.make_command(0xbe, int_to_bytes(0x01)) )
|
||||||
|
|
||||||
|
def update_device(self):
|
||||||
|
'(unknown)'
|
||||||
|
self.send( self.make_command(0xa9, int_to_bytes(0x00)) )
|
||||||
|
|
||||||
|
def start_lattice(self):
|
||||||
|
'Start rolling paper'
|
||||||
|
self.send( self.make_command(0xa6, bytearray(
|
||||||
|
[0xaa, 0x55, 0x17, 0x38, 0x44, 0x5f, 0x5f, 0x5f, 0x44, 0x38, 0x2c]
|
||||||
|
)) )
|
||||||
|
|
||||||
|
def end_lattice(self):
|
||||||
|
'End rolling paper'
|
||||||
|
self.send( self.make_command(0xa6, bytearray(
|
||||||
|
[ 0xaa, 0x55, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17 ]
|
||||||
|
)) )
|
||||||
|
|
||||||
|
def retract_paper(self, steps: int):
|
||||||
|
'Retract the paper for `steps`'
|
||||||
|
self.send( self.make_command(0xa0, int_to_bytes(steps)) )
|
||||||
|
|
||||||
|
def feed_paper(self, steps: int):
|
||||||
|
'Feed the paper for `steps`'
|
||||||
|
self.send( self.make_command(0xa1, int_to_bytes(steps)) )
|
||||||
|
|
||||||
|
def set_energy(self, amount: int):
|
||||||
|
'Set thermal energy, max to `0xffff`'
|
||||||
|
self.send( self.make_command(0xbe, int_to_bytes(amount)) )
|
||||||
|
|
||||||
|
def draw_bitmap(self, bitmap_data: bytearray):
|
||||||
|
'Print `bitmap_data`. Also does the bit-reversing job.'
|
||||||
|
data = bytearray( map(reverse_bits, bitmap_data) )
|
||||||
|
self.send( self.make_command(0xa2, data) )
|
||||||
|
|
||||||
|
def draw_compressed_bitmap(self, bitmap_data: bytearray):
|
||||||
|
'TODO. Print `bitmap_data`, compress if worthy so'
|
||||||
|
self.draw_bitmap(bitmap_data)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send(self, data):
|
||||||
|
'Send data to device, or whatever'
|
||||||
|
...
|
38
printer_lib/i18n.py
Normal file
38
printer_lib/i18n.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
'Minimal internationalization lib'
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import locale
|
||||||
|
|
||||||
|
class I18nLib():
|
||||||
|
''' Minimal implementation of current frontend i18n in Python.
|
||||||
|
Not Complete (yet)!
|
||||||
|
'''
|
||||||
|
|
||||||
|
lang: str
|
||||||
|
fallback: str
|
||||||
|
data: dict = {}
|
||||||
|
|
||||||
|
def __init__(self, search_path='lang', lang=None, fallback=None):
|
||||||
|
self.lang = lang or locale.getdefaultlocale()[0].replace('_', '-')
|
||||||
|
self.fallback = fallback or 'en-US'
|
||||||
|
with open(os.path.join(search_path, self.fallback + '.json'),
|
||||||
|
'r', encoding='utf-8') as file:
|
||||||
|
self.data = json.load(file)
|
||||||
|
path = self.lang + '.json'
|
||||||
|
if path in os.listdir(search_path):
|
||||||
|
with open(os.path.join(search_path, path), 'r', encoding='utf-8') as file:
|
||||||
|
data = json.load(file)
|
||||||
|
for key in data:
|
||||||
|
self.data[key] = data[key]
|
||||||
|
|
||||||
|
def __getitem__(self, keys):
|
||||||
|
if not isinstance(keys, tuple):
|
||||||
|
keys = (keys, )
|
||||||
|
string = self.data.get(keys[0], keys[0])
|
||||||
|
if len(keys) > 1:
|
||||||
|
if isinstance(keys[-1], dict):
|
||||||
|
string = string.format(*keys[1:-1], **keys[-1])
|
||||||
|
else:
|
||||||
|
string = string.format(*keys[1:])
|
||||||
|
return string
|
@ -1,56 +1,45 @@
|
|||||||
''' Provide *very* basic CUPS/IPP support
|
''' Provide *very* basic CUPS/IPP support
|
||||||
Extracted from version 0.0.2, do more cleaning later...
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
import io
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
from .pf2 import int16be, int32be
|
||||||
|
|
||||||
|
def int8(b: bytes):
|
||||||
|
'Translate 1 byte as signed 8-bit int'
|
||||||
|
u = b[0]
|
||||||
|
return u - ((u >> 7 & 0b1) << 8)
|
||||||
|
|
||||||
class IPP():
|
class IPP():
|
||||||
'https://datatracker.ietf.org/doc/html/rfc8010'
|
'https://datatracker.ietf.org/doc/html/rfc8010'
|
||||||
server = None
|
server = None
|
||||||
printer = None
|
def __init__(self, server):
|
||||||
def __init__(self, server, printer):
|
|
||||||
self.server = server
|
self.server = server
|
||||||
self.printer = printer
|
def handle_ipp(self):
|
||||||
async def handle_ipp(self, data):
|
|
||||||
'Handle an IPP protocol request'
|
'Handle an IPP protocol request'
|
||||||
server = self.server
|
server = self.server
|
||||||
# len_data = len(data)
|
content_length = int(server.headers.get('Content-Length'))
|
||||||
# ipp_version_number = data[0:2]
|
buffer = io.BytesIO(server.rfile.read(content_length))
|
||||||
# ipp_operation_id = data[2:4]
|
_ipp_version = (int8(buffer.read(1)), int8(buffer.read(1)))
|
||||||
# ipp_request_id = data[4:8]
|
_ipp_operation_id = int16be(buffer.read(2))
|
||||||
ipp_operation_attributes_tag = data[8]
|
_ipp_request_id = int32be(buffer.read(4))
|
||||||
|
ipp_operation_attributes_tag = int8(buffer.read(1))
|
||||||
attributes = {}
|
attributes = {}
|
||||||
data_to_print = b''
|
data = b''
|
||||||
# this is silly. i want to use io.BytesIO
|
|
||||||
if ipp_operation_attributes_tag == 0x01:
|
if ipp_operation_attributes_tag == 0x01:
|
||||||
pointer = 9
|
while int8(buffer.read(1)) != 0x03:
|
||||||
next_name_length_at = 10
|
buffer.seek(-1, 1)
|
||||||
next_value_length_at = 10
|
tag = int8(buffer.read(1))
|
||||||
name = b''
|
if tag < 0x10: # delimiter-tag
|
||||||
value = b''
|
|
||||||
while data[pointer] != 0x03:
|
|
||||||
tag = data[pointer:pointer + 1]
|
|
||||||
pointer += 1
|
|
||||||
if tag[0] < 0x10: # delimiter-tag
|
|
||||||
continue
|
continue
|
||||||
next_name_length_at = pointer + data[pointer] * 0x0100 + data[pointer + 1] + 2
|
name = buffer.read(int16be(buffer.read(2)))
|
||||||
pointer += 2
|
value = buffer.read(int16be(buffer.read(2)))
|
||||||
while pointer < next_name_length_at:
|
|
||||||
name = name + data[pointer:pointer + 1]
|
|
||||||
pointer += 1
|
|
||||||
next_value_length_at = pointer + data[pointer] * 0x0100 + data[pointer + 1] + 2
|
|
||||||
pointer += 2
|
|
||||||
while pointer < next_value_length_at:
|
|
||||||
value = value + data[pointer:pointer + 1]
|
|
||||||
pointer += 1
|
|
||||||
attributes[name] = (tag, value)
|
attributes[name] = (tag, value)
|
||||||
name = b''
|
data = buffer.read()
|
||||||
value = b''
|
|
||||||
pointer += 1
|
|
||||||
data_to_print = data[pointer:]
|
|
||||||
# there are hard coded minimal response. this "just works" on cups
|
# there are hard coded minimal response. this "just works" on cups
|
||||||
if data_to_print == b'':
|
if data == b'':
|
||||||
try:
|
try:
|
||||||
server.send_response(200)
|
server.send_response(200)
|
||||||
server.send_header('Content-Type', 'application/ipp')
|
server.send_header('Content-Type', 'application/ipp')
|
||||||
@ -61,6 +50,14 @@ class IPP():
|
|||||||
except BrokenPipeError:
|
except BrokenPipeError:
|
||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
if data.startswith(b'%!PS-Adobe'):
|
||||||
|
self.handle_postscript(data)
|
||||||
|
else:
|
||||||
|
identifier = server.path[1:]
|
||||||
|
server.printer.print(io.BytesIO(data), mode='text', identifier=identifier)
|
||||||
|
def handle_postscript(self, data):
|
||||||
|
'Print PostScript data to printer, converting to PBM first with GhostScript `gs`'
|
||||||
|
server = self.server
|
||||||
platform_system = platform.system()
|
platform_system = platform.system()
|
||||||
# https://ghostscript.com/doc/9.54.0/Use.htm#Output_device
|
# https://ghostscript.com/doc/9.54.0/Use.htm#Output_device
|
||||||
if platform_system == 'Windows':
|
if platform_system == 'Windows':
|
||||||
@ -76,15 +73,12 @@ class IPP():
|
|||||||
'-dFitPage', '-dFitPage',
|
'-dFitPage', '-dFitPage',
|
||||||
'-sOutputFile=-', '-'
|
'-sOutputFile=-', '-'
|
||||||
], executable=gsexe, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
], executable=gsexe, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
pbm_data, _ = gsproc.communicate(data_to_print)
|
pbm_data, _ = gsproc.communicate(data)
|
||||||
try:
|
try:
|
||||||
if gsproc.wait() == 0:
|
if gsproc.wait() == 0:
|
||||||
info = server.path[1:]
|
identifier = server.path[1:]
|
||||||
is_found = await server.printer.filter_device(info, server.settings.scan_time)
|
# TODO: Make IPP can report some errors
|
||||||
if not is_found:
|
server.printer.print(io.BytesIO(pbm_data), mode='pbm', identifier=identifier)
|
||||||
... # TODO: Make IPP can report some errors
|
|
||||||
raise Exception(f'No printer found with info: {info}')
|
|
||||||
await server.printer.print_data(pbm_data)
|
|
||||||
else:
|
else:
|
||||||
raise Exception('Error on invoking Ghostscript')
|
raise Exception('Error on invoking Ghostscript')
|
||||||
server.send_response(200)
|
server.send_response(200)
|
||||||
@ -97,4 +91,3 @@ class IPP():
|
|||||||
server.send_response(500)
|
server.send_response(500)
|
||||||
server.send_header('Content-Type', 'application/ipp')
|
server.send_header('Content-Type', 'application/ipp')
|
||||||
server.end_headers()
|
server.end_headers()
|
||||||
server.wfile.write(b'')
|
|
16
printer_lib/models.py
Normal file
16
printer_lib/models.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
'Printer model specifications'
|
||||||
|
|
||||||
|
class Model():
|
||||||
|
'A printer model'
|
||||||
|
paper_width: int
|
||||||
|
is_new_kind: bool
|
||||||
|
def __init__(self, width, is_new):
|
||||||
|
self.paper_width = width
|
||||||
|
self.is_new_kind = is_new
|
||||||
|
|
||||||
|
Models = {
|
||||||
|
'GB01': Model(384, False),
|
||||||
|
'GB02': Model(384, False),
|
||||||
|
'GB03': Model(384, True),
|
||||||
|
'GT01': Model(384, False),
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
'Python lib to read PF2 font file: http://grub.gibibit.com/New_font_format'
|
''' Python lib for reading PF2 font files: http://grub.gibibit.com/New_font_format
|
||||||
|
I'd like to put it in Public Domain.
|
||||||
|
Don't forget to see how it's used in `text_print.py`
|
||||||
|
'''
|
||||||
|
|
||||||
import io
|
import io
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple
|
||||||
@ -57,12 +60,16 @@ class PF2():
|
|||||||
descent: int
|
descent: int
|
||||||
character_index: Dict[int, Tuple[int, int]]
|
character_index: Dict[int, Tuple[int, int]]
|
||||||
data_offset: int
|
data_offset: int
|
||||||
data_io: io.IOBase
|
data_io: io.BufferedIOBase
|
||||||
|
|
||||||
def __init__(self, path='font.pf2', *, read_to_mem=False, missing_character: str='?'):
|
def __init__(self, path='font.pf2', *, read_to_mem=True, missing_character: str='?'):
|
||||||
self.missing_character_code = ord(missing_character)
|
self.missing_character_code = ord(missing_character)
|
||||||
self.in_memory = read_to_mem
|
self.in_memory = read_to_mem
|
||||||
file = open(path, 'rb')
|
file = open(path, 'rb')
|
||||||
|
if read_to_mem:
|
||||||
|
self.data_io = io.BytesIO(file.read())
|
||||||
|
file.close()
|
||||||
|
file = self.data_io
|
||||||
self.is_pf2 = (file.read(12) == b'FILE\x00\x00\x00\x04PFF2')
|
self.is_pf2 = (file.read(12) == b'FILE\x00\x00\x00\x04PFF2')
|
||||||
if not self.is_pf2:
|
if not self.is_pf2:
|
||||||
return
|
return
|
||||||
@ -80,13 +87,7 @@ class PF2():
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
elif name == b'DATA':
|
elif name == b'DATA':
|
||||||
if read_to_mem:
|
file.seek(0)
|
||||||
self.data_io = io.BytesIO(file.read())
|
|
||||||
self.data_offset = -file.tell()
|
|
||||||
file.close()
|
|
||||||
else:
|
|
||||||
self.data_io = file
|
|
||||||
self.data_offset = 0
|
|
||||||
break
|
break
|
||||||
data = file.read(data_length)
|
data = file.read(data_length)
|
||||||
if name == b'NAME':
|
if name == b'NAME':
|
||||||
@ -116,7 +117,7 @@ class PF2():
|
|||||||
info = self.character_index[self.missing_character_code]
|
info = self.character_index[self.missing_character_code]
|
||||||
_compression, offset = info
|
_compression, offset = info
|
||||||
data = self.data_io
|
data = self.data_io
|
||||||
data.seek(offset + self.data_offset)
|
data.seek(offset)
|
||||||
char = Character()
|
char = Character()
|
||||||
char.width = uint16be(data.read(2))
|
char.width = uint16be(data.read(2))
|
||||||
char.height = uint16be(data.read(2))
|
char.height = uint16be(data.read(2))
|
||||||
@ -130,7 +131,5 @@ class PF2():
|
|||||||
|
|
||||||
__getitem__ = get_char
|
__getitem__ = get_char
|
||||||
|
|
||||||
def close(self):
|
def __del__(self):
|
||||||
'Close the data IO, if it\'s a real file'
|
self.data_io.close()
|
||||||
if not self.in_memory:
|
|
||||||
self.data_io.close()
|
|
87
printer_lib/text_print.py
Normal file
87
printer_lib/text_print.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
'Things used by Text Printing feature'
|
||||||
|
|
||||||
|
from .pf2 import PF2
|
||||||
|
|
||||||
|
class TextCanvas():
|
||||||
|
'Canvas for text printing, requires PF2 lib'
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
canvas: bytearray = None
|
||||||
|
rtl: bool
|
||||||
|
wrap: bool
|
||||||
|
pf2 = None
|
||||||
|
def __init__(self, width, *, wrap=False, rtl=False):
|
||||||
|
self.pf2 = PF2()
|
||||||
|
self.width = width
|
||||||
|
self.wrap = wrap
|
||||||
|
self.rtl = rtl
|
||||||
|
self.height = self.pf2.max_height + self.pf2.descent
|
||||||
|
self.flush_canvas()
|
||||||
|
def flush_canvas(self):
|
||||||
|
'Flush the canvas, returning the canvas data'
|
||||||
|
if self.canvas is None:
|
||||||
|
pbm_data = None
|
||||||
|
else:
|
||||||
|
pbm_data = bytearray(self.canvas)
|
||||||
|
self.canvas = bytearray(self.width * self.height // 8)
|
||||||
|
return pbm_data
|
||||||
|
def puttext(self, text):
|
||||||
|
''' Put the specified text to canvas.
|
||||||
|
It's a generator, will `yield` the data produced, per line.
|
||||||
|
'''
|
||||||
|
text = text.replace('\t', ' ' * 4)
|
||||||
|
canvas_length = len(self.canvas)
|
||||||
|
pf2 = self.pf2
|
||||||
|
current_width = 0
|
||||||
|
last_space_at = -1
|
||||||
|
width_at_last_space = 0
|
||||||
|
break_points = set()
|
||||||
|
characters = {}
|
||||||
|
for i, s in enumerate(text):
|
||||||
|
if s not in characters:
|
||||||
|
characters[s] = pf2[s]
|
||||||
|
char = characters[s]
|
||||||
|
if s == ' ':
|
||||||
|
last_space_at = i
|
||||||
|
width_at_last_space = current_width
|
||||||
|
if (current_width > self.width and
|
||||||
|
last_space_at != -1):
|
||||||
|
break_points.add(last_space_at)
|
||||||
|
current_width -= width_at_last_space
|
||||||
|
if s == '\n':
|
||||||
|
current_width = 0
|
||||||
|
continue
|
||||||
|
current_width += pf2.point_size // 2 # + char.x_offset
|
||||||
|
current_width = 0
|
||||||
|
for i, s in enumerate(text):
|
||||||
|
char = characters[s]
|
||||||
|
if (i in break_points or s == '\n' or
|
||||||
|
current_width + char.width > self.width):
|
||||||
|
# print(current_width, end=' ')
|
||||||
|
yield self.flush_canvas()
|
||||||
|
current_width = 0
|
||||||
|
if s in ' ':
|
||||||
|
continue
|
||||||
|
if ord(s) in range(0x00, 0x20): # glyphs that should not be printed out
|
||||||
|
continue
|
||||||
|
for x in range(char.width):
|
||||||
|
for y in range(char.height):
|
||||||
|
rtl_current_width = self.width - current_width - char.width - 1
|
||||||
|
target_x = x + char.x_offset
|
||||||
|
target_y = pf2.ascent + (y - char.height) - char.y_offset
|
||||||
|
canvas_byte = (self.width * target_y + current_width + target_x) // 8
|
||||||
|
canvas_bit = 7 - (self.width * target_y + current_width + target_x) % 8
|
||||||
|
if self.rtl:
|
||||||
|
canvas_byte = (self.width * target_y + rtl_current_width + target_x) // 8
|
||||||
|
canvas_bit = 7 - (self.width * target_y + rtl_current_width + target_x) % 8
|
||||||
|
else:
|
||||||
|
canvas_byte = (self.width * target_y + current_width + target_x) // 8
|
||||||
|
canvas_bit = 7 - (self.width * target_y + current_width + target_x) % 8
|
||||||
|
if canvas_byte < 0 or canvas_byte >= canvas_length:
|
||||||
|
continue
|
||||||
|
char_byte = (char.width * y + x) // 8
|
||||||
|
char_bit = 7 - (char.width * y + x) % 8
|
||||||
|
self.canvas[canvas_byte] |= (
|
||||||
|
char.bitmap_data[char_byte] & (0b1 << char_bit)
|
||||||
|
) >> char_bit << canvas_bit
|
||||||
|
current_width += char.device_width
|
@ -9,27 +9,16 @@
|
|||||||
|
|
||||||
Gegenwärtig:
|
Gegenwärtig:
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|-------------|-------------------|
|
|----|----|
|
||||||
| Supported | GB01, GB02, GT01 |
|
| Supported | GB01, GB02, GT01, GB03 |
|
||||||
| Maybe | GB03 |
|
<!-- | Maybe | N/A | -->
|
||||||
| Planned | N/A |
|
<!-- | Planned | N/A | -->
|
||||||
|
|
||||||
## Funktionen
|
## Funktionen
|
||||||
|
|
||||||
*Derzeit befindet sich die Software im Alpha-Stadium. Mehr wird es bald geben!*
|
*Derzeit befindet sich die Software im Alpha-Stadium. Mehr wird es bald geben!*
|
||||||
|
|
||||||
| Available | Partial | Planned |
|
|
||||||
|-----------------|-----------|---------------|
|
|
||||||
| Web Interface | CUPS/IPP* | Visual Editor |
|
|
||||||
| Print a Picture | | Help/Manual |
|
|
||||||
| Command-line | | Text Printing |
|
|
||||||
|
|
||||||
<!-- May comment the line below if there are none -->
|
|
||||||
\* In development code. Will be released in a short period.
|
|
||||||
|
|
||||||
*Along with…*
|
|
||||||
|
|
||||||
- Simple!
|
- Simple!
|
||||||
- Bedienung über eine Web-UI direkt im Browser,
|
- Bedienung über eine Web-UI direkt im Browser,
|
||||||
- oder besorgen Sie sich die Android-Version!
|
- oder besorgen Sie sich die Android-Version!
|
||||||
|
@ -9,27 +9,14 @@
|
|||||||
|
|
||||||
目前有:
|
目前有:
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|-----------|-------------------|
|
|----|----|
|
||||||
| 支持 | GB01, GB02, GT01 |
|
| 支持 | GB01, GB02, GT01, GB03 |
|
||||||
| 可能支持 | GB03 |
|
|
||||||
| 计划 | 暂无 |
|
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
*当前仍在继续开发。以后会有更多!*
|
*当前仍在继续开发。以后会有更多!*
|
||||||
|
|
||||||
| 可用 | 部分 | 计划 |
|
|
||||||
|-----------|-----------|---------------|
|
|
||||||
| 网页界面 | CUPS/IPP* | 可视化编辑器 |
|
|
||||||
| 打印图片 | | 帮助/文档 |
|
|
||||||
| 命令行 | | 文本打印 |
|
|
||||||
|
|
||||||
<!-- 若没有 *,可注释下一句 -->
|
|
||||||
\* 存在于开发代码中。将在短时间内发布。
|
|
||||||
|
|
||||||
*当然还有……*
|
|
||||||
|
|
||||||
- 简易!
|
- 简易!
|
||||||
- 在网页界面进行操作,
|
- 在网页界面进行操作,
|
||||||
- 或者获取安卓应用!
|
- 或者获取安卓应用!
|
||||||
@ -38,6 +25,17 @@
|
|||||||
- 语言支持!您可参与翻译!
|
- 语言支持!您可参与翻译!
|
||||||
- 良好的用户界面,可适应桌面/移动端/明暗主题!
|
- 良好的用户界面,可适应桌面/移动端/明暗主题!
|
||||||
|
|
||||||
|
- 功能丰富!
|
||||||
|
- 网页界面,所有人都可以用!
|
||||||
|
- 控制打印机配置
|
||||||
|
- 打印照片,或单纯地进行测试
|
||||||
|
- 命令行,技术爱好者必备!
|
||||||
|
- 使用一些参数控制打印机
|
||||||
|
- 简易、简化的文字打印
|
||||||
|
- 让程序的每一部分发挥作用
|
||||||
|
- 其他一些好东西!
|
||||||
|
- 服务器也具有 CUPS/IPP 能力
|
||||||
|
|
||||||
- 跨平台!
|
- 跨平台!
|
||||||
- 较新的 Windows 10 及以上
|
- 较新的 Windows 10 及以上
|
||||||
- GNU/Linux
|
- GNU/Linux
|
||||||
@ -76,11 +74,17 @@ python3 server.py
|
|||||||
sudo pacman -S bluez bluez-utils
|
sudo pacman -S bluez bluez-utils
|
||||||
```
|
```
|
||||||
|
|
||||||
|
*以后将有软件包!*
|
||||||
|
|
||||||
### MacOS
|
### MacOS
|
||||||
|
|
||||||
MacOS 用户请首先安装 [Python 3](https://www.python.org/)。
|
MacOS 用户请首先安装 [Python 3](https://www.python.org/),
|
||||||
|
然后在终端使用 `pip` 安装 `pyobjc` 和 `bleak`:
|
||||||
|
```bash
|
||||||
|
pip3 install pyobjc bleak
|
||||||
|
```
|
||||||
|
|
||||||
然后获取“纯净(pure)”版,并做同样的事情:
|
然后获取“单一(bare)”版,并做同样的事情:
|
||||||
```bash
|
```bash
|
||||||
python3 server.py
|
python3 server.py
|
||||||
```
|
```
|
||||||
@ -94,15 +98,13 @@ python3 server.py
|
|||||||
当已安装 [Python 3](https://www.python.org/) 时,您可以直接获取“纯净(pure)”版,
|
当已安装 [Python 3](https://www.python.org/) 时,您可以直接获取“纯净(pure)”版,
|
||||||
或在已使用 `pip` 安装 `bleak` 时使用“单一(bare)”版。
|
或在已使用 `pip` 安装 `bleak` 时使用“单一(bare)”版。
|
||||||
|
|
||||||
命令行高手?直接用 `printer.py`!
|
|
||||||
|
|
||||||
查看所有[发布版本](https://github.com/NaitLee/Cat-Printer/releases)!
|
查看所有[发布版本](https://github.com/NaitLee/Cat-Printer/releases)!
|
||||||
|
|
||||||
## 有问题?
|
## 有问题?
|
||||||
|
|
||||||
有想法?开个 issue!
|
有想法?去 Discussion 讨论!
|
||||||
|
|
||||||
如果能行,PR 也可以!
|
如果能行,Pull Request 也可以!
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
@ -126,7 +128,7 @@ Copyright © 2022 NaitLee Soft. 保留一些权利。
|
|||||||
|
|
||||||
- 当然不能没有 Python 和 Web 技术!
|
- 当然不能没有 Python 和 Web 技术!
|
||||||
- [Bleak](https://bleak.readthedocs.io/en/latest/) 跨平台蓝牙低功耗库,牛!
|
- [Bleak](https://bleak.readthedocs.io/en/latest/) 跨平台蓝牙低功耗库,牛!
|
||||||
- [roddeh-i18n](https://github.com/roddeh/i18njs),很好!
|
- [roddeh-i18n](https://github.com/roddeh/i18njs),当前内置的国际化脚本受此启发
|
||||||
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/),虽然有些麻烦的地方
|
- [python-for-android](https://python-for-android.readthedocs.io/en/latest/),虽然有些麻烦的地方
|
||||||
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) 从 Java 拯救了我的生命
|
- [AdvancedWebView](https://github.com/delight-im/Android-AdvancedWebView) 从 Java 拯救了我的生命
|
||||||
- Stack Overflow 和整个互联网,你们让我从零开始了解了安卓“活动” `Activity`
|
- Stack Overflow 和整个互联网,你们让我从零开始了解了安卓“活动” `Activity`
|
||||||
|
218
server.py
218
server.py
@ -1,16 +1,25 @@
|
|||||||
'Cat Printer - Serve a Web UI'
|
'Cat Printer - Serve a Web UI'
|
||||||
|
|
||||||
# if pylint is annoying you, see file .pylint-rc
|
# if pylint is annoying you, see file .pylintrc
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import asyncio
|
|
||||||
import platform
|
import platform
|
||||||
# Don't use ThreadingHTTPServer if you're going to use pyjnius!
|
from http.server import BaseHTTPRequestHandler
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer #, ThreadingHTTPServer
|
|
||||||
from bleak.exc import BleakDBusError, BleakError
|
# For now we can't use `ThreadingHTTPServer`
|
||||||
from printer import PrinterDriver
|
from http.server import HTTPServer
|
||||||
|
|
||||||
|
# import `printer` first, to diagnostic some common errors
|
||||||
|
from printer import PrinterDriver, PrinterError, I18n, info
|
||||||
|
|
||||||
|
from bleak.exc import BleakDBusError, BleakError # pylint: disable=wrong-import-order
|
||||||
|
|
||||||
|
from printer_lib.ipp import IPP
|
||||||
|
|
||||||
|
IsAndroid = (os.environ.get("P4A_BOOTSTRAP") is not None)
|
||||||
|
|
||||||
class DictAsObject(dict):
|
class DictAsObject(dict):
|
||||||
""" Let you use a dict like an object in JavaScript.
|
""" Let you use a dict like an object in JavaScript.
|
||||||
@ -20,23 +29,8 @@ class DictAsObject(dict):
|
|||||||
def __setattr__(self, key, value):
|
def __setattr__(self, key, value):
|
||||||
self[key] = value
|
self[key] = value
|
||||||
|
|
||||||
class PrinterServerError(Exception):
|
class PrinterServerError(PrinterError):
|
||||||
'Error of PrinterServer'
|
'Error of PrinterServer'
|
||||||
code: int
|
|
||||||
name: str
|
|
||||||
details: str
|
|
||||||
def __init__(self, *args, code=1):
|
|
||||||
super().__init__(*args)
|
|
||||||
len_args = len(args)
|
|
||||||
self.code = code
|
|
||||||
if len_args > 0:
|
|
||||||
self.name = args[0]
|
|
||||||
if len_args > 1:
|
|
||||||
self.details = args[1]
|
|
||||||
|
|
||||||
def log(message):
|
|
||||||
'For logging a message'
|
|
||||||
print(message)
|
|
||||||
|
|
||||||
mime_type = {
|
mime_type = {
|
||||||
'html': 'text/html;charset=utf-8',
|
'html': 'text/html;charset=utf-8',
|
||||||
@ -51,24 +45,34 @@ def mime(url: str):
|
|||||||
'Get pre-defined MIME type of a certain url by extension name'
|
'Get pre-defined MIME type of a certain url by extension name'
|
||||||
return mime_type.get(url.rsplit('.', 1)[-1], mime_type['octet-stream'])
|
return mime_type.get(url.rsplit('.', 1)[-1], mime_type['octet-stream'])
|
||||||
|
|
||||||
class PrinterServer(BaseHTTPRequestHandler):
|
class PrinterServerHandler(BaseHTTPRequestHandler):
|
||||||
'(Local) server for Cat Printer Web interface'
|
'(Local) server handler for Cat Printer Web interface'
|
||||||
|
|
||||||
buffer = 4 * 1024 * 1024
|
buffer = 4 * 1024 * 1024
|
||||||
|
|
||||||
max_payload = buffer * 16
|
max_payload = buffer * 16
|
||||||
|
|
||||||
settings = DictAsObject({
|
settings = DictAsObject({
|
||||||
'config_path': 'config.json',
|
'config_path': 'config.json',
|
||||||
|
'version': 1,
|
||||||
'is_android': False,
|
'is_android': False,
|
||||||
'printer': None,
|
'scan_timeout': 5.0,
|
||||||
'scan_time': 3,
|
|
||||||
'frequency': 0.8,
|
|
||||||
'dry_run': False
|
'dry_run': False
|
||||||
})
|
})
|
||||||
printer = PrinterDriver()
|
_settings_blacklist = (
|
||||||
ipp = None
|
'printer', 'is_android'
|
||||||
|
)
|
||||||
|
|
||||||
|
printer: PrinterDriver = PrinterDriver()
|
||||||
|
|
||||||
|
ipp: IPP = None
|
||||||
|
|
||||||
def log_request(self, _code=200, _size=0):
|
def log_request(self, _code=200, _size=0):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def log_error(self, *_args):
|
def log_error(self, *_args):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
'Called when server get a GET http request'
|
'Called when server get a GET http request'
|
||||||
path = 'www' + self.path
|
path = 'www' + self.path
|
||||||
@ -91,24 +95,28 @@ class PrinterServer(BaseHTTPRequestHandler):
|
|||||||
if not self.wfile.write(chunk):
|
if not self.wfile.write(chunk):
|
||||||
break
|
break
|
||||||
return
|
return
|
||||||
def api_success(self):
|
|
||||||
'Called when a simple API call is being considered successful'
|
def api_success(self, body_json=None):
|
||||||
|
'Called when an API call is being considered successful'
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header('Content-Type', mime('json'))
|
self.send_header('Content-Type', mime('json'))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(b'{}')
|
if body_json is None:
|
||||||
def api_fail(self, error_json, error=None):
|
self.wfile.write(b'{}')
|
||||||
|
else:
|
||||||
|
self.wfile.write(json.dumps(body_json).encode('utf-8'))
|
||||||
|
|
||||||
|
def api_fail(self, error_json):
|
||||||
'Called when an API call is failed'
|
'Called when an API call is failed'
|
||||||
self.send_response(500)
|
self.send_response(500)
|
||||||
self.send_header('Content-Type', mime('json'))
|
self.send_header('Content-Type', mime('json'))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(json.dumps(error_json).encode('utf-8'))
|
self.wfile.write(json.dumps(error_json).encode('utf-8'))
|
||||||
self.wfile.flush()
|
self.wfile.flush()
|
||||||
if isinstance(error, Exception):
|
|
||||||
raise error
|
|
||||||
def load_config(self):
|
def load_config(self):
|
||||||
'Load config file, or if not exist, create one with default'
|
'Load config file, or if not exist, create one with default'
|
||||||
if os.environ.get("P4A_BOOTSTRAP") is not None:
|
if IsAndroid:
|
||||||
self.settings['is_android'] = True
|
self.settings['is_android'] = True
|
||||||
from android.storage import app_storage_path # pylint: disable=import-error
|
from android.storage import app_storage_path # pylint: disable=import-error
|
||||||
settings_path = app_storage_path()
|
settings_path = app_storage_path()
|
||||||
@ -118,19 +126,38 @@ class PrinterServer(BaseHTTPRequestHandler):
|
|||||||
)
|
)
|
||||||
if os.path.exists(self.settings.config_path):
|
if os.path.exists(self.settings.config_path):
|
||||||
with open(self.settings.config_path, 'r', encoding='utf-8') as file:
|
with open(self.settings.config_path, 'r', encoding='utf-8') as file:
|
||||||
self.settings = DictAsObject(json.load(file))
|
settings = DictAsObject(json.load(file))
|
||||||
|
if (settings.version is None or
|
||||||
|
settings.version < self.settings.version):
|
||||||
|
# Version too old, start over
|
||||||
|
# TODO: selective?
|
||||||
|
self.save_config()
|
||||||
|
return
|
||||||
|
self.settings = settings
|
||||||
else:
|
else:
|
||||||
self.save_config()
|
self.save_config()
|
||||||
|
|
||||||
def save_config(self):
|
def save_config(self):
|
||||||
'Save config file'
|
'Save config file'
|
||||||
with open(self.settings.config_path, 'w', encoding='utf-8') as file:
|
with open(self.settings.config_path, 'w', encoding='utf-8') as file:
|
||||||
json.dump(self.settings, file, indent=4)
|
settings = {}
|
||||||
|
for i in self.settings:
|
||||||
|
if i not in self._settings_blacklist:
|
||||||
|
settings[i] = self.settings[i]
|
||||||
|
json.dump(settings, file, indent=4)
|
||||||
|
|
||||||
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.frequency = float(self.settings.frequency)
|
self.printer.scan_timeout = self.settings.scan_timeout
|
||||||
|
self.printer.fake = self.settings.fake
|
||||||
|
self.printer.dump = self.settings.dump
|
||||||
|
self.printer.flip_h = self.settings.flip_h
|
||||||
|
self.printer.flip_v = self.settings.flip_v
|
||||||
if self.settings.printer is not None:
|
if self.settings.printer is not None:
|
||||||
self.printer.name, self.printer.address = self.settings.printer.split(',')
|
name, address = self.settings.printer.split(',')
|
||||||
|
self.printer.connect(name, address)
|
||||||
|
|
||||||
def handle_api(self):
|
def handle_api(self):
|
||||||
'Handle API request from POST'
|
'Handle API request from POST'
|
||||||
content_length = int(self.headers.get('Content-Length'))
|
content_length = int(self.headers.get('Content-Length'))
|
||||||
@ -138,121 +165,110 @@ class PrinterServer(BaseHTTPRequestHandler):
|
|||||||
api = self.path[1:]
|
api = self.path[1:]
|
||||||
if api == 'print':
|
if api == 'print':
|
||||||
self.update_printer()
|
self.update_printer()
|
||||||
loop = asyncio.new_event_loop()
|
self.printer.print(io.BytesIO(body))
|
||||||
try:
|
self.api_success()
|
||||||
devices = loop.run_until_complete(
|
|
||||||
self.printer.print_data(body)
|
|
||||||
)
|
|
||||||
self.api_success()
|
|
||||||
finally:
|
|
||||||
loop.close()
|
|
||||||
return
|
return
|
||||||
data = DictAsObject(json.loads(body))
|
data = DictAsObject(json.loads(body))
|
||||||
if api == 'devices':
|
if api == 'devices':
|
||||||
loop = asyncio.new_event_loop()
|
self.printer.connect(None)
|
||||||
try:
|
|
||||||
devices = loop.run_until_complete(
|
|
||||||
self.printer.search_all_printers(float(self.settings.scan_time))
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
loop.close()
|
|
||||||
devices_list = [{
|
devices_list = [{
|
||||||
'name': device.name,
|
'name': device.name,
|
||||||
'address': device.address
|
'address': device.address
|
||||||
} for device in devices]
|
} for device in self.printer.scan()]
|
||||||
self.send_response(200)
|
self.api_success({
|
||||||
self.send_header('Content-Type', mime('json'))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(json.dumps({
|
|
||||||
'devices': devices_list
|
'devices': devices_list
|
||||||
}).encode('utf-8'))
|
})
|
||||||
return
|
return
|
||||||
if api == 'query':
|
if api == 'query':
|
||||||
self.load_config()
|
self.load_config()
|
||||||
self.send_response(200)
|
self.api_success(self.settings)
|
||||||
self.send_header('Content-Type', mime('json'))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(json.dumps(self.settings).encode('utf-8'))
|
|
||||||
return
|
return
|
||||||
if api == 'set':
|
if api == 'set':
|
||||||
for key in data:
|
for key in data:
|
||||||
self.settings[key] = data[key]
|
self.settings[key] = data[key]
|
||||||
self.save_config()
|
self.save_config()
|
||||||
|
self.update_printer()
|
||||||
self.api_success()
|
self.api_success()
|
||||||
return
|
return
|
||||||
if api == 'exit':
|
if api == 'exit':
|
||||||
self.api_success()
|
self.api_success()
|
||||||
self.save_config()
|
self.exit()
|
||||||
# Only usable when using ThreadingHTTPServer
|
|
||||||
# server.shutdown()
|
def exit(self):
|
||||||
sys.exit(0)
|
'Stop correctly & cleanly'
|
||||||
|
self.save_config()
|
||||||
|
self.printer.unload()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
'Called when server get a POST http request'
|
'Called when server get a POST http request'
|
||||||
content_length = int(self.headers.get('Content-Length', -1))
|
content_length = int(self.headers.get('Content-Length', -1))
|
||||||
if (content_length == -1 or
|
if (content_length < -1 or
|
||||||
content_length > self.max_payload
|
content_length > self.max_payload
|
||||||
):
|
):
|
||||||
self.send_response(400)
|
|
||||||
self.send_header('Content-Type', mime('txt'))
|
|
||||||
self.end_headers()
|
|
||||||
return
|
return
|
||||||
if self.headers.get('Content-Type') == 'application/ipp':
|
if self.headers.get('Content-Type') == 'application/ipp':
|
||||||
if self.ipp is None:
|
if self.ipp is None:
|
||||||
try:
|
self.ipp = IPP(self)
|
||||||
from additional.ipp import IPP
|
self.ipp.handle_ipp()
|
||||||
self.load_config()
|
|
||||||
except ImportError:
|
|
||||||
# TODO: Better response?
|
|
||||||
return
|
|
||||||
self.ipp = IPP(self, self.printer)
|
|
||||||
self.update_printer()
|
|
||||||
body = self.rfile.read(content_length)
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
try:
|
|
||||||
loop.run_until_complete(self.ipp.handle_ipp(body))
|
|
||||||
finally:
|
|
||||||
loop.close()
|
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self.handle_api()
|
self.handle_api()
|
||||||
return
|
return
|
||||||
except BleakDBusError as e:
|
except BleakDBusError as e:
|
||||||
|
# TODO: better error reporting
|
||||||
self.api_fail({
|
self.api_fail({
|
||||||
'code': -2,
|
|
||||||
'name': e.dbus_error,
|
'name': e.dbus_error,
|
||||||
'details': e.dbus_error_details
|
'details': e.dbus_error_details
|
||||||
})
|
})
|
||||||
except BleakError as e:
|
except BleakError as e:
|
||||||
self.api_fail({
|
self.api_fail({
|
||||||
'code': -3,
|
|
||||||
'name': 'BleakError',
|
'name': 'BleakError',
|
||||||
'details': str(e)
|
'details': str(e)
|
||||||
})
|
})
|
||||||
except PrinterServerError as e:
|
except PrinterError as e:
|
||||||
self.api_fail({
|
self.api_fail({
|
||||||
'code': e.code,
|
'name': e.message,
|
||||||
'name': e.name,
|
'details': e.message_localized
|
||||||
'details': e.details
|
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.api_fail({
|
self.api_fail({
|
||||||
'code': -1,
|
|
||||||
'name': 'Exception',
|
'name': 'Exception',
|
||||||
'details': str(e)
|
'details': str(e)
|
||||||
}, e)
|
})
|
||||||
|
raise
|
||||||
|
|
||||||
|
class PrinterServer(HTTPServer):
|
||||||
|
''' (local) server for Cat Printer Web Interface
|
||||||
|
The reason to override is to only init the handler once,
|
||||||
|
avoiding confliction, and stop cleanly
|
||||||
|
'''
|
||||||
|
|
||||||
|
handler: PrinterServerHandler = None
|
||||||
|
|
||||||
|
def finish_request(self, request, client_address):
|
||||||
|
if self.handler is None:
|
||||||
|
self.handler = PrinterServerHandler(request, client_address, self)
|
||||||
|
return
|
||||||
|
self.handler.__init__(request, client_address, self)
|
||||||
|
|
||||||
|
def server_close(self):
|
||||||
|
if self.handler is not None:
|
||||||
|
self.handler.exit()
|
||||||
|
super().server_close()
|
||||||
|
|
||||||
|
|
||||||
def serve():
|
def serve():
|
||||||
'Start server'
|
'Start server'
|
||||||
address, port = '127.0.0.1', 8095
|
address, port = '127.0.0.1', 8095
|
||||||
listen_all = False
|
listen_all = False
|
||||||
if '-a' in sys.argv:
|
if '-a' in sys.argv:
|
||||||
print('Will listen on ALL addresses')
|
info(I18n['will-listen-on-all-addresses'])
|
||||||
listen_all = True
|
listen_all = True
|
||||||
# Again, Don't use ThreadingHTTPServer if you're going to use pyjnius!
|
server = PrinterServer(('' if listen_all else address, port), PrinterServer)
|
||||||
server = HTTPServer(('' if listen_all else address, port), PrinterServer)
|
|
||||||
service_url = f'http://{address}:{port}/'
|
service_url = f'http://{address}:{port}/'
|
||||||
if '-s' in sys.argv:
|
if '-s' in sys.argv:
|
||||||
print(service_url)
|
info(I18n['serving-at-0', service_url])
|
||||||
else:
|
else:
|
||||||
operating_system = platform.uname().system
|
operating_system = platform.uname().system
|
||||||
if operating_system == 'Windows':
|
if operating_system == 'Windows':
|
||||||
@ -262,11 +278,11 @@ def serve():
|
|||||||
# TODO: I don't know about macOS
|
# TODO: I don't know about macOS
|
||||||
# elif operating_system == 'macOS':
|
# elif operating_system == 'macOS':
|
||||||
else:
|
else:
|
||||||
print(f'Will serve application at: {service_url}')
|
info(I18n['serving-at-0', service_url])
|
||||||
try:
|
try:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
server.server_close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
serve()
|
serve()
|
||||||
|
49
www/i18n-ext.js
Normal file
49
www/i18n-ext.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Methods to know which string to use, per language
|
||||||
|
*/
|
||||||
|
var I18nExtensions = (function() {
|
||||||
|
|
||||||
|
/** @type {ExtensionOf<'en-US'>} */
|
||||||
|
function english(things, conditions) {
|
||||||
|
let text = conditions;
|
||||||
|
for (let index in things) {
|
||||||
|
let value = things[index];
|
||||||
|
if (value == 1) text = conditions['single'];
|
||||||
|
else text = conditions['multiple'];
|
||||||
|
if (!text && typeof value === 'number') {
|
||||||
|
if (value < 10 || value > 20) {
|
||||||
|
if (value % 10 === 1) text = conditions['1st'];
|
||||||
|
else if (value % 10 === 2) text = conditions['2nd'];
|
||||||
|
else if (value % 10 === 3) text = conditions['3rd'];
|
||||||
|
else text = conditions['nth'];
|
||||||
|
} else text = conditions['nth'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {ExtensionOf<'zh-CN'>} */
|
||||||
|
function chinese(things, conditions) {
|
||||||
|
let text = conditions;
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {ExtensionOf<'de-DE'>} */
|
||||||
|
function german(things, conditions) {
|
||||||
|
let text = conditions;
|
||||||
|
for (let index in things) {
|
||||||
|
let value = things[index];
|
||||||
|
if (value == 1) text = conditions['single'];
|
||||||
|
else text = conditions['multiple'];
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'en-US': english,
|
||||||
|
'zh-CN': chinese,
|
||||||
|
'de-DE': german
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
32
www/i18n.d.ts
vendored
Normal file
32
www/i18n.d.ts
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
type DictOf<T> = { [key: string]: T };
|
||||||
|
type Conditions = DictOf<string>;
|
||||||
|
type ConditionsOf<K extends Languages> = AllConditions[K];
|
||||||
|
type LanguageData = DictOf<Conditions | string>;
|
||||||
|
type Things = { [index: number | string]: number | string } | Array<number | string>;
|
||||||
|
type Extension = (things: Things, conditions: Conditions) => string;
|
||||||
|
type ExtensionOf<K extends Languages> = (things: Things, conditions: ConditionsOf<K>) => string;
|
||||||
|
type Languages = keyof AllConditions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All known possible condition keys, per language
|
||||||
|
*/
|
||||||
|
type AllConditions = {
|
||||||
|
'en-US': {
|
||||||
|
'single': string,
|
||||||
|
'multiple': string,
|
||||||
|
'1st': string,
|
||||||
|
'2nd': string,
|
||||||
|
'3rd': string,
|
||||||
|
'nth': string
|
||||||
|
},
|
||||||
|
'de-DE': {
|
||||||
|
'single': string,
|
||||||
|
'multiple': string
|
||||||
|
},
|
||||||
|
'zh-CN': {}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface I18nCallable extends I18n {
|
||||||
|
(text: string, things: Things, can_change_things: boolean): string;
|
||||||
|
}
|
109
www/i18n.js
Normal file
109
www/i18n.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yet another i18n solution
|
||||||
|
*/
|
||||||
|
class I18n {
|
||||||
|
/** @type {DictOf<LanguageData>} */
|
||||||
|
database;
|
||||||
|
/** @type {DictOf<Extension>} */
|
||||||
|
extensions;
|
||||||
|
/** @type {Languages} */
|
||||||
|
language;
|
||||||
|
constructor(language = 'en') {
|
||||||
|
this.database = {};
|
||||||
|
this.extensions = {};
|
||||||
|
this.useLanguage(language);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Use this language as main language
|
||||||
|
* @param {Languages} language
|
||||||
|
*/
|
||||||
|
useLanguage(language) {
|
||||||
|
this.language = language;
|
||||||
|
if (!this.database[language])
|
||||||
|
this.database[language] = {};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add data as corresponding language, also to
|
||||||
|
* other (added) languages as fallback
|
||||||
|
* @param {Languages} language
|
||||||
|
* @param {LanguageData} data
|
||||||
|
*/
|
||||||
|
add(language, data) {
|
||||||
|
if (!this.database[language])
|
||||||
|
this.database[language] = {};
|
||||||
|
for (let key in data) {
|
||||||
|
let value = data[key];
|
||||||
|
this.database[language][key] = value;
|
||||||
|
for (let lang in this.database)
|
||||||
|
if (!this.database[lang][key])
|
||||||
|
this.database[lang][key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Use extension in the language
|
||||||
|
* @param {Languages} language
|
||||||
|
* @param {Extension} extension
|
||||||
|
*/
|
||||||
|
extend(language, extension) {
|
||||||
|
this.extensions[language] = extension;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Alias a language code to another, usually formal/more used/as fallback
|
||||||
|
* @param {DictOf<Languages>} aliases
|
||||||
|
*/
|
||||||
|
alias(aliases) {
|
||||||
|
for (let alt_code in aliases) {
|
||||||
|
let code = aliases[alt_code];
|
||||||
|
this.database[alt_code] = this.database[code];
|
||||||
|
this.extensions[alt_code] = this.extensions[code];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Translate a string ("text"), using "things" such as numbers
|
||||||
|
* @param {string} text
|
||||||
|
* @param {Things} things
|
||||||
|
* @param {boolean} can_change_things
|
||||||
|
*/
|
||||||
|
translate(text, things, can_change_things = true) {
|
||||||
|
let conditions = this.database[this.language][text] || text;
|
||||||
|
if (!things) return conditions;
|
||||||
|
if (!can_change_things) things = { ... things };
|
||||||
|
if (this.extensions[this.language] && typeof conditions !== 'string')
|
||||||
|
text = this.extensions[this.language](things, conditions);
|
||||||
|
else text = conditions;
|
||||||
|
for (let key in things) {
|
||||||
|
text = text.replace(`{${key}}`, things[key].toString());
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A i18n instance that is directly callable
|
||||||
|
* @type {I18nCallable}
|
||||||
|
*/
|
||||||
|
var i18n = (function() {
|
||||||
|
|
||||||
|
let instance = new I18n();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @param {Things} things
|
||||||
|
* @param {boolean} can_change_things
|
||||||
|
*/
|
||||||
|
let i18n_callable = function(text, things, can_change_things = true) {
|
||||||
|
return instance.translate.call(i18n_callable, text, things, can_change_things);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.setPrototypeOf(i18n_callable, instance);
|
||||||
|
|
||||||
|
if (typeof I18nExtensions === 'object') {
|
||||||
|
for (let key in I18nExtensions)
|
||||||
|
instance.extend(key, I18nExtensions[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n_callable;
|
||||||
|
})();
|
@ -87,15 +87,15 @@
|
|||||||
<input type="number" name="scan-time" id="scan-time" min="1" max="10" value="3" />
|
<input type="number" name="scan-time" id="scan-time" min="1" max="10" value="3" />
|
||||||
<span data-i18n="-seconds">seconds</span>
|
<span data-i18n="-seconds">seconds</span>
|
||||||
<br />
|
<br />
|
||||||
<label for="frequency" data-i18n="transmission-speed-">Data Speed:</label>
|
<input type="checkbox" name="flip-h" id="flip-h" />
|
||||||
<select id="frequency">
|
<label for="flip-h" data-i18n="flip-horizontally">Flip Horizontally</label>
|
||||||
<option value="1.0" data-i18n="low">Low</option>
|
<input type="checkbox" name="flip-v" id="flip-v" />
|
||||||
<option value="0.8" data-i18n="moderate" selected>Moderate</option>
|
<label for="flip-v" data-i18n="flip-vertically">Flip Vertically</label>
|
||||||
<option value="0.6" data-i18n="high">High</option>
|
|
||||||
</select>
|
|
||||||
<br />
|
<br />
|
||||||
<input type="checkbox" name="dry-run" id="dry-run" />
|
<input type="checkbox" name="dry-run" id="dry-run" />
|
||||||
<label for="dry-run" data-i18n="dry-run">Dry Run</label>
|
<label for="dry-run" data-i18n="dry-run">Dry Run</label>
|
||||||
|
<input type="checkbox" name="dump" id="dump" />
|
||||||
|
<label for="dump" data-i18n="dump-traffic">Dump Traffic</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub panel">
|
<div class="sub panel">
|
||||||
<a href="#" data-i18n="system">System</a>
|
<a href="#" data-i18n="system">System</a>
|
||||||
|
@ -42,10 +42,16 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="i18n.js">i18n.js</a></td>
|
<td><a href="i18n.js">i18n.js</a></td>
|
||||||
<td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
|
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0-only</a></td>
|
||||||
<td><a href="https://github.com/roddeh/i18njs">i18n.js</a></td>
|
<td><a href="i18n.js">i18n.js</a></td>
|
||||||
<td>For internationalization (language support)</td>
|
<td>For internationalization (language support)</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="i18n-ext.js">i18n-ext.js</a></td>
|
||||||
|
<td><a href="http://creativecommons.org/publicdomain/zero/1.0/legalcode">CC0-1.0-only</a></td>
|
||||||
|
<td><a href="i18n-ext.js">i18n-ext.js</a></td>
|
||||||
|
<td>I18n "extensions"</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="main.js">main.js</a></td>
|
<td><a href="main.js">main.js</a></td>
|
||||||
<td><a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU-GPL-3.0-or-later</a></td>
|
<td><a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU-GPL-3.0-or-later</a></td>
|
||||||
|
@ -1,65 +1,61 @@
|
|||||||
{
|
{
|
||||||
"values": {
|
"cat-printer": "Cat Printer",
|
||||||
"cat-printer": "Cat Printer",
|
"printer": "Drucker",
|
||||||
"printer": "Drucker",
|
"device-": "Gerät:",
|
||||||
"device-": "Gerät:",
|
"refresh": "Aktualisieren",
|
||||||
"refresh": "Aktualisieren",
|
"mode-": "Modus:",
|
||||||
"mode-": "Modus:",
|
"canvas": "Leinwand",
|
||||||
"canvas": "Leinwand",
|
"document": "Dokument",
|
||||||
"document": "Dokument",
|
"insert-picture": "Bild einfügen",
|
||||||
"insert-picture": "Bild einfügen",
|
"help": "Hilfe",
|
||||||
"help": "Hilfe",
|
"javascript-license-information": "Informationen zur JavaScript-Lizenz",
|
||||||
"javascript-license-information": "Informationen zur JavaScript-Lizenz",
|
"settings": "Einstellungen",
|
||||||
"settings": "Einstellungen",
|
"image": "Bild",
|
||||||
"image": "Bild",
|
"monochrome-algorithm-": "Schwarzweiß-Algorithmus:",
|
||||||
"monochrome-algorithm-": "Schwarzweiß-Algorithmus:",
|
"direct": "Direkt",
|
||||||
"direct": "Direkt",
|
"floyd-steinberg": "Floyd Steinberg",
|
||||||
"floyd-steinberg": "Floyd Steinberg",
|
"halftone": "Halbtone",
|
||||||
"halftone": "Halbtone",
|
"wave": "Wave",
|
||||||
"wave": "Wave",
|
"fall": "Fall",
|
||||||
"fall": "Fall",
|
"legacy": "Legacy",
|
||||||
"legacy": "Legacy",
|
"threshold-": "Schwellwert",
|
||||||
"threshold-": "Schwellwert",
|
"transmission-speed-": "Übertragungsgeschwindigkeit:",
|
||||||
"transmission-speed-": "Übertragungsgeschwindigkeit:",
|
"low": "Gering",
|
||||||
"low": "Gering",
|
"moderate": "Moderat",
|
||||||
"moderate": "Moderat",
|
"high": "Hoch",
|
||||||
"high": "Hoch",
|
"transparent-as-white": "Transparent als Weiß",
|
||||||
"transparent-as-white": "Transparent als Weiß",
|
"misc": "Sonstiges",
|
||||||
"misc": "Sonstiges",
|
"system": "System",
|
||||||
"system": "System",
|
"disable-page-animation": "Seitenanimation ausschalten",
|
||||||
"disable-page-animation": "Seitenanimation ausschalten",
|
"exit": "Exit",
|
||||||
"exit": "Exit",
|
"error-message": "Fehlermeldung",
|
||||||
"error-message": "Fehlermeldung",
|
"preview": "Vorschau",
|
||||||
"preview": "Vorschau",
|
"print": "Drucken",
|
||||||
"print": "Drucken",
|
"expand": "Erweitern",
|
||||||
"expand": "Erweitern",
|
"crop": "Zuschneiden",
|
||||||
"crop": "Zuschneiden",
|
"scanning-for-devices": "Scannen nach Geräten…",
|
||||||
"scanning-for-devices": "Scannen nach Geräten…",
|
"scan-time-": "Scanzeit:",
|
||||||
"scan-time-": "Scanzeit:",
|
"-seconds": "Sekunden",
|
||||||
"-seconds": "Sekunden",
|
"no-available-devices-found": "Keine verfügbaren Geräte gefunden",
|
||||||
"no-available-devices-found": "Keine verfügbaren Geräte gefunden",
|
"found-0-available-devices": {
|
||||||
"found-1-available-devices": [
|
"single": "{0} verfügbares Gerät gefunden",
|
||||||
[1, 1, "1 verfügbares Gerät gefunden"],
|
"multiple": "{0} verfügbare Geräte gefunden"
|
||||||
[2, null, "%n verfügbare Geräte gefunden"]
|
},
|
||||||
],
|
"please-check-if-the-printer-is-down": "Bitte prüfe, ob der Drucker ausgeschaltet ist",
|
||||||
"please-check-if-the-printer-is-down": "Bitte prüfe, ob der Drucker ausgeschaltet ist",
|
"printing": "Drucken…",
|
||||||
"printing": "Drucken…",
|
"finished": "Fertiggestellt",
|
||||||
"finished": "Fertiggestellt",
|
"coming-soon-": "Demnächst verfügbar…",
|
||||||
"coming-soon-": "Demnächst verfügbar…",
|
"dry-run": "Testlauf",
|
||||||
"dry-run": "Testlauf",
|
"dry-run-test-print-process-only": "Testlauf: nur Probedruckvorgang",
|
||||||
"dry-run-test-print-process-only": "Testlauf: nur Probedruckvorgang",
|
"you-can-close-this-page-manually": "Sie können diese Seite manuell schließen",
|
||||||
"you-can-close-this-page-manually": "Sie können diese Seite manuell schließen",
|
"please-enable-bluetooth": "Bitte aktivieren Sie Bluetooth",
|
||||||
"please-enable-bluetooth": "Bitte aktivieren Sie Bluetooth",
|
"error-happened-please-check-error-message": "Fehler aufgetreten, bitte Fehlermeldung einsehen",
|
||||||
"error-happened-please-check-error-message": "Fehler aufgetreten, bitte Fehlermeldung einsehen",
|
"you-can-seek-for-help-with-detailed-info-below": "Sie können mit den nachstehenden ausführlichen Informationen Hilfe bekommen",
|
||||||
"you-can-seek-for-help-with-detailed-info-below": "Sie können mit den nachstehenden ausführlichen Informationen Hilfe bekommen.",
|
"or-try-to-scan-longer": "Oder versuchen Sie, länger zu suchen",
|
||||||
"or-try-to-scan-longer": "Oder versuchen Sie, länger zu suchen",
|
"print-to-cat-printer": "PBM-Bild auf Cat Printer drucken",
|
||||||
"print-pbm-image-to-cat-printer": "PBM-Bild auf Cat Printer drucken.",
|
"supported-models-": "Unterstützte Modelle:",
|
||||||
"supported-models-": "Unterstützte Modelle:",
|
"path-to-input-file-dash-for-stdin": "Pfad zur Datei. '-' für stdin",
|
||||||
"path-to-pbm-file-dash-for-stdin": "Pfad zur PBM-Datei. '-' für stdin.",
|
"scan-for-specified-seconds": "Suchlauf für die angegebenen Sekunden",
|
||||||
"scan-for-specified-seconds": "Suchlauf für die angegebenen Sekunden",
|
"dump-the-traffic": "Den Datenverkehr auf dem Drucker ausgeben und PBM-Bild beim Textdruck",
|
||||||
"specify-printer-mac-address": "Geben Sie die MAC-Adresse des Druckers an",
|
"text-printing-mode": "Textdruckmodus"
|
||||||
"communication-frequency-0.8-or-1-recommended": "Übermittlungsfrequenz. 0,8 oder 1 empfohlen.",
|
|
||||||
"dump-the-traffic-to-printer-and-pbm-image-when-text-printing": "Den Datenverkehr auf dem Drucker ausgeben und PBM-Bild beim Textdruck.",
|
|
||||||
"text-printing-mode-input-text-from-stdin": "Textdruckmodus, Eingabe von stdin."
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,67 +1,87 @@
|
|||||||
{
|
{
|
||||||
"values": {
|
"cat-printer": "Cat Printer",
|
||||||
"cat-printer": "Cat Printer",
|
"printer": "Printer",
|
||||||
"printer": "Printer",
|
"device-": "Device:",
|
||||||
"device-": "Device:",
|
"refresh": "Refresh",
|
||||||
"refresh": "Refresh",
|
"mode-": "Mode:",
|
||||||
"mode-": "Mode:",
|
"canvas": "Canvas",
|
||||||
"canvas": "Canvas",
|
"document": "Document",
|
||||||
"document": "Document",
|
"insert-picture": "Insert Picture",
|
||||||
"insert-picture": "Insert Picture",
|
"help": "Help",
|
||||||
"help": "Help",
|
"javascript-license-information": "JavaScript License Information",
|
||||||
"javascript-license-information": "JavaScript License Information",
|
"settings": "Settings",
|
||||||
"settings": "Settings",
|
"image": "Image",
|
||||||
"image": "Image",
|
"monochrome-algorithm-": "Monochrome Algorithm:",
|
||||||
"monochrome-algorithm-": "Monochrome Algorithm:",
|
"direct": "Direct",
|
||||||
"direct": "Direct",
|
"floyd-steinberg": "Floyd Steinberg",
|
||||||
"floyd-steinberg": "Floyd Steinberg",
|
"halftone": "Halftone",
|
||||||
"halftone": "Halftone",
|
"wave": "Wave",
|
||||||
"wave": "Wave",
|
"fall": "Fall",
|
||||||
"fall": "Fall",
|
"legacy": "Legacy",
|
||||||
"legacy": "Legacy",
|
"threshold-": "Threshold",
|
||||||
"threshold-": "Threshold",
|
"transmission-speed-": "Transmission Speed:",
|
||||||
"transmission-speed-": "Transmission Speed:",
|
"low": "Low",
|
||||||
"low": "Low",
|
"moderate": "Moderate",
|
||||||
"moderate": "Moderate",
|
"high": "High",
|
||||||
"high": "High",
|
"transparent-as-white": "Transparent as White",
|
||||||
"transparent-as-white": "Transparent as White",
|
"misc": "Misc",
|
||||||
"misc": "Misc",
|
"system": "System",
|
||||||
"system": "System",
|
"disable-page-animation": "Disable Page Animation",
|
||||||
"disable-page-animation": "Disable Page Animation",
|
"exit": "Exit",
|
||||||
"exit": "Exit",
|
"error-message": "Error Message",
|
||||||
"error-message": "Error Message",
|
"preview": "Preview",
|
||||||
"preview": "Preview",
|
"print": "Print",
|
||||||
"print": "Print",
|
"expand": "Expand",
|
||||||
"expand": "Expand",
|
"crop": "Crop",
|
||||||
"crop": "Crop",
|
"scanning-for-devices": "Scanning for devices…",
|
||||||
"scanning-for-devices": "Scanning for devices…",
|
"scan-time-": "Scan time:",
|
||||||
"scan-time-": "Scan time:",
|
"-seconds": "seconds",
|
||||||
"-seconds": "seconds",
|
"no-available-devices-found": "No available devices found",
|
||||||
"no-available-devices-found": "No available devices found",
|
"found-0-available-devices": {
|
||||||
"found-1-available-devices": [
|
"single": "Found {0} available device",
|
||||||
[1, 1, "Found 1 available device"],
|
"multiple": "Found {0} available devices"
|
||||||
[2, null, "Found %n available devices"]
|
},
|
||||||
],
|
"please-check-if-the-printer-is-down": "Please check if the printer is down",
|
||||||
"please-check-if-the-printer-is-down": "Please check if the printer is down",
|
"printing": "Printing…",
|
||||||
"printing": "Printing…",
|
"finished": "Finished",
|
||||||
"finished": "Finished",
|
"coming-soon-": "Coming Soon…",
|
||||||
"coming-soon-": "Coming Soon…",
|
"dry-run": "Dry Run",
|
||||||
"dry-run": "Dry Run",
|
"dry-run-test-print-process-only": "Dry Run: test print process only",
|
||||||
"dry-run-test-print-process-only": "Dry Run: test print process only",
|
"you-can-close-this-page-manually": "You can close this page manually",
|
||||||
"you-can-close-this-page-manually": "You can close this page manually",
|
"please-enable-bluetooth": "Please enable Bluetooth",
|
||||||
"please-enable-bluetooth": "Please enable Bluetooth",
|
"error-happened-please-check-error-message": "Error happened, please check error message",
|
||||||
"error-happened-please-check-error-message": "Error happened, please check error message",
|
"you-can-seek-for-help-with-detailed-info-below": "You can seek for help with detailed info below",
|
||||||
"you-can-seek-for-help-with-detailed-info-below": "You can seek for help with detailed info below.",
|
|
||||||
|
|
||||||
"or-try-to-scan-longer": "Or try to scan longer",
|
"or-try-to-scan-longer": "Or try to scan longer",
|
||||||
"print-pbm-image-to-cat-printer": "Print PBM image to Cat Printer.",
|
"print-to-cat-printer": "Print to Cat Printer",
|
||||||
"supported-models-": "Supported models:",
|
"supported-models-": "Supported models:",
|
||||||
"path-to-pbm-file-dash-for-stdin": "Path to PBM file. '-' for stdin.",
|
"path-to-input-file-dash-for-stdin": "Path to input file. '-' for stdin",
|
||||||
"scan-for-specified-seconds": "Scan for specified seconds",
|
"scan-for-specified-seconds": "Scan for specified seconds",
|
||||||
"specify-printer-mac-address": "Specify printer MAC address",
|
|
||||||
"communication-frequency-0.8-or-1-recommended": "Communication frequency. 0.8 or 1 recommended.",
|
|
||||||
|
|
||||||
"dump-the-traffic-to-printer-and-pbm-image-when-text-printing": "Dump the traffic to printer, and PBM image when text printing.",
|
"dump-the-traffic": "Dump the traffic",
|
||||||
"text-printing-mode-input-text-from-stdin": "Text printing mode, input from stdin."
|
"text-printing-mode": "Text printing mode",
|
||||||
}
|
|
||||||
|
"please-install-pyobjc-via-pip": "Please install `pyobjc` via pip",
|
||||||
|
"please-install-bleak-via-pip": "Please install `bleak` via pip",
|
||||||
|
"folder-printer_lib-is-incomplete-or-missing-please-check": "Folder `printer_lib` is incomplete or missing, please check",
|
||||||
|
"input-is-not-pbm-image": "Input is not PBM image",
|
||||||
|
"unsuitable-image-width-expected-0-got-1": "Unsuitable image width, expected {0}, got {1}",
|
||||||
|
"broken-pbm-image": "Broken PBM image",
|
||||||
|
"input-is-not-text-file": "Input is not text file",
|
||||||
|
"match-printer-with-this-name-or-address": "Match printer with this name or address",
|
||||||
|
"virtual-run-on-specified-model": "Virtual run on specified model",
|
||||||
|
"font-size-0": "Font size {0}",
|
||||||
|
"stopping": "Stopping",
|
||||||
|
"connecting": "Connecting",
|
||||||
|
"model-0-is-not-supported-yet": "Model '{0}' is not supported yet",
|
||||||
|
"invalid-address-0": "Invalid address: '{0}'",
|
||||||
|
"will-listen-on-all-addresses": "Will listen on ALL addresses",
|
||||||
|
"serving-at-0": "Serving at {0}",
|
||||||
|
"disconnecting-from-printer": "Disconnecting from printer",
|
||||||
|
"connected-to-0-1": "Connected to {0} {1}",
|
||||||
|
"flip-horizontally": "Flip Horizontally",
|
||||||
|
"flip-vertically": "Flip Vertically",
|
||||||
|
"dump-traffic": "Dump Traffic",
|
||||||
|
"right-to-left-text-order": "Right-to-left text order",
|
||||||
|
"auto-wrap-line": "Auto wrap line"
|
||||||
}
|
}
|
@ -1,66 +1,83 @@
|
|||||||
{
|
{
|
||||||
"values": {
|
"cat-printer": "猫咪打印机",
|
||||||
"cat-printer": "猫咪打印机",
|
"printer": "打印机",
|
||||||
"printer": "打印机",
|
"device-": "设备:",
|
||||||
"device-": "设备:",
|
"refresh": "刷新",
|
||||||
"refresh": "刷新",
|
"mode-": "模式:",
|
||||||
"mode-": "模式:",
|
"canvas": "画布",
|
||||||
"canvas": "画布",
|
"document": "文档",
|
||||||
"document": "文档",
|
"insert-picture": "插入图片",
|
||||||
"insert-picture": "插入图片",
|
"help": "帮助",
|
||||||
"help": "帮助",
|
"javascript-license-information": "JavaScript 许可证信息",
|
||||||
"javascript-license-information": "JavaScript 许可证信息",
|
"settings": "设置",
|
||||||
"settings": "设置",
|
"monochrome-algorithm-": "单色化算法:",
|
||||||
"monochrome-algorithm-": "单色化算法:",
|
"direct": "直接",
|
||||||
"direct": "直接",
|
"image": "图像",
|
||||||
"image": "图像",
|
"floyd-steinberg": "科学",
|
||||||
"floyd-steinberg": "科学",
|
"halftone": "点状",
|
||||||
"halftone": "点状",
|
"wave": "波纹",
|
||||||
"wave": "波纹",
|
"fall": "下落",
|
||||||
"fall": "下落",
|
"legacy": "旧版",
|
||||||
"legacy": "旧版",
|
"threshold-": "阈值:",
|
||||||
"threshold-": "阈值:",
|
"transmission-speed-": "传输速度:",
|
||||||
"transmission-speed-": "传输速度:",
|
"low": "低",
|
||||||
"low": "低",
|
"moderate": "适中",
|
||||||
"moderate": "适中",
|
"high": "高",
|
||||||
"high": "高",
|
"transparent-as-white": "透明为白色",
|
||||||
"transparent-as-white": "透明为白色",
|
"misc": "杂项",
|
||||||
"misc": "杂项",
|
"system": "系统",
|
||||||
"system": "系统",
|
"disable-page-animation": "禁用页面动画",
|
||||||
"disable-page-animation": "禁用页面动画",
|
"exit": "退出",
|
||||||
"exit": "退出",
|
"error-message": "错误消息",
|
||||||
"error-message": "错误消息",
|
"preview": "预览",
|
||||||
"preview": "预览",
|
"print": "打印",
|
||||||
"print": "打印",
|
"expand": "扩大",
|
||||||
"expand": "扩大",
|
"crop": "裁减",
|
||||||
"crop": "裁减",
|
"scanning-for-devices": "正在扫描设备……",
|
||||||
"scanning-for-devices": "正在扫描设备……",
|
"scan-time-": "扫描时间:",
|
||||||
"scan-time-": "扫描时间:",
|
"-seconds": "秒",
|
||||||
"-seconds": "秒",
|
"no-available-devices-found": "未发现可用设备",
|
||||||
"no-available-devices-found": "未发现可用设备",
|
"found-0-available-devices": "发现 {0} 个可用设备",
|
||||||
"found-1-available-devices": [
|
"please-check-if-the-printer-is-down": "请检查打印机是否已关闭",
|
||||||
[1, null, "发现 %n 个可用设备"]
|
"printing": "打印中……",
|
||||||
],
|
"finished": "完成",
|
||||||
"please-check-if-the-printer-is-down": "请检查打印机是否已关闭",
|
"coming-soon-": "即将到来……",
|
||||||
"printing": "打印中……",
|
"dry-run": "干运行",
|
||||||
"finished": "完成",
|
"dry-run-test-print-process-only": "干运行:仅测试打印流程",
|
||||||
"coming-soon-": "即将到来……",
|
"you-can-close-this-page-manually": "您可手动关闭此页面",
|
||||||
"dry-run": "干运行",
|
"please-enable-bluetooth": "请启用蓝牙",
|
||||||
"dry-run-test-print-process-only": "干运行:仅测试打印流程",
|
"error-happened-please-check-error-message": "发生错误,请检查错误消息",
|
||||||
"you-can-close-this-page-manually": "您可手动关闭此页面",
|
"you-can-seek-for-help-with-detailed-info-below": "您可以使用以下详细信息寻求帮助",
|
||||||
"please-enable-bluetooth": "请启用蓝牙",
|
|
||||||
"error-happened-please-check-error-message": "发生错误,请检查错误消息",
|
|
||||||
"you-can-seek-for-help-with-detailed-info-below": "您可以使用以下详细信息寻求帮助。",
|
|
||||||
|
|
||||||
"or-try-to-scan-longer": "或者尝试延长扫描时间",
|
"or-try-to-scan-longer": "或者尝试延长扫描时间",
|
||||||
"print-pbm-image-to-cat-printer": "打印 PBM 图片到猫咪打印机。",
|
"print-to-cat-printer": "打印到猫咪打印机。",
|
||||||
"supported-models-": "支持的型号:",
|
"supported-models-": "支持的型号:",
|
||||||
"path-to-pbm-file-dash-for-stdin": "PBM 文件的位置。“-” 作为标准输入。",
|
"path-to-input-file-dash-for-stdin": "输入文件的位置。使用 '-' 作为标准输入",
|
||||||
"scan-for-specified-seconds": "扫描指定的时长。",
|
"scan-for-specified-seconds": "扫描指定的时长",
|
||||||
"specify-printer-mac-address": "指定打印机的 MAC 地址",
|
|
||||||
"communication-frequency-0.8-or-1-recommended": "通讯频率。推荐 0.8 或 1。",
|
|
||||||
|
|
||||||
"dump-the-traffic-to-printer-and-pbm-image-when-text-printing": "转储到打印机的数据,和文字打印模式的 PBM 图像。",
|
"dump-the-traffic": "转储数据",
|
||||||
"text-printing-mode-input-text-from-stdin": "文字打印模式,将从标准输入读取文字。"
|
"text-printing-mode": "文字打印模式",
|
||||||
}
|
|
||||||
|
"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": "自动折行"
|
||||||
}
|
}
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
var fallbacks = [
|
var fallbacks = [
|
||||||
// main scripts, which we will directly modify
|
// main scripts, which we will directly modify
|
||||||
'i18n.js', 'image.js', 'main.js',
|
'i18n-ext.js', 'i18n.js', 'image.js', 'main.js',
|
||||||
// "compatibility" script, produced with eg. typescript tsc
|
// "compatibility" script, produced with eg. typescript tsc
|
||||||
'main.comp.js'
|
'main.comp.js'
|
||||||
];
|
];
|
||||||
|
29
www/main.js
29
www/main.js
@ -38,13 +38,13 @@ class _Notice {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.element = document.getElementById('notice');
|
this.element = document.getElementById('notice');
|
||||||
}
|
}
|
||||||
_message(message, ...args) {
|
_message(message, things) {
|
||||||
this.element.innerText = i18n(message, ...args) || message;
|
this.element.innerText = i18n(message, things) || message;
|
||||||
}
|
}
|
||||||
makeLogger(class_name) {
|
makeLogger(class_name) {
|
||||||
return (message, ...args) => {
|
return (message, things) => {
|
||||||
this.element.classList.value = class_name;
|
this.element.classList.value = class_name;
|
||||||
this._message(message, ...args);
|
this._message(message, things);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notice = this.makeLogger('notice');
|
notice = this.makeLogger('notice');
|
||||||
@ -373,7 +373,7 @@ class Main {
|
|||||||
putEvent('#button-exit', 'click', this.exit, this);
|
putEvent('#button-exit', 'click', this.exit, this);
|
||||||
putEvent('#button-print', 'click', this.print, this);
|
putEvent('#button-print', 'click', this.print, this);
|
||||||
putEvent('#device-refresh', 'click', this.searchDevices, this);
|
putEvent('#device-refresh', 'click', this.searchDevices, this);
|
||||||
this.attachSetter('#scan-time', 'change', 'scan_time');
|
this.attachSetter('#scan-time', 'change', 'scan_timeout');
|
||||||
this.attachSetter('#device-options', 'input', 'printer');
|
this.attachSetter('#device-options', 'input', 'printer');
|
||||||
this.attachSetter('input[name="algo"]', 'change', 'mono_algorithm');
|
this.attachSetter('input[name="algo"]', 'change', 'mono_algorithm');
|
||||||
this.attachSetter('#transparent-as-white', 'change', 'transparent_as_white');
|
this.attachSetter('#transparent-as-white', 'change', 'transparent_as_white');
|
||||||
@ -387,7 +387,9 @@ class Main {
|
|||||||
this.attachSetter('#threshold', 'change', 'threshold',
|
this.attachSetter('#threshold', 'change', 'threshold',
|
||||||
(value) => this.canvasController.threshold = value
|
(value) => this.canvasController.threshold = value
|
||||||
);
|
);
|
||||||
this.attachSetter('#frequency', 'change', 'frequency');
|
this.attachSetter('#flip-h', 'change', 'flip_h');
|
||||||
|
this.attachSetter('#flip-v', 'change', 'flip_v');
|
||||||
|
this.attachSetter('#dump', 'change', 'dump');
|
||||||
await this.loadConfig();
|
await this.loadConfig();
|
||||||
this.searchDevices();
|
this.searchDevices();
|
||||||
resolve();
|
resolve();
|
||||||
@ -488,7 +490,7 @@ class Main {
|
|||||||
hint('#device-refresh');
|
hint('#device-refresh');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Notice.notice('found-1-available-devices', devices.length);
|
Notice.notice('found-0-available-devices', [devices.length]);
|
||||||
hint('#insert-picture');
|
hint('#insert-picture');
|
||||||
devices.forEach(device => {
|
devices.forEach(device => {
|
||||||
let option = document.createElement('option');
|
let option = document.createElement('option');
|
||||||
@ -519,18 +521,15 @@ class Main {
|
|||||||
}
|
}
|
||||||
async initI18n() {
|
async initI18n() {
|
||||||
if (typeof i18n === 'undefined') return;
|
if (typeof i18n === 'undefined') return;
|
||||||
let language_list = navigator.languages;
|
i18n.useLanguage(navigator.languages[0]);
|
||||||
let loaded_languages = [];
|
for (let language of navigator.languages) {
|
||||||
let data;
|
let data = await fetch(`/lang/${language}.json`)
|
||||||
for (let i = language_list.length - 1; i >= 0; i--) {
|
|
||||||
data = await fetch(`/lang/${language_list[i]}.json`)
|
|
||||||
.then(response => response.ok ? response.json() : null);
|
.then(response => response.ok ? response.json() : null);
|
||||||
if (data !== null) {
|
if (data !== null) {
|
||||||
i18n.translator.add(data);
|
i18n.add(language, data);
|
||||||
loaded_languages.unshift(language_list[i]);
|
console.log('Loaded language:', language);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Language stack:', loaded_languages);
|
|
||||||
let elements = document.querySelectorAll('*[data-i18n]');
|
let elements = document.querySelectorAll('*[data-i18n]');
|
||||||
let i18n_data, translated_string;
|
let i18n_data, translated_string;
|
||||||
elements.forEach(element => {
|
elements.forEach(element => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user