This commit is contained in:
admin 2025-10-27 22:24:25 -07:00
parent 6ac35f19ad
commit 1505e3861b
34 changed files with 2575 additions and 1371 deletions

5
data/sys_settings.json Normal file
View File

@ -0,0 +1,5 @@
{
"print-on-connect": false,
"enable-led-pulse": true,
"led-pulse-duration": 1000
}

View File

@ -1,26 +1,100 @@
beautifulsoup4 asgiref==3.9.1
blinker beautifulsoup4==4.13.3
bs4 bidict==0.23.1
certifi blinker==1.9.0
cffi Brlapi==0.8.3
charset-normalizer bs4==0.0.2
click certifi==2025.1.31
cryptography cffi==1.17.1
Flask channels==4.3.1
Flask-Cors chardet==4.0.0
idna charset-normalizer==3.4.1
itsdangerous click==8.1.8
Jinja2 colorama==0.4.4
MarkupSafe configobj==5.0.6
psutil cryptography==3.4.8
pyarmor cupshelpers==1.0
pycparser dbus-python==1.2.18
pycups defer==1.0.6
pyudev distro==1.7.0
requests distro-info==1.1+ubuntu0.2
soupsieve Django==5.2.6
spidev dnspython==2.8.0
swig evdev==1.9.2
urllib3 eventlet==0.40.3
Werkzeug Flask==3.1.0
gunicorn Flask-Cors==5.0.0
flask-sock==0.7.0
Flask-SocketIO==5.5.1
greenlet==3.2.4
gunicorn==23.0.0
h11==0.16.0
httplib2==0.20.2
idna==3.10
importlib-metadata==4.6.4
iotop==0.6
itsdangerous==2.2.0
jeepney==0.7.1
Jinja2==3.1.5
keyring==23.5.0
launchpadlib==1.10.16
lazr.restfulclient==0.14.4
lazr.uri==1.0.6
louis==3.20.0
MarkupSafe==3.0.2
more-itertools==8.10.0
MouseInfo==0.1.3
netifaces==0.11.0
oauthlib==3.2.0
packaging==24.2
pillow==11.3.0
psutil==5.9.0
pyarmor==9.0.7
pyarmor.cli.core==7.6.4
PyAutoGUI==0.9.54
pycairo==1.20.1
pycparser==2.22
pycups==2.0.1
PyGetWindow==0.0.9
PyGObject==3.42.1
PyJWT==2.3.0
PyMsgBox==2.0.1
pynput==1.8.1
pyparsing==2.4.7
pyperclip==1.10.0
PyRect==0.2.0
PyScreeze==1.0.1
python-apt==2.4.0+ubuntu4
python-dateutil==2.8.1
python-debian==0.1.43+ubuntu1.1
python-engineio==4.12.2
python-socketio==5.13.0
python-xapp==2.2.1
python-xlib==0.33
python3-xlib==0.15
pytweening==1.2.0
pyudev==0.24.3
pyxdg==0.27
PyYAML==5.4.1
requests==2.32.3
SecretStorage==3.3.1
setproctitle==1.2.2
simple-websocket==1.1.0
six==1.16.0
soupsieve==2.6
spidev==3.6
sqlparse==0.5.3
swig==4.3.0
terminator==2.1.1
typing_extensions==4.12.2
ubuntu-drivers-common==0.0.0
ubuntu-pro-client==8001
unattended-upgrades==0.1
urllib3==1.26.5
uvicorn==0.35.0
wadllib==1.3.6
Werkzeug==3.1.3
wsproto==1.2.0
xdg==5
xkit==0.0.0
zipp==1.0.0

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
# Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-03-14T02:02:35.798448 # Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-10-27T22:23:03.382081
from pyarmor_runtime_000000 import __pyarmor__ from pyarmor_runtime_000000 import __pyarmor__
__pyarmor__(__name__, __file__, b'PY000000\x00\x03\n\x00o\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00x\x02\x00\x00\x12\t\x04\x00S\xe3\\\x0f5d8 \xcf\x05\x074%\xa1\x04\x8e\x00\x00\x00\x00\x00\x00\x00\x00E\x91\xe1\x86\x0ea\xa1\x16\x0e\xb4\x1b\x1b\xea\xc4\xe8N\xcc\xb5\x08\x1e\x92\x90\x01S\xac\xab\xf0\xee\xbc\xcdr\xc3\xad\xa6\xee\x19"\\B\x1f\xd8\x83\xaaU`\xb2\xa5\'\xf3_=\xeb@\x10D&\xbc1P|\x81\xf5^\x01Z\xd6\xbc\xcc6Y\x1d\xe4q\x16\xda\xee\n\xa9\x88\xf5`M"4\x97\x9f\xef\xd9\xb6%\xf8\x1d\x8f\xd5\xee\xa9\x922\xe79\xf4\xea,\xff\tm\xc1S\x91\xc6M\xe1,I\xef\xb9\xeb\x9d\x93\xc5}\x03,i.\xf4$\x9c\x8b\xeaF\xa9\xc2\x94\xc8\xf3\xf5\x9d\xa0C\x97\xe3[p\xa3\xdem\x0f\x9bwW\xca\xca.\x043\xf064g\x1f\xd5l\x9e\xd5U$.\xda$QY\xa2\xd3\xfa\xb9P\x1f\x84\xfa\x9c\x86/\xc8X\xab+Zn\x14\xe0\xf3\xa5H\xabF\x9f\x89\xec\xdb3\xf74\xf6e1\x06\xc5$\xb7\x8cH\x7f\xd50\x0e`\x8f\x1d&\x0e&\xef\x92\xca\xc3\x85\xef\xea/"\x1b)\xf7\xfd\x9c\x1d9l\xbe\xee\xae\xf0\x96}\xc0`\x06]\xdb\x9a=\\\xe8\xdbU\x8ac5S\x84J\x8f\xdd\xda\x82\xfd\x02\xe0\xb3D\xd4R=\x90|r\xa8\xdd/\xd0\xd3.6\x7f\x1fD\xf7!\x1e\xeb\x92\xc0)\x95qN\xd8\x0b\x126\xf7B\x88\x12\x98j\x1f\xb8\x08\xdf_\xbb\x1c\x0f7K!\xb2\xc3\xe4\xfe\x80\xacc\x18\xb7\x0c\x94\xbe\xbc\xec\x93<\xf60\x97\x87\x811\xe6\xa1\xad 0\x08\xe1Q#\'S\xb6\xa0!e\xa7\xe8\xb1\xe8\x14\x198\x14I\xf2\x1a$\x8f\xe6\xb6&\x90B\xae\xddm\xd06@\x1f\x98\xf29\x10\x7f\xab=\xf2y\xdex\x08\x1b\x93\xe9\x00\xff\xb2\x9f\x07P\xbe$\xa3\xe8\x10\xd3\xf4R\x98\xac\xe22\xf1A\xa3B\xb7\x81l\x18\xb6:mHf\xc53Zo&Q0\x1e4\xc5\xaa\xca\x07\xc5\x8f\x15\xad\xb3\x8f}\xa1<\xb1Z\xc8\x16\xf53r\xd8\xdf~\xa6\xdbn\xc3nN{Pp\xbc\x87\n\xde\xaf \xeb=\xd9\x87\x9dGhX\xa67.\x80\xe7\xd2\xb9P\xc3\xbfP;\xd9\x9e\x82\xa8,\xca\x18/n\x9es\x15\xd7\xc0\xdc\x05i\xbb)\xbc\xad\xeb\xe4>\x88H)\x10\xb5<\xd9\x8a\x82\xdc\xfdq\xa7\xb2\x91\rL\x1d\xd2\x1f\xc4\xd6\xaf\x1d\x1d\xed\xe9\xb1\xf0\xde,\xdbaZH\x1an\xddO@[0\nC\xfc\xfe\x15N8\xcd&o!\x1a\xb5f\x13{4\x85ED\xdd0\x1bm\xbbXb\xbc\x8dL\x81\xfez2\x19$\x1f\xe61\x95\xd8\x1c\xfd\x8e8i\xd4\xa7\xae\x01\xdb{b\x17X\xc3)\xd0\x9f\xafX\x0c<\xd0v\xd2\x08\xef\x0f\x91Y\n\xa2') __pyarmor__(__name__, __file__, b'PY000000\x00\x03\n\x00o\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00x\x02\x00\x00\x12\t\x04\x00\xd9\xc2\x8c\x97\x8b\xfc8\xac@\x8e&\x0f-\xe4\xdd?\x00\x00\x00\x00\x00\x00\x00\x00\x08\x85\xca\xd8\x99u\xc0Jp\xfd\x1e\xffCe~B\xb3\xd0\x05-\xad\xd9\xa7\xb2%\x8e}\xebB?A?\xb4\x11\xf0\x81v\xf5-\x18\xe3}\n\x02\x1a|\xbf\x1du\x9c\xff\xaa\xcc\xcf\xf5\x08f\x0ea\x02\xc1=)\xb3\xbe0\x08Ig\xf8!P\xef\x97Hl\xc5d\xd13^[\xee\x15*J\xed6\xad6R&\xba\xd0\xb3\x95+I\x9a\xe6$\\=\xf0\xc5\xf5vx\x93d\x9b\xb1\xda\xa3B\x8a\xd6da$\xed>\xc3\xcaP\xc5\xa0\xf1\x1b"\xfc\x99m:\x18\xee\xac\xfd\xe7%=\xe6\x00\xbb\xfcA\x10\xe0\x15\xa2h]\xf4g\xf6\x08K\xdf\xb3\x9b\xc9\x13\x90\x12\xa0\x93h\xd3\xb0\x19aP}J4hX7L\xf3L\xf0\x14\x0f\xa5&\x04\'\x8a\xed\xba|\xe7]T8\x18l\xbfw\xb1\xe5\xed&jw\xc4O\x07\x86\x1dw\xaa\xd6G\xb1P\xa2D\xd4\xd7\xbao\xc8TVbOl\xbf!\\\xbc\x05.)>\xcf\xbcq\xd3vG-\xbc\xbd\xfd]\xa5{\xe7n\x04H\t\xc3\xd4\xf0!\xad\xc6\x15}\xeb\x80\xf2P\x06\xbb9\xdd\x89\xf4\x7f\xd2\xeeK\xd5\x84\x0f]\x92\x9c\xd8\xb6\x03Y\x96\xf8\xf3t2y\x00\x9e%w\xfa\x1d<\x9ao*\xb3$\xf9c<`,\xd8\xb4\xb1\xd68u]\xc5\xdf\xfe.E\xb3\xe8\xc4\x8f(\xb9\xa6\xafD\xd1\xe9\xcc\xcf\xffx\x80\x9f~\xc6M\xcfz\xe4\x1f\xc9\x15\xe4\x802Ck(\xcbAFuU\x0bv\xac\x01\xe5\x9a4A\xe4\xd6\xd3(\x11\x01\x98\xffb\x16C\xc8*\xe2\n\xe4q\xb5?\x954X6\xadm\x9a(\x92\xa5Ux\xbe\xd0\xc4\x8d\xfa\x02\xd6gy"\xb2pK\xd1\xb7,zL\xc3\x14Y\x03\xee\xc1\xc6\x82\xc8O\x07\xc8]\x08\x86.Bz\x1d1B\x8d\x86\xaf\xfa\x13\x87g}\x0e\x05:{\xb8\xbb\'\x07\x84B\xc8\xc99i\x174 [\xd3\xc68\xab\x03X\x9c\x18&\x18\xad\x0f\xd29\xe5\x18\x13\x0cz\x17_X?j\xe6\x96\xcb/\xa9\x18\xc4Q}\xbe\xb1\xef#\x7f(\x8f\xef\xe49\xba\\\xc5\xd4\xec4\xabrO\xf3\x01\x85\xf5\x8a:\x11%d\xad2_\x194\xa6%\xb1\x88_\x99\x11-\xfa.\xc5\x19\xf6\xf8H\xaa\xd5\xac\xf1\xa5\xd5\xaf\x0bO8\xf5{\xf4t\x98u,\xd8\x1b\xeb\x8e\'\x06\xf4\xd64W\xeaT\x04Z\xd4B\xfel\x7f\xc1\x8b}Rt\x00\xc6(\x91\xa1I\xc0p\xd0\x1c?.\xeeq\x9b\xf4\xa9\xbc8\xf9{\xd0q\xd9\xbcQ\xcf6\x87\xd5\x1f&\xf7\x187\x94\x8d\xb4\xd9;\x06\xbex\xd1E\x0e\r')

View File

