Frontend Big Update

This commit is contained in:
NaitLee 2022-04-22 01:14:40 +08:00
parent 205f5ad5ca
commit 33d0157b15
20 changed files with 876 additions and 423 deletions

3
TODO
View File

@ -7,12 +7,9 @@ Note: not ordered. do whatever I/you want
+ 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
+ ...
? Consider more control to something like 'energy'

View File

@ -1,6 +1,6 @@
#!/bin/sh
p4a apk --private .. --dist_name="cat-printer" --package="io.github.naitlee.catprinter" --name="Cat Printer" \
--icon=icon.png --version="0.2.0" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
--icon=icon.png --version="0.3.0" --bootstrap=webview --window --requirements=android,pyjnius,bleak \
--blacklist-requirements=sqlite3,openssl --port=8095 --arch=arm64-v8a --blacklist="blacklist.txt" \
--presplash=blank.png --presplash-color=black --add-source="advancedwebview" --orientation=user \
--permission=BLUETOOTH --permission=BLUETOOTH_SCAN --permission=BLUETOOTH_CONNECT \

View File

@ -6,6 +6,10 @@
.pylintrc
?-*.sh
# symlinks
*.pf2
*.pbm
# junk
__pycache__
.directory

View File

@ -1,5 +1,6 @@
#!/bin/sh
for i in $(find | grep -E '.*\.pyc'); do rm $i; done
for i in $(find | grep -E '__pycache__'); do rm -d $i; done
python3 bundle.py $1
python3 bundle.py -w $1
python3 bundle.py -b $1

View File

