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{
--brand: #1e40af; /* blue-800 */
--brand-hover: #15327f;
--warn: #f59e0b; /* amber-500 */
--warn-hover:#d97706;
--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;
}
}
.header-img { *{ box-sizing: border-box; }
margin-top: 30px; /* Adjust the margin as needed */ html, body{ height: 100%; }
margin-bottom: 30px; body{
width: auto; margin: 0;
height: 70px; /* Adjust the height as needed */ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
} color: var(--text);
.center-content { background: var(--bg);
display: flex; line-height: 1.55;
flex-direction: column; }
align-items: center;
}
h1 {
margin-bottom: 5px;
}
label, select, #media-sizes, button { #navbar{ position: sticky; top: 0; z-index: 20; }
margin: 5px 0;
text-align: center;
font-size: larger;
font-weight: 200;
}
select { .content-wrapper{
padding: 3px; max-width: 900px;
font-size: 1.1em; margin: 0 auto;
width: 250px; padding: 16px;
} }
@media (min-width: 980px){
.content-wrapper{ padding: 24px; }
}
#media-sizes ul { .card{
list-style-type: none; padding: 20px;
padding: 0; border: 1px solid var(--border);
} border-radius: 12px;
background: var(--bg-alt);
}
#media-sizes li { .center-content{
display: flex; display: flex;
align-items: center; flex-direction: column;
margin-bottom: 5px; align-items: center;
} text-align: center;
gap: 14px;
}
#media-sizes input[type="checkbox"] { h1{ margin: 0; font-size: clamp(1.5rem, 2vw, 2rem); }
transform: scale(1.5); .muted{ color: var(--muted); }
margin-right: 12px;
}
#media-sizes span { .header-img{
font-size: 1.2em; margin: 10px 0 6px;
} width: auto;
height: 72px;
}
button { .actions{
background-color: red; display: flex;
color: white; gap: 12px;
padding: 10px 20px; flex-wrap: wrap;
border: none; justify-content: center;
border-radius: 5px; margin-top: 8px;
font-size: 1.2em; }
cursor: pointer;
width: 150px;
margin-bottom: 30px;
}
button:hover { button{
background-color: darkblue; appearance: none;
} border: 1px solid transparent;
</style> border-radius: 10px;
padding: 10px 16px;
font-size: 1rem;
cursor: pointer;
color: #fff;
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;
}
.spinner{
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>
</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 =
</script> '<div class="card" role="alert">Navigation failed to load.</div>';
});
</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.
</div> </p>
</div>
</body>
<script> <div class="actions" role="group" aria-label="Power controls">
$(document).ready(function() { <button type="button" id="reboot-button" class="btn-warn">
<i class="fa fa-rotate-right" aria-hidden="true"></i>
$('#reboot-button').click(function() { Reboot
$.post('/reboot', function(response) { </button>
alert("Reboot initiated"); <button type="button" id="shutdown-button" class="btn-danger">
}).fail(function() { <i class="fa fa-power-off" aria-hidden="true"></i>
alert("Reboot failed"); Shut Down
}); </button>
</div>
<div id="status" class="status" role="status" aria-live="polite"></div>
</section>
</main>
<script>
const rebootBtn = document.getElementById('reboot-button');
const shutdownBtn = document.getElementById('shutdown-button');
const statusEl = document.getElementById('status');
function setBusy(btn, busy){
btn.disabled = busy;
if (busy) btn.setAttribute('aria-busy', 'true'); else btn.removeAttribute('aria-busy');
}
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.');
}
}
$('#shutdown-button').click(function() { rebootBtn?.addEventListener('click', async () => {
$.post('/shutdown', function(response) { const confirmed = confirm('Reboot now? All services will restart.');
alert("Shutdown initiated"); if (!confirmed) return;
}).fail(function() { setBusy(rebootBtn, true);
alert("Shutdown failed"); 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);
}); });
</script>
</html> shutdownBtn?.addEventListener('click', async () => {
const confirmed = confirm('Shut down now? The device will power off.');
if (!confirmed) return;
setBusy(rebootBtn, true);
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>
</body>
</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;
}
#media-sizes ul { h1 { margin: 0 0 16px; font-size: clamp(1.5rem, 2vw, 2rem); }
list-style-type: none; h3 { margin: 0 0 12px; font-size: 1.125rem; }
padding: 0;
}
#media-sizes li { p { margin: 0 0 12px; }
display: flex;
align-items: center;
margin-bottom: 5px;
}
#media-sizes input[type="checkbox"] { a { color: var(--brand); text-decoration: underline; text-underline-offset: 2px; }
transform: scale(1.5); a:hover { color: var(--brand-hover); }
margin-right: 12px;
}
#media-sizes span { .content-wrapper{
font-size: 1.2em; display: grid;
} grid-template-columns: 1fr;
gap: 20px;
max-width: 1100px;
margin: 0 auto;
padding: 16px;
}
button { @media (min-width: 840px){
background-color: blue; .content-wrapper{
color: white; grid-template-columns: 300px 1fr;
padding: 10px 20px; gap: 24px;
border: none; padding: 24px;
border-radius: 5px; }
font-size: 1.2em; .left-column{
cursor: pointer; position: sticky;
} top: 72px; /* keep below navbar */
align-self: start;
}
}
button:hover { .left-column, .right-column { width: 100%; }
background-color: darkblue;
}
p { .card{
display: block; padding: 16px;
margin-top: 1em; border: 1px solid var(--border);
margin-bottom: 1em; border-radius: 10px;
margin-left: 0; background: var(--bg-alt);
margin-right: 0; }
} .info-box{ margin-bottom: 16px; }
.license-section { .kv p{
margin-top: 30px; display: flex;
text-align: center; 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-status { .license-section{
font-size: 1.1em; margin-top: 20px;
margin-bottom: 15px; text-align: left;
} }
.license-status{ font-size: 1rem; margin: 0 0 10px; }
.license-input { .field{
margin-top: 10px; display: grid;
text-align: left; grid-template-columns: 140px 1fr auto;
} gap: 10px;
align-items: center;
margin: 10px 0;
}
@media (max-width: 520px){
.field{ grid-template-columns: 1fr; }
.field label{ margin-bottom: -4px; }
}
.license-input input { input[type="text"], input[type="password"]{
padding: 10px; padding: 10px 12px;
width: 300px; font-size: 1rem;
font-size: 1.1em; border: 1px solid var(--border);
margin-right: 10px; border-radius: 8px;
} background: var(--bg);
color: var(--text);
}
input[readonly]{
background: rgba(0,0,0,0.03);
}
.license-input button { button{
background-color: #4CAF50; appearance: none;
padding: 10px 20px; border: none;
font-size: 1.1em; border-radius: 8px;
cursor: pointer; padding: 10px 16px;
border-radius: 5px; font-size: 1rem;
border: none; 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);
}
.license-input button:hover { .actions{
background-color: #45a049; display: flex;
} gap: 10px;
flex-wrap: wrap;
}
/* New Styles for Layout */ .qr{
.content-wrapper { width: 140px;
display: flex; max-width: 40vw;
} height: auto;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
}
.left-column { .muted{ color: var(--muted); font-size: 0.95rem; }
width: 280px; </style>
padding-right: 20px; <script src="/static/js/crypto-js.min.js" defer></script>
box-sizing: border-box;
}
.right-column {
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>
<script src="/static/js/crypto-js.min.js"></script>
</head> </head>
<body> <body>
<div id="navbar"></div> <div id="navbar" role="navigation" aria-label="Primary"></div>
<script>
fetch('/static/html/nav.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
</script>
<div class="content-wrapper"> <script>
<div class="left-column"> // Load navbar with basic error handling
<div class="info-box"> fetch('/static/html/nav.html')
<h3>System Info</h3> .then(r => r.ok ? r.text() : Promise.reject(r.status))
<p>Version: {{info.software_version}} .then(html => { document.getElementById('navbar').innerHTML = html; })
<p>CPU %: {{info.cpu}}</p> .catch(() => { document.getElementById('navbar').innerHTML = '<div class="card" role="alert">Navigation failed to load.</div>'; });
<p>CPU T: {{info.cpu_t}}</p> </script>
<p>Disk Size: {{info.disk_size}}</p>
<p>Disk Used: {{info.disk_used}}</p>
<p>RAM Size: {{info.ram_size}}</p>
<p>RAM Used: {{info.ram_used}}</p>
<p>Up Time: {{info.uptime}}</p>
</div>
<div class="license-box"> <main class="content-wrapper">
<h3>License Info </h3> <!-- Sidebar -->
<p>License: {{info.license}}</p> <aside class="left-column" aria-label="System and License Information">
<!--p>Image Magic: {{info.image_magic}}</p--> <section class="card info-box" aria-labelledby="sysinfo-heading">
<!--p>Hashtag: {{info.hash}}</p--> <h3 id="sysinfo-heading">System Info</h3>
<!--p>Drop Folder: {{info.drop_folder}}</p--> <div class="kv">
</div> <p><span>Version</span><span>{{info.software_version}}</span></p>
<p><span>CPU %</span><span>{{info.cpu}}</span></p>
<p><span>CPU Temp</span><span>{{info.cpu_t}}</span></p>
<p><span>Disk Size</span><span>{{info.disk_size}}</span></p>
<p><span>Disk Used</span><span>{{info.disk_used}}</span></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="right-column"> <section class="card" aria-labelledby="licenseinfo-heading">
<h1>About Printio</h1> <h3 id="licenseinfo-heading">License Info</h3>
<p> <p class="muted">Status: <strong>{{info.license}}</strong></p>
Printio is a professional printing solution designed for seamless <!-- Optional fields kept commented
printing from iPads. It is particularly popular in the photobooth <p>ImageMagick: {{info.image_magic}}</p>
industry, supporting a wide range of sublimation printers. Printio <p>Drop Folder: {{info.drop_folder}}</p>
is compatible with well-known brands such as DNP, Sinfonia, Hiti, -->
and Mitsubishi, as well as many other printers, including Zebra label </section>
printers, and most HP and Epson models. This versatility makes Printio </aside>
an ideal choice for diverse printing needs.
</p>
<p>
Warning: Unauthorized reproduction or distribution of this product
is strictly prohibited and may result in severe civil and criminal
penalties.
</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;">
<!-- Main -->
<section class="right-column">
<h1>About Printio</h1>
<p>
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.
</p>
<p class="muted">
Warning: Unauthorized reproduction or distribution of this product is strictly prohibited and may result in civil and criminal penalties.
</p>
<div class="license-section" {{info.hidden}}> <p>
<div class="license-input"> <a href="https://www.ataphotobooths.com/knowledge-base/category/printio-setup/" target="_blank" rel="noopener noreferrer">
<label>__________________________________________________________</label> Printio Help: Click me!
</div> </a>
<div class="license-input"> </p>
<label for="activation-serial">ID Code:</label>
<input type="text" id="idcode" value={{info.idcode}} readonly> <figure aria-label="Printio Help QR Code" style="margin: 16px 0;">
<button onclick="copyToClipboard()">Copy</button> <img class="qr" src="/static/images/printio_help_qr.jpg" alt="Scan for Printio setup help" />
</div> </figure>
<div class="license-input">
<label for="activation-serial">SerialNo</label> <!-- License Section (conditionally hidden) -->
<input type="password" id="license-password" placeholder="Enter Activation Key"> <section class="license-section card" {{info.hidden}} aria-labelledby="license-activation-heading">
<button onclick="activateLicense()">Activate</button> <h3 id="license-activation-heading">License Activation</h3>
</div> <p class="license-status muted" id="license-status" aria-live="polite"></p>
</div>
<div class="field">
<label for="idcode">ID Code</label>
<input type="text" id="idcode" value="{{info.idcode}}" readonly aria-readonly="true" />
<div class="actions">
<button type="button" id="copy-btn" class="btn-ok" aria-describedby="copy-hint">
<i class="fa fa-copy" aria-hidden="true"></i> Copy
</button>
</div>
</div> </div>
</div> <p class="muted" id="copy-hint">Copies your ID Code to the clipboard.</p>
<script> <div class="field">
function encryptPassword(password) { <label for="license-password">Activation Key</label>
// Simple encryption using CryptoJS (you can replace with your method) <input type="password" id="license-password" placeholder="Enter Activation Key" autocomplete="one-time-code" />
return password; <div class="actions">
<button type="button" id="activate-btn">
<i class="fa fa-bolt" aria-hidden="true"></i> Activate
</button>
</div>
</div>
</section>
</section>
</main>
<script>
// Optional: simple (placeholder) hashing before sending; replace with real logic if needed.
function hashPass(plain) {
try {
// Example using CryptoJS if you choose to enable hashing later:
// return CryptoJS.SHA256(plain).toString();
return plain; // currently pass-through; replace when backend expects a hash
} catch {
return plain;
}
}
function setStatus(msg, ok = true){
const el = document.getElementById('license-status');
if (!el) return;
el.textContent = msg || '';
el.style.color = ok ? 'inherit' : '#dc2626'; // red-600 in light; acceptable in dark
}
async function activateLicense() {
const input = document.getElementById('license-password');
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 {
setStatus('Activation failed. Please verify your key and try again.', false);
alert('Activation failed. Try again.');
} }
} catch (err) {
console.error('Activation error:', err);
setStatus('Error during activation attempt.', false);
alert('Error during activation attempt.');
}
}
function activateLicense() { async function copyToClipboard() {
const password = document.getElementById('license-password').value; const el = document.getElementById('idcode');
const encryptedPassword = encryptPassword(password); const btn = document.getElementById('copy-btn');
fetch('/activate', { if (!el) return alert('No ID Code field found.');
method: 'POST', const text = el.value || el.textContent || '';
headers: { if (!text) return alert('Nothing to copy.');
'Content-Type': 'application/json'
}, try {
body: JSON.stringify({ pass: encryptedPassword }) if (navigator.clipboard?.writeText) {
}) await navigator.clipboard.writeText(text);
.then(response => response.json()) } else {
.then(data => { // Fallback
console.log("Activation result:", data); const ta = document.createElement('textarea');
if (data.reply == true) { ta.value = text;
alert("Activation Successful!!!"); ta.style.position = 'fixed';
window.location.href = "/about"; // Redirect to /about page ta.style.opacity = '0';
} else { document.body.appendChild(ta);
alert("Activation Failed.... Try Again"); ta.select();
} document.execCommand('copy');
}) document.body.removeChild(ta);
.catch(error => {
console.error("Error during activation attempt:", error);
alert("Error during activation attempt.");
});
} }
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);
function copyToClipboard() { // Allow Enter key to activate from password field
const idCode = document.getElementById('idcode'); document.getElementById('license-password')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') activateLicense();
// Check if the element exists });
if (!idCode) { </script>
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 {
// Create a temporary textarea for copying
const tempTextarea = document.createElement('textarea');
tempTextarea.value = textToCopy;
tempTextarea.style.position = 'fixed'; // Prevent scrolling issues
tempTextarea.style.opacity = '0'; // Make it invisible
document.body.appendChild(tempTextarea);
tempTextarea.focus();
tempTextarea.select();
// Attempt to copy
const successful = document.execCommand('copy');
document.body.removeChild(tempTextarea);
if (successful) {
console.log('Fallback copy successful');
//alert('ID code copied to clipboard (using fallback)');
} else {
console.warn('Fallback copy failed: execCommand returned false');
alert('Failed to copy to clipboard: Copy command unsuccessful');
}
} catch (err) {
console.error('Fallback copy error:', err);
alert('Failed to copy to clipboard: ' + err.message);
}
}
</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>
.header-img { :root{
margin-top: 30px; --brand: #1e40af; /* blue-800 */
margin-bottom: 30px; --brand-hover: #15327f;
width: auto; --danger: #ef4444; /* red-500 */
height: 100px; --ok: #16a34a; /* green-600 */
} --ok-hover: #118039;
h2 { --border: #d1d5db; /* gray-300 */
margin-top: 0; --text: #111827; /* gray-900 */
font-size: 24px; --muted: #4b5563; /* gray-600 */
color: #565656; --bg: #ffffff;
} --bg-alt: #f9fafb; /* gray-50 */
table { --focus: #2563eb; /* blue-600 */
border-collapse: collapse; }
width: 100%; @media (prefers-color-scheme: dark){
margin-top: 20px; :root{
box-sizing: border-box; --brand: #60a5fa;
} --brand-hover: #3b82f6;
th, td { --danger: #f87171;
border: 1px solid #ccc; --ok: #34d399;
padding: 10px; --ok-hover: #10b981;
text-align: left; --border: #374151;
box-sizing: border-box; --text: #e5e7eb;
} --muted: #9ca3af;
th { --bg: #0b0f14;
background-color: #f8f8f8; --bg-alt: #111827;
} --focus: #60a5fa;
.button-red { }
padding: 5px 10px; }
background-color: #f44336;
color: white; *{ box-sizing: border-box; }
border: none; html, body{ height: 100%; }
border-radius: 5px; body{
cursor: pointer; margin: 0;
} font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
.button-test { color: var(--text);
padding: 5px 10px; background: var(--bg);
background-color: #4CAF50; line-height: 1.55;
color: white; }
border: none;
border-radius: 5px; #navbar{ position: sticky; top: 0; z-index: 20; }
cursor: pointer;
} h1, h2{ margin: 0 0 12px; }
.placeholder-img { h1{ font-size: clamp(1.5rem, 2vw, 2rem); }
position: absolute; h2{ font-size: clamp(1.1rem, 1.6vw, 1.4rem); color: var(--muted); }
top: 20px;
right: 20px; .content-wrapper{
} max-width: 1100px;
.placeholder-img img { margin: 0 auto;
top: 20px; padding: 16px;
width: 60px; }
height: 60px; @media (min-width: 980px){
} .content-wrapper{ padding: 24px; }
/* Styles for the message log */ }
#messageLog {
display: none; .card{
margin-top: 20px; padding: 16px;
width: 100%; border: 1px solid var(--border);
height: 350px; border-radius: 10px;
box-sizing: border-box; background: var(--bg-alt);
resize: none; }
overflow: auto;
white-space: nowrap; .header-img{
} margin: 24px 0;
.toggle-button { width: auto;
margin-top: 20px; height: 100px;
background-color: #007BFF; }
color: white;
border: none; /* Switch row */
padding: 3px 20px; .switch-row{
cursor: pointer; display: flex;
border-radius: 5px; align-items: center;
} justify-content: center;
</style> 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{
border-collapse: collapse;
width: 100%;
min-width: 720px;
}
thead th{
position: sticky;
top: 0;
background: var(--bg-alt);
color: var(--text);
border-bottom: 1px solid var(--border);
text-align: left;
padding: 10px;
font-weight: 600;
}
tbody td{
border-top: 1px solid var(--border);
padding: 10px;
vertical-align: middle;
}
tbody tr:hover{
background: rgba(0,0,0,0.03);
}
.actions{
display: inline-flex;
gap: 8px;
flex-wrap: wrap;
}
/* Job history */
details{
margin-top: 16px;
}
summary{
list-style: none;
cursor: pointer;
user-select: none;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--brand);
}
summary::-webkit-details-marker{ display: none; }
.log{
margin-top: 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg);
padding: 12px;
max-height: 360px;
overflow: auto;
}
.log pre{
margin: 0;
white-space: pre;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-size: 0.9rem;
line-height: 1.4;
}
.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>
</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">
<tr>
<th>Printer Name</th>
<th>Status</th>
<th>Prints Left</th>
<th>Actions</th>
</tr>
{% for printer in printers %}
<tr>
<td>{{ printer.name }}</td>
<td>{{ printer.state }}</td>
<td>{{ printer.prints_left_message }}</td>
<td>
<form action="{{ url_for('cancel_all_jobs', printer_name=printer.name) }}" method="post" style="display:inline;">
<button type="submit" class="button-red">Kill Jobs</button>
</form>
<form action="{{ url_for('test_print', printer_name=printer.name) }}" method="post" style="display:inline;">
<button type="submit" class="button-test">Test Print</button>
</form>
</td>
</tr>
{% endfor %}
</table>
<!-- Button to toggle the message log --> <div class="switch-row">
<button class="toggle-button" onclick="toggleMessageLog()">Job History</button> <label class="switch-label" for="printOnConnect">Print on Connect</label>
<!-- Hidden textarea for the message log --> <input
<textarea id="messageLog" placeholder="Job Status Log..." readonly></textarea> 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>
<th scope="col">Printer Name</th>
<th scope="col">Status</th>
<th scope="col">Prints Left</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for printer in printers %}
<tr>
<td>{{ printer.name }}</td>
<td>{{ printer.state }}</td>
<td>{{ printer.prints_left_message }}</td>
<td>
<div class="actions">
<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 action="{{ url_for('test_print', printer_name=printer.name) }}" method="post">
<button type="submit" class="btn-ok">
<i class="fa fa-print" aria-hidden="true"></i> Test Print
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
<script> <details id="jobHistory">
// Toggles the visibility of the message log textarea <summary>
function toggleMessageLog() { <i class="fa fa-history" aria-hidden="true"></i>
var messageLog = document.getElementById('messageLog'); Job History
if (messageLog.style.display === '' || messageLog.style.display === 'none') { </summary>
messageLog.style.display = 'block'; <div class="log" id="messageLog" aria-live="off">
loadPrinterJobs(); // Load job messages when showing the textarea <pre id="logText">Loading…</pre>
} else { </div>
messageLog.style.display = 'none'; </details>
} </section>
} </main>
// Load the printer job information and show in the textarea <script>
function loadPrinterJobs() { /**
var messageLog = document.getElementById('messageLog'); * Initialize the "Print on Connect" checkbox behavior.
var jobs = {{ job_history | tojson | safe }}; */
var logText = ""; function initPrintOnConnect() {
const checkbox = document.getElementById('printOnConnect');
jobs.forEach(function(job, index) { const errorDiv = document.getElementById('printOnConnectError');
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";
});
// Set the value of the textarea to display the job information if (!checkbox) return;
messageLog.value = logText;
} checkbox.addEventListener('change', () => {
</script> 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 job history on demand.
*/
function loadPrinterJobs() {
const jobs = {{ job_history | tojson | safe }};
const pre = document.getElementById('logText');
if (!pre) return;
if (!Array.isArray(jobs) || jobs.length === 0) {
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}`;
});
pre.textContent = lines.join('\n');
}
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>
</body> </body>
</html> </html>

View File

@ -1,350 +1,288 @@
<!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;
.button-container { --primary: #3b82f6;
display: flex; --primary-700: #1d4ed8;
justify-content: space-between; --ring: rgba(59,130,246,.35);
align-items: center; --radius: 14px;
margin-top: 20px; --gap: 16px;
} --ok: #22c55e;
.btn { --warn: #f59e0b;
padding: 10px 20px; --err: #ef4444;
font-size: 1.2em; }
cursor: pointer; html, body { height:100%; }
background-color: blue; body{
color: white; margin:0; color:var(--muted);
width: 150px; background: linear-gradient(180deg, #0b0d10 0%, #0e1217 100%);
border-radius: 5px; font: 500 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, sans-serif;
border: none; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
text-decoration: none; }
} a{ color: inherit; }
.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 */
}
.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; }
</style> .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>
</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()) </script>
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
</script>
<div class="content-wrapper"> <main class="wrapper">
<div class="content"> <header class="page-header">
<h1>WiFi Internet Access</h1> <img src="/static/images/internet_icon.png" alt="internet" class="header-img" />
<img src="/static/images/internet_icon.png" alt="internet" class="header-img"> <div>
<label id="label-status">Status:</label> <h1>WiFi Internet Access</h1>
<div class="button-container"> <div class="subtitle"><span id="label-status">Status:</span></div>
<button class="btn" id="scanButton" onclick="scanWifi()">Scan</button> </div>
<div class="small-btn-container"> </header>
<button class="btn small-btn" id="forgetButton" onclick="checkForgetNetworks()">Forget all</button>
<button class="btn small-btn" id="testButton" onclick="checkInternetAccess()">Test Internet</button> <!-- Networks card -->
</div> <details class="card" open>
</div> <summary><span class="chev"></span>Networks</summary>
<div class="scan-container"> <div class="card-body">
<div id="scanStatus" class="status"></div> <div class="actions">
</div> <button class="btn" id="scanButton" onclick="scanWifi()"><i class="fa-solid fa-wifi"></i> Scan</button>
<div class="network-list"> <div class="small-btn-group">
<table id="networksTable"> <button class="btn alt" id="forgetButton" onclick="checkForgetNetworks()"><i class="fa-regular fa-trash-can"></i> Forget all</button>
<thead> <button class="btn success" id="testButton" onclick="checkInternetAccess()"><i class="fa-solid fa-globe"></i> Test Internet</button>
<tr> </div>
<th>SSID</th>
<th>Freq</th>
<th>Signal</th>
<th>Bssid</th>
</tr>
</thead>
<tbody>
<!-- List of networks will be appended here -->
</tbody>
</table>
</div>
<div class="connect-container">
<button class="btn" id="connectToNetworkButton"onclick="connectToNetwork()">Connect</button>
</div>
<details>
<summary>Connection Status</summary>
<div class="status" id="connectionStatus"></div>
</details>
<details>
<summary>Notice</summary>
<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>
</div>
</div> </div>
</div>
<script>
window.onload = function() { OnPageLoad(); };
function scanWifi() { <div id="scanStatus" class="status"></div>
console.log("Starting WiFi scan...");
document.getElementById('connectionStatus').innerText = ""
document.getElementById('scanStatus').innerText = "Scanning for networks...";
fetch('/wifi_scan')
.then(response => response.json())
.then(data => {
console.log("Scan complete. Data received:", data);
document.getElementById('scanStatus').innerText = "Scan complete.";
let tableBody = document.getElementById('networksTable').getElementsByTagName('tbody')[0];
tableBody.innerHTML = '';
data.networks.forEach(network => {
let row = tableBody.insertRow();
let cell1 = row.insertCell(0);
let cell2 = row.insertCell(1);
let cell3 = row.insertCell(2);
let cell4 = row.insertCell(3);
cell1.textContent = network.ssid || '';
cell2.textContent = network.Freq || '';
cell3.textContent = network.signal || '';
cell4.textContent = network.bssid || '';
row.addEventListener('click', function() {
const rows = document.querySelectorAll('#networksTable tbody tr');
rows.forEach(r => r.classList.remove('selected'));
row.classList.add('selected');
});
});
})
.catch(error => {
console.error("Error during scan:", error);
document.getElementById('scanStatus').innerText = "Error during scan.";
});
}
function connectToNetwork() { <div class="grid-2">
console.log("Attempting to connect to network..."); <div class="network-list">
const table = document.getElementById('networksTable'); <table id="networksTable">
const selectedRow = table.querySelector('tbody tr.selected'); <thead>
if (!selectedRow) { <tr>
document.getElementById('connectionStatus').innerText = "Please select a network to connect to."; <th>SSID</th>
return; <th>Freq</th>
} <th>Signal</th>
const ssid = selectedRow.cells[0].textContent; <th>Bssid</th>
const bssid = selectedRow.cells[3].textContent; </tr>
const password = prompt("Enter the WiFi password for " + ssid + ":"); </thead>
if (!password) { <tbody></tbody>
document.getElementById('connectionStatus').innerText = "Connection cancelled."; </table>
return; </div>
}
document.getElementById('connectionStatus').innerText = "Please wait...";
document.getElementById('connectToNetworkButton').disabled = true;
document.getElementById('scanButton').disabled = true;
document.getElementById('testButton').disabled = true;
document.getElementById('forgetButton').disabled = true;
fetch('/wifi_connect', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ssid: ssid, bssid: bssid, password: password })
})
.then(response => response.json())
.then(data => {
console.log("Connection attempt result:", data);
document.getElementById('connectionStatus').innerText = data.message;
checkWifiStatus();
})
.catch(error => {
console.error("Error during connection attempt:", error);
document.getElementById('connectionStatus').innerText = "Error during connection attempt.";
})
.finally(() => {
// Re-enable buttons after the connection attempt
document.getElementById('connectToNetworkButton').disabled = false;
document.getElementById('scanButton').disabled = false;
document.getElementById('testButton').disabled = false;
document.getElementById('forgetButton').disabled = false;
});
}
function checkInternetAccess() { <div class="actions" style="align-items:flex-start;">
fetch('/wifi_test') <button class="btn warn" id="connectToNetworkButton" onclick="connectToNetwork()">
.then(response => response.json()) <i class="fa-solid fa-plug"></i> Connect
.then(data => { </button>
const statusMessage = data.success ? "Internet Access Ok" : "No Internet Access"; </div>
alert(statusMessage); // Display message in a popup </div>
}) </div>
.catch(error => { </details>
console.error("Error checking internet access:", error);
alert("Error checking internet access."); // Display error in a popup
});
}
function checkForgetNetworks() {
fetch('/wifi_forget')
.then(response => response.json())
.then(data => {
const statusMessage = data.success ? "All networks forgotten" : "Nothing happened";
alert(statusMessage); // Display message in a popup
})
.catch(error => {
console.error("Error forgetting networks:", error);
alert("Error forgetting networks."); // Display error in a popup
});
}
function checkWifiStatus() { <!-- Connection status -->
fetch('/wifi_status') <details class="card">
.then(response => response.json()) <summary><span class="chev"></span>Connection Status</summary>
.then(data => { <div class="card-body">
console.log("Data received:", data); <div class="status" id="connectionStatus"></div>
document.getElementById('label-status').innerText = "Status: " + data.msg; </div>
}) </details>
.catch(error => {
console.error("Error getting wifi status:", error);
document.getElementById('label-status').innerText = "Status: ...";
});
}
function OnPageLoad(){ <!-- Notice -->
checkWifiStatus(); <details class="card">
} <summary><span class="chev"></span>Notice</summary>
<div class="card-body">
</script> <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>
</main>
<script>
window.onload = function() { OnPageLoad(); };
function scanWifi() {
console.log("Starting WiFi scan...");
document.getElementById('connectionStatus').innerText = "";
const ss = document.getElementById('scanStatus');
ss.innerText = "Scanning for networks...";
fetch('/wifi_scan')
.then(response => response.json())
.then(data => {
console.log("Scan complete. Data received:", data);
ss.innerText = "Scan complete.";
let tableBody = document.getElementById('networksTable').getElementsByTagName('tbody')[0];
tableBody.innerHTML = '';
(data.networks || []).forEach(network => {
let row = tableBody.insertRow();
row.insertCell(0).textContent = network.ssid || '';
row.insertCell(1).textContent = network.Freq || '';
row.insertCell(2).textContent = network.signal || '';
row.insertCell(3).textContent = network.bssid || '';
row.addEventListener('click', function() {
const rows = document.querySelectorAll('#networksTable tbody tr');
rows.forEach(r => r.classList.remove('selected'));
row.classList.add('selected');
});
});
})
.catch(error => {
console.error("Error during scan:", error);
ss.innerText = "Error during scan.";
});
}
function connectToNetwork() {
console.log("Attempting to connect to network...");
const table = document.getElementById('networksTable');
const selectedRow = table.querySelector('tbody tr.selected');
if (!selectedRow) {
document.getElementById('connectionStatus').innerText = "Please select a network to connect to.";
return;
}
const ssid = selectedRow.cells[0].textContent;
const bssid = selectedRow.cells[3].textContent;
const password = prompt("Enter the WiFi password for " + ssid + ":");
if (!password) {
document.getElementById('connectionStatus').innerText = "Connection cancelled.";
return;
}
document.getElementById('connectionStatus').innerText = "Please wait...";
document.getElementById('connectToNetworkButton').disabled = true;
document.getElementById('scanButton').disabled = true;
document.getElementById('testButton').disabled = true;
document.getElementById('forgetButton').disabled = true;
fetch('/wifi_connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ssid: ssid, bssid: bssid, password: password })
})
.then(response => response.json())
.then(data => {
console.log("Connection attempt result:", data);
document.getElementById('connectionStatus').innerText = data.message;
checkWifiStatus();
})
.catch(error => {
console.error("Error during connection attempt:", error);
document.getElementById('connectionStatus').innerText = "Error during connection attempt.";
})
.finally(() => {
document.getElementById('connectToNetworkButton').disabled = false;
document.getElementById('scanButton').disabled = false;
document.getElementById('testButton').disabled = false;
document.getElementById('forgetButton').disabled = false;
});
}
function checkInternetAccess() {
fetch('/wifi_test')
.then(response => response.json())
.then(data => {
const statusMessage = data.success ? "Internet Access Ok" : "No Internet Access";
alert(statusMessage);
})
.catch(error => {
console.error("Error checking internet access:", error);
alert("Error checking internet access.");
});
}
function checkForgetNetworks() {
fetch('/wifi_forget')
.then(response => response.json())
.then(data => {
const statusMessage = data.success ? "All networks forgotten" : "Nothing happened";
alert(statusMessage);
})
.catch(error => {
console.error("Error forgetting networks:", error);
alert("Error forgetting networks.");
});
}
function checkWifiStatus() {
fetch('/wifi_status')
.then(response => response.json())
.then(data => {
console.log("Data received:", data);
document.getElementById('label-status').innerText = "Status: " + (data.msg || '...');
})
.catch(error => {
console.error("Error getting wifi status:", error);
document.getElementById('label-status').innerText = "Status: ...";
});
}
function OnPageLoad(){ checkWifiStatus(); }
</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>
:root{
--brand: #1e40af; /* blue-800 */
--brand-hover:#15327f;
--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;
}
}
<style> *{ box-sizing: border-box; }
.activation-message { html, body{ height: 100%; }
text-align: center; body{
font-size: 1.5em; margin: 0;
color: #ff1493; /* Neon pink/red color */ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
} color: var(--text);
background: var(--bg);
line-height: 1.55;
}
.activation-message a { #navbar{ position: sticky; top: 0; z-index: 20; }
color: blue; /* Blue color for the link */
text-decoration: none;
}
.activation-message a:hover { .content-wrapper{
text-decoration: underline; max-width: 900px;
} margin: 0 auto;
padding: 16px;
}
@media (min-width: 980px){
.content-wrapper{ padding: 24px; }
}
.activation-message .primary-message { .card{
margin-bottom: 20px; padding: 20px;
font-weight: bold; border: 1px solid var(--border);
} border-radius: 12px;
</style> 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;
}
.btn:hover{ background: var(--brand-hover); }
.btn:focus-visible{
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.muted{ color: var(--muted); font-size: 0.96rem; }
.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>
</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 =
</script> '<div class="card" role="alert">Navigation failed to load.</div>';
});
</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>
<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 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..."