@ -1,3 +1,3 @@
# Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-03-14T02:02:35.823082 # Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-10-27T22:23:03.404795
from pyarmor_runtime_000000 import __pyarmor__ from pyarmor_runtime_000000 import __pyarmor__
__pyarmor__(__name__, __file__, b'PY000000\x00\x03\n\x00o\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00-\x04\x00\x00\x12\t\x04\x00\xf8\xe9\xce]\xe7y;\xf5\x82\x91+~x=\xc2\x8d\x00\x00\x00\x00\x00\x00\x00\x00\xa5\xdb\x0b\x90\xdeN\x00J\xe3\x18 \xae\xd5]\xabH9\x052\x8f\xfc\xcf\x9a\xc1\xe0?)_)l\xe4\xb4H\x8f~\xb6*L\xb7y&\x9f\\E\xd4\x04:\x04\xe3\xb9"\x8f\xe1\x86\x88\xb1\xf3\xa3\xb4\x98\x97\xf2\xb6\x1a|\x16w(^\x96\x13\xa6\xf0\x91\xfd\r\x11<\x10\xe7\xbd\xf6\x10\xfe\xa5\xb98C\x84[\x18\xb0j\xce\xd6\xa7[\xf1\xcb;R\x9ec\xaa\x05\xa34\xa6lo\xbe\xa9\x03U\x9fU\xc6\x16\x03\x9c^\x18|(\xd7\xa0}T\xce^\xb87\x88nH~\xe7\xd0\xe3\x10\x14+\x8b\x04\xb49!\x0b\x08\xbbx\xd5\x98\x7f\xee\xfb\x04\x8f\x0f;\x96\xbe\xf8\xa7\xeb\xf1\xed_\xa0\xfdl]\x9f\x1a6"\x185<\xca\xf7\xaa\xa6\xe1wC\xbe\xcfzE\x8fM.9\xcc\xc3\xc1\xf9*\x14\x86I\x9fz\xda\xf9\xd2\x149\xe9\x8a\xd0i\\\x81\xa0_X\x07\x8a5\xb4\xdf\x7fc\xd2\x1ag\x11\x1fj\xc8\x1d\x02\x88X\x05|\x81\xd0\xc8\x81x\xab@r\x13\xdbY\xcem\xf4v\x0e\x9a\'\xbb\x1b\xd0\x95\xb9\xc9J\x80\x94\xbe{\xab\x7f\x12\xeca)-\xc05\x02u\xb3\xb2\x9e\xf0\xaf \xf8a\xcc\xee\x1ayj\xdb|\td\x05*2\xd9d8\xb0\xdb\xf2\xa9\x98\x0cH@\xd5\x0f\x9b\xbb.\x95xdr\x85\xb4\xcb\xc6\xba\xf8\xc8\xbf\\\x16\xd7\xdfZd\xa5\xb8\xc4\xf5\xff\x12\xcc@\x9cp\xe6\xe2G"\x11\xac\xdd>\xdd\xfe\xbc9\xf6@\xaa\xd4\xe7\x0f\xf6\xa5\xfe\x85\x98\xe3\xcc\xd2\xde\x9a\x98\xd0\xe9\x9eR\x95\xe3\x1f\x0e\xc1\xff\xaf\x00-Z_\x08CA\xe8\x97\x96q\x93\x0b\x8d\xdaO\x93\x9c\x90\xfa>k\xe1\x90?\xc4E\x98\xe3L\xaf\x02_\x06\xe9\xc0\x18\xc90X\xfe\x17i\xbc\x86;\xae\x91\xc0\x1dV\xb9*VG\xa76}\xc7\xba`x\xe9/g\xc4\x02#\xa5\xe8\xc9|3\x1c?@\xf2\xd6R\xa3w"\'$EY\t~)\xc0\xec\xb1\x18\xb9l\x1e\x18P/\xa7\xaf\xdf\xf4\xd3\xd0\x85\xe7]n\xae\x1e\x88Hi\xc7O<R\x06\xfc\x04Y\x01\xfeS\x94\x0f\xa7pFRe\xb1D5\xaa\xee\xec\x00\x90\x8e\xc5\xf1\x99\x8fo=\x88k\x96\xb0\xdfD\x94|\x92\xe6\xba1#\x8a\xb9L\xec8e:\x8e\x9e\xb8\x01AQ\x04\xeaz\xa0\x92\x16\xf2\xab2D\xfb\xd8\x86\xe1=\xfb\xbf\x7f\xa0\x97Z\x81\xb6\xa4\x7f*\x8c\xa2k\xccV\xdb.\x05\x8f]\xce\xf3"\xefL\xa4V\xfd\xc7\x81\xf3>\x8b\x00\x81\xca@\xdc[\\7\x89\x00\xa5b\xfb\xd0\t\xa4}\x96\xfe.=A\xc2\xfe\xa7\xcf+}\xea\xae}J\xe1\xf8\xc5\xaf\x94"\xa0\xd7\x1f\x96\x17\xach\xf6\x94#\x02\x9et\xd0S\xf9@\x8cA\xc9o\x1c\xbc\xa5$dD\xceY#\x8e\xe7\xdf\xdb\xb2\t\xf1V\xa0\x90\xcc\xc0\x90\xbc\x1b\xe3\xdd\x96=\x80/(\xca\x18L\x087\xfe\x98B\x94v\xe5j\x17\xb13\xcb,\xd5\x8aT\xf3!\x98\xeb\x11\xef\xcd\xf8\x1e\x15\xcf\xee3\x8d\x86\xa1\xeaE3\x9c\xc0@mp\xc4\xd3\x16;\x9eS\x15\x83\x8e\xb1\xc1\xba\xbaZ:\xa3D\xd8<\x1c\xb7\xe9\x1d\x0e06\x8c\xa4\x98\x9bISa\x7f\xe3\xdd\x90R6WZ\x11Z9\x87\x1fS}\xdf*\xf2\x0f\xb2\xd8\x11\x16\x18\xfa\xcb\xd6\x14\xfby"\xa5&\xb53\xeb\x03\xc1\xbc\xfa\x11!H\x9d\xa7\x1a\x03wx\xed\xa5\x9e\x90\xa1\x99\xba\x05\x1e\xf8\xd4IO\rs$`\x1d\x06\xa6\x98\xa3\x14\xeb\xed@\xb1f\xf3\xc7\xae[r\r\\5\xf3\xee\t\xb5I\xd3\x16\xc4\x02\x84&5\xae\xc1\x03]+\xcf#c\xaf\xd5l6\xf5?\x1e\nPB\xd9\xd6\x89\x7f \x94\xfaS\xd6\xac\xb5\xfeB\x85\xcf\xeb\x86w\x1b\x1bY\xa0\x83\xa9\xf5\xd1\x1b&\xdc\x05\xe32!>\xb0\xc3\x9f\xf8\xc8\xe1%:W\x03i\xda\xb4\xc4\xb0\xbd\xf6\x07! \x01\x14\x1b\xcc\x9b\x8d\xa6\xbc\x84\tY>\x08\xd0\x00\xcbR<\x0e\x13\xb5\xfdSO5\x83\x1c\xd5T/\xa3\xc5q\x9di\xe5i\xbf\x10\xceE\xd4&\xf6h\xe9\xcd\x83\xf3%\xcb\xbe\xc0\xcda\xd8P\xb2m\xb7\xde\xf8\x97m\x08H\xc0t\xfd\x9e\x85d\xad\x90\x1a%K\xd2\x82\xf1\x8a\x86\x80\xf0\xa1\x89\x87\x9b\x15%\xd2t\x17\x1b\x8c\xc4\x91\x04\xbd\x97\x8aZ\xd2\x8f\xf7\x90\xb9\xb4\x99\xec\x98P\x13z\x0b\xa8\xfc%\xd2\xfd\x80(\x10\x998i\xe0d!') __pyarmor__(__name__, __file__, b'PY000000\x00\x03\n\x00o\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00-\x04\x00\x00\x12\t\x04\x00\xf9 \x82\x98\x98\xe4A\xc6\x05V\xae\x86\xed\x9c\xc1\x99\x00\x00\x00\x00\x00\x00\x00\x00\x98\xbd\xc8\x83y\x93<\x1b\xd7P=\xaa\x8a\xbf\xd3G\xde\x13\xa4\x02\xbf~\xaf\xbec \xd3\xdf\xd9;\xa7oOE\xeb\x0e\xadW\xd7\x0f8[\xf7r\x9b\x8e\x8e\xd1\xfa\xb7<\xccO\xdc\x9c\x05gr\xc9\xc4\x1c\x06]\x8a\x891z0pX\x8c\x164\x194\xc0\xcbD\x10g\x18\'~\xed\xdch3\xf7\x1a(x"\x93.\x83h\xcc\x08~\xd3qT\x8b16^\x8a\xb7Q\xdf\x8c|\x99)9:\xeb\xc0^WA\x14\xbez\xb3\x08\x9c\\h\xa1\xe1Rm\xa3\x8f\xba\x16:l\x08\x86\xfc\x8b\xa4h\xd5M\xbe\x0b>\xf4\x97\x95\x90\x9a\xeag\xd9\x1e\xc68@\xfd\xad\xe8\xb7Ke\xe7\xf5a/C&y\xb1\x8dR>\\\x89\xabN\xa4\x1a\x02\xbb\x99\x80p6\xdct\xc8\xe8p\xf4\xc5AI\x80\xcf\xec\xd4\xa1\xaaW\xa4\x88b\xf9\rbMG\x92\xc0\xfaH%,\xbf\x0c\xdb\xbev\x887,-N\xa8\x1c\xca\xfb;\xa3p\xd4\x1a\x9b%\x0bR\x14\x80\xb5\x01\t\xed\x0e\x0f\xdft*_B\'\x9dI\x0bP\xb8 \xbaJtd\xa0U6\x10k\xa5\x9c\xc1\xb5\xa7\xd93y\xed\x899\xc4^\x9c\x1b`\x93\xfe\x89Ww\xd8w\\E]\xde\x18\xd6\xb5\x9f6\xd1\xd6\xa4\xc9\xee\'\xb3\xba\x02D[\xbaT\xc6\x7f\xd9\x19\x01\xd6\xdc3\xfev\r\xa17\xe94\x03\xb2Y\xd2@7mL\xf4\xc94\xc9\xc4\x88\x1b\xa9\x82\xd8\xd5\x96o\x92\x10l\\\xca\xde\xa0{KR\xda\x8d\xca\x7f\xf7\xbft\x1aQ\x1d\xfa,Om\xbch\xe0\xa5\xae\x9a\x85.nIi\x177\xce\xb4\r\x15B3>!\x19\x1a\xd8\x19\xbdt\xb5\xf59[\xac?\x05\xb8\xee:\xe3\x1e\xad\xe0\x8cB\xe7\xce\xc3\x9eWh\xf1\x82\xa6\xf5"3\xf7&~E\x00`\x12\x8eA\x0f\x03\xef\x07\xeap\xcb\x87I\xce\x03\x83\x191\xbe\xb7\x01\x13\x17.\xe2\x16\x94\x81\xb9\\\x16\x82\x97$O?\x92\xcd=\xa3\xe8^hs\xa5u\x8c\xab\x06y\xbd\xdd\xc5\xaal\x9d\xeaZ\xc7\xe6\xe1\xacL\xc1\xd3\x16F\n8.\x81Y\x95w\x0f\x95:\x88\xa6\x01@WU\x8f\r\xf1\x88\xa6\x95\x10B\x9d\xf0\xbc\x1e\xd8\xdd\xf4+o\x88`i@$\xb4\x979D\xfc\xd0+V\xac\x84<t\xad\xa5\xcbU<R\xd7\xc4\xa9\xa9\xc5\xc3\x88y\x82\xd7xt\x0b\x80\xc1s\xb7\xdc\xed\xb6\xb7\xd10cJ\xc88\xec8\xf6\xd9\xb2\x9e\x03>g\x97w|\x99\x9d-\xb2o\x0b-\xea\xf9\xe3\x11\xcd\xe4\xc9\x9b\xd1\xe9K?7\xf9f\xf05\xa7\x8c\x08\xff\xa7\xd7:\xa9\x16\x0b\x99\x998xw:=\x0e\x0b3\xcd,\xe4\xd1i\xc0\x08\x92\xed\x9a\x90\x89\xc0\x9b\x07\x10>\x9e\x95Q\xd7` c\x88\x8b\xfb9\x9a\xe4SG\xc3o\xab(\xcd\x0e\xf3s\x93\xd9*\xc17\xcdM4\x07\x87\xbe \xc4\xe5\xbaAowE\x9a\x07\x91Vd\x01\xcd\xb1B\x14N\xde\x9b\xf1\xd2\x14UJr\xc7\xc5\x93\xd2\xf5\x85/\xe4\xa6\x11\xc4\xc0 \xf84Gn\xcc~\xb9DP 3\xfe\x94\xac\xf6{)\x07\xb0\xfa9-\x88^\xc0\xb7\\\x043@)\xdc\x87%k\x80\x05&\x11&\x02\x94\xcdlV{\xa6Mr,p\xce8\x7f\x12\tE\xf7)\xa1/\x88\xdb\x08\x01\x84\xa2\xbb\xd1\x91\xd2\xdc\x96%\xa2\xfa\x99nV\xfe\xe3\x17\x1a\xf0\xa5\xceg\xb44\x89\x1aN\xeeS^\xd5\x85\x06j\xe1\xb8\x0f\xbb\xbc\xb5\xd7\xb1z\xf1\x82\xa9\x83u\xe1\xe6\x9f=\xb2\xb9\xfd\x88(\xfd9\xa3\xfc\x91\x9e\xb3\x14\xccP\xdc\xb9\xceY+q\x1c\xbcZ\xde\xccm7,O\x1a\x85W\xea\x1dR\x0b\x969\xf3\xc3\xfca\xabk!\xf7\x8dy\x8egE\x9a=Q\xbe\x11\xc4\xbb\xd7\x82\x8d]\xd9\xa1\x94}yr\x98^\xac\r\xf0\xef[\xe8\x80\xc2blN\xe79^\x07\x82\xcdK\xfc\xe3-5[\n\xd7\xe0P\x146\xdf\x02\xeao\x9c\x05.\xa2\xb8\x1c\x13[\x9a\xe6-\xdf}\x8fv\x15\xa6\xf8LF#q;\xc8\x82[\x14\xee%\x8cf[\x9b\x11\xe1>(\xb8\xe6\x10\xc73M\x1f\x18\x9e\xe7\x1e\x1c\xbb>\x04\x10\xff=\xaeP}\xadW\x06\x15\xbc\xc7\xfaI\xb4\xe9\xa8\xf8~\xcfQ2\x9b\x0b\rf\x8bu\x85\x9cc\x96n|\xb4V\xdc&aH[)\xc8w\xc4\x89\\\xbf\xd3\xfdm\xa0\xa5r\x98\x96\xb2\xa3\xc3\xbaG')

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
# Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-03-14T02:02:35.897509 # Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-10-27T22:23:03.483996
from pyarmor_runtime_000000 import __pyarmor__ from pyarmor_runtime_000000 import __pyarmor__
__pyarmor__(__name__, __file__, b'PY000000\x00\x03\n\x00o\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00\x84\x00\x00\x00\x12\t\x04\x00\xb0*\x98l\xa4\xf4\x12\x14\xb8\n_\xa9\x03\xce\xedi\x00\x00\x00\x00\x00\x00\x00\x00\x1d\xe6\x1aC\xa8\xe6#\xbf{\x99\xc4\xce\x88\xad[4\xc8\xfbs\xb4\x9d8\xde\xe3!\xaab\xf9\xd3\xcf\xc9\xd7\x84+\xa8;\xcb\x11\x0c\xc5\x1b\xd1\xa9\x90\x08\xab\xad\x8fE\xd0b\xad\x15\xb0\'7TK\x82L\xc9\xbd\xda=x(\xdf\x83\xd0\x84f\xe2\xd3\xdd\xb1\xa8lU\xde\x0f`\xa9\x07`\xeeg4\xa4w\xeb\n%\xdck\x12\\\xde\xd1Z\xeaZ\x90\x86o\xa6\xa7\xb9\x7f\xba\xda\x06\xe3\x0c\xadv\x9f$\xc6\x87\xfc"\xb1E\x8e@T`\xd40\xa2\xfbc') __pyarmor__(__name__, __file__, b'PY000000\x00\x03\n\x00o\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00\x84\x00\x00\x00\x12\t\x04\x00\x97^\xd8\x95\x16B\xef\xdb\x8d%3\x04\xd13\x9f<\x00\x00\x00\x00\x00\x00\x00\x00b\xf3\x9d\xb0\x8f\x0f\rE^\x08\xa1\xd4\xf2\x08<\xe9\x8e\x08\x94s\xe3\t\xee\r\xa1\x9b\xf6\xca\x0cU\x86\n\xfce?\x1b\xee\x86\xfb\xbb?\xe49\x1d\x9c\x9b\x02\xe1\x9a\xe3\x92\x84B\xf74\xeaK\x11!@\x1e\xb9\xa0&\xd7\xcab\x95\xc1\xf0!\\\x98\xfbq\x85\x1c\xa8\x15\xfd\x05.ruG\xe1I\xd9V\tp\x94\xeaB7g\x8e\xae:8\xd5Sc\xe2~\xfaZ\xd7C\x85\x134j\xf9y[B\xf0\x95\xc5\x8ddWkn\x07\x12\xc7J\xf0\x0b\x98')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
# Pyarmor 9.0.7 (trial), 000000, 2025-03-14T02:02:35.420965 # Pyarmor 9.0.7 (trial), 000000, 2025-10-27T22:23:02.994132
from .pyarmor_runtime import __pyarmor__ from .pyarmor_runtime import __pyarmor__

