mirror of
https://github.com/NaitLee/Cat-Printer.git
synced 2025-05-16 07:10:30 -07:00
Frontend Big Update
This commit is contained in:
parent
205f5ad5ca
commit
33d0157b15
3
TODO
3
TODO
@ -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'
|
||||
|
@ -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 \
|
||||
|
@ -6,6 +6,10 @@
|
||||
.pylintrc
|
||||
?-*.sh
|
||||
|
||||
# symlinks
|
||||
*.pf2
|
||||
*.pbm
|
||||
|
||||
# junk
|
||||
__pycache__
|
||||
.directory
|
||||
|
@ -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
|
||||
|
32
printer.py
32
printer.py
@ -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:
|
||||
|
11
server.py
11
server.py
@ -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)
|
||||
|
@ -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
40
www/about.html
Normal 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 <<a target="_blank" href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>>.</p>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
13
www/i18n.js
13
www/i18n.js
@ -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;
|
||||
}
|
||||
}
|
||||
|
BIN
www/icon.png
BIN
www/icon.png
Binary file not shown.
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
98
www/image.js
98
www/image.js
@ -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.
|
||||
|
189
www/index.html
189
www/index.html
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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
5
www/lang/list.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"en-US": "English (US)",
|
||||
"zh-CN": "中文(简体)",
|
||||
"de-DE": "Deutsch"
|
||||
}
|
@ -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": "退出中……"
|
||||
}
|
386
www/main.css
386
www/main.css
@ -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 {
|
||||
|
327
www/main.js
327
www/main.js
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user