@ -304,17 +304,19 @@ class PrinterDriver(Commander):
def connect(self, name=None, address=None):
''' Connect to this device, and operate on it
'''
self._pending_data = io.BytesIO()
if self.fake:
return
if (self.device is not None and address is not None and
(self.device.address.lower() == address.lower())):
return
if self.device is not None and self.device.is_connected:
self.loop(
self.device.stop_notify(self.rx_characteristic),
self.device.disconnect()
)
else:
try:
if self.device is not None and self.device.is_connected:
self.loop(self.device.stop_notify(self.rx_characteristic))
self.loop(self.device.disconnect())
except: # pylint: disable=bare-except
pass
finally:
self.device = None
if name is None and address is None:
return
@ -356,6 +358,7 @@ class PrinterDriver(Commander):
devices = self.loop(
scanner.discover(self.scan_timeout)
)
devices = [dev for dev in devices if dev.name in Models]
if identifier is not None:
if identifier in Models:
devices = [dev for dev in devices if dev.name == identifier]
@ -371,11 +374,11 @@ class PrinterDriver(Commander):
Currently, available modes are `pbm` and `text`.
If no devices were connected, scan & connect to one first.
'''
self._pending_data = io.BytesIO()
if self.device is None:
self.scan(identifier, use_result=True)
if self.device is None and not self.fake:
error('no-available-devices-found', exception=PrinterError)
self._pending_data = io.BytesIO()
if mode == 'pbm' or mode == 'default':
printer_data = PrinterData(self.model.paper_width, file)
self._print_bitmap(printer_data)
@ -509,6 +512,8 @@ class PrinterDriver(Commander):
# CLI procedure
Printer = None
def _main():
'Main routine for direct command line execution'
parser = argparse.ArgumentParser(
@ -542,6 +547,8 @@ def _main():
help=I18n['virtual-run-on-specified-model'])
parser.add_argument('-m', '--dump', required=False, action='store_true',
help=I18n['dump-the-traffic'])
parser.add_argument('-n', '--nothing', required=False, action='store_true',
help=I18n['do-nothing'])
args = parser.parse_args()
info(I18n['cat-printer'])
printer = PrinterDriver()
@ -556,17 +563,22 @@ def _main():
if args.fake:
printer.fake = args.fake
printer.model = Models[args.fake]
else:
info(I18n['connecting'])
printer.scan(args.identifier, use_result=True)
printer.dump = args.dump
if args.file == '-':
file = sys.stdin.buffer
else:
file = open(args.file, 'rb')
if args.nothing:
global Printer
Printer = printer
return
try:
info(I18n['connecting'])
printer.print(
file,
mode = 'text' if args.text else 'pbm',
identifier = args.identifier
mode = 'text' if args.text else 'pbm'
)
info(I18n['finished'])
except KeyboardInterrupt:

View File

@ -39,6 +39,7 @@ mime_type = {
'txt': 'text/plain;charset=utf-8',
'json': 'application/json;charset=utf-8',
'png': 'image/png',
'svg': 'image/svg+xml;charset=utf-8',
'octet-stream': 'application/octet-stream'
}
def mime(url: str):
@ -55,6 +56,7 @@ class PrinterServerHandler(BaseHTTPRequestHandler):
settings = DictAsObject({
'config_path': 'config.json',
'version': 1,
'first_run': True,
'is_android': False,
'scan_timeout': 5.0,
'dry_run': False
@ -73,6 +75,14 @@ class PrinterServerHandler(BaseHTTPRequestHandler):
def log_error(self, *_args):
pass
def handle_one_request(self):
try:
# this handler would have only one instance
# broken pipe could make it die. ignore
super().handle_one_request()
except BrokenPipeError:
pass
def do_GET(self):
'Called when server get a GET http request'
path = 'www' + self.path
@ -154,6 +164,7 @@ class PrinterServerHandler(BaseHTTPRequestHandler):
self.printer.dump = self.settings.dump
self.printer.flip_h = self.settings.flip_h
self.printer.flip_v = self.settings.flip_v
self.printer.rtl = self.settings.force_rtl
if self.settings.printer is not None:
name, address = self.settings.printer.split(',')
self.printer.connect(name, address)

View File

@ -4,15 +4,35 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Python Webview Loading</title>
<title>Loading</title>
<style>
:root {
--fore-color: #111;
--back-color: #eee;
--notice-error: rgba(255, 0, 0, 0.2);
--font-size: 1.2rem;
--line-height: 1.8em;
--anim-time: 0.5s;
}
body {
background-color: var(--back-color);
color: var(--fore-color);
font-size: var(--font-size);
line-height: var(--line-height);
}
a:link, a:visited {
color: #33f;
}
a:hover, a:active {
color: #22f;
}
@media (prefers-reduced-motion) {
body {
transition-duration: 0ms !important;
transition: none;
animation-timing-function: steps(1);
animation-duration: 0ms !important;
}
}
@media (prefers-color-scheme: dark) {
:root {
@ -20,9 +40,78 @@
--back-color: #333;
}
}
@keyframes jump {
0% { transform: translateY(0); }
50% { transform: translateY(var(--font-size)); }
100% { transform: translateY(0); }
}
#loading-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--back-color);
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
z-index: 1;
opacity: 1;
transition: opacity var(--anim-time) var(--curve);
}
.logo {
background-image: url('icon.svg');
background-position: center;
background-repeat: no-repeat;
background-size: contain;
width: 80%;
max-width: 16em;
height: 80%;
max-height: 16em;
margin: 0 auto;
}
#loading-screen.hidden {
opacity: 0;
}
#loading-screen>.dots {
display: flex;
flex-direction: row;
justify-content: center;
}
#loading-screen>.dots>span {
display: inline-block;
width: var(--font-size);
height: var(--font-size);
margin: var(--font-size);
background-color: var(--fore-color);
border-radius: calc(var(--font-size) / 2);
animation: jump 1s ease 0s infinite;
}
#loading-screen>.dots>span:nth-child(1) { animation-delay: 0s; }
#loading-screen>.dots>span:nth-child(2) { animation-delay: 0.3s; }
#loading-screen>.dots>span:nth-child(3) { animation-delay: 0.6s; }
a {
transition: all var(--anim-time) ease-out;
}
</style>
</head>
<body>
<div id="loading-screen">
<div class="logo"></div>
<div class="dots">
<span></span>
<span></span>
<span></span>
</div>
<noscript>
<p class="noscript">
<span>Please enable JavaScript!</span><br />
<a href="jslicense.html" data-i18n="javascript-license-information"
data-jslicense="1" >JavaScript License Information</a>
</p>
</noscript>
</div>
</body>
</html>

40
www/about.html Normal file
View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About</title>
<link rel="stylesheet" href="main.css" />
<link rel="icon" href="icon.svg" />
</head>
<body>
<main>
<div>
<h1 data-i18n="cat-printer">Cat Printer</h1>
<p>
<span data-i18n="home-page-">Home Page:</span>
<a target="_blank" href="https://github.com/NaitLee/Cat-Printer">GitHub</a>
</p>
<h2 data-i18n="contributors">Contributors</h2>
<dl>
<dt>
<a target="_blank" href="https://github.com/NaitLee">NaitLee</a>
</dt>
<dd data-i18n="developer">Developer</dd>
<dt>
<a target="_blank" href="https://github.com/frankenstein91">frankenstein91</a>
</dt>
<dd data-i18n="developer">Developer</dd>
<dd data-i18n="translator">Translator</dd>
<dt data-i18n="all-testers-and-users">All testers & users</dt>
<dd data-i18n="everyone-is-awesome">Everyone is awesome!</dd>
</dl>
<h2 data-i18n="license">License</h2>
<p>This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.</p>
<p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p>
<p>You should have received a copy of the GNU General Public License along with this program. If not, see &lt;<a target="_blank" href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>&gt;.</p>
</div>
</main>
</body>
</html>

View File

@ -21,24 +21,27 @@ class I18n {
* @param {Languages} language
*/
useLanguage(language) {
this.language = language;
if (this.language)
this.database[language] = this.database[this.language];
if (!this.database[language])
this.database[language] = {};
this.language = language;
}
/**
* Add data as corresponding language, also to
* other (added) languages as fallback
* Add data as corresponding language,
* also to other (added) languages as fallback,
* or override
* @param {Languages} language
* @param {LanguageData} data
*/
add(language, data) {
add(language, data, override = false) {
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])
if (override || !this.database[lang][key])
this.database[lang][key] = value;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -69,8 +69,8 @@ function monoSteinberg(data, w, h, t) {
for (let i = 0; i < w; i++) {
p = j * w + i;
m = data[p];
n = m > t ? 255 : 0;
o = m - n;
n = m > 128 ? 255 : 0;
o = m - n + t;
data[p] = n;
adjust(i + 1, j , o * 7 / 16);
adjust(i - 1, j + 1, o * 3 / 16);
@ -85,100 +85,6 @@ function monoSteinberg(data, w, h, t) {
*/
function monoHalftone(data, w, h, t) {}
/**
* My own toy algorithm used in old versions. Not so natual.
* It have 2 pass, horizonally and vertically.
* @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place**
* @param {number} w width of image
* @param {number} h height of image
* @param {number} t threshold
*/
function monoLegacy(data, w, h, t) {
let data_h = data.slice();
let data_v = data.slice();
monoLegacyH(data_h, w, h, t);
monoLegacyV(data_v, w, h, t);
for (let i = 0; i < data.length; i++) {
data[i] = data_h[i] & data_v[i];
}
}
function monoLegacyH(data, w, h, t) {
let v = 0, p;
for (let j = 0; j < h; j++) {
for (let i = 0; i < w; i++) {
p = j * w + i;
v += data[p];
if (v >= t) {
data[p] = 255;
v = 0;
} else data[p] = 0;
}
v = 0;
}
}
function monoLegacyV(data, w, h, t) {
let v = 0, p;
for (let i = 0; i < w; i++) {
for (let j = 0; j < h; j++) {
p = j * w + i;
v += data[p];
if (v >= t) {
data[p] = 255;
v = 0;
} else data[p] = 0;
}
v = 0;
}
}
/**
* Slightly modified from `monoLegacy`, but still messy.
* But, try the horizonal and vertical sub algorithm!
* @param {Uint8ClampedArray} data the grayscale data, mentioned in `monoGrayscale`. **will be modified in-place**
* @param {number} w width of image
* @param {number} h height of image
* @param {number} t threshold
*/
function monoNew(data, w, h, t) {
let data_h = data.slice();
let data_v = data.slice();
monoNewH(data_h, w, h, t);
monoNewV(data_v, w, h, t);
for (let i = 0; i < data.length; i++) {
data[i] = data_h[i] & data_v[i];
}
}
function monoNewH(data, w, h, t) {
t = (t - 127) / 4 + 1;
let v = 0, p;
for (let j = 0; j < w; j++) {
for (let i = 0; i < h; i++) {
p = j * h + i;
v += data[p] + t;
if (v >= 255) {
data[p] = 255;
v -= 255;
} else data[p] = 0;
}
v = 0;
}
}
function monoNewV(data, w, h, t) {
t = (t - 127) / 4 + 1;
let v = -1, p;
for (let i = 0; i < h; i++) {
for (let j = 0; j < w; j++) {
p = j * h + i;
v += data[p] + t;
if (v >= 255) {
data[p] = 255;
v -= 255;
} else data[p] = 0;
}
v = 0;
}
}
/**
* Convert a monochrome image data to PBM mono image file data.
* Returns a Blob containing the file data.

View File

@ -5,84 +5,63 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Printer</title>
<link rel="stylesheet" href="main.css" />
<link rel="icon" href="icon.png" />
<link rel="icon" href="icon.svg" />
</head>
<body>
<main>
<div class="left">
<main class="hard-hidden">
<div class="menu-side">
<h1 id="title" data-i18n="cat-printer">Cat Printer</h1>
<div id="notice">
<noscript>
<span class="noscript">Please enable JavaScript!</span>
</noscript>
</div>
<div class="panel" id="panel-print">
<a href="#" data-i18n="print" data-default>Print</a>
<label for="device-options" data-i18n="device-">Device:</label>
<select id="device-options">
<!-- Initialized by script -->
</select>
<button id="device-refresh" data-i18n="refresh">Refresh</button>
<br />
<label data-i18n="mode-">Mode:</label>
<label>
<input type="radio" name="mode" value="mode-canvas" checked />
<span data-i18n="canvas">Canvas</span>
</label>
<!-- <label>
<input type="radio" name="mode" value="mode-document" />
<span data-i18n="document">Document</span>
</label><br /> -->
<button id="insert-picture" data-i18n="insert-picture">Insert Picture</button><br />
</div>
<div class="panel expanded" id="panel-help">
<a href="#" data-i18n="help">Help</a>
<div>
<p data-i18n="coming-soon-">Coming Soon...</p>
<p>
<!-- LibreJS doesn't work with dynamically inserted script tag. Going to complain -->
<a href="jslicense.html" data-jslicense="1"
data-i18n="javascript-license-information">JavaScript License Information</a>
</p>
</div>
</div>
<div class="panel" id="panel-settings">
<a href="#" data-i18n="settings">Settings</a>
<div class="sub panel">
<a href="#" data-i18n="image">Image</a>
<label data-i18n="monochrome-algorithm-">Monochrome Algorithm:</label><br />
<div class="menu">
<div class="panel" id="panel-print" data-default>
<label for="device-options" data-i18n="device-">Device:</label>
<select id="device-options">
</select>
<button id="device-refresh" data-i18n="refresh">Refresh</button>
<hr />
<label data-i18n="mode-">Mode:</label>
<label>
<input type="radio" name="mode" value="mode-canvas" checked />
<span data-i18n="canvas">Canvas</span>
</label>
<!-- <label>
<input type="radio" name="mode" value="mode-document" />
<span data-i18n="document">Document</span>
</label><br /> -->
<button id="insert-picture" data-i18n="insert-picture">Insert Picture</button>
<br />
<label data-i18n="process-as-">Process as:</label>
<label>
<input type="radio" name="algo" value="algo-direct" />
<span data-i18n="direct">Direct</span>
<span data-i18n="text">Text</span>
</label>
<label>
<input type="radio" name="algo" value="algo-steinberg" checked />
<span data-i18n="floyd-steinberg">Floyd Steinberg</span>
</label><br />
<!-- <label>
<input type="radio" name="algo" value="algo-halftone" />
<span data-i18n="halftone">Halftone</span>
</label> -->
<label>
<input type="radio" name="algo" value="algo-new-h" />
<span data-i18n="wave">Wave</span>
<span data-i18n="picture">Picture</span>
</label>
<!-- <label>
<input type="radio" name="algo" value="algo-new-v" />
<span data-i18n="fall">Fall</span>
</label> -->
<label>
<input type="radio" name="algo" value="algo-legacy" />
<span data-i18n="legacy">Legacy</span>
</label><br />
<input type="radio" name="algo" value="algo-halftone" />
<span data-i18n="pattern">Pattern</span>
</label> --><br />
<label for="threshold" data-i18n="threshold-">Threshold:</label>
<input type="range" min="0" max="255" value="128" id="threshold" step="8" data-default />
<input type="range" min="0" max="256" value="128" step="8" id="threshold" data-default />
<br />
<input type="checkbox" name="transparent-as-white" id="transparent-as-white" checked />
<label for="transparent-as-white" data-i18n="transparent-as-white">Transparent as White</label>
</div>
<div class="sub panel">
<a href="#" data-i18n="printer">Printer</a>
<div class="panel active" id="panel-help">
<div>
<p data-i18n="coming-soon">Coming Soon...</p>
<p>
<a id="link-about" href="about.html" target="frame" data-i18n="about">About</a>
<!-- LibreJS doesn't work with dynamically inserted script tag. Going to complain -->
<a href="jslicense.html" data-jslicense="1"
data-i18n="javascript-license-information">JavaScript License Information</a>
</p>
</div>
</div>
<div class="panel" id="panel-settings">
<label for="scan-time" data-i18n="scan-time-">Scan Time:</label>
<input type="number" name="scan-time" id="scan-time" min="1" max="10" value="3" />
<span data-i18n="-seconds">seconds</span>
@ -91,43 +70,97 @@
<label for="flip-h" data-i18n="flip-horizontally">Flip Horizontally</label>
<input type="checkbox" name="flip-v" id="flip-v" />
<label for="flip-v" data-i18n="flip-vertically">Flip Vertically</label>
<br />
<hr />
<input type="checkbox" name="dry-run" id="dry-run" />
<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>
<br />
<button id="set-accessibility">
<span>🌎</span>
<span data-i18n="accessibility">Accessibility</span>
</button>
<button class="hidden" data-panel="panel-error" data-i18n="error-message">Error Message</button>
<div class="center">
<button id="button-exit" data-i18n="exit">Exit</button>
</div>
</div>
<div class="sub panel">
<a href="#" data-i18n="system">System</a>
<input type="checkbox" name="no-animation" id="no-animation" />
<label for="no-animation" data-i18n="disable-page-animation">Disable Page Animation</label>
</div>
<div class="center">
<button id="button-exit" data-i18n="exit">Exit</button>
<div class="panel" id="panel-error">
<p data-i18n="you-can-seek-for-help-with-detailed-info-below">You can seek for help with detailed info below.</p>
<div id="error-record"></div>
</div>
</div>
<div class="panel hidden" id="panel-error">
<a href="#" data-i18n="error-message">Error Message</a>
<p data-i18n="you-can-seek-for-help-with-detailed-info-below">You can seek for help with detailed info below.</p>
<div id="error-record"></div>
<div class="compact-menu">
<div class="compact-button" data-panel="panel-print" data-i18n="print">Print</div>
<div class="compact-button active" data-panel="panel-help" data-i18n="help">Help</div>
<div class="compact-button" data-panel="panel-settings" data-i18n="settings">Settings</div>
</div>
<div class="center">
<!-- <button id="button-preview" data-i18n="preview">Preview</button> -->
<button id="button-print" data-i18n="print">Print</button>
<!-- -->
</div>
</div>
<div class="right">
<div class="canvas-side">
<canvas id="control-canvas" class="disabled" width="384" height="384"></canvas>
<div id="control-document" class="disabled" contenteditable="true"></div>
<canvas id="preview" width="384" height="384"></canvas>
<div class="center">
<div class="center buttons">
<!-- <button id="canvas-expand" data-i18n="expand">Expand</button>
<button id="canvas-crop" data-i18n="crop">Crop</button> -->
<!-- <button id="button-preview" data-i18n="preview">Preview</button> -->
<button id="button-print" data-i18n="print">Print</button>
</div>
<div class="blank"></div>
</div>
</main>
<div id="hidden" class="hidden">
<div id="hidden" class="hard-hidden">
<!-- Hidden area for putting elements -->
<input type="file" id="file" />
<div id="accessibility">
<div>
<h2 data-i18n="language">Language</h2>
<div id="select-language">
</div>
</div>
<div>
<h2 data-i18n="layout">Layout</h2>
<input type="checkbox" name="no-animation" id="no-animation" />
<label for="no-animation" data-i18n="disable-animation">Disable Animation</label>
<br />
<input type="checkbox" name="force-rtl" id="force-rtl" />
<label for="force-rtl" data-i18n="right-to-left-text-order">Right-to-left text order</label>
<br />
<input type="checkbox" name="large-font" id="large-font" />
<label for="large-font" data-i18n="large-font">Large Font</label>
</div>
</div>
<iframe id="frame" src="about.html" name="frame" title="frame"></iframe>
</div>
<div id="dialog" class="hidden">
<div class="shade"></div>
<div class="content">
<div id="dialog-content">
<!-- Dialog content -->
</div>
<div id="dialog-choices">
<input id="dialog-input" type="text" id="dialog-input" placeholder="">
<hr />
</div>
</div>
</div>
<div id="loading-screen">
<div class="logo"></div>
<div class="dots">
<span></span>
<span></span>
<span></span>
</div>
<noscript>
<p class="noscript">
<span>Please enable JavaScript!</span><br />
<a href="jslicense.html" data-i18n="javascript-license-information"
data-jslicense="1" >JavaScript License Information</a>
</p>
</noscript>
</div>
<script src="loader.js"></script>
</body>

View File

@ -5,13 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript License Information</title>
<link rel="stylesheet" href="main.css" />
<link rel="icon" href="icon.png" />
<link rel="icon" href="icon.svg" />
</head>
<body>
<main>
<div>
<h1>JavaScript License Information</h1>
<p>You can see all JavaScript used along with this application are Free Software.<sup><a href="#footer-1">[1]</sup></a></p>
<p>You can see all JavaScript programs used along with this application are Free Software.<sup><a href="#footer-1">[1]</sup></a></p>
<p class="center">
<a href="/">Go Back</a>
</p>
@ -70,12 +70,6 @@
<td><a href="main.js">main.js</a></td>
<td>A bundle of transpiled scripts (polyfill.js, image.js, i18n.js, main.js), for compatibility to old browsers.</td>
</tr>
<tr>
<td><a href="vconsole.js">vconsole.js</a></td>
<td><a href="http://opensource.org/licenses/MIT">Expat</a></td>
<td><a href="https://github.com/Tencent/vConsole">vconsole.js</a></td>
<td>A mini console for debugging on mobile, will only load when explicitly invoke.</td>
</tr>
</tbody>
</table>
@ -90,7 +84,7 @@
<a href="https://www.gnu.org/philosophy/free-sw.html">Free Software</a>
</dt>
<dd>
<span>Software that respects your freedom.</span>
<span>Software that respects your computing freedom.</span>
</dd>
</dl>
</footer>

View File

@ -1,4 +1,5 @@
{
"$language": "Deutsch",
"cat-printer": "Cat Printer",
"printer": "Drucker",
"device-": "Gerät:",
@ -11,13 +12,6 @@
"javascript-license-information": "Informationen zur JavaScript-Lizenz",
"settings": "Einstellungen",
"image": "Bild",
"monochrome-algorithm-": "Schwarzweiß-Algorithmus:",
"direct": "Direkt",
"floyd-steinberg": "Floyd Steinberg",
"halftone": "Halbtone",
"wave": "Wave",
"fall": "Fall",
"legacy": "Legacy",
"threshold-": "Schwellwert",
"transmission-speed-": "Übertragungsgeschwindigkeit:",
"low": "Gering",
@ -26,7 +20,7 @@
"transparent-as-white": "Transparent als Weiß",
"misc": "Sonstiges",
"system": "System",
"disable-page-animation": "Seitenanimation ausschalten",
"disable-animation": "Seitenanimation ausschalten",
"exit": "Exit",
"error-message": "Fehlermeldung",
"preview": "Vorschau",
@ -44,7 +38,7 @@
"please-check-if-the-printer-is-down": "Bitte prüfe, ob der Drucker ausgeschaltet ist",
"printing": "Drucken…",
"finished": "Fertiggestellt",
"coming-soon-": "Demnächst verfügbar…",
"coming-soon": "Demnächst verfügbar…",
"dry-run": "Testlauf",
"dry-run-test-print-process-only": "Testlauf: nur Probedruckvorgang",
"you-can-close-this-page-manually": "Sie können diese Seite manuell schließen",

View File

@ -1,4 +1,5 @@
{
"$language": "English (US)",
"cat-printer": "Cat Printer",
"printer": "Printer",
"device-": "Device:",
@ -11,13 +12,6 @@
"javascript-license-information": "JavaScript License Information",
"settings": "Settings",
"image": "Image",
"monochrome-algorithm-": "Monochrome Algorithm:",
"direct": "Direct",
"floyd-steinberg": "Floyd Steinberg",
"halftone": "Halftone",
"wave": "Wave",
"fall": "Fall",
"legacy": "Legacy",
"threshold-": "Threshold",
"transmission-speed-": "Transmission Speed:",
"low": "Low",
@ -26,7 +20,7 @@
"transparent-as-white": "Transparent as White",
"misc": "Misc",
"system": "System",
"disable-page-animation": "Disable Page Animation",
"disable-animation": "Disable Animation",
"exit": "Exit",
"error-message": "Error Message",
"preview": "Preview",
@ -44,7 +38,7 @@
"please-check-if-the-printer-is-down": "Please check if the printer is down",
"printing": "Printing…",
"finished": "Finished",
"coming-soon-": "Coming Soon…",
"coming-soon": "Coming Soon…",
"dry-run": "Dry Run",
"dry-run-test-print-process-only": "Dry Run: test print process only",
"you-can-close-this-page-manually": "You can close this page manually",
@ -83,5 +77,26 @@
"flip-vertically": "Flip Vertically",
"dump-traffic": "Dump Traffic",
"right-to-left-text-order": "Right-to-left text order",
"auto-wrap-line": "Auto wrap line"
"auto-wrap-line": "Auto wrap line",
"process-as-": "Process as:",
"text": "Text",
"picture": "Picture",
"pattern": "Pattern",
"large-font": "Large Font",
"accessibility": "Accessibility",
"language": "Language",
"layout": "Layout",
"ok": "OK",
"cancel": "Cancel",
"yes": "Yes",
"no": "No",
"about": "About",
"home-page-": "Home Page:",
"contributors": "Contributors",
"developer": "Developer",
"translator": "Translator",
"all-testers-and-users": "All testers & users",
"everyone-is-awesome": "Everyone is awesome!",
"license": "License",
"exiting": "Exiting…"
}

5
www/lang/list.json Normal file
View File

@ -0,0 +1,5 @@
{
"en-US": "English (US)",
"zh-CN": "中文(简体)",
"de-DE": "Deutsch"
}

View File

@ -1,4 +1,5 @@
{
"$language": "中文(简体)",
"cat-printer": "猫咪打印机",
"printer": "打印机",
"device-": "设备:",
@ -10,14 +11,6 @@
"help": "帮助",
"javascript-license-information": "JavaScript 许可证信息",
"settings": "设置",
"monochrome-algorithm-": "单色化算法:",
"direct": "直接",
"image": "图像",
"floyd-steinberg": "科学",
"halftone": "点状",
"wave": "波纹",
"fall": "下落",
"legacy": "旧版",
"threshold-": "阈值:",
"transmission-speed-": "传输速度:",
"low": "低",
@ -26,7 +19,7 @@
"transparent-as-white": "透明为白色",
"misc": "杂项",
"system": "系统",
"disable-page-animation": "禁用页面动画",
"disable-animation": "禁用动画",
"exit": "退出",
"error-message": "错误消息",
"preview": "预览",
@ -41,7 +34,7 @@
"please-check-if-the-printer-is-down": "请检查打印机是否已关闭",
"printing": "打印中……",
"finished": "完成",
"coming-soon-": "即将到来……",
"coming-soon": "即将到来……",
"dry-run": "干运行",
"dry-run-test-print-process-only": "干运行:仅测试打印流程",
"you-can-close-this-page-manually": "您可手动关闭此页面",
@ -79,5 +72,26 @@
"flip-vertically": "垂直翻转",
"dump-traffic": "转储数据",
"right-to-left-text-order": "从右到左的文字顺序",
"auto-wrap-line": "自动折行"
"auto-wrap-line": "自动折行",
"process-as-": "处理方式:",
"text": "文本",
"picture": "照片",
"pattern": "图案",
"large-font": "大字体",
"accessibility": "无障碍",
"language": "语言",
"layout": "布局",
"ok": "确定",
"cancel": "取消",
"yes": "是",
"no": "否",
"about": "关于",
"home-page-": "主页:",
"contributors": "贡献者",
"developer": "开发者",
"translator": "翻译",
"all-testers-and-users": "所有测试及正式用户",
"everyone-is-awesome": "每个人都是好样的!",
"license": "许可证",
"exiting": "退出中……"
}

View File

@ -1,6 +1,7 @@
:root {
--font-size: 1.2em;
--font-size: 1.2rem;
--line-height: 1.8em;
--span: 8px;
--span-half: calc(var(--span) / 2);
--span-double: calc(var(--span) * 2);
@ -13,9 +14,10 @@
--canvas-back: #fff;
--curve: cubic-bezier(.08,.82,.17,1);
--panel-height: 20em;
--target-color: rgba(0, 255, 0, 0.2);
--notice-color: rgba(0, 0, 255, 0.2);
--notice-warning: rgba(255, 128, 0, 0.2);
--target-color: rgba(0, 255, 255, 0.2);
--notice-wait: rgba(0, 128, 255, 0.2);
--notice-note: rgba(0, 255, 0, 0.2);
--notice-warn: rgba(255, 128, 0, 0.2);
--notice-error: rgba(255, 0, 0, 0.2);
}
@ -27,29 +29,41 @@ body.no-animation *::after {
animation-timing-function: steps(1);
animation-duration: 0ms !important;
}
body.large-font,
#large-font+label {
font-size: calc(var(--font-size) * 1.2);
line-height: calc(var(--line-height) * 1.2);
}
body.force-rtl,
#force-rtl+label {
direction: rtl;
}
body {
border: none;
background-color: var(--back-color);
color: var(--fore-color);
font-size: var(--font-size);
line-height: var(--line-height);
font-family: 'Noto Sans', 'Segoe UI', sans-serif;
overflow: auto;
margin: 1em 0;
user-select: none;
}
h1 {
font-size: 1.5em;
h1, h2 {
font-weight: normal;
margin: var(--span-half) 0;
}
h1 { font-size: 1.5em; }
h2 { font-size: 1.2em; }
a:link, a:visited {
color: #33f;
}
a:hover, a:active {
color: #22f;
}
a {
transition: all var(--anim-time) ease-out;
a+a {
margin-left: var(--font-size);
}
.center {
text-align: center;
@ -57,16 +71,23 @@ a {
button, input, select, textarea {
font: inherit;
color: var(--fore-color);
/* background-color: var(--back-color); */
background-color: transparent;
}
select[multiple] {
width: 8em;
padding: var(--border);
margin: var(--span-half) var(--span);
}
button, input[type="number"], input[type="text"], select {
margin: var(--span-half) var(--span);
border: var(--border) solid var(--fore-color);
padding: var(--span-half) var(--span);
background-color: transparent;
transition: all var(--anim-time) var(--curve);
cursor: pointer;
min-width: 6em;
display: inline-block;
line-height: calc(var(--font-size) + var(--span));
}
input[type="number"], input[type="text"] {
width: 6em;
@ -80,37 +101,50 @@ button:hover {
button:active {
box-shadow: 0 0 var(--span) inset var(--fore-color);
}
@keyframes notice-fade {
to { background-color: transparent; }
}
@keyframes notice-wait {
50% { background-color: transparent; }
}
#notice {
min-height: var(--font-size);
background-color: var(--notice-color);
/* border: var(--border) solid var(--fore-color); */
}
#notice.warning, #button-exit {
background-color: var(--notice-warning);
#notice span {
display: block;
}
#notice.error {
#notice .note {
background-color: var(--notice-note);
animation: notice-fade 1s ease-out 1s 1 forwards;
}
#notice .wait {
background-color: var(--notice-wait);
animation: notice-wait 2s ease-in-out 0s infinite forwards;
}
#notice .warn {
background-color: var(--notice-warn);
animation: notice-fade 1s ease-out 1s 1 forwards;
}
#notice .error {
background-color: var(--notice-error);
animation: notice-fade 1s ease-out 1s 1 forwards;
}
#button-exit {
background-color: var(--notice-warn);
}
.noscript {
margin: var(--span-double);
text-align: center;
background-color: var(--notice-error);
display: block;
}
main, header, footer {
max-width: 45em;
margin: 1em auto;
display: flex;
flex-direction: row;
/* flex-wrap: wrap; */
}
main>.left {
flex-grow: 1;
/* position: sticky; */
/* top: 0; */
height: 100%;
overflow: auto;
margin: var(--span);
min-width: 12em;
}
main>.right {
main>.canvas-side {
flex-grow: 0;
margin: 0 auto;
height: 100%;
@ -118,6 +152,40 @@ main>.right {
text-align: center;
min-width: calc(var(--paper-width) + var(--border-double) + var(--span-double));
}
main>.menu-side {
flex-grow: 1;
position: sticky;
top: 0;
height: 100%;
overflow: auto;
margin: var(--span);
min-width: 12em;
}
main>.menu-side>.menu {
border: var(--border) solid var(--fore-color);
border-bottom: none;
margin-top: var(--span);
}
.compact-menu {
display: flex;
flex-direction: row;
justify-content: space-around;
background-color: var(--back-color);
}
.compact-button {
width: max-content;
height: 2em;
flex-grow: 1;
line-height: 2em;
text-align: center;
cursor: pointer;
border-top: var(--border) solid var(--fore-color);
border-bottom: var(--border) solid transparent;
}
.compact-button.active {
border: var(--border) solid var(--fore-color);
border-top: var(--border) solid transparent;
}
canvas#preview, canvas#control-canvas, #control-document {
border: var(--border) solid var(--fore-color);
background-color: var(--canvas-back);
@ -157,54 +225,17 @@ p {
margin: var(--span) 0;
}
.panel {
border: var(--border) solid currentColor;
height: calc(var(--font-size) + 8px);
overflow: hidden;
/* scrollbar-width: thin; */
transition: height var(--anim-time) var(--curve);
padding: 0 var(--span);
margin: var(--span) 0;
height: 0;
}
.panel::before {
float: left;
}
.panel:not(.sub)::before { content: '📌'; }
.panel.sub::before { content: '📎'; }
.panel.sub {
border-width: var(--border) 0 0 0;
}
.panel.expanded {
.panel.active {
height: var(--panel-height);
animation: delay-scrollable var(--anim-time) steps(1) 0s 1 forwards;
padding: var(--span-double) var(--span);
/* overflow-y: scroll; */
}
.panel.sub.expanded {
.panel.sub.active {
height: calc(var(--panel-height) / 2);
}
.panel>:nth-child(1),
.panel>:nth-child(1):link,
.panel>:nth-child(1):visited {
display: block;
margin: var(--span-half);
text-decoration: none;
text-align: center;
color: var(--fore-color);
}
.panel>:nth-child(1)::before {
content: '>>';
display: inline-block;
position: relative;
opacity: 0;
width: 0;
overflow: visible;
right: 2.5em;
transition: all var(--anim-time) var(--curve);
}
.panel>:nth-child(1):hover::before,
.panel>:nth-child(1):active::before {
opacity: 1;
right: 1.5em;
}
input[type="range"] {
width: 10em;
vertical-align: middle;
@ -218,7 +249,16 @@ input[type="range"] {
.hint {
animation: hint 3s ease-out 0.1s infinite;
}
#hidden, .hidden { display: none; }
.hidden {
/* visibility: hidden; */
height: 0;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
#hidden, .hard-hidden {
display: none;
}
#error-record {
font-family: 'DejaVu Sans Mono', 'Consolas', monospace;
width: 100%;
@ -244,11 +284,148 @@ dl {
margin: var(--span) 0;
display: block;
}
dd { display: inline; }
dd+dd { margin-left: var(--font-size); }
hr {
border: none;
border-top: var(--border) solid var(--fore-color);
}
#frame {
width: 100%;
height: inherit;
border: none;
background-color: transparent;
}
.blank {
height: 0em;
}
.shade {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: var(--back-color);
opacity: 0.95;
z-index: -1;
}
#dialog {
position: fixed;
width: 100%;
top: calc(50% - 10em);
text-align: center;
z-index: 2;
opacity: 2;
transition: opacity var(--anim-time) var(--curve);
}
#dialog>.content {
max-width: 100%;
box-sizing: border-box;
width: 40em;
margin: auto;
border: var(--border) solid var(--fore-color);
transition: transform var(--anim-time) var(--curve);
}
#dialog.hidden {
opacity: 0;
height: unset;
}
#dialog.hidden>.content {
transform: scaleY(0);
}
#dialog-content {
height: 10em;
margin: auto;
padding: var(--span);
}
#dialog-choices {
margin: auto;
padding: var(--span);
}
#choice-input {
max-width: 100%;
width: 16em;
}
#accessibility {
text-align: initial;
display: flex;
flex-direction: row;
}
#accessibility>*:nth-child(1) {
flex-grow: 1;
}
#select-language {
width: calc(100% - var(--span-double));
height: 8em;
border: var(--border) solid var(--fore-color);
padding: var(--span);
margin: var(--span);
box-sizing: border-box;
}
#select-language option {
cursor: pointer;
}
#select-language option:hover {
text-decoration: underline;
}
#accessibility>*:nth-child(2) {
flex-grow: 1;
}
@keyframes jump {
0% { transform: translateY(0); }
50% { transform: translateY(var(--font-size)); }
100% { transform: translateY(0); }
}
#loading-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--back-color);
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
z-index: 1;
opacity: 1;
transition: opacity var(--anim-time) var(--curve);
}
.logo {
background-image: url('icon.svg');
background-position: center;
background-repeat: no-repeat;
background-size: contain;
width: 80%;
max-width: 16em;
height: 80%;
max-height: 16em;
margin: 0 auto;
}
#loading-screen.hidden {
opacity: 0;
}
#loading-screen>.dots {
display: flex;
flex-direction: row;
justify-content: center;
}
#loading-screen>.dots>span {
display: inline-block;
width: var(--font-size);
height: var(--font-size);
margin: var(--font-size);
background-color: var(--fore-color);
border-radius: calc(var(--font-size) / 2);
animation: jump 1s ease 0s infinite;
}
#loading-screen>.dots>span:nth-child(1) { animation-delay: 0s; }
#loading-screen>.dots>span:nth-child(2) { animation-delay: 0.3s; }
#loading-screen>.dots>span:nth-child(3) { animation-delay: 0.6s; }
a {
transition: all var(--anim-time) ease-out;
}
@keyframes delay-scrollable {
from { overflow: hidden; }
to { overflow: auto; }
@ -256,23 +433,69 @@ hr {
@media (max-width: 800px) {
:root {
--panel-height: 16em;
/* --font-size: 1em; */
--font-size: 1em;
}
main {
flex-direction: column;
}
main>.left {
/* height: 16em; */
overflow: auto;
width: calc(100% - var(--span-double) - var(--border-double));
}
main>.right {
#title { display: none; }
main>.canvas-side {
min-width: unset;
width: 100%;
overflow-x: hidden;
overflow-y: auto;
position: fixed;
top: var(--line-height);
height: calc(100% - var(--panel-height) - 2em);
z-index: 0;
}
main>.canvas-side>.buttons,
main>.menu-side>.buttons {
position: sticky;
bottom: 2em;
width: 100%;
z-index: 1;
}
main>.canvas-side>.buttons button,
main>.menu-side>.buttons button {
background-color: var(--back-color);
}
main>.menu-side {
overflow-x: hidden;
overflow-y: auto;
position: fixed;
background-color: var(--back-color);
top: unset;
bottom: 0;
left: 0;
height: var(--panel-height);
margin: 0;
width: 100%;
box-sizing: border-box;
}
main>.menu-side>.menu {
height: var(--panel-height);
margin: 0;
}
#notice {
position: fixed;
top: 0;
width: 100%;
}
main>.menu-side>.compact-menu {
position: fixed;
bottom: 0;
width: 100%;
z-index: 0;
}
.blank {
height: 2em;
}
}
@media (max-width: 384px) {
canvas#preview, canvas#control-canvas, #control-document {
width: calc(100% - var(--border-double));
width: 100%;
box-sizing: border-box;
}
}
@media (prefers-color-scheme: dark) {
@ -288,7 +511,18 @@ hr {
color: #77f;
}
canvas#preview, canvas#control-canvas, #control-document {
filter: brightness(0.5);
filter: brightness(0.6);
}
.logo {
filter: brightness(0.6);
}
}
@media (prefers-reduced-motion) {
body {
transition-duration: 0ms !important;
transition: none;
animation-timing-function: steps(1);
animation-duration: 0ms !important;
}
}
@font-face {

View File

@ -7,7 +7,7 @@
* Double-tap the "Cat Printer" title to activate
*/
function debug() {
let script = document.createElement('script');
const script = document.createElement('script');
script.src = 'vconsole.js';
document.body.appendChild(script);
script.addEventListener('load', () => new window.VConsole());
@ -17,15 +17,16 @@ document.getElementById('title').addEventListener('dblclick', debug);
var hidden_area = document.getElementById('hidden');
const hint = (function() {
let hints = [];
let callback = (event) => {
let hints;
const callback = (event) => {
event.stopPropagation();
event.currentTarget.classList.remove('hint');
event.currentTarget.removeEventListener('click', callback);
}
return function(selector) {
hints.forEach(element => element.classList.remove('hint'));
hints = document.querySelectorAll(selector);
if (hints)
hints.forEach(element => element.classList.remove('hint'));
hints = typeof selector === 'string' ? document.querySelectorAll(selector) : selector;
hints.forEach(element => {
element.classList.add('hint');
element.addEventListener('click', callback);
@ -33,28 +34,95 @@ const hint = (function() {
}
})();
class _Notice {
element;
constructor() {
this.element = document.getElementById('notice');
const Notice = (function() {
const notice = document.getElementById('notice');
let last_span;
function put(message, things, class_name) {
let text = i18n(message, things) || message;
if (last_span) last_span.remove();
let span = document.createElement('span');
span.innerText = text;
span.classList.add(class_name);
notice.appendChild(span);
last_span = span;
}
_message(message, things) {
this.element.innerText = i18n(message, things) || message;
return {
note: (message, things) => put(message, things, 'note'),
wait: (message, things) => put(message, things, 'wait'),
warn: (message, things) => put(message, things, 'warn'),
error: (message, things) => put(message, things, 'error')
}
makeLogger(class_name) {
return (message, things) => {
this.element.classList.value = class_name;
this._message(message, things);
})();
const Dialog = (function() {
const dialog = document.getElementById('dialog');
const dialog_content = document.getElementById('dialog-content');
const dialog_choices = document.getElementById('dialog-choices');
const dialog_input = document.getElementById('dialog-input');
let last_choices;
function clean_up() {
if (last_choices)
for (let choice of last_choices)
choice.remove();
// elements
for (let element of dialog_content.children)
hidden_area.appendChild(element);
// text nodes
for (let node of dialog_content.childNodes)
node.remove();
}
function show(argument, as_string = false) {
dialog.classList.remove('hidden');
if (as_string)
dialog_content.innerText = argument;
else
dialog_content.appendChild(document.querySelector(argument));
}
function apply_callback(callback, have_input = false, ... choices) {
last_choices = [];
dialog_input.value = '';
dialog_input.style.display = have_input ? 'unset' : 'none';
for (let choice of choices) {
let button = document.createElement('button');
button.setAttribute('data-i18n', choice);
button.innerText = i18n(choice);
if (!have_input)
button.addEventListener('click', () => dialog_input.value = choice);
dialog_choices.appendChild(button);
last_choices.push(button);
}
last_choices[0].addEventListener('click', () => {
if (callback) callback(dialog_input.value);
dialog.classList.add('hidden');
});
if (last_choices.length > 1)
last_choices[1].addEventListener('click', () => {
if (callback) callback(null);
dialog.classList.add('hidden');
});
hint([last_choices[0]]);
}
return {
alert: function(selector, callback, as_string = false) {
clean_up();
apply_callback(callback, false, 'ok');
show(selector, as_string);
},
confirm: function(selector, callback, as_string = false) {
clean_up();
apply_callback(callback, false, 'yes', 'no');
show(selector, as_string);
},
prompt: function(selector, callback, as_string = false) {
clean_up();
apply_callback(callback, true, 'ok', 'cancel');
show(selector, as_string);
}
}
notice = this.makeLogger('notice');
warn = this.makeLogger('warning');
error = this.makeLogger('error');
}
const Notice = new _Notice();
})();
class _ErrorHandler {
// TODO make better
recordElement;
constructor() {
this.recordElement = document.getElementById('error-record');
@ -65,8 +133,11 @@ class _ErrorHandler {
*/
report(error, output) {
Notice.error('error-happened-please-check-error-message');
let hidden_panel = this.recordElement.parentElement;
if (hidden_panel) hidden_panel.classList.remove('hidden');
let button = document.querySelector('button[data-panel="panel-error"]');
if (button) {
button.classList.remove('hidden');
button.click();
}
let div = document.createElement('div');
div.innerText = (error.stack || (error.name + ': ' + error.message)) + '\n' + output;
this.recordElement.appendChild(div);
@ -150,57 +221,21 @@ function putEvent(selector, type, callback, thisArg) {
return new EventPutter(selector, type, callback, thisArg);
}
class PanelController {
last;
panels;
outerPanels;
subPanels;
constructor(selector = '.panel') {
const class_expanded = 'expanded';
const class_sub = 'sub';
let panels = this.panels = [... document.querySelectorAll(selector)];
let outer_panels = this.outerPanels = panels.filter(e => !e.classList.contains(class_sub));
let sub_panels = this.subPanels = panels.filter(e => e.classList.contains(class_sub));
const expand = (panel) => panel.classList.add(class_expanded);
const fold = (panel) => {
panel.classList.remove(class_expanded);
}
const fold_all_outer = () => outer_panels.forEach(e => fold(e));
const fold_all_sub = () => sub_panels.forEach(e => fold(e));
// const fold_all = () => panels.forEach(e => e.classList.remove(class_expanded));
fold_all_outer();
putEvent(selector + '>:nth-child(1)', 'click', event => {
(function() {
let panels = document.querySelectorAll('.panel');
let buttons = document.querySelectorAll('*[data-panel]');
panels.forEach(panel => {
let button = document.querySelector(`*[data-panel="${panel.id}"]`);
if (button) button.addEventListener('click', event => {
event.stopPropagation();
event.cancelBubble = true;
let current = event.currentTarget.parentElement,
last = this.last;
this.last = current;
if (!last) {
expand(current);
this.last = current;
return;
}
let is_sub = current.classList.contains(class_sub),
last_is_sub = last.classList.contains(class_sub);
if (current.classList.contains(class_expanded)) {
fold(current);
return;
}
fold_all_outer();
if (is_sub && last_is_sub) {
fold(last);
expand(current.parentElement);
last.scrollTo(0, 0);
} else if (is_sub && !last_is_sub) {
fold_all_sub();
expand(last);
} else if (!is_sub && last_is_sub) {
last.parentElement.scrollTo(0, 0);
}
expand(current);
}, this);
}
}
panels.forEach(p => p.classList.remove('active'));
buttons.forEach(b => b.classList.remove('active'));
panel.classList.add('active');
button.classList.add('active');
});
if (panel.hasAttribute('data-default')) button.click();
});
})();
class CanvasController {
/** @type {HTMLCanvasElement} */
@ -211,6 +246,7 @@ class CanvasController {
isCanvas;
algorithm;
threshold;
thresholdRange;
transparentAsWhite;
previewData;
static defaultHeight = 384;
@ -226,6 +262,7 @@ class CanvasController {
this.canvas = document.getElementById('control-canvas');
this.div = document.getElementById('control-document');
this.height = CanvasController.defaultHeight;
this.thresholdRange = document.getElementById('threshold');
putEvent('input[name="mode"]', 'change', (event) => this.enableMode(event.currentTarget.value), this);
putEvent('input[name="algo"]', 'change', (event) => this.useAlgorithm(event.currentTarget.value), this);
@ -259,6 +296,7 @@ class CanvasController {
}
useAlgorithm(name) {
this.algorithm = name;
this.thresholdRange.value = 128;
this.activatePreview();
}
expand(length = CanvasController.defaultHeight) {
@ -267,7 +305,7 @@ class CanvasController {
crop() {}
activatePreview() {
let preview = this.preview;
let t = this.threshold;
let t = Math.min(this.threshold, 255);
if (this.isCanvas) {
let canvas = this.canvas;
let w = canvas.width, h = canvas.height;
@ -281,7 +319,7 @@ class CanvasController {
monoDirect(mono_data, w, h, t);
break;
case 'algo-steinberg':
monoSteinberg(mono_data, w, h, t);
monoSteinberg(mono_data, w, h, Math.floor(t / 2 - 64));
break;
case 'algo-halftone':
// monoHalftone(mono_data, w, h, t);
@ -291,7 +329,7 @@ class CanvasController {
monoNew(mono_data, w, h, t);
break;
case 'algo-new-h':
monoNewH(mono_data, w, h, t);
monoNewH(mono_data, w, h, Math.floor(t / 2 - 64));
break;
case 'algo-new-v':
monoNewV(mono_data, w, h, t);
@ -325,7 +363,7 @@ class CanvasController {
context.drawImage(img, 0, 0, canvas.width, canvas.height);
this.crop();
this.activatePreview();
hint('#button-print, #panel-settings');
hint('#button-print');
});
}
}
@ -344,10 +382,68 @@ class CanvasController {
}
}
/** @param {Document} doc */
function applyI18nToDom(doc) {
doc = doc || document;
let elements = doc.querySelectorAll('*[data-i18n]');
let i18n_data, translated_string;
elements.forEach(element => {
i18n_data = element.getAttribute('data-i18n');
translated_string = i18n(i18n_data);
if (translated_string === i18n_data) return;
// element.innerText = translated_string;
if (element.firstChild.textContent !== translated_string)
element.firstChild.textContent = translated_string;
});
}
async function initI18n() {
if (typeof i18n === 'undefined') return;
/** @type {HTMLOptionElement} */
let language_options = document.getElementById('select-language');
/** @type {{ [code: string]: string }} */
let list = await fetch('/lang/list.json').then(r => r.json());
let use_language = async (value) => {
i18n.useLanguage(value);
i18n.add(value, await fetch(`/lang/${value}.json`).then(r => r.json()), true);
applyI18nToDom();
}
for (let code in list) {
let option = document.createElement('option');
option.value = code;
option.innerText = list[code];
option.addEventListener('click', (event) => {
let option = event.currentTarget;
let value = option.value;
use_language(value);
});
language_options.appendChild(option);
}
apply_default:
for (let code of navigator.languages) {
if (list[code]) {
for (let option of language_options.children) {
if (option.value === code) {
// option.setAttribute('data-default', '');
option.setAttribute('data-default', '');
option.click();
i18n.useLanguage(navigator.languages[0]);
for (let language of navigator.languages) {
if (!list[language]) return;
let data = await fetch(`/lang/${language}.json`)
.then(response => response.ok ? response.json() : null);
if (data !== null) {
i18n.add(language, data);
}
}
break apply_default;
}
}
}
}
}
class Main {
promise;
/** @type {PanelController} */
panelController;
/** @type {CanvasController} */
canvasController;
deviceOptions;
@ -367,23 +463,38 @@ class Main {
this.setters = {};
// window.addEventListener('unload', () => this.exit());
this.promise = new Promise(async (resolve, reject) => {
await this.initI18n();
this.panelController = new PanelController();
await initI18n();
/** @type {HTMLIFrameElement} */
let iframe = document.getElementById('frame');
iframe.addEventListener('load', () => {
applyI18nToDom(iframe.contentDocument);
});
this.canvasController = new CanvasController();
putEvent('#button-exit', 'click', this.exit, this);
putEvent('#button-print', 'click', this.print, this);
putEvent('#device-refresh', 'click', this.searchDevices, this);
putEvent('#set-accessibility', 'click', () => Dialog.alert('#accessibility'));
putEvent('#link-about', 'click', () => Dialog.alert('#frame'));
this.attachSetter('#scan-time', 'change', 'scan_timeout');
this.attachSetter('#device-options', 'input', 'printer');
this.attachSetter('input[name="algo"]', 'change', 'mono_algorithm');
this.attachSetter('#transparent-as-white', 'change', 'transparent_as_white');
this.attachSetter('#select-language option', 'click', 'language');
this.attachSetter('#dry-run', 'change', 'dry_run',
(checked) => checked && Notice.notice('dry-run-test-print-process-only')
(checked) => checked && Notice.note('dry-run-test-print-process-only')
);
this.attachSetter('#no-animation', 'change', 'no_animation',
(checked) => checked ? document.body.classList.add('no-animation')
: document.body.classList.remove('no-animation')
);
this.attachSetter('#large-font', 'change', 'large_font',
(checked) => checked ? document.body.classList.add('large-font')
: document.body.classList.remove('large-font')
);
this.attachSetter('#force-rtl', 'change', 'force_rtl',
(checked) => checked ? document.body.classList.add('force-rtl')
: document.body.classList.remove('force-rtl')
);
this.attachSetter('#threshold', 'change', 'threshold',
(value) => this.canvasController.threshold = value
);
@ -392,6 +503,8 @@ class Main {
this.attachSetter('#dump', 'change', 'dump');
await this.loadConfig();
this.searchDevices();
document.querySelector('main').classList.remove('hard-hidden');
document.getElementById('loading-screen').classList.add('hidden');
resolve();
});
}
@ -409,6 +522,8 @@ class Main {
*/
async loadConfig() {
this.settings = await callApi('/query');
if (this.settings['first_run'])
Dialog.alert('#accessibility', () => this.set({ first_run: false }));
for (let key in this.settings) {
let value = this.settings[key];
if (this.setters[key] === undefined) continue;
@ -423,8 +538,14 @@ class Main {
if (element.value !== value) return;
element.checked = value;
break;
default:
case 'text':
case 'number':
case 'range':
element.value = value;
break;
default:
if (element.value === value)
element.click();
}
element.dispatchEvent(new Event('change'));
});
@ -461,11 +582,12 @@ class Main {
}).bind(this), this);
}
async exit() {
Notice.wait('exiting');
await this.set(this.settings);
await callApi('/exit');
window.close();
// Browser may block the exit
Notice.notice('you-can-close-this-page-manually');
Notice.note('you-can-close-this-page-manually');
}
/** @param {Response} response */
async bluetoothProblemHandler(response) {
@ -473,24 +595,25 @@ class Main {
let error_details = await response.json();
if (
error_details.name === 'org.bluez.Error.NotReady' ||
error_details.details.indexOf('not turned on') !== -1 ||
error_details.details.indexOf('WinError -2147020577') !== -1
error_details.name === 'org.freedesktop.DBus.Error.UnknownObject' ||
error_details.details.includes('not turned on') ||
error_details.details.includes('WinError -2147020577')
) Notice.warn('please-enable-bluetooth');
else throw new Error('Unknown Bluetooth Problem');
return null;
}
async searchDevices() {
Notice.notice('scanning-for-devices');
Notice.wait('scanning-for-devices');
let search_result = await callApi('/devices', null, this.bluetoothProblemHandler);
if (search_result === null) return;
let devices = search_result.devices;
[... this.deviceOptions.children].forEach(e => e.remove());
if (devices.length === 0) {
Notice.notice('no-available-devices-found');
Notice.note('no-available-devices-found');
hint('#device-refresh');
return;
}
Notice.notice('found-0-available-devices', [devices.length]);
Notice.note('found-0-available-devices', [devices.length]);
hint('#insert-picture');
devices.forEach(device => {
let option = document.createElement('option');
@ -501,12 +624,12 @@ class Main {
this.deviceOptions.dispatchEvent(new Event('input'));
}
async print() {
Notice.notice('printing');
Notice.wait('printing');
await fetch('/print', {
method: 'POST',
body: this.canvasController.makePbm()
}).then(async (response) => {
if (response.ok) Notice.notice('finished')
if (response.ok) Notice.note('finished')
else {
let error_data = await response.json();
if (/address.+not found/.test(error_data.details))
@ -519,28 +642,6 @@ class Main {
}
});
}
async initI18n() {
if (typeof i18n === 'undefined') return;
i18n.useLanguage(navigator.languages[0]);
for (let language of navigator.languages) {
let data = await fetch(`/lang/${language}.json`)
.then(response => response.ok ? response.json() : null);
if (data !== null) {
i18n.add(language, data);
console.log('Loaded language:', language);
}
}
let elements = document.querySelectorAll('*[data-i18n]');
let i18n_data, translated_string;
elements.forEach(element => {
i18n_data = element.getAttribute('data-i18n');
translated_string = i18n(i18n_data);
if (translated_string === i18n_data) return;
// element.innerText = translated_string;
if (element.firstChild.textContent !== translated_string)
element.firstChild.textContent = translated_string;
});
}
}
var main = new Main();