View File

@ -1,3 +1,3 @@
# Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-03-14T02:02:36.425013 # Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-10-27T22:23:04.037554
from pyarmor_runtime_000000 import __pyarmor__ from pyarmor_runtime_000000 import __pyarmor__
__pyarmor__(__name__, __file__, b'PY000000\x00\x03\n\x00o\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00\x0b\x03\x00\x00\x12\t\x04\x00\xfcN\x18\xea\xa1JKN\xb1\xb4\xf5O?\x92\x1a\xce\x00\x00\x00\x00\x00\x00\x00\x00\xb3d\x87\xc41\xfc\x8fS\x9d\x9aBs\xc0y]b\xaeK\xc2\xc40m\xdc\xbf\xbe\xdf\x8b\xab\xae\x87Fc\x07\x1a\x11n\xbd\xbd\x8fS\x9eZ\x8d\xb5"\xaa\xe8\xc3\x8bdt\xdf\xe7\xdf\x84\xe2r\xda\xf5Z\x00p\xf2\xe5@\x8e\x0e4j\xa5;\xbd\xff\x14?\xf6\xa7\xc4[\x19\x9f\x9b\x1f7\xd0Iy\xcb\xa4\x96\xff\xbeF\x12q0q\xf7>~2\xb5n\x03\x14\x10\xa8\x1f\x1fl\xd4\x8f;\x02\x1d\'\xcf_\xf3\x85\xc4\xe2\x84\xdb\x9c}\xfb\xabF\xe3\xa4A\x97\xd7\x80\xdbz^\x1e\xf3\x95\t\xba\xb2HM\x97\xf9\xa5\x18\xa9\x12\xffY\xa7\x0f\x1b\xca7}C9Q5\xf64^\xe2\xfd{TM\xfe\xb8\xb0\x16\x9bj\x8eJ \xd7\xd3\xc9\xe2\x0fW\xfe\x11C\xb5\xb7\x0e\xe4\xa4\x9b\xad\x9f\xce\xeeb\xf2\xc4HW6\xc2I\xac\xf87\x96\xa8\xc1\xee\x1f\xab@\x9d\xb3|\x85\xbd\xf2\x98\x95\xbb\x1b\xb99\xb7\xca\xf3\x90\x81Tk9R\t+\xb4H\x14\xae\xba8\x02\xe6\xca\x9dm\r]-\xa9b,\x8f\t\xe7m\x1b\x88M\x8f\xb4\x16"\xffT\x04\xda,zV\x82\xa0\x98\x00\x1d\r0i\xa6\xfd\xb3\xef\x18\x13<\xbc\xd9\xf8\xa0 \xedn\xcf\x86\xe4\x91\x10\xd5\xf6\x9a\xe8%|\xc7Rn<Y\xa2\x95\x0f)\xb1\x89P\xd4p9\xb1\x14\xc4Em\xc9%F\x9c\xb5\x985\xcb\x07\xfb\xf7V\xe4\xc8{\xdd\xad?\xc7r\xc1\x8aDA\xbdc\xd6\x0c\xb1\x08f\xe1XE\x1d\xc6\x15@\xe0\xb3\xc5\xbaf\xfa\x90\xb3\xddps\xf3\xd1\xcfqM\xce\xcb\r\xb2\xfc\x95d\x1c\xc4\x8e\x85`\x0f\x92\xad\\v\x07v\x16w2\xe6\xf7h\xab\xbf\xaf\x81e\xea\xaf\xcd\xd6P\xb5\x02x\xf2\xf6\xb0Q\x84)\x1d\x94~\xdc\xfbL/F\x15\xcb,\xc1\xd0G0\xb3\x1dNW\x8fa6F!\xd8\xbf9\xa5k?\x8c\n\'t3\x1a\x1d\xafk\xca\xd4\xda\x8d)\xc6\xf0\xc6\n\x9f\xaa\x1c\xd7b\x8c\xee\x96\x1c\xc9\x9d\xe9\xb0\xf6L\x87m\xeb\xf3\x0eI\xf7\xf3\x93^FL\xa2\xe2\xf1R~\xb9q\xc6K6\xc7\xf9\xee\xce\xc5\x1c\xb7\x8fbQ=\x02\x89\xb1\xbd \xb3\x04\x98,\x05\xddv\x10~\x05\xf2\x158D\xcb\xf2\xef\xeb\xd2\xcc\xd8\r\xd6\x8fe\'\xa66\x8f\xa8I\xe5`\x0bH\xda\xa3W\n\xdc\x05\xfb\xb8\x81\xaax\xed\xe5*\xba\x97\xfe\xbev\xbe\x9aL\xc0\xf0\x88\xf0\xd2V\xeaT\xee\x19\xb1\tT\x1f\xc4\x92\xc0\xc3Z\xcd`\x8e\xc4\xaa\x8e1*\xb3\xc3\xaf\xd9\x15P\x8f-\x8c\x91GD\x92\xf33J#\x1cM\x01\x9b$\xf2\xc5L0\x1e4\xbe_\xebt\x16\x0f\x19x[\xe6\x95m\xb3\xbf\x90Yt\xb6\x16\xa2L\x895\xa2\xcd\x0c\x00Vz\xc7zs\x81*\x81%CU\x98l\x15\xb0\x80\xb8KN\xfa\x9c\xacYg\'\x99\xe5\xc3\xdcb\x15\xc7t\xb8Bk\x96\x04\xf1!G\xef\xfbs\xcf\xe7\xa5\xb3h?\x9c\xd7\xa8\xd8[\r\xb1|\xc6\x0f\xe4\xee\xe1\x10\xd9\xfe\xa5\x9b\xf2\xdf])\x89]x\x01\xee\xb7\xdaR\xc2\xa5\xa2A\xee\x87 w\x19\x1fF\xb3\x01\xae@\xb2\x94\xfd\xbd\xbc&\x8c\xc28\xff\xd7&\xd6\xc1\r5\xb5') __pyarmor__(__name__, __file__, b'PY000000\x00\x03\n\x00o\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00\x0b\x03\x00\x00\x12\t\x04\x00\xf1\x1cjC\xb5\xb6\x08yreT3\x1b\x1b\xc10\x00\x00\x00\x00\x00\x00\x00\x00Jb-}n.\x18&\x85\x8aqR\xce\x1f\x1a\xf30L\xb4\xcc*9#\xde~M\xb4\x86\x80\x89\x99\xefu2\x12\xa3\xd1GQC\x04\x92\x06>\x01\xdb\x11\xfc\xf04\x15\xb2\xe2B\xa9\x16\xa1\xf7<\xd11\xdc6\xd3\xdb\xd8\x1dn\x83\xcf$\xc4\xa5\x8e\x08\xa7\\\xca\x9e@\xbf\x8c\x13\xf1[\xa9\xdf\xe6$E%\xfa\xa9\xa8\xe7\xbeq\xf1\xe2,\xed0_\x89z\n\x87\x13\xbe\x9e\x0f0\xccY\xe57L\xf5\xe7\xe1:\xc3_\xadF\x8es\x8fK\\\xc4\x1e\xa0\xf6\xe4\xa1q\x04\x05VCW\x1a\x8b\xfa\xbe\x0ePu\x81\x18\x19\xd7h\xc5P\xe7\xcf\xf7\x9a\xaan\xe9:\xa8&\x17t\x82-&\xff\xd3\xc0r\x05\x1a\x7f\xfb\xdbBM:\xbf\xca\x14y\x90\xb5\x98<`\xc0X\x10\x0c\xa4>\xc5\xce\t$\xe3(A\xfe\x7fN\xb4R#\xda\xf8O\xde\xda"t\xaeD.\x00R\xa3\xb9\xf5jYy\xd0\x8e\xe4\x06\xf9A\x18\x14\xcc\x01\xb8e\x7f\x9b\x1ae\x97\xa3\x02kwY\x81\xfe16C\x85\x90:B\xec\xa6M\xdc\x80\xc73Y\xe3/\x96\xe70f\xb3\xce\x84\x0c\xa9k\xf1z\x8f\xfe\xc8q\xdf\x83Q\x10f\x01\x9a\xed\xfd\x9bh0t\x8d\xdb\xa8\xc1px\xb5\xd6k\x1e\xdf\xfd7a)I\xb4\xd7\x81Y/\x83\xcd\xf3\x99BTZ\x16\x1d\xc3\xe0\xf5\xef$\xd1\xae4\xd6\x8d\x0e\xe5g\xeaw\\\xfcl\x8c\xd5\x1b\xa5^\x96\xf4\xe7\xb1-\xd6G\xf1\xa0(U\x1b\xdf\xf5\xc1\x02\x89\x16\xa6_\xcdfU\x80\x10N\x83\xb6zt\xc1~\xdd\xf8I\x19\xf2\t\x83\xac1\x13} (\x12o\x8d\x06(\x1cN-\x92T\xed\xcf\xa0\x0c\x1al\x08G.\xf6mX\xf4\xa0\xef\x117\xf6\xe2E\xae\xa9\xccP\x96\x97\x1f\x86\x850\x15\x15\x03\t$*\x85&Z\x15\x07O\xa9L-\x8bz\x0fh\x96\x06\xf6\xe89?\x13;\x9d\xa0\xa6\x87\xaa\xd1\xdaeo\xc1\x10"\xb1\xdcsO\x10F,]\xeeg+J\x9d\x97\x06\xc9`\x89\x03h\xf8\xdat\x9a\xa6\xcd~W\xfa\x7f".\x04\x9c\xb2\xa2\x81FS\xe6\x0e\x15i\xa8\xdaZ\x17\xae\x1d\x92^\xbc4\xf1k\x1f\x94\x91\xf3V\x95f\xf8E\xbb\xaf\xe8J\xec\xceX\xbd3*d\xa7\xd6ZF\x94\xe4\x81\xc3p\xa0\xe9{\x87\xbev\xd7Y\xc2f\xf4\xee\x9c\xa0Y\xe44Y\x90\x12z\x07\xc1sJ\xf6\x8d\x89\x86\x1a\x00\x1c\x04r\x07\xc7\x9a\x98\xb6\xcc{h\x87\xa0\xb8\xde}?\x17=\x17\xaa\no\xb1\xaatz\xcd\xdd\x04z\x11_y\xa0\xd0\x14\xbc\x87AL\xd4\xa3\xb7\x01\x9d\xb1Y\xb8\xbb\xac\xa4U\xd8\x04\xd0\xf3\xd2NB\x0f\x87&\xd4\xfc\x83t\xb6\x12r\x82\xb8V\xe0\xd3\x98\x11^\xea\x8a\x0f\xea\x8a\x8fS\xc0\xce3\n\xfe\'\xf9\xa5V}\xf7\xa8\xce\xb1\xcd\x97\x95\xa5\x8d\xa3\x02\xe9\xe5\x1e\x02\xe0k*i\xae&\xc6\xfe\xcb6\xa2\x1b\xabV!\xd0Eh\xad?\xd2\xe1\xec\xf2K\x10h2g\x7f\x0e\xb3\x7f\xc5\xe46\xcf\x17\x9f\xa2\xdf\x92(\x80\x17S\xcaHqz\x96@\xeaS1n\x96{\x9eM\x1dK\xd0\x92\x8eT\xb0["\x92\x99\x87\x14\xd9\xde\x8f\x13\x11')

File diff suppressed because one or more lines are too long

15
static/css/switches.css Normal file
View File

@ -0,0 +1,15 @@
/* switches.css - tiny, local CSS-only slide switch component */
:root{--switch-bg:#d1d5db;--switch-on:var(--brand, #2563eb);--switch-knob:#ffffff}
.switch{display:inline-flex;align-items:center;gap:10px;user-select:none}
.visually-hidden{position:absolute!important;height:1px;width:1px;overflow:hidden;clip:rect(1px,1px,1px,1px);white-space:nowrap;border:0;padding:0;margin:-1px}
.switch-toggle{position:relative;width:48px;height:28px;display:inline-block}
.switch-toggle input{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;z-index:2;cursor:pointer}
.switch-toggle .slider{position:absolute;left:0;top:0;right:0;bottom:0;background:var(--switch-bg);border-radius:999px;transition:background .18s ease,box-shadow .18s ease;box-shadow:inset 0 0 0 1px rgba(0,0,0,0.03)}
.switch-toggle .slider::before{content:"";position:absolute;left:4px;top:4px;width:20px;height:20px;background:var(--switch-knob);border-radius:50%;transition:transform .18s ease,box-shadow .18s ease;box-shadow:0 1px 2px rgba(0,0,0,0.12)}
.switch-toggle input:checked + .slider{background:var(--switch-on)}
.switch-toggle input:checked + .slider::before{transform:translateX(20px)}
.switch-toggle input:focus-visible + .slider{outline:3px solid rgba(37,99,235,0.18);outline-offset:2px}
.switch-label{font-weight:600;color:var(--text,#111827)}
.switch-toggle.small{width:36px;height:20px}
.switch-toggle.small .slider::before{width:12px;height:12px;left:3px;top:3px}
.switch-toggle.small input:checked + .slider::before{transform:translateX(16px)}

View File

@ -1,119 +1,250 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Printer Media Sizes</title> <title>Power Options</title>
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico"> <link rel="icon" type="image/x-icon" href="/static/images/favicon.ico" />
<link rel="stylesheet" href="/static/css/styles.css"> <link rel="stylesheet" href="/static/css/styles.css" />
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css"> <link rel="stylesheet" href="/static/fontawesome/css/all.min.css" />
<script src="/static/js/jquery-3.7.1.js"></script>
<style> <style>
:root{
.header-img { --brand: #1e40af; /* blue-800 */
margin-top: 30px; /* Adjust the margin as needed */ --brand-hover: #15327f;
margin-bottom: 30px; --warn: #f59e0b; /* amber-500 */
width: auto; --warn-hover:#d97706;
height: 70px; /* Adjust the height as needed */ --danger: #ef4444; /* red-500 */
--danger-hover:#dc2626;
--border: #d1d5db; /* gray-300 */
--text: #111827; /* gray-900 */
--muted: #4b5563; /* gray-600 */
--bg: #ffffff;
--bg-alt: #f9fafb; /* gray-50 */
--focus: #2563eb; /* blue-600 */
} }
@media (prefers-color-scheme: dark){
:root{
--brand: #60a5fa;
--brand-hover:#3b82f6;
--warn:#fbbf24;
--warn-hover:#f59e0b;
--danger:#f87171;
--danger-hover:#ef4444;
--border:#374151;
--text:#e5e7eb;
--muted:#9ca3af;
--bg:#0b0f14;
--bg-alt:#111827;
--focus:#60a5fa;
}
}
*{ box-sizing: border-box; }
html, body{ height: 100%; }
body{
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
color: var(--text);
background: var(--bg);
line-height: 1.55;
}
#navbar{ position: sticky; top: 0; z-index: 20; }
.content-wrapper{
max-width: 900px;
margin: 0 auto;
padding: 16px;
}
@media (min-width: 980px){
.content-wrapper{ padding: 24px; }
}
.card{
padding: 20px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--bg-alt);
}
.center-content{ .center-content{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
}
h1 {
margin-bottom: 5px;
}
label, select, #media-sizes, button {
margin: 5px 0;
text-align: center; text-align: center;
font-size: larger; gap: 14px;
font-weight: 200;
} }
select { h1{ margin: 0; font-size: clamp(1.5rem, 2vw, 2rem); }
padding: 3px; .muted{ color: var(--muted); }
font-size: 1.1em;
width: 250px; .header-img{
margin: 10px 0 6px;
width: auto;
height: 72px;
} }
#media-sizes ul { .actions{
list-style-type: none;
padding: 0;
}
#media-sizes li {
display: flex; display: flex;
align-items: center; gap: 12px;
margin-bottom: 5px; flex-wrap: wrap;
} justify-content: center;
margin-top: 8px;
#media-sizes input[type="checkbox"] {
transform: scale(1.5);
margin-right: 12px;
}
#media-sizes span {
font-size: 1.2em;
} }
button{ button{
background-color: red; appearance: none;
color: white; border: 1px solid transparent;
padding: 10px 20px; border-radius: 10px;
border: none; padding: 10px 16px;
border-radius: 5px; font-size: 1rem;
font-size: 1.2em;
cursor: pointer; cursor: pointer;
width: 150px; color: #fff;
margin-bottom: 30px; display: inline-flex;
align-items: center;
gap: 8px;
}
button:focus-visible{
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.btn-warn{
background: var(--warn);
border-color: var(--warn-hover);
}
.btn-warn:hover{ background: var(--warn-hover); }
.btn-danger{
background: var(--danger);
border-color: var(--danger-hover);
}
.btn-danger:hover{ background: var(--danger-hover); }
.status{
min-height: 1.2em;
margin-top: 6px;
font-size: 0.98rem;
}
.status[aria-live]{ /* ensure space even when empty */ display: block; }
.help{
margin-top: 4px;
font-size: 0.95rem;
} }
button:hover { .spinner{
background-color: darkblue; display: inline-block;
width: 1em; height: 1em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
vertical-align: -2px;
margin-right: 6px;
}
@keyframes spin{ to { transform: rotate(360deg); } }
.sr-only{
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0,0,1,1);
white-space: nowrap; border: 0;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="navbar"></div> <div id="navbar" role="navigation" aria-label="Primary"></div>
<script> <script>
fetch('/static/html/nav.html') fetch('/static/html/nav.html')
.then(response => response.text()) .then(r => r.ok ? r.text() : Promise.reject(r.status))
.then(data => { .then(html => { document.getElementById('navbar').innerHTML = html; })
document.getElementById('navbar').innerHTML = data; .catch(() => {
document.getElementById('navbar').innerHTML =
'<div class="card" role="alert">Navigation failed to load.</div>';
}); });
</script> </script>
<div class="content-wrapper"> <main class="content-wrapper">
<div class="content center-content"> <section class="card center-content" aria-labelledby="power-heading">
<h1>Power Options</h1> <h1 id="power-heading">Power Options</h1>
<img src="/static/images/switch-icon.png" alt="cards" class="header-img"> <img src="/static/images/switch-icon.png" alt="Power switch icon" class="header-img" />
<button id="reboot-button">Reboot</button> <p class="muted help">
<button id="shutdown-button">Shutdown</button> These actions affect the entire device. Save your work before proceeding.
</p>
<div class="actions" role="group" aria-label="Power controls">
<button type="button" id="reboot-button" class="btn-warn">
<i class="fa fa-rotate-right" aria-hidden="true"></i>
Reboot
</button>
<button type="button" id="shutdown-button" class="btn-danger">
<i class="fa fa-power-off" aria-hidden="true"></i>
Shut Down
</button>
</div> </div>
</div>
</body> <div id="status" class="status" role="status" aria-live="polite"></div>
</section>
</main>
<script> <script>
$(document).ready(function() { const rebootBtn = document.getElementById('reboot-button');
const shutdownBtn = document.getElementById('shutdown-button');
const statusEl = document.getElementById('status');
$('#reboot-button').click(function() { function setBusy(btn, busy){
$.post('/reboot', function(response) { btn.disabled = busy;
alert("Reboot initiated"); if (busy) btn.setAttribute('aria-busy', 'true'); else btn.removeAttribute('aria-busy');
}).fail(function() { }
alert("Reboot failed");
function setStatus(message, isError = false){
statusEl.textContent = '';
if (!message) return;
statusEl.innerHTML = (isError ? '' : '<span class="spinner"></span>') + message;
statusEl.style.color = isError ? 'var(--danger)' : 'inherit';
}
async function postAction(url, startMsg, successMsg){
setStatus(startMsg, false);
try{
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}); });
if (!res.ok) throw new Error('Network error');
let data = null;
try { data = await res.json(); } catch {}
const ok = (data && (data.reply === true || data.ok === true)) || res.ok;
if (!ok) throw new Error((data && data.message) || 'Server error');
// Success: message without spinner
statusEl.innerHTML = successMsg;
statusEl.style.color = 'inherit';
} catch (e){
console.error(e);
setStatus('Operation failed. Please try again.', true);
alert('The request did not complete successfully.');
}
}
rebootBtn?.addEventListener('click', async () => {
const confirmed = confirm('Reboot now? All services will restart.');
if (!confirmed) return;
setBusy(rebootBtn, true);
setBusy(shutdownBtn, true);
await postAction('/reboot', 'Rebooting device… This may take up to a minute.', 'Reboot initiated.');
// Keep buttons disabled briefly to prevent repeats
setTimeout(() => { setBusy(rebootBtn, false); setBusy(shutdownBtn, false); }, 4000);
}); });
$('#shutdown-button').click(function() { shutdownBtn?.addEventListener('click', async () => {
$.post('/shutdown', function(response) { const confirmed = confirm('Shut down now? The device will power off.');
alert("Shutdown initiated"); if (!confirmed) return;
}).fail(function() { setBusy(rebootBtn, true);
alert("Shutdown failed"); setBusy(shutdownBtn, true);
}); await postAction('/shutdown', 'Shutting down… The device will power off shortly.', 'Shutdown initiated.');
}); // Keep disabled; device may go offline
setTimeout(() => { setBusy(rebootBtn, false); setBusy(shutdownBtn, false); }, 4000);
}); });
</script> </script>
</body>
</html> </html>

119
static/html_orig/power.html Normal file
View File

@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Printer Media Sizes</title>
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico">
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css">
<script src="/static/js/jquery-3.7.1.js"></script>
<style>
.header-img {
margin-top: 30px; /* Adjust the margin as needed */
margin-bottom: 30px;
width: auto;
height: 70px; /* Adjust the height as needed */
}
.center-content {
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
margin-bottom: 5px;
}
label, select, #media-sizes, button {
margin: 5px 0;
text-align: center;
font-size: larger;
font-weight: 200;
}
select {
padding: 3px;
font-size: 1.1em;
width: 250px;
}
#media-sizes ul {
list-style-type: none;
padding: 0;
}
#media-sizes li {
display: flex;
align-items: center;
margin-bottom: 5px;
}
#media-sizes input[type="checkbox"] {
transform: scale(1.5);
margin-right: 12px;
}
#media-sizes span {
font-size: 1.2em;
}
button {
background-color: red;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 1.2em;
cursor: pointer;
width: 150px;
margin-bottom: 30px;
}
button:hover {
background-color: darkblue;
}
</style>
</head>
<body>
<div id="navbar"></div>
<script>
fetch('/static/html/nav.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
</script>
<div class="content-wrapper">
<div class="content center-content">
<h1>Power Options</h1>
<img src="/static/images/switch-icon.png" alt="cards" class="header-img">
<button id="reboot-button">Reboot</button>
<button id="shutdown-button">Shutdown</button>
</div>
</div>
</body>
<script>
$(document).ready(function() {
$('#reboot-button').click(function() {
$.post('/reboot', function(response) {
alert("Reboot initiated");
}).fail(function() {
alert("Reboot failed");
});
});
$('#shutdown-button').click(function() {
$.post('/shutdown', function(response) {
alert("Shutdown initiated");
}).fail(function() {
alert("Shutdown failed");
});
});
});
</script>
</html>

View File

@ -1,309 +1,357 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>About Printio</title> <title>About Printio</title>
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico"> <link rel="icon" type="image/x-icon" href="/static/images/favicon.ico" />
<link rel="stylesheet" href="/static/css/styles.css"> <link rel="stylesheet" href="/static/css/styles.css" />
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css"> <link rel="stylesheet" href="/static/fontawesome/css/all.min.css" />
<style> <style>
h1 { :root{
margin-bottom: 20px; --brand: #1e40af; /* blue-800 */
--brand-hover: #15327f;
--ok: #16a34a; /* green-600 */
--ok-hover: #118039;
--border: #d1d5db; /* gray-300 */
--text: #111827; /* gray-900 */
--muted: #4b5563; /* gray-600 */
--bg: #ffffff;
--bg-alt: #f9fafb; /* gray-50 */
--focus: #2563eb; /* blue-600 */
}
@media (prefers-color-scheme: dark){
:root{
--brand: #60a5fa;
--brand-hover: #3b82f6;
--ok: #34d399;
--ok-hover: #10b981;
--border: #374151;
--text: #e5e7eb;
--muted: #9ca3af;
--bg: #0b0f14;
--bg-alt: #111827;
--focus: #60a5fa;
}
} }
label, select, #media-sizes, button { * { box-sizing: border-box; }
margin: 5px 0; html, body { height: 100%; }
text-align: center; body{
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
color: var(--text);
background: var(--bg);
line-height: 1.55;
} }
select { #navbar { position: sticky; top: 0; z-index: 20; }
padding: 3px;
font-size: 1.1em; h1 { margin: 0 0 16px; font-size: clamp(1.5rem, 2vw, 2rem); }
h3 { margin: 0 0 12px; font-size: 1.125rem; }
p { margin: 0 0 12px; }
a { color: var(--brand); text-decoration: underline; text-underline-offset: 2px; }
a:hover { color: var(--brand-hover); }
.content-wrapper{
display: grid;
grid-template-columns: 1fr;
gap: 20px;
max-width: 1100px;
margin: 0 auto;
padding: 16px;
} }
#media-sizes ul { @media (min-width: 840px){
list-style-type: none; .content-wrapper{
padding: 0; grid-template-columns: 300px 1fr;
gap: 24px;
padding: 24px;
}
.left-column{
position: sticky;
top: 72px; /* keep below navbar */
align-self: start;
}
} }
#media-sizes li { .left-column, .right-column { width: 100%; }
.card{
padding: 16px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg-alt);
}
.info-box{ margin-bottom: 16px; }
.kv p{
display: flex; display: flex;
justify-content: space-between;
gap: 12px;
border-bottom: 1px dashed var(--border);
padding: 6px 0;
margin: 0;
font-size: 0.95rem;
}
.kv p span:first-child{ color: var(--muted); }
.kv p span:last-child{ font-variant-numeric: tabular-nums; }
.license-section{
margin-top: 20px;
text-align: left;
}
.license-status{ font-size: 1rem; margin: 0 0 10px; }
.field{
display: grid;
grid-template-columns: 140px 1fr auto;
gap: 10px;
align-items: center; align-items: center;
margin-bottom: 5px; margin: 10px 0;
}
@media (max-width: 520px){
.field{ grid-template-columns: 1fr; }
.field label{ margin-bottom: -4px; }
} }
#media-sizes input[type="checkbox"] { input[type="text"], input[type="password"]{
transform: scale(1.5); padding: 10px 12px;
margin-right: 12px; font-size: 1rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
} }
input[readonly]{
#media-sizes span { background: rgba(0,0,0,0.03);
font-size: 1.2em;
} }
button{ button{
background-color: blue; appearance: none;
color: white;
padding: 10px 20px;
border: none; border: none;
border-radius: 5px; border-radius: 8px;
font-size: 1.2em; padding: 10px 16px;
font-size: 1rem;
cursor: pointer; cursor: pointer;
color: #fff;
background: var(--brand);
}
button:hover{ background: var(--brand-hover); }
button:focus-visible{
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.btn-ok{
background: var(--ok);
}
.btn-ok:hover{
background: var(--ok-hover);
} }
button:hover { .actions{
background-color: darkblue;
}
p {
display: block;
margin-top: 1em;
margin-bottom: 1em;
margin-left: 0;
margin-right: 0;
}
.license-section {
margin-top: 30px;
text-align: center;
}
.license-status {
font-size: 1.1em;
margin-bottom: 15px;
}
.license-input {
margin-top: 10px;
text-align: left;
}
.license-input input {
padding: 10px;
width: 300px;
font-size: 1.1em;
margin-right: 10px;
}
.license-input button {
background-color: #4CAF50;
padding: 10px 20px;
font-size: 1.1em;
cursor: pointer;
border-radius: 5px;
border: none;
}
.license-input button:hover {
background-color: #45a049;
}
/* New Styles for Layout */
.content-wrapper {
display: flex; display: flex;
gap: 10px;
flex-wrap: wrap;
} }
.left-column { .qr{
width: 280px; width: 140px;
padding-right: 20px; max-width: 40vw;
box-sizing: border-box; height: auto;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
} }
.right-column { .muted{ color: var(--muted); font-size: 0.95rem; }
width: 70%;
}
.info-box {
padding: 15px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
.license-box {
padding: 15px;
border: 1px solid #ccc;
border-radius: 5px;
}
#p {
margin-left: 10px;
}
</style> </style>
<script src="/static/js/crypto-js.min.js"></script> <script src="/static/js/crypto-js.min.js" defer></script>
</head> </head>
<body> <body>
<div id="navbar"></div> <div id="navbar" role="navigation" aria-label="Primary"></div>
<script> <script>
// Load navbar with basic error handling
fetch('/static/html/nav.html') fetch('/static/html/nav.html')
.then(response => response.text()) .then(r => r.ok ? r.text() : Promise.reject(r.status))
.then(data => { .then(html => { document.getElementById('navbar').innerHTML = html; })
document.getElementById('navbar').innerHTML = data; .catch(() => { document.getElementById('navbar').innerHTML = '<div class="card" role="alert">Navigation failed to load.</div>'; });
});
</script> </script>
<div class="content-wrapper"> <main class="content-wrapper">
<div class="left-column"> <!-- Sidebar -->
<div class="info-box"> <aside class="left-column" aria-label="System and License Information">
<h3>System Info</h3> <section class="card info-box" aria-labelledby="sysinfo-heading">
<p>Version: {{info.software_version}} <h3 id="sysinfo-heading">System Info</h3>
<p>CPU %: {{info.cpu}}</p> <div class="kv">
<p>CPU T: {{info.cpu_t}}</p> <p><span>Version</span><span>{{info.software_version}}</span></p>
<p>Disk Size: {{info.disk_size}}</p> <p><span>CPU %</span><span>{{info.cpu}}</span></p>
<p>Disk Used: {{info.disk_used}}</p> <p><span>CPU Temp</span><span>{{info.cpu_t}}</span></p>
<p>RAM Size: {{info.ram_size}}</p> <p><span>Disk Size</span><span>{{info.disk_size}}</span></p>
<p>RAM Used: {{info.ram_used}}</p> <p><span>Disk Used</span><span>{{info.disk_used}}</span></p>
<p>Up Time: {{info.uptime}}</p> <p><span>RAM Size</span><span>{{info.ram_size}}</span></p>
<p><span>RAM Used</span><span>{{info.ram_used}}</span></p>
<p><span>Up Time</span><span>{{info.uptime}}</span></p>
</div> </div>
</section>
<div class="license-box"> <section class="card" aria-labelledby="licenseinfo-heading">
<h3>License Info </h3> <h3 id="licenseinfo-heading">License Info</h3>
<p>License: {{info.license}}</p> <p class="muted">Status: <strong>{{info.license}}</strong></p>
<!--p>Image Magic: {{info.image_magic}}</p--> <!-- Optional fields kept commented
<!--p>Hashtag: {{info.hash}}</p--> <p>ImageMagick: {{info.image_magic}}</p>
<!--p>Drop Folder: {{info.drop_folder}}</p--> <p>Drop Folder: {{info.drop_folder}}</p>
</div> -->
</div> </section>
</aside>
<div class="right-column"> <!-- Main -->
<section class="right-column">
<h1>About Printio</h1> <h1>About Printio</h1>
<p> <p>
Printio is a professional printing solution designed for seamless Printio is a professional printing solution designed for seamless printing from iPads. Its widely used in the photobooth industry and supports a range of dye-sublimation printers. Printio works with well-known brands such as DNP, Sinfonia, HiTi, and Mitsubishi, as well as many other printers, including Zebra label printers and most HP and Epson models—making it a versatile choice for diverse printing needs.
printing from iPads. It is particularly popular in the photobooth
industry, supporting a wide range of sublimation printers. Printio
is compatible with well-known brands such as DNP, Sinfonia, Hiti,
and Mitsubishi, as well as many other printers, including Zebra label
printers, and most HP and Epson models. This versatility makes Printio
an ideal choice for diverse printing needs.
</p> </p>
<p class="muted">
Warning: Unauthorized reproduction or distribution of this product is strictly prohibited and may result in civil and criminal penalties.
</p>
<p> <p>
Warning: Unauthorized reproduction or distribution of this product <a href="https://www.ataphotobooths.com/knowledge-base/category/printio-setup/" target="_blank" rel="noopener noreferrer">
is strictly prohibited and may result in severe civil and criminal Printio Help: Click me!
penalties. </a>
</p> </p>
<a href="https://www.ataphotobooths.com/knowledge-base/category/printio-setup/" target="_blank">Printio Help: Click me!</a>
<div></div>
<img src="/static/images/printio_help_qr.jpg" alt="Printio" style="width: 120px; margin-top: 20px;">
<figure aria-label="Printio Help QR Code" style="margin: 16px 0;">
<img class="qr" src="/static/images/printio_help_qr.jpg" alt="Scan for Printio setup help" />
</figure>
<div class="license-section" {{info.hidden}}> <!-- License Section (conditionally hidden) -->
<div class="license-input"> <section class="license-section card" {{info.hidden}} aria-labelledby="license-activation-heading">
<label>__________________________________________________________</label> <h3 id="license-activation-heading">License Activation</h3>
</div> <p class="license-status muted" id="license-status" aria-live="polite"></p>
<div class="license-input">
<label for="activation-serial">ID Code:</label> <div class="field">
<input type="text" id="idcode" value={{info.idcode}} readonly> <label for="idcode">ID Code</label>
<button onclick="copyToClipboard()">Copy</button> <input type="text" id="idcode" value="{{info.idcode}}" readonly aria-readonly="true" />
</div> <div class="actions">
<div class="license-input"> <button type="button" id="copy-btn" class="btn-ok" aria-describedby="copy-hint">
<label for="activation-serial">SerialNo</label> <i class="fa fa-copy" aria-hidden="true"></i> Copy
<input type="password" id="license-password" placeholder="Enter Activation Key"> </button>
<button onclick="activateLicense()">Activate</button>
</div> </div>
</div> </div>
<p class="muted" id="copy-hint">Copies your ID Code to the clipboard.</p>
<div class="field">
<label for="license-password">Activation Key</label>
<input type="password" id="license-password" placeholder="Enter Activation Key" autocomplete="one-time-code" />
<div class="actions">
<button type="button" id="activate-btn">
<i class="fa fa-bolt" aria-hidden="true"></i> Activate
</button>
</div> </div>
</div> </div>
</section>
</section>
</main>
<script> <script>
function encryptPassword(password) { // Optional: simple (placeholder) hashing before sending; replace with real logic if needed.
// Simple encryption using CryptoJS (you can replace with your method) function hashPass(plain) {
return password;
}
function activateLicense() {
const password = document.getElementById('license-password').value;
const encryptedPassword = encryptPassword(password);
fetch('/activate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ pass: encryptedPassword })
})
.then(response => response.json())
.then(data => {
console.log("Activation result:", data);
if (data.reply == true) {
alert("Activation Successful!!!");
window.location.href = "/about"; // Redirect to /about page
} else {
alert("Activation Failed.... Try Again");
}
})
.catch(error => {
console.error("Error during activation attempt:", error);
alert("Error during activation attempt.");
});
}
function copyToClipboard() {
const idCode = document.getElementById('idcode');
// Check if the element exists
if (!idCode) {
console.error('Element with ID "idcode" not found');
alert('Error: Could not find the text to copy');
return;
}
// Get the value or text content (depending on element type)
const textToCopy = idCode.value || idCode.textContent || '';
console.log('Text to copy:', textToCopy);
// Ensure there's something to copy
if (!textToCopy) {
console.warn('No text to copy');
alert('Nothing to copy');
return;
}
// First, try the Clipboard API if available
if (navigator.clipboard && navigator.clipboard.writeText) {
console.log('Attempting to use Clipboard API');
navigator.clipboard.writeText(textToCopy)
.then(() => {
console.log('Clipboard API success');
alert('ID code copied to clipboard');
})
.catch(err => {
console.error('Clipboard API failed:', err);
attemptFallbackCopy(textToCopy);
});
} else {
console.log('Clipboard API unavailable, using fallback');
attemptFallbackCopy(textToCopy);
}
}
function attemptFallbackCopy(textToCopy) {
try { try {
// Create a temporary textarea for copying // Example using CryptoJS if you choose to enable hashing later:
const tempTextarea = document.createElement('textarea'); // return CryptoJS.SHA256(plain).toString();
tempTextarea.value = textToCopy; return plain; // currently pass-through; replace when backend expects a hash
tempTextarea.style.position = 'fixed'; // Prevent scrolling issues } catch {
tempTextarea.style.opacity = '0'; // Make it invisible return plain;
document.body.appendChild(tempTextarea); }
tempTextarea.focus(); }
tempTextarea.select();
// Attempt to copy function setStatus(msg, ok = true){
const successful = document.execCommand('copy'); const el = document.getElementById('license-status');
document.body.removeChild(tempTextarea); if (!el) return;
el.textContent = msg || '';
el.style.color = ok ? 'inherit' : '#dc2626'; // red-600 in light; acceptable in dark
}
if (successful) { async function activateLicense() {
console.log('Fallback copy successful'); const input = document.getElementById('license-password');
//alert('ID code copied to clipboard (using fallback)'); const pass = (input?.value || '').trim();
if (!pass){
setStatus('Please enter your activation key.', false);
input?.focus();
return;
}
setStatus('Activating…');
try {
const res = await fetch('/activate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pass: hashPass(pass) })
});
if (!res.ok) throw new Error('Network error');
const data = await res.json();
if (data.reply === true) {
alert('Activation successful!');
window.location.href = '/about';
} else { } else {
console.warn('Fallback copy failed: execCommand returned false'); setStatus('Activation failed. Please verify your key and try again.', false);
alert('Failed to copy to clipboard: Copy command unsuccessful'); alert('Activation failed. Try again.');
} }
} catch (err) { } catch (err) {
console.error('Fallback copy error:', err); console.error('Activation error:', err);
alert('Failed to copy to clipboard: ' + err.message); setStatus('Error during activation attempt.', false);
alert('Error during activation attempt.');
} }
} }
async function copyToClipboard() {
const el = document.getElementById('idcode');
const btn = document.getElementById('copy-btn');
if (!el) return alert('No ID Code field found.');
const text = el.value || el.textContent || '';
if (!text) return alert('Nothing to copy.');
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
// Fallback
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
btn?.classList.add('copied');
btn?.setAttribute('aria-label', 'Copied!');
setStatus('ID Code copied to clipboard.');
setTimeout(() => {
btn?.removeAttribute('aria-label');
setStatus('');
}, 1500);
} catch (e) {
console.error('Clipboard error:', e);
alert('Failed to copy to clipboard.');
}
}
// Wire up buttons
document.getElementById('copy-btn')?.addEventListener('click', copyToClipboard);
document.getElementById('activate-btn')?.addEventListener('click', activateLicense);
// Allow Enter key to activate from password field
document.getElementById('license-password')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') activateLicense();
});
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,157 +1,369 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Printio</title> <title>Printio</title>
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico"> <link rel="icon" type="image/x-icon" href="/static/images/favicon.ico" />
<link rel="stylesheet" href="/static/css/styles.css"> <link rel="stylesheet" href="/static/css/styles.css" />
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css"> <link rel="stylesheet" href="/static/fontawesome/css/all.min.css" />
<style> <style>
:root{
--brand: #1e40af; /* blue-800 */
--brand-hover: #15327f;
--danger: #ef4444; /* red-500 */
--ok: #16a34a; /* green-600 */
--ok-hover: #118039;
--border: #d1d5db; /* gray-300 */
--text: #111827; /* gray-900 */
--muted: #4b5563; /* gray-600 */
--bg: #ffffff;
--bg-alt: #f9fafb; /* gray-50 */
--focus: #2563eb; /* blue-600 */
}
@media (prefers-color-scheme: dark){
:root{
--brand: #60a5fa;
--brand-hover: #3b82f6;
--danger: #f87171;
--ok: #34d399;
--ok-hover: #10b981;
--border: #374151;
--text: #e5e7eb;
--muted: #9ca3af;
--bg: #0b0f14;
--bg-alt: #111827;
--focus: #60a5fa;
}
}
*{ box-sizing: border-box; }
html, body{ height: 100%; }
body{
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
color: var(--text);
background: var(--bg);
line-height: 1.55;
}
#navbar{ position: sticky; top: 0; z-index: 20; }
h1, h2{ margin: 0 0 12px; }
h1{ font-size: clamp(1.5rem, 2vw, 2rem); }
h2{ font-size: clamp(1.1rem, 1.6vw, 1.4rem); color: var(--muted); }
.content-wrapper{
max-width: 1100px;
margin: 0 auto;
padding: 16px;
}
@media (min-width: 980px){
.content-wrapper{ padding: 24px; }
}
.card{
padding: 16px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg-alt);
}
.header-img{ .header-img{
margin-top: 30px; margin: 24px 0;
margin-bottom: 30px;
width: auto; width: auto;
height: 100px; height: 100px;
} }
h2 {
margin-top: 0; /* Switch row */
font-size: 24px; .switch-row{
color: #565656; display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin: 12px 0 6px;
}
.switch-label{ font-weight: 600; color: var(--text); }
.switch-input{ transform: scale(1.1); }
.error{
color: var(--danger);
text-align: center;
margin: 8px 0 0;
display: none;
}
/* Actions */
button{
appearance: none;
border: none;
border-radius: 8px;
padding: 8px 14px;
font-size: 0.98rem;
cursor: pointer;
color: #fff;
background: var(--brand);
}
button:hover{ background: var(--brand-hover); }
button:focus-visible{
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.btn-danger{ background: var(--danger); }
.btn-ok{ background: var(--ok); }
.btn-ok:hover{ background: var(--ok-hover); }
.btn-ghost{
background: transparent;
color: var(--brand);
border: 1px solid var(--brand);
}
.btn-ghost:hover{
color: #fff;
background: var(--brand);
}
/* Table */
.table-wrap{
margin-top: 16px;
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
background: var(--bg);
}
.table-scroll{
width: 100%;
overflow-x: auto;
} }
table{ table{
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
margin-top: 20px; min-width: 720px;
box-sizing: border-box;
} }
th, td { thead th{
border: 1px solid #ccc; position: sticky;
padding: 10px; top: 0;
background: var(--bg-alt);
color: var(--text);
border-bottom: 1px solid var(--border);
text-align: left; text-align: left;
box-sizing: border-box; padding: 10px;
font-weight: 600;
} }
th { tbody td{
background-color: #f8f8f8; border-top: 1px solid var(--border);
padding: 10px;
vertical-align: middle;
} }
.button-red { tbody tr:hover{
padding: 5px 10px; background: rgba(0,0,0,0.03);
background-color: #f44336; }
color: white;
border: none; .actions{
border-radius: 5px; display: inline-flex;
gap: 8px;
flex-wrap: wrap;
}
/* Job history */
details{
margin-top: 16px;
}
summary{
list-style: none;
cursor: pointer; cursor: pointer;
user-select: none;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--brand);
} }
.button-test { summary::-webkit-details-marker{ display: none; }
padding: 5px 10px;
background-color: #4CAF50; .log{
color: white; margin-top: 12px;
border: none; border: 1px solid var(--border);
border-radius: 5px; border-radius: 10px;
cursor: pointer; background: var(--bg);
} padding: 12px;
.placeholder-img { max-height: 360px;
position: absolute;
top: 20px;
right: 20px;
}
.placeholder-img img {
top: 20px;
width: 60px;
height: 60px;
}
/* Styles for the message log */
#messageLog {
display: none;
margin-top: 20px;
width: 100%;
height: 350px;
box-sizing: border-box;
resize: none;
overflow: auto; overflow: auto;
white-space: nowrap;
} }
.toggle-button { .log pre{
margin-top: 20px; margin: 0;
background-color: #007BFF; white-space: pre;
color: white; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
border: none; font-size: 0.9rem;
padding: 3px 20px; line-height: 1.4;
cursor: pointer; }
border-radius: 5px;
.sr-only{
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0, 0, 1, 1);
white-space: nowrap; border: 0;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="navbar"></div> <div id="navbar" role="navigation" aria-label="Primary"></div>
<script> <script>
fetch('/static/html/nav.html') fetch('/static/html/nav.html')
.then(response => response.text()) .then(r => r.ok ? r.text() : Promise.reject(r.status))
.then(data => { .then(html => { document.getElementById('navbar').innerHTML = html; })
document.getElementById('navbar').innerHTML = data; .catch(() => { document.getElementById('navbar').innerHTML = '<div class="card" role="alert">Navigation failed to load.</div>'; });
});
</script> </script>
<div class="content-wrapper"> <main class="content-wrapper">
<div class="content"> <section class="content card" aria-labelledby="active-printers-heading">
<img src="/static/images/printio_logo.png" alt="Printio" class="header-img"> <img src="/static/images/printio_white_logo.png" alt="Printio logo" class="header-img" />
<h2>Active Printers</h2> <h1 id="active-printers-heading">Active Printers</h1>
<table border="1">
<div class="switch-row">
<label class="switch-label" for="printOnConnect">Print on Connect</label>
<input
type="checkbox"
id="printOnConnect"
name="printOnConnect"
class="switch-input"
aria-describedby="printOnConnectHelp printOnConnectError"
{{ settings.printOnConnect }}>
</div>
<p id="printOnConnectHelp" class="sr-only">If enabled, a test or queued job can print automatically upon printer connection.</p>
<div id="printOnConnectError" class="error" role="alert" aria-live="polite"></div>
<div class="table-wrap" role="region" aria-label="Printer list">
<div class="table-scroll">
<table>
<thead>
<tr> <tr>
<th>Printer Name</th> <th scope="col">Printer Name</th>
<th>Status</th> <th scope="col">Status</th>
<th>Prints Left</th> <th scope="col">Prints Left</th>
<th>Actions</th> <th scope="col">Actions</th>
</tr> </tr>
</thead>
<tbody>
{% for printer in printers %} {% for printer in printers %}
<tr> <tr>
<td>{{ printer.name }}</td> <td>{{ printer.name }}</td>
<td>{{ printer.state }}</td> <td>{{ printer.state }}</td>
<td>{{ printer.prints_left_message }}</td> <td>{{ printer.prints_left_message }}</td>
<td> <td>
<form action="{{ url_for('cancel_all_jobs', printer_name=printer.name) }}" method="post" style="display:inline;"> <div class="actions">
<button type="submit" class="button-red">Kill Jobs</button> <form action="{{ url_for('cancel_all_jobs', printer_name=printer.name) }}" method="post">
<button type="submit" class="btn-danger">
<i class="fa fa-ban" aria-hidden="true"></i> Kill Jobs
</button>
</form> </form>
<form action="{{ url_for('test_print', printer_name=printer.name) }}" method="post" style="display:inline;"> <form action="{{ url_for('test_print', printer_name=printer.name) }}" method="post">
<button type="submit" class="button-test">Test Print</button> <button type="submit" class="btn-ok">
<i class="fa fa-print" aria-hidden="true"></i> Test Print
</button>
</form> </form>
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table> </table>
</div>
</div>
<!-- Button to toggle the message log --> <details id="jobHistory">
<button class="toggle-button" onclick="toggleMessageLog()">Job History</button> <summary>
<!-- Hidden textarea for the message log --> <i class="fa fa-history" aria-hidden="true"></i>
<textarea id="messageLog" placeholder="Job Status Log..." readonly></textarea> Job History
</div> </summary>
<div class="log" id="messageLog" aria-live="off">
<pre id="logText">Loading…</pre>
</div> </div>
</details>
</section>
</main>
<script> <script>
// Toggles the visibility of the message log textarea /**
function toggleMessageLog() { * Initialize the "Print on Connect" checkbox behavior.
var messageLog = document.getElementById('messageLog'); */
if (messageLog.style.display === '' || messageLog.style.display === 'none') { function initPrintOnConnect() {
messageLog.style.display = 'block'; const checkbox = document.getElementById('printOnConnect');
loadPrinterJobs(); // Load job messages when showing the textarea const errorDiv = document.getElementById('printOnConnectError');
} else {
messageLog.style.display = 'none'; if (!checkbox) return;
}
checkbox.addEventListener('change', () => {
const value = checkbox.checked;
fetch('/printer-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ 'print-on-connect': value })
})
.then((resp) => {
if (!resp.ok) throw new Error('Server rejected the setting');
return resp.json();
})
.then((json) => {
if (!json.reply) throw new Error(json.message || 'Server error');
// Clear any prior error
errorDiv.style.display = 'none';
errorDiv.textContent = '';
})
.catch((err) => {
console.error('Failed to save print-on-connect:', err);
// Revert checkbox
checkbox.checked = !value;
// Show error
errorDiv.textContent = 'Failed to save setting to server';
errorDiv.style.display = 'block';
setTimeout(() => {
errorDiv.style.display = 'none';
}, 4000);
});
});
} }
// Load the printer job information and show in the textarea /**
* Load job history on demand.
*/
function loadPrinterJobs() { function loadPrinterJobs() {
var messageLog = document.getElementById('messageLog'); const jobs = {{ job_history | tojson | safe }};
var jobs = {{ job_history | tojson | safe }}; const pre = document.getElementById('logText');
var logText = ""; if (!pre) return;
jobs.forEach(function(job, index) { if (!Array.isArray(jobs) || jobs.length === 0) {
logText += "Job: " + job.job_id + ", Time: " + job.time + ", Size: " + job.job_size + ", Printer: " + job.printer_name + ", State: " + job.job_state + ', Reason: ' + job.job_reason + "\r\n"; pre.textContent = 'No jobs found.';
return;
}
// Build a readable, tabular-ish log
const lines = jobs.map(j => {
const id = j.job_id ?? '';
const t = j.time ?? '';
const size = j.job_size ?? '';
const pn = j.printer_name ?? '';
const state = j.job_state ?? '';
const reason = j.job_reason ?? '';
return `Job ${id} | Time: ${t} | Size: ${size} | Printer: ${pn} | State: ${state} | Reason: ${reason}`;
}); });
// Set the value of the textarea to display the job information pre.textContent = lines.join('\n');
messageLog.value = logText;
} }
document.addEventListener('DOMContentLoaded', () => {
initPrintOnConnect();
// Lazy-load job history when the <details> opens
const details = document.getElementById('jobHistory');
if (details) {
details.addEventListener('toggle', () => {
if (details.open) loadPrinterJobs();
});
}
});
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,194 +1,131 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WiFi Configuration</title> <title>WiFi Configuration</title>
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico"> <link rel="icon" type="image/x-icon" href="/static/images/favicon.ico" />
<link rel="stylesheet" href="/static/css/styles.css"> <link rel="stylesheet" href="/static/css/styles.css" />
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css"> <link rel="stylesheet" href="/static/fontawesome/css/all.min.css" />
<style> <style>
h1 { :root{
margin-top: 30px; --bg: #0b0d10;
font-size: 36px; --panel: #12161b;
color: #333; --card: #141a20;
--muted: #c7d1db;
--primary: #3b82f6;
--primary-700: #1d4ed8;
--ring: rgba(59,130,246,.35);
--radius: 14px;
--gap: 16px;
--ok: #22c55e;
--warn: #f59e0b;
--err: #ef4444;
} }
.button-container { html, body { height:100%; }
display: flex; body{
justify-content: space-between; margin:0; color:var(--muted);
align-items: center; background: linear-gradient(180deg, #0b0d10 0%, #0e1217 100%);
margin-top: 20px; font: 500 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, sans-serif;
} -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
.btn {
padding: 10px 20px;
font-size: 1.2em;
cursor: pointer;
background-color: blue;
color: white;
width: 150px;
border-radius: 5px;
border: none;
text-decoration: none;
}
.btn:hover {
background-color: #0056b3;
}
.small-btn {
width: 100px;
padding: 5px 10px;
font-size: 0.8em;
background-color: darkslateblue;
}
.small-btn-container {
margin-left: auto;
}
.scan-container {
margin-top: 20px;
}
.scan-container .btn {
background-color: #28a745;
}
.scan-container .btn:hover {
background-color: #218838;
}
.connect-container {
margin-top: 20px;
}
.connect-container .btn {
padding: 5px 15px;
border-radius: 5px;
background-color: #ffc107;
margin-bottom: 30px;
}
.connect-container .btn:hover {
background-color: #e0a800;
}
.network-list {
margin-top: 20px;
width: 100%;
max-height: 200px;
overflow-y: auto;
border: 1px solid #ccc;
background-color: white;
box-sizing: border-box;
}
.network-list table {
width: 100%;
border-collapse: collapse;
table-layout: auto;
box-sizing: border-box;
}
.network-list th, .network-list td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
white-space: nowrap;
box-sizing: border-box;
}
.network-list th {
background-color: #f2f2f2;
}
.network-list tr.selected {
background-color: #d1e7dd;
}
.status {
margin-top: 20px;
font-size: 18px;
color: #555;
}
.home-btn {
position: absolute;
top: 20px;
left: 20px;
padding: 10px 20px;
background-color: #6c757d;
color: white;
text-decoration: none;
border-radius: 5px;
}
.home-btn:hover {
background-color: #5a6268;
}
.placeholder-img {
position: absolute;
top: 20px;
right: 20px;
}
.placeholder-img img {
top: 20px;
width: 70px;
height: 70px;
}
.header-img {
margin-top: 20px; /* Adjust the margin as needed */
margin-bottom: 30px;
width: auto;
height: 80px; /* Adjust the height as needed */
position: absolute;
left: 30px;
top: 60px;
}
button:disabled {
opacity: 0.5; /* Makes the button look visually disabled */
cursor: not-allowed; /* Changes the cursor to indicate it's disabled */
}
details {
width: 100%;
margin-bottom: 20px; /* Adds space between details sections */
} }
a{ color: inherit; }
.details-container { /* Navbar inject target */
width: 80%; /* Set container width to keep content centered */ #navbar { position: sticky; top:0; z-index: 100; backdrop-filter: blur(6px); }
margin: 0 auto;
} .wrapper{ max-width: 1100px; margin: 0 auto; padding: 24px 16px 56px; }
summary {
font-size: 1.3em; /* Header */
font-weight: bold; .page-header{ display:flex; align-items:center; gap:16px; justify-content:flex-start; margin: 4px 0 12px; }
cursor: pointer; .page-header .header-img{ width:auto; height:72px; margin:0; position:static !important; filter: drop-shadow(0 4px 14px rgba(0,0,0,.4)); }
display: inline-block; h1{ font-size: clamp(22px, 3vw, 32px); font-weight: 700; letter-spacing:.2px; margin:0; }
width: 250px; /* Set a fixed width for alignment */ .subtitle{ opacity:.9; font-size:.95rem; margin-top:6px; }
text-align: left; /* Ensures left alignment of the text */
white-space: nowrap; /* Prevents wrapping */ /* Cards */
} details.card{ background: var(--card); border: 1px solid rgba(255,255,255,.06); border-radius: var(--radius);
summary::before { overflow:hidden; box-shadow: 0 10px 30px rgba(0,0,0,.35); margin-bottom: 18px; }
content: "▼ "; /* Bullet symbol */ details.card[open]{ box-shadow: 0 16px 38px rgba(0,0,0,.45); }
color: black; /* Bullet color */ details.card > summary{ list-style:none; display:flex; align-items:center; gap:10px; cursor:pointer;
margin-right: 8px; /* Space between bullet and text */ padding: 14px 16px; background: linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.00)); font-weight:700; }
font-size: 1.3em; /* Matches summary font size */ details.card > summary::-webkit-details-marker{ display:none; }
} .chev{ width: 10px; height:10px; border-right:2px solid var(--muted); border-bottom:2px solid var(--muted);
.details-content { transform: rotate(-45deg); transition: transform .18s ease; margin-right: 4px; opacity:.9; }
display: flex; details.card[open] > summary .chev{ transform: rotate(45deg); }
flex-direction: column; .card-body{ padding: 14px 16px 16px; display:grid; gap:18px; background: var(--panel); border-top: 1px solid rgba(255,255,255,.06); }
align-items: center;
text-align: center; /* Actions / Buttons */
.actions{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
.actions .small-btn-group{ margin-left:auto; display:flex; gap:10px; flex-wrap:wrap; }
button.btn{ background: var(--primary); color:white; border:1px solid rgba(255,255,255,.08); border-radius: 10px;
padding: 10px 14px; font-weight:700; letter-spacing:.2px; cursor:pointer;
transition: transform .06s ease, background .15s ease, box-shadow .15s ease; min-width:140px; }
button.btn:hover{ background: var(--primary-700); }
button.btn:active{ transform: translateY(1px); }
button.btn[disabled]{ opacity:.5; cursor:not-allowed; }
button.btn.success{ background: var(--ok); }
button.btn.warn{ background: var(--warn); color:#0b0d10; }
button.btn.alt{ background: #64748b; }
/* Status badges/text */
.status{ font-size: 1rem; opacity:.95; }
.status.ok{ color: var(--ok); }
.status.err{ color: var(--err); }
.status.warn{ color: var(--warn); }
/* Table */
.network-list{ width:100%; max-height: 320px; overflow:auto; border:1px solid rgba(255,255,255,.08);
background:#0f141a; border-radius: 12px; }
table#networksTable{ width:100%; border-collapse: separate; border-spacing: 0; }
#networksTable thead th{
position: sticky; top:0; background: #0f1620; color:#dbe7f3;
text-align:left; padding:10px 12px; font-weight:700; border-bottom:1px solid rgba(255,255,255,.08);
} }
#networksTable tbody td{ padding:10px 12px; border-bottom:1px solid rgba(255,255,255,.06); white-space: nowrap; }
#networksTable tbody tr{ cursor:pointer; }
#networksTable tbody tr:hover{ background: rgba(59,130,246,.08); }
#networksTable tbody tr.selected{ background: rgba(34,197,94,.18); }
/* Grid helpers */
.grid-2{ display:grid; grid-template-columns: 1fr; gap: var(--gap); }
@media (min-width: 900px){ .grid-2{ grid-template-columns: 1.2fr .8fr; } }
/* Keep original IDs styling overrides (safely) */
#label-status{ display:block; margin-top: 6px; }
#scanStatus{ min-height: 20px; }
</style> </style>
</head> </head>
<body> <body>
<div id="navbar"></div> <div id="navbar"></div>
<script> <script>
fetch('/static/html/nav.html') fetch('/static/html/nav.html').then(r=>r.text()).then(html=>{ document.getElementById('navbar').innerHTML = html; });
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
</script> </script>
<div class="content-wrapper"> <main class="wrapper">
<div class="content"> <header class="page-header">
<img src="/static/images/internet_icon.png" alt="internet" class="header-img" />
<div>
<h1>WiFi Internet Access</h1> <h1>WiFi Internet Access</h1>
<img src="/static/images/internet_icon.png" alt="internet" class="header-img"> <div class="subtitle"><span id="label-status">Status:</span></div>
<label id="label-status">Status:</label> </div>
<div class="button-container"> </header>
<button class="btn" id="scanButton" onclick="scanWifi()">Scan</button>
<div class="small-btn-container"> <!-- Networks card -->
<button class="btn small-btn" id="forgetButton" onclick="checkForgetNetworks()">Forget all</button> <details class="card" open>
<button class="btn small-btn" id="testButton" onclick="checkInternetAccess()">Test Internet</button> <summary><span class="chev"></span>Networks</summary>
<div class="card-body">
<div class="actions">
<button class="btn" id="scanButton" onclick="scanWifi()"><i class="fa-solid fa-wifi"></i> Scan</button>
<div class="small-btn-group">
<button class="btn alt" id="forgetButton" onclick="checkForgetNetworks()"><i class="fa-regular fa-trash-can"></i> Forget all</button>
<button class="btn success" id="testButton" onclick="checkInternetAccess()"><i class="fa-solid fa-globe"></i> Test Internet</button>
</div> </div>
</div> </div>
<div class="scan-container">
<div id="scanStatus" class="status"></div> <div id="scanStatus" class="status"></div>
</div>
<div class="grid-2">
<div class="network-list"> <div class="network-list">
<table id="networksTable"> <table id="networksTable">
<thead> <thead>
@ -199,50 +136,57 @@
<th>Bssid</th> <th>Bssid</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody></tbody>
<!-- List of networks will be appended here -->
</tbody>
</table> </table>
</div> </div>
<div class="connect-container">
<button class="btn" id="connectToNetworkButton"onclick="connectToNetwork()">Connect</button> <div class="actions" style="align-items:flex-start;">
<button class="btn warn" id="connectToNetworkButton" onclick="connectToNetwork()">
<i class="fa-solid fa-plug"></i> Connect
</button>
</div> </div>
<details> </div>
<summary>Connection Status</summary> </div>
</details>
<!-- Connection status -->
<details class="card">
<summary><span class="chev"></span>Connection Status</summary>
<div class="card-body">
<div class="status" id="connectionStatus"></div> <div class="status" id="connectionStatus"></div>
</div>
</details> </details>
<details>
<summary>Notice</summary> <!-- Notice -->
<label id="label-wifi">*WPA3 is not currently supported. If your hotsport/Access point is setup for WPA3, please change it to WPA2 before trying to connect to it.</label> <details class="card">
<summary><span class="chev"></span>Notice</summary>
<div class="card-body">
<label id="label-wifi">*WPA3 is not currently supported. If your hotspot/Access Point is set for WPA3, please change it to WPA2 before trying to connect.</label>
</div>
</details> </details>
</div> </main>
</div>
</div>
<script> <script>
window.onload = function() { OnPageLoad(); }; window.onload = function() { OnPageLoad(); };
function scanWifi() { function scanWifi() {
console.log("Starting WiFi scan..."); console.log("Starting WiFi scan...");
document.getElementById('connectionStatus').innerText = "" document.getElementById('connectionStatus').innerText = "";
document.getElementById('scanStatus').innerText = "Scanning for networks..."; const ss = document.getElementById('scanStatus');
ss.innerText = "Scanning for networks...";
fetch('/wifi_scan') fetch('/wifi_scan')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
console.log("Scan complete. Data received:", data); console.log("Scan complete. Data received:", data);
document.getElementById('scanStatus').innerText = "Scan complete."; ss.innerText = "Scan complete.";
let tableBody = document.getElementById('networksTable').getElementsByTagName('tbody')[0]; let tableBody = document.getElementById('networksTable').getElementsByTagName('tbody')[0];
tableBody.innerHTML = ''; tableBody.innerHTML = '';
data.networks.forEach(network => { (data.networks || []).forEach(network => {
let row = tableBody.insertRow(); let row = tableBody.insertRow();
let cell1 = row.insertCell(0); row.insertCell(0).textContent = network.ssid || '';
let cell2 = row.insertCell(1); row.insertCell(1).textContent = network.Freq || '';
let cell3 = row.insertCell(2); row.insertCell(2).textContent = network.signal || '';
let cell4 = row.insertCell(3); row.insertCell(3).textContent = network.bssid || '';
cell1.textContent = network.ssid || '';
cell2.textContent = network.Freq || '';
cell3.textContent = network.signal || '';
cell4.textContent = network.bssid || '';
row.addEventListener('click', function() { row.addEventListener('click', function() {
const rows = document.querySelectorAll('#networksTable tbody tr'); const rows = document.querySelectorAll('#networksTable tbody tr');
rows.forEach(r => r.classList.remove('selected')); rows.forEach(r => r.classList.remove('selected'));
@ -252,7 +196,7 @@
}) })
.catch(error => { .catch(error => {
console.error("Error during scan:", error); console.error("Error during scan:", error);
document.getElementById('scanStatus').innerText = "Error during scan."; ss.innerText = "Error during scan.";
}); });
} }
@ -278,9 +222,7 @@
document.getElementById('forgetButton').disabled = true; document.getElementById('forgetButton').disabled = true;
fetch('/wifi_connect', { fetch('/wifi_connect', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json'
},
body: JSON.stringify({ ssid: ssid, bssid: bssid, password: password }) body: JSON.stringify({ ssid: ssid, bssid: bssid, password: password })
}) })
.then(response => response.json()) .then(response => response.json())
@ -294,7 +236,6 @@
document.getElementById('connectionStatus').innerText = "Error during connection attempt."; document.getElementById('connectionStatus').innerText = "Error during connection attempt.";
}) })
.finally(() => { .finally(() => {
// Re-enable buttons after the connection attempt
document.getElementById('connectToNetworkButton').disabled = false; document.getElementById('connectToNetworkButton').disabled = false;
document.getElementById('scanButton').disabled = false; document.getElementById('scanButton').disabled = false;
document.getElementById('testButton').disabled = false; document.getElementById('testButton').disabled = false;
@ -307,11 +248,11 @@
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const statusMessage = data.success ? "Internet Access Ok" : "No Internet Access"; const statusMessage = data.success ? "Internet Access Ok" : "No Internet Access";
alert(statusMessage); // Display message in a popup alert(statusMessage);
}) })
.catch(error => { .catch(error => {
console.error("Error checking internet access:", error); console.error("Error checking internet access:", error);
alert("Error checking internet access."); // Display error in a popup alert("Error checking internet access.");
}); });
} }
@ -320,11 +261,11 @@
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const statusMessage = data.success ? "All networks forgotten" : "Nothing happened"; const statusMessage = data.success ? "All networks forgotten" : "Nothing happened";
alert(statusMessage); // Display message in a popup alert(statusMessage);
}) })
.catch(error => { .catch(error => {
console.error("Error forgetting networks:", error); console.error("Error forgetting networks:", error);
alert("Error forgetting networks."); // Display error in a popup alert("Error forgetting networks.");
}); });
} }
@ -333,7 +274,7 @@
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
console.log("Data received:", data); console.log("Data received:", data);
document.getElementById('label-status').innerText = "Status: " + data.msg; document.getElementById('label-status').innerText = "Status: " + (data.msg || '...');
}) })
.catch(error => { .catch(error => {
console.error("Error getting wifi status:", error); console.error("Error getting wifi status:", error);
@ -341,10 +282,7 @@
}); });
} }
function OnPageLoad(){ function OnPageLoad(){ checkWifiStatus(); }
checkWifiStatus();
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,11 @@
{# Jinja macro for a slide switch component #}
{% macro switch(id='switch', label='Toggle', checked=false, small=false) -%}
<label class="switch" for="{{ id }}">
<span class="switch-label">{{ label }}</span>
<span class="switch-toggle{{ ' small' if small else '' }}" aria-hidden="false">
<input id="{{ id }}" type="checkbox" {{ 'checked' if checked else '' }} />
<span class="slider" aria-hidden="true"></span>
</span>
<span class="visually-hidden">Toggle {{ label }}</span>
</label>
{%- endmacro %}

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +1,162 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico"> <title>Activation Required</title>
<link rel="stylesheet" href="/static/css/styles.css"> <link rel="icon" type="image/x-icon" href="/static/images/favicon.ico" />
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css"> <link rel="stylesheet" href="/static/css/styles.css" />
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css" />
<style> <style>
.activation-message { :root{
text-align: center; --brand: #1e40af; /* blue-800 */
font-size: 1.5em; --brand-hover:#15327f;
color: #ff1493; /* Neon pink/red color */ --danger: #ef4444; /* red-500 */
--danger-ink: #7f1d1d; /* dark red text */
--border: #d1d5db; /* gray-300 */
--text: #111827; /* gray-900 */
--muted: #4b5563; /* gray-600 */
--bg: #ffffff;
--bg-alt: #f9fafb; /* gray-50 */
--focus: #2563eb; /* blue-600 */
}
@media (prefers-color-scheme: dark){
:root{
--brand: #60a5fa;
--brand-hover:#3b82f6;
--danger:#f87171;
--danger-ink:#fecaca;
--border:#374151;
--text:#e5e7eb;
--muted:#9ca3af;
--bg:#0b0f14;
--bg-alt:#111827;
--focus:#60a5fa;
}
} }
.activation-message a { *{ box-sizing: border-box; }
color: blue; /* Blue color for the link */ html, body{ height: 100%; }
body{
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
color: var(--text);
background: var(--bg);
line-height: 1.55;
}
#navbar{ position: sticky; top: 0; z-index: 20; }
.content-wrapper{
max-width: 900px;
margin: 0 auto;
padding: 16px;
}
@media (min-width: 980px){
.content-wrapper{ padding: 24px; }
}
.card{
padding: 20px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--bg-alt);
}
.center{
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 14px;
}
h1{ margin: 0; font-size: clamp(1.5rem, 2vw, 2rem); }
.alert{
width: 100%;
display: grid;
grid-template-columns: auto 1fr;
gap: 12px;
align-items: start;
padding: 12px 14px;
border: 1px solid var(--danger);
border-radius: 10px;
background: color-mix(in srgb, var(--danger) 12%, transparent);
}
.alert i{ color: var(--danger); }
.alert strong{ color: var(--danger-ink); }
.actions{
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
margin-top: 6px;
}
.btn{
appearance: none;
border: 1px solid transparent;
border-radius: 10px;
padding: 10px 16px;
font-size: 1rem;
cursor: pointer;
color: #fff;
background: var(--brand);
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none; text-decoration: none;
} }
.btn:hover{ background: var(--brand-hover); }
.activation-message a:hover { .btn:focus-visible{
text-decoration: underline; outline: 3px solid var(--focus);
outline-offset: 2px;
} }
.activation-message .primary-message { .muted{ color: var(--muted); font-size: 0.96rem; }
margin-bottom: 20px;
font-weight: bold; .sr-only{
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0,0,1,1);
white-space: nowrap; border: 0;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="navbar"></div> <div id="navbar" role="navigation" aria-label="Primary"></div>
<script> <script>
fetch('/static/html/nav.html') fetch('/static/html/nav.html')
.then(response => response.text()) .then(r => r.ok ? r.text() : Promise.reject(r.status))
.then(data => { .then(html => { document.getElementById('navbar').innerHTML = html; })
document.getElementById('navbar').innerHTML = data; .catch(() => {
document.getElementById('navbar').innerHTML =
'<div class="card" role="alert">Navigation failed to load.</div>';
}); });
</script> </script>
<div class="activation-message"> <main class="content-wrapper">
<p class="primary-message">This software is not activated!</p> <section class="card center" aria-labelledby="activation-heading">
<p>Please go to the <a href="/about">about</a> page to activate this software.</p> <h1 id="activation-heading">Activation Required</h1>
<div class="alert" role="alert" aria-live="polite">
<i class="fa fa-triangle-exclamation" aria-hidden="true"></i>
<div>
<p class="primary"><strong>This software is not activated.</strong></p>
<p class="muted">To continue, activate your license from the About page.</p>
</div> </div>
</div>
<div class="actions" role="group" aria-label="Activation actions">
<a class="btn" href="/about">
<i class="fa fa-key" aria-hidden="true"></i>
Go to About to Activate
</a>
</div>
</section>
</main>
</body> </body>
</html> </html>

View File

@ -1,25 +0,0 @@
#!/bin/bash
source /home/orangepi/printio/.venv/bin/activate
# Stop the services
echo "printio stopping..."
sudo systemctl stop printio
#echo "printio_monitor stopping..."
#sudo systemctl stop printio_monitor
echo ""
# Use pyarmor to protect Python scripts in the printio directory
pyarmor gen -O /home/orangepi/printio/src/ /home/orangepi/printio_master/src_master/*.py
# Use rsync to copy files if they are different
rsync -av --progress /home/orangepi/printio_master/static/ /home/orangepi/printio/static/
rsync -av --progress /home/orangepi/printio_master/templates/ /home/orangepi/printio/templates/
rsync -av --progress /home/orangepi/printio_master/data/ /home/orangepi/printio/data/
# Start the services back up
echo ""
sudo systemctl start printio
echo "printio restarted..."
#sudo systemctl start printio_monitor
#echo "printio_monitor restarted..."