This commit is contained in:
admin 2025-10-29 19:07:51 -07:00
parent b75ffdb4f8
commit 9f30122846
29 changed files with 1086 additions and 104 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

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,3 +1,3 @@
# Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-10-27T16:50:17.383794 # Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-10-29T19:05:40.562157
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\x00e\x03\x00\x00\x12\t\x04\x00\x89\xec\x16\xa6\x1b\xb5\xfc\xed\xdf\xc8 \x8a\xa4b\x01\xcd\x00\x00\x00\x00\x00\x00\x00\x00,\x004\xe5\xcb\xc6\xd5\x84\xf3\x86\x80\xe0P6\x8a}\xba\x1b\xbb\xb74\x8b[Iki6\x1a\xc4\xff\x10\xf1&\xec\xf6\x83\xe7\x06\xd8\xbc\x16\xf4c\xe3\xd3j\xbe\x9b)\xc6\xf0\x98I\x94\x97\x85\x00\xe44\xbd\xdf\x97I,\xa0No=Z\xb2\xb8Lw\x99\x06{\xf8S\x05OcX\x15\xd5\xfc\x81\xae\x8e;R\x89\x06\xb8\xf4h\x025|Q\xf3\x87%>)i$\xa5\xa0}\x9d\'\x9dz\xf2\x97?\xa1\x19M\x91\xa3\'\xaeX\x07dgT\x96?\x8dx\xe3\x14\xaeu\xa4E\\!\xbc\xe8X\xfey\x14\x8c\x0f\xe4Iy\xe9\xccnB\x85&\x8d\xd9\xaa\xd6\xdd\x91\xc2\xe4\xa9\xf6\x12\xca/\xd1\xdag\xbbd\x85\xd2\x1ctE\xb9\xb1\xb9C0\xb5\xb3pn\xb3[\'&\xecD`j\xef\x92M\x98\xfeA\xe2Q\x9b\xc2,\xc3,\x8c\xceW_\xbb|\xcdH]\x0e\xd2\x8a\xe0Q\t\xc7\xcd\x89\xdc\xb5\x18\x8ad\xae\x0eV\xdd\x8fN\x18\x8bQ\x0c\xce\xd7\x1b\xa5\x05\xe9\x01.l\xad\xc8\xf3\xc6\xf4\xa9(Y[tt\xd7\x00\x0e\x0b\x02`\n\xde\xcdT\x994h\x03\xe0ws6:\x10\xdf\x8d\xb8\xedo\x08\xc4\xfd\x19\xcd\x19\x95g\xf0\x1d\xcf\x0b\xfd{\\N\x863\x14\xc6\xfc\xa8wv\xa5\xe1\x80a+j:\xb6\xd4r\xe9g\x10\xf5\x8e\xe4\x10\\\xeea\x83.\x9d\x167\x9fb\xcf8\x9c\xda\xfa\x96\xe8\xc1V_8\x94\xfc\x97\xed\xcd.id\xccq\xaa\\1h\xa7\xc3\x8dm\'ie\xcc\xcex8\x05\xdb\x88\x93Y/\x7f\x9a\xf4\xe2s\xdb\x13\x1eg\x06\xf6\x83\x8b\xd7}I\x19\xea$\x8b\xb0\x91"\x1e\x7f\xa7\'`b\xa7s\xe2\x8d07q\x170\xce\xa4\xde\xa2{^\xb34A~\xc4\x82\xc0x \xcaw^\x92\xeb\xbdI\xc6\x97D\x19}g\x90\xb7\r5d\xb5\xbd\xebuX*\xccC\x9a9\x9f\x02\xf3\xdajG\xb1AdD\x9d`\xd4\x93Z\xfc\xde\xdd\xbe\xea\xd9\xce\xc15}yB\xa7R\xa2^\'\x88\xe2\xe5#-Z\x1d\xa5\xe7.\xeb\x92\xb9\x99%GC\x00?\x90\xc3\xb4\xeb\x8a\\\x9d\x0c^\xe5\xa4>\xa4M\x8b=\x83\xfa\xd4\x9f\xbd\xb2F;%\xe2\xd4\xc3\xd84\x8cJ\xab\xb6\x8fi\x0c\x01i\xbe\xff\xf3\xaep\x15\xa1\xc7\xb7\x18\xa5\x0c\xd8\xd2\xe9H6\xf20\xe7\xda\xda5\x02\xbf&\xa2\xb0S3]\xe2\xf5Nd\x9c\x82\'\x1c\xaf\xe4\xe4`s\xf1\x0e*\xcd`\xa6\x8ea\t\xb4"L\xeb\xd8_.9\xff\x0f\xc8\xb9\x10G\xe1\x19?\xa8\xb1.z\xad\x97\x1aCJ\x8b\xa8X\xee\xb7\xa4Ww\x1b\xb7?\xd3T\x86\xe8P\x15\x02\xc1\x96$\xfa\\\x8c\xbc8\xf1\xb3\xb1F\x0eU%\x83Hh\xa3044\xda\x05~\xd8\x7flI\x9c\xf3\x1b\xb1T\xb6y\\\xd5H\xb0\xf1\x99\xce$\xc5\xf1\xaf\xb1\xea\x94p\x9d\xd0\x8d;\xd0\xb1W =P\x8e\xd0\xfb\xc1@\x80#>a\x0b\x96\xfc\xf2\x19B\xeb\xc0\x9b\x89\xc2\xcd\x15\xbb \xe8\x15&M\x904\xfe.\xecE\xde0\r\xd7+~\xec\x83\xab\x85yu\xb6\xd6\xf9\xf6\xd2P\xa0\x1d\x9d\xca\x948+\xfd\xaf\xbb\x07\x94\xd3\t\xd1\xaa;Y\xf0\xf7n\x0f\xc5\x84\x8c\xf2\xc5!\x0f\xf1\x17D\xe9g\xa6\xeb\x90\xfb\xc1_\x1aO\xefJ\x84[\xd6Y|\xfet\xacP\x08\xb2D\x84\xbc\xda9\x91\xe1 =\x95\xd2aJ\x8b\x8b\xac\xcf\xf5[\xd8w\xfa\x81*\xfb\xb6\x84\x82k\xd0\xe1\xd7B\xd0\xa7\x07s\x98\x10\xac.\x89\x1f\xb3\x8c\xad\xd7\xeb4M\x11\xe9') __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_\x03\x00\x00\x12\t\x04\x00w\x8c\xfb\xac\xfa\x8e\xa0G\x14\xbc\x8e\xc1VY\xcd\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x92\xfa\xe0?\x1a\x1b\x9c\x81\xab\x98\x81\xed<\x87\xee\xfe\x05\xb5\xc1\xd8c\x8e\x95w\x82\x85\x04\xbb\xe8\x9c*q\xf6\xe1\x8a\x932\x13|\xc5\x9bY\x1b\xe8\x17t\xf4\xcf\xe7\x1c1\xb6g \xfb\x0b}\x85\xdd\xa8b\xf3S\xc9U\xe0\xb4O\xa1\xa2BY9e\x14\xf7IP7\x8c\xe0\x85\x0f\x1c\r\xd7\xa1vP:\xfe\x98\x82\x8b\xf2\x84\xde\x10j\x96!ho\xe5\xf8\x02\x8di2(\xfaAU5\xb7\x0bm\x84<s\x0ckR&\x83\xfc\'\xcd\x87\x8cD\xa1\x0b)\xda\x97{\xac\xe5?\xf9\xeb{\x1eZ\xdf\x0bco\xd2\x9a\xb9P\x03\xcc\x15\xdb\xcdi\xc2X\xc3\xeb\x7fQ1\xefSN\xd0\xd3\xa6\xf6\xe1^?\xb2\xad\xdc\x0b)\x9d\xf3\xdf\xc8anO\xa5\x8e\xa9L\x0b\xcc.\xe6\xb8\xe0\x02M\x1e\xdc?2\xdcz\x0c5\xbf;\xc7\xe9\xa72eO\xef\x10\x81\xa3*\xdb,\xc3\x06\xf9\xf7\x11T+\x04J(\xa7\xd1\xf3\x16\x97\xf1I\xca\xbc\x04\xaa\x12B/\x8f\xc7\xc7Yc\x84\x16\xf3\x91\x16[\xd9\xce\xc4\xdf\x97\xe4\xb0A3\xf2:Ti\x08\xc7\xe5\x8a\xa2B\xa8y$\xdb\xff-R{\x92p\xb4\xa7\x9d)$\xd6Z\x04h\x14i\xc3E\x8c\xa4\xc1\x8a@6j\x0b\x84\x06\xdb\xc8l\xdf\x98\xc1]\x17\xbc\xa1\xbc\xabS\xb8\x9d\xff\xd1\\F\xfd\xcc\x9d`l2\x99\xf3\xb0\x19`u\x0b\xf2n\xd4\xd8\xafl\x83v\xd3\xb8\xc1^t\x9a\xf1I\xfb\xe4a\x11p\x8dv^U\x1a\x9c?\xfe9\xf0<r\xf4\xd8\x15\xc4\xaf5{\xcb\x8d\xe60\xf33\td\x83Z\xeb\xf8\xd4\xce\xbe\'\x0f\x1e\xfb\x04\xff\xa3\xeddF\xfa\x08\x1a\xc3)\xc8\xe3Z\xf6\xb6_\x06\xd3\xc9|:\xee\r:\'\xb9kUa\xf0 \xf4\xed1\x0b\xe8\x84\x9aPs\xad\xfeB\xa5$\xa6q\xda?DB\xab\xc3\x89T%\x07O\xccd\xcf\x0b\xf4%\xaf0\xb7.\xef\xee\xa7x\x82BD5\xe7\xf53\xc8\x973\xfd\x00Z\xfa2\xba\x81\xb2\xbf\xe5/\xab\xcd\xadl3\xa2\xcdX\x187\xddQ\xfa\x84\xb7&\xae3\xc69\x7f\x8da\x90]>\xb2)\xc8\x80f\xbd\'\xbbr\x89\xad\xc8-\xd7+\x85-\xfd\x80\xa5\xf4#P\x08IF\xd6A\xe5E\xd3Olz\x96\x0e\xd1\xddD\x15\x8f_\x8eUj\xe9\x18\xe6R*\xa9&\xae#\x9a\xf2\xdd\x05j^\x89\x0f\x02\xdal\x02R\x00\xa4^\xb2)\xa9.\x0c(\x94\xa2\xde\x8cS\x9f?I\x81\x06\xa8F,J9\x8aG\xd9\xc9\x0e\xa8{bz;\xac<\x9c\x95\xc8G\x80=]\xbf1\xa87\xf0\xba}a\xbf\x04~\x13z*\xbc\x9e\xe4\xd2\x8bYI\x80"W\x1a\xac\xf4G\x98\xc92\xcb\xf2f\x06\xb3\x92\xae\x05KM\xe5\x9b\xeb\xd7\xdd\x84v.\x1d\x8f\xcb~7\xac;5\x9bJ\xa5\x86\n\x96C\xe4\xc7|=\x96\x9dS\xbfY\x91e\t2\xf8\xdc\x7f\xfc\xa5\xee\xe1\x8a\xc2\x15\x03iLSe,1\xca\x81l\xeb\xef\xcd3\x13\'\xa7\xa1\x9fpT\x07_(\xc2\xa4\x0f\xbfcFy4\x81\xd7\x03\xbb\xb7}\x8e\x8e\xb6\x818\xfaa\xa2\t\x8d\x82\xd7\xdf\xe8+u\'\x9aQ\xbd\x11h\xe8\xb3}Pv\xbf\xee\x90z!?\x8f\x19\xaa\xe1\xab(Vz!\xb9\x14\xe63\xd4\xcf\x0f\xedA5qa\n\xb1\x05_\x07\xb5\x13\x8a\x14\xa5\xff\xc7\xb9\xcei\xaeA\xb5\xf1\x98G\xa9\x9bi\x16\xe4\xd0{\xc0\xc5\xcc}\xec)\xe7\xb1p\xfc\xf7\x0c\xfez]z\xc7\xfa\xb4I\xd6\xd4\xc0\xf6P')

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-10-27T16:50:16.868581 # Pyarmor 9.0.7 (trial), 000000, 2025-10-29T19:05:39.888707
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-10-27T16:50:17.706277 # Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-10-29T19:05:40.892337
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\xb3i\xd3\xd0\x15\xe3\xc3\xcf\xa3\xf2\xe6\xbd\xefh^-\x00\x00\x00\x00\x00\x00\x00\x00\xf5|\xc4u\xd8\xc0\xe4\xc84@\xdc\x06\xde\xe3\xc9"p\x84\xbd)\xd0\xcbV\x0c\x02\xdf-\x95\x10If\x11\x81\x0f\xf2jrM\xb4`k6\x81\xa3A\xeb\xc3y\x85\x1e\x17\x91\xd6`\xde5{\xca\xfe\xac\x13\xde+\x05\xdaA\xad\xdb\xf4y\xc5\x8f\x17\xc6\x8f\x1d\xcd\x93\\j\xe5-\x902\x02\xeeD\xa1\xac\xf8\x9a\xde\x9e\x9d"UKT2\x04N[\x9a\xa3\xff\x08\x16\xf5\xe8\xc6\\\x12\xc3R\xdd\x9bt\xdf\x0f=\xe06h\x1e1\xff\x95\xe1d\xc5\x05Sn\xb1\xcbG:\x94*i\xbcM\x14\x89t!\xd7 0~\x87\xf1\xcc\xa1\xf4|\x89/\'oS\xe2\xb6\xd1\xba\x198[\xc1\xb8TJ\xa2Y%7\xa4R\'\x00\x16\xd5,\xde\x12\x9bk\xe5\x9c\xa4=\xa8yc\x99\x17AZ\xc0\xc4\x90^\x04\x94wg1L6A\x08\xd8&\xa0\x063\\\x9d\x1e\xaa\xf4;\xb1\xd5\xf0\xc5_\xcch\xc5U\x06\x19\xfbr\xe7\xce\xf4\xe8\xa6\x05{\xba\xd6\xf5\x05/x]DM\xf9\xd8\xed\x06\x10\xaaY\xc1r\xfc\x16\x83\xbaU)4\x95\xa6\xd2\x1a:\x80\xc6\xbb;\xb7\xaa0\\\x97C\x17\xfdC\xbe\xa7\x18\x08I\x9a\xd3|;>V\x85\x13\x82"\x86g\x88\n?;\xfa\x9fiIK\xe6\xaa\xcc\xa6_\x1e\xb1%\x95\xb1\xf2\x15\x990[\xb9H\x0e\xea\xce\xab\xf7c\x8a\xec\xc4\x9d\xe8\xe3\t\xda\xaa\xdeCp\x89q\xb4\xc8\x03\xd7\xaf\x9d\x15xc?@\xff\xa2\x1b\xad .\x800\x82\xf6\xd3\xf1H\x1a@\xbe\xfdU\x90\xdf\xaa\xa4B\x82\xc9\xc5hV\xaa\xbd\x8b\xa8;\xe0\xb7\xef\xc9\ry\xa9\xa7\xc5SdFz\x99|\xafl\x9a\xb4A\x87\x8cb\xd8F\xd09\xf6\x1d z:\'\xa7w\xdf\x08\xb3\xbd\x95O\xf8"\xb53\xb4\xda\x8dt)\xdc\xebE\x88dC\x96\xf1\x03\x96\xf26\xb2\xf3\x19.\x8e\xfb\xdf\xe7\x80\xef!\xca\x0e\x82\x13\x156\xb3\x0b\xf88[\x8d\x8f\x0c0\xb1^\xfd%}C\x06(\x1c$F=\xd3]\xb69P\xcf{\xca\xb7\xca\x08PG \xcf\xb4\x14\xdf\xea^\x1d\x9eYv\x1b\xe5\xcb\xb6\x06\xf7vF\xd0;\x0cV\xa6\xf9)&\xa9e\x84\xa3;\xa4z\xf1\x84\xa5y\x8b-\xff\xa0O\x8fa\x9dV\xc8\xd4Y\xce\xc4\xdb\xe6\xcb}\x00\x9fD5\xd4*\xef\x83C\xdfd \xe3\x89\xcc\xb0oz\x19\'E#\x8b\x92\xc4CZL\xa5T\xb3\x8c\xebd\x95b\x00ia\x9a\xb8\x1ac\xe7\xb9\xac\x11\x0c\xed_\x07\xa1\xb8\\O\n\x92a\xc1GW\xdf\xa1\xbe$\xefH{\xa3\xcb$\xd2nv[fC\x0f\xc39\xe8\xbbwr\x0e:?\x13\xef\xbf\xb6b\x0fq8}\x05Gs*\xcd#\x8c_k\xcf\xae\x8e@\xae\x9c\xabT\xc3\xda\t\xd5\x94C\x12\xa8\x0b\xc9<|b\xbaRV-\xf8Y0\xbcv\x98\x85\x85\x8d\x88\xe2\x91\xc2\xda|\x109\xfa\x1d,\xfd\x97\xf3\x995\xdb\x1c#\xf6\x9dDag&\xf9Y\xa5\x81r\xdd\xd8\x1a\xef\xdb\x89\x08\x19\xd4\x90\x18\xc9\xc7\x08&\xc9H\xc8ef\xbed\xb6\xd5\x87\xd3e:\xfb\x91\x8bN\xdcS\x0c\xbck|\x0eQ\x07\x94%w\xef\x86\xcb\x8e"') __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\x05\x03\x00\x00\x12\t\x04\x00\xe9\xed\xba#\x82\xf1\xcd]\xa5\xb1\xd2\x99\xa2H~k\x00\x00\x00\x00\x00\x00\x00\x00\xbc5\x88dvD\x98\x93.\x0e\x82\xa1h\x0e\x9cKsYO\xd6n\xfaac&\xdc\xee\xd6\xbc\xe5C\xb8L\xd0BB{\x9f\xfb\xf2\xa6L\xac/t\xcdy^\xa8\x8f\xde$\x87\x8b\x9d\x83Y"\xa6\x0f\xaa\x88\xa1\x88\x13]\xe1"\xe1\x1c\nm\x11f\xeb\xab&\xcblF(\x9f* VxC\xc7X;\xc5\x84\t&\xe67\xf4f\xd4\xf6\xd0`X\t?f.mr\xdf\xa9\xa7\xc1\xd9\x85s^E\x1d\'\xf6\xe0_\x93\xcb=\xc38\xb6\x9b4\x0fI\x02l\x13G\xf6{\x94m\x83Uy\x91\xc9\x1e"~\xa3Y\xc51V\xa9\x03\xa7\xa3`Jx\xc70\xfb4\x03\x0fp\xa7\xb5\xfc\xd2\x9c\xbc6\xa1\xe8&KC\xae\x85\x1c\xef]l:;0\xd7\xcf\xe4\x8e\x0b\xac\x0c\x1c\x0f\xebx\x1bJ\xeb.b\x13\x17\xcc\x96\xb3\xd5\xa7\xeb\x06\xc0o\xe3k\xba\xff.\xf8\xf5\xc2l#\xd3\xe9\xf2[\xb4\xfe\xca/C\'\x1f\xf7\x9d\xaf0\xadLh)&`\xd1\x059\xc9\xb9\xfb\xb15-\x16S\xf1w\xf4l\x90\xcb.\x8e\xc8\x94\xd6;\x13X\xf2A\xc86\xb3\x8e\xd5a\xa4O<\xa8\x08\x17*\xf1\x7f\n\xe2\xbb(\x1bE?\xd9\xe9\x97\x80]\x986?\xd1\x88\xf6\x9aa\xd7\x000\x82\xf2\xbe\x1dS$BX*\xae0\x97K\xf4\x94\x80)\xce\x0f*(E\x81j\xbf\xf4\xfe\xe8\x9d-\x85<jM\\\x10\x04\xf9\xa3b\x91\x7f\xe3\xff\xdf#\xa5\x8dA\n\xcc\xbf\xb0\x82\t\x16\xa1\xf4E \xed7\x06\x854;\x8f\xe0\xeb\xad\xe6\xcbU\xbb\xde\xa2y\x81\xf2N\xff\xf2fa\xf1{f\xdd\xfe#\x9f\xc1\xf0\x06\xad\x11\xfa6<\xc7\xba]\xd7\xe8\x07\x10\x06\xcd\x97P\x1e|\x9c\x8fU\x8b\x9b`VHp\x1f\xb2K\xb0o\xc45\x1d\xab\xd9\xc2K#a\xd16]\xafTbj\xdc\xce\xab\xe3n\xc0\x87\xc4.z\xef\xd0\xacE-\xb9\xe8\xbe\x00B\xbb\x84^\x988>\x86\x01\xfa\x95\x95\xe40&\x9d\xa6\x7f_\x99Z\x03\xa7\xbf\xe9\xb3\x8c\x80<p\xb9\x83\xf6%\x08#\xc1\xf9\x97\x9e\xac\xf0T@\xa0/\xa7\xb3RN\xc2\xb83\x84.\xa2\xcb\xb4\xae\xccp0z\xb6m\xd6\xe0\xa8{\x0e!\x04\xe0\xb7\xa2a\x19\x85\x1c\xf9[7[\xcd5\xb9\x0b[\x91~&%\x89\xbc\x15.a$\xe5\xc2w\x03L\xa9\xddV\xf2\x80\xfe\xb3A\xab\xf1U{\x13@B\x8cz\x13\x9aB\x1e\xe78\\\xea%U\xf2\xce\xeb\xd3s\xbc\xadBi\x9c\xa1Q!\xe1\xe9\xb8m\x80\xbf/l\xe6 \x84\xa3;\xa8\x85\xe1\xfa\xe6\xa27\x162\x9c\xd9\xc1\x1c\xe3\xaf\xd8\xa8\xbd\x04\xc8\xf8O\x95\xdbOTc\xd24ihN\'|\x0f\xa1\xe3Y\xe0\tr\xb7\xd3\xe4B\'\x06oc\r\xfaR\xeer\x8c\xccw*\xc8xz\xa5\xa9n\x91}f\x9c\xfe\xb2\xb8\xbe"\xfc~\xe1\nN\x19\xfd\xd4:\xe9\x13|\x8ar\xa1}\x13$\x89\xe5\xe0\x05py02\xdd\xb5F\xd8\xb7\xc1\x9b\xa4\xac\xe0\xbf\xdfv3\xcf\xd2\x80\x02\x0b\xd3\x0e\xc8\xe4\t\x81\xfa\xa3\xce\xe1\xcc\xacas\x9b\x10\xc8\xcb\x89\xe1q{zd|\x10')

File diff suppressed because one or more lines are too long

3
src/version.py Normal file
View File

@ -0,0 +1,3 @@
# Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-10-29T19:05:41.007880
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\xcb\x01\x00\x00\x12\t\x04\x00\xdc5<\xfa\xf7\x0c@\x9d\xc2,\x0eH\xbdqB\xb4\x00\x00\x00\x00\x00\x00\x00\x00\xc5\xf3\x8c\xf9\x02\\\xf3\xbb"\xcd%y\x1c\r\xbe\xb6g\nG\xa8\xe2\x9b\x7f\x87\xe6&pf\xa8\x10\xb18C\xbb.U\x82\xd2i\xce\xf1}7l\xca\xfc;\\\x0e\x91\x12\xaf\x14\x80\xb7\xaf\xa2\xd6\x90\xc2U\xb8\x88+\xd1D%?\xa7L~7\xfb\xff)m\xab.h\x8f\xb0\xda\xca\xcbS\xfc\x898\xe5\x0e\x16\xb6C\xc0\x1d\x96|R%\x88az$\x08.\xbf\x8eS\x1b\xc3\xa8\xb8\xe5\xc1F\x08\xda\xf2\xc5\x05\xb9\xd4\x14/\xed4\x832\xc6\xa7e\xce\x07\xc0\x83\x0b\xcf-G\xd4\x08\x9b\x04Q}9P\n7(\x95\x18\x8dL\x9e\xed.\xf2\xde^\x9f\xc2,\x81~FTa\xee\xa9\xf0\xe5\xea\xa1\xbeb\x80\x18\xf5\x03\xfc\xc04\x19h>\x8e\xd8(M\x92\x81\xe8\x03\xdamUd\xbd\xbe(\xd8\xb6\x92$\xe1^\x061~\xea\x89\xfe\xba;H\xfey\x1e\xf8\x9c\xc4\xe94\x14\xf7[\x84P\'\x0eS\xf7\x90\xf2s\x7f\xd3\xdd\x11\xe5p\x13\x06\xf4s\xa8D\xe1HIZ\x0e\xac\xfe\x016\xbd\x18\xd1\xad\xf1\xa8.\x8a\x16\xcd\r\xfe\xd3\x0b\x9fbA\xea\x823\x10g\x1f\xe2\xbc^K\xd1\xd1\x18W\x1b\xa2~\x96\x88\xd7\xcb\x9d\x90o\x93`6\xf9L\xc2\rjv\xeb\xe4\x8a\x85\xea\xa5\xd7\xc7O\x89\x8c\xc9\x1e{\xc9*M\x01<\xc1\xc3\xb9\x94\xb6t\x7f:\x9e\xf4\x04\xca\xdc\x8e\x12\x0b}N\xd5yp\x98z(J\xc1PSX\x9d\xfd\xd4\x84i\xd5\xfbG\xdb\x9d\xee\xc5\xa6\xf1e\xdb\xfaR\xfb\x1f\x87\xa2\xa4\xce\xc8\xa5\xf5**"f&\xfa\xceZ\xda\xb1!}\x8c;`0\x96\x1b.C&`.\x81%`\xbb\xd1\x92;\x07\xf8\xc2]\x8cC\xf0\xb1\xb9HMS\x8a\xe03\x8f\x06g\xf6*_\xfa\xf3\x8f\xe7\xc7X\xe8\x7f\x13\x88\xfe\xb1\xde\xdbK\xe3\xe0C\xb3\x0f\x08R^\xf5\xdf\x1d')

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-10-27T16:50:18.242402 # Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-10-29T19:05:41.447012
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\x9a\x01\x00\x00\x12\t\x04\x002\xe9\t\xea^\x18sD\x02k\xdb\x9e\xdc\x8b}#\x00\x00\x00\x00\x00\x00\x00\x00\xc6\xa3\xeb\xa8M\xce\x88-\xb4\x1b\xe5&z\xd1\xee\xc0\x84D`j&e\x9dr\xfa!Z\xach\x94d\xc4\xe9\xc8\xba\x8f\xcd\x05\xdf\xacT)\x18K\xd5\xd3\x99\xd8Y\xfe4\x89\x9d$\xaa\xe9\x0e\xeaA!\xfe\x92\xfd%\xf7\x8e\x8a~v\xb5\x12o\xddo!\xb9\x18\xba\xcc\x97\x9a\xb0\xafg\x8b\xce\xd7\x1b\x0eE\xec<\xa1\xf4M\xe0\xbf\t)=Wq\x88\xea\x7f)\xafk"\x8c\xd5\xc2\xdc\x8d\x01\xec\xb2zH\x8d\x95{\x1d?z\xf4P\x8d\x05`9\xbb\xc5m\xa1T\xda\xc6\x86\x8eZ\xae\x14g\xab\xc9S~\xfc\x844=M\xa7\xf3\x98\x18\x1b\x91u\x96\xf4y\xa3\xf3\x1d\xa6\xb1\x18\xdd\xf90Sw.\xa9\x18\x9c\x015C^\xa8\x94\xb8t\x06\xb6\x8d\x1a\xaa\xc6\x0b\xab9/\x0cja\x82YrO\x9b\xfe\xfao\x1ah\x07\x03\xdd2=[\x08\xeah~\xac\xb0\xa5o\x8cw\xd8\xc1M@\xfe\xac\x07\x07\xd4\x96\xbcl\xf2}?\xc9\xafr\x9c\x1f\xc0\xc1\xca\x94T\xb1\xb5\xadl\x11_\xd9aa+\xed\xcb\xccF2\t\x16\x07\x8f\xd5{\x0b\xb8\x11\x955\xdc\xa44\x08\\\x9c~n\x13\x88\xab\xfb0o\xc9\x8a\xeb\xd9\x12,\xb8\xcda\xed\xa1@\xcf\xae\x8b\xdc\x9f-Fp\x7f5\xe9\rOP^\xcb\xe1\x8bi\x17S\x8dh9T\x04\xd9>YV\x86l\xcd\xfcl\xab\x95\xc0\xc7Q4\x10\xcf\xa5]W)\xe6\x92U7\xee\xc6\x9aE8\x84\x18\xf2\xc4\xd0RSR\x7fH ]\x1c\xb8\x12\x84\xe4\xeb\x00\x00\xa7\x1b`D\xc6\xac<\xe9/\xe2B\x04<\xd2?\xfb\xfb\xadJ\xd9t\x84\xfe\x88\x98\x10ZQ\xb9*\xb0\x80') __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\x97\x01\x00\x00\x12\t\x04\x00l\xe7\xadR\xe2\x08v\xd0x)\x8f\xb30F\xea[\x00\x00\x00\x00\x00\x00\x00\x00l\xea\xc7\x8bG\x93c\xc1\x00\x895\xb3z^\x16+\xa0C\xe0\xb4\xa9b\xc7\xf4\xa3"\x07\x08+p\n\xbb\xec\xf3bx\xe8\x8c\x9d\x1b\x08\xd6:\xa0R\xc6\xa8\x08\xa9\xcf=\x8a\xc6\xea\xb6c\xab\xec:RwY\x9c\xd9V\x8f\xbcv\xfd\xc6\n\x15\xd4F\xdf\xb1\xad\x1ew\xf7P;\xb3\x97b}$\xd6\xfbyR\xc3\xd9\\~\xc0\x0f\xf2,\xe7L\x83\xfe\xe3\x08\xfe\xce\xa5\xd2\x1cY\xb2\x0e\x0c\x8d\xe8>\xb7\xd6\x7f\xa9\xb08\x08\x14\x00\xb8IU\xb2\x87\x9f\x89\xebO1\x8dtW>V\'\x91\xe4d\xa9\x9cLz\x0b4\xf5\xa8\xde\x01\xdb\x81UP\x15\xf1\xea\xe6\xb1\xa8\xd0i\xed\x93$\xb90X\xa6\xbb\x9d\x83\xd7m\x9e=E\xd8\x01\x03o\\\xea\xcf\xf9\x05\x12k\xfa\xa0\xb1M:mm\x8a\xe7\x85/S\xaf\xceS\x9d\x8d\xee$\xc5\xa2cL\xb9\x05\xe1\x13\x9a,\x16c\xae{y\x91\x0c\xae\xe1\xbf\x81]6\x95\x9c\x91\xc4\x10\x89\x8a\xec\x9ay\xc9\xc5\x99u{\xfe3u\xedsa`\xa7\x7f\xe7\xe9\x8c\xa7\x9eR\x9e\x87z3\xe7\xe8\xbb\xe8e~}\xd9\xcai\x15\xdf)j\x89A\xbb\x1e5Y\x0b\x1b|+\xb0\x7f\xd54\xb1C-e\xf7.\x1d\nP\xa1\x12\x94NQ\xbd\xd8t\xc1\x00\xac%02y\xdfA\xe4\xda\xc8\xf87i\x9e\x00r\xfc\xb1\xd2\xf6\x1dILQn\xc6\x92\xc0\xac\xf5\xee\xbb\xcd\xe4\xbafX\xbc\xd2^\x1e\xb6\xd1\xee\xea=P\xb1\x19\x9f\xd8\x1co\x94\xf1\x004\xea\xac\x1cD\xe3\xa0\xda,\xedC~\x160>\x03W!}\x1f\xc9\x17\xc6\x16\xb7O\xfc\x03+\xc3\xad\rg\xd1\xe8')

BIN
static/images/usb_blk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

468
templates/index copy.html Normal file
View File

@ -0,0 +1,468 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<title>Media Dashboard</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" />
<style>
/* High-contrast text everywhere */
body,
.content,
h1, h2, h3, h4, h5, h6,
p, label, .status,
.checkbox-inline,
.container,
.form-group,
.form-group * {
color: #1f2937; /* gray-800 */
}
*{ box-sizing: border-box; }
html, body{ height: 100%; }
body{
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
line-height: 1.55;
/* Page/background image/color is controlled by your existing CSS; unchanged */
}
#navbar{ position: sticky; top: 0; z-index: 50; }
.content-wrapper{
position: relative; /* anchor for the underlay image */
max-width: 1100px;
margin: 0 auto;
padding: 16px;
z-index: 1; /* keep main content above the underlay */
}
@media (min-width: 980px){
.content-wrapper{ padding: 24px; }
}
.content{
position: relative;
z-index: 2; /* ensure UI is above the image */
display: flex;
flex-direction: column; /* stack containers vertically */
align-items: center; /* center containers horizontally */
gap: 20px;
}
/* Ensure the main title spans full width and is centered above the panels */
.content > h1{
flex-basis: 100%;
text-align: center;
margin-bottom: 8px;
}
/* Underlay image: left-aligned, same size, no layout impact */
.side-img{
position: absolute;
left: 0;
top: 110px; /* adjust as you like */
width: 230px; /* keep current size */
height: auto;
z-index: 0; /* behind everything */
pointer-events: none; /* clicks go through */
opacity: 1; /* fully visible; change if you want subtler */
}
h1{
margin: 0 0 8px;
font-size: clamp(1.6rem, 2.2vw, 2.2rem);
color: #333; /* explicit, high-contrast */
}
h2{
margin: 0 0 10px;
font-size: clamp(1.1rem, 1.8vw, 1.4rem);
color: #333;
}
/* Card containers background color kept EXACTLY as before */
.container{
width: 100%;
max-width: 420px;
margin: 0 0 16px;
padding: 16px;
border: 2px solid #ccc;
border-radius: 10px;
background-color: #ffffffb6; /* unchanged */
position: relative; /* creates its own stacking context above image */
z-index: 1;
}
.form-group{ margin-bottom: 14px; }
.form-group label{ display: block; margin-bottom: 6px; font-weight: 600; }
.form-group input,
.form-group select,
.form-group textarea{
width: 100%;
padding: 10px 12px;
font-size: 1rem;
border: 1px solid #d1d5db;
border-radius: 8px;
background: #fff;
color: #1f2937;
}
.checkbox-row{
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.checkbox-inline{
display: inline-flex;
align-items: center;
gap: 12px; /* more space between box and label */
font-weight: 600;
padding: 4px 6px; /* larger hit area for touch */
border-radius: 6px;
}
/* Larger, touch-friendly checkboxes while preserving native behavior */
.checkbox-inline input[type="checkbox"]{
/* scale is reliable across browsers for visually larger boxes */
transform: scale(1.3);
-webkit-transform: scale(1.3);
margin: 0; /* reset default margins so gap controls spacing */
/* align the scaled box nicely with text */
transform-origin: left center;
-webkit-transform-origin: left center;
width: 18px; /* keep a sensible layout size in some UAs */
height: 18px;
appearance: auto; /* keep native look (checked glyph) */
accent-color: #1e40af; /* modern browsers: use theme color for check mark */
}
/* utility: push an inline checkbox/label to the far right inside a flex row */
.checkbox-inline.right { margin-left: auto; }
.button-group{
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-start;
margin: 12px 0 6px;
}
.button-row{
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 12px;
}
.button-row .controls{ display:flex; gap:10px; }
.button-row .actions{ margin-left: auto; }
.btn{
appearance: none;
border: 1px solid transparent;
border-radius: 8px;
padding: 10px 16px;
font-size: 1rem;
cursor: pointer;
color: #fff; /* keep buttons readable */
background: #1e40af; /* blue-800 */
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
.btn:hover{ background: #15327f; }
.btn:focus-visible{
outline: 3px solid #2563eb;
outline-offset: 2px;
}
.btn[disabled]{ opacity: 0.6; cursor: not-allowed; }
.status{
margin-top: 8px;
font-size: 0.98rem;
min-height: 1.2em;
}
.input-label{ text-align: left; }
.form-group textarea{
resize: none;
overflow-wrap: break-word;
white-space: pre-wrap;
}
/* Optional: make sure the underlay doesn't collide on very small screens */
@media (max-width: 480px){
.side-img{ top: 140px; width: 190px; }
}
</style>
</head>
<body>
<div id="navbar"></div>
<script>
fetch('/static/html/nav.html')
.then(r => r.ok ? r.text() : Promise.reject(r.status))
.then(html => { document.getElementById('navbar').innerHTML = html; })
.catch(() => { document.getElementById('navbar').innerHTML = '<div class="container" role="alert">Navigation failed to load.</div>'; });
</script>
<div class="background-image"></div>
<main class="content-wrapper">
<!-- Underlay image (beneath everything, left-aligned, fixed size) -->
<img src="/static/images/helio-posh.png" alt="Helio Posh" class="side-img" />
<div class="content">
<h1>Media Dashboard</h1>
<section class="container" aria-labelledby="playlist-heading">
<h2 id="playlist-heading">Playlist Loop</h2>
<div class="checkbox-row">
<label class="checkbox-inline" for="autoPlayAtBoot">
<input type="checkbox" id="autoPlayAtBoot" name="autoPlayAtBoot">
Autostart @ Boot
</label>
<label class="checkbox-inline" for="saveSettings">
<input type="checkbox" id="saveSettings" name="saveSettings">
Save
</label>
</div>
<div class="form-group">
<label class="input-label" for="mediaLocation">Media Sources</label>
<select id="mediaLocation" name="mediaLocation" aria-describedby="mediaHelp">
<option value="USB">No Media Available</option>
</select>
<div id="mediaHelp" class="sr-only">Choose a folder to loop through images.</div>
</div>
<div class="form-group">
<label class="input-label" for="imageDuration">Image Duration (secs)</label>
<input type="number" id="imageDuration" name="imageDuration" min="1" max="60" inputmode="numeric">
</div>
<div class="button-group" role="group" aria-label="Playlist loop controls">
<button class="btn" id="btnStartLoop" onclick="startMediaLoop()">
<i class="fa fa-play" aria-hidden="true"></i> Start
</button>
<button class="btn" id="btnStopLoop" onclick="stopMediaLoop()">
<i class="fa fa-stop" aria-hidden="true"></i> Stop
</button>
</div>
<div class="status" id="mediaLoopStatus" role="status" aria-live="polite">status</div>
</section>
<section class="container" aria-labelledby="gallery-heading">
<h2 id="gallery-heading">Web Gallery</h2>
<div class="checkbox-row" style="justify-content:flex-start;">
<label class="checkbox-inline" for="autoStart">
<input type="checkbox" id="autoStart" name="autoStart">
Autostart
</label>
<label class="checkbox-inline right" for="saveWebSettings">
<input type="checkbox" id="saveWebSettings" name="saveWebSettings">
Save
</label>
</div>
<div class="form-group">
<label class="input-label" for="galleryURL">URL</label>
<textarea id="galleryURL" name="galleryURL" rows="3" placeholder="https://yahoo.com"></textarea>
</div>
<div class="button-row" role="group" aria-label="Web gallery controls">
<div class="controls">
<button class="btn" id="btnStartWeb" onclick="startWebGallery()">
<i class="fa fa-play" aria-hidden="true"></i> Start
</button>
<button class="btn" id="btnStopWeb" onclick="stopWebGallery()">
<i class="fa fa-stop" aria-hidden="true"></i> Stop
</button>
</div>
<div class="actions">
<button class="btn" type="button" onclick="clearURL()">
<i class="fa fa-eraser" aria-hidden="true"></i> Clear
</button>
</div>
</div>
<div class="status" id="webGalleryStatus" role="status" aria-live="polite">status</div>
</section>
</div>
</main>
<script>
function setBusy(el, busy){
if (!el) return;
el.disabled = !!busy;
if (busy) el.setAttribute('aria-busy','true'); else el.removeAttribute('aria-busy');
}
function setText(id, text){
const el = document.getElementById(id);
if (el) el.textContent = text || '';
}
async function loadMediaSources(){
try{
const res = await fetch('../get_media_sources');
const data = await res.json();
const select = document.getElementById('mediaLocation');
if (!select) return;
// Clear existing
select.innerHTML = '';
const arr = Array.isArray(data?.folders) ? data.folders : [];
if (arr.length === 0){
const opt = document.createElement('option');
opt.value = 'USB';
opt.text = 'No Media Available';
select.add(opt);
return;
}
arr.forEach(m => {
const opt = document.createElement('option');
opt.text = m.folder_name_display;
opt.value = m.folder_path;
select.add(opt);
});
}catch(err){
console.error('Error loading media sources:', err);
}
}
async function loadLastUsedData(){
try{
const res = await fetch('../get_screen_settings');
const data = await res.json();
// Only default if undefined (avoid forcing true when false)
const apb = (data.autoPlayAtBoot === undefined) ? true : !!data.autoPlayAtBoot;
const dur = (data.imageDuration === undefined) ? 5 : Number(data.imageDuration);
const as = (data.autoStart === undefined) ? true : !!data.autoStart;
const url = (data.url === undefined) ? 'google.com' : String(data.url);
document.getElementById('autoPlayAtBoot').checked = apb;
document.getElementById('imageDuration').value = dur;
document.getElementById('autoStart').checked = as;
document.getElementById('galleryURL').value = url;
document.getElementById('saveSettings').checked = false;
}catch(err){
console.error('Error loading screen settings:', err);
}
}
async function startMediaLoop(){
const mediaLocation = document.getElementById('mediaLocation').value;
const imageDuration = document.getElementById('imageDuration').value;
const autoPlayAtBoot = document.getElementById('autoPlayAtBoot').checked;
const saveSettings = document.getElementById('saveSettings').checked;
// Reset the save toggle after reading
document.getElementById('saveSettings').checked = false;
const btnStart = document.getElementById('btnStartLoop');
const btnStop = document.getElementById('btnStopLoop');
setBusy(btnStart, true); setBusy(btnStop, true);
setText('mediaLoopStatus', 'Starting media loop…');
try{
const res = await fetch('../start_media_loop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaLocation, imageDuration, autoPlayAtBoot, saveSettings })
});
const data = await res.json();
setText('mediaLoopStatus', data.message || 'Started.');
}catch(err){
console.error('Error during start media loop:', err);
setText('mediaLoopStatus', 'Error during start media loop.');
}finally{
setBusy(btnStart, false); setBusy(btnStop, false);
}
}
async function stopMediaLoop(){
const btnStart = document.getElementById('btnStartLoop');
const btnStop = document.getElementById('btnStopLoop');
setBusy(btnStart, true); setBusy(btnStop, true);
setText('mediaLoopStatus', 'Stopping media loop…');
try{
const res = await fetch('../stop_media_loop', { method: 'POST' });
const data = await res.json();
setText('mediaLoopStatus', data.message || 'Stopped.');
}catch(err){
console.error('Error during stop media loop:', err);
setText('mediaLoopStatus', 'Error during stop media loop.');
}finally{
setBusy(btnStart, false); setBusy(btnStop, false);
}
}
async function startWebGallery(){
const autoStart = document.getElementById('autoStart').checked;
const url = document.getElementById('galleryURL').value;
const btnStart = document.getElementById('btnStartWeb');
const btnStop = document.getElementById('btnStopWeb');
const saveWebSettings = document.getElementById('saveWebSettings').checked;
setBusy(btnStart, true); setBusy(btnStop, true);
setText('webGalleryStatus', 'Starting web gallery…');
try{
const res = await fetch('../start_web_gallery', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, autoStart, saveWebSettings })
});
const data = await res.json();
setText('webGalleryStatus', data.message || 'Started.');
}catch(err){
console.error('Error during start web gallery:', err);
setText('webGalleryStatus', 'Error during start web gallery.');
}finally{
setBusy(btnStart, false); setBusy(btnStop, false);
document.getElementById('saveWebSettings').checked = false;
}
}
async function stopWebGallery(){
const btnStart = document.getElementById('btnStartWeb');
const btnStop = document.getElementById('btnStopWeb');
setBusy(btnStart, true); setBusy(btnStop, true);
setText('webGalleryStatus', 'Stopping web gallery…');
try{
const res = await fetch('../stop_web_gallery', { method: 'POST' });
const data = await res.json();
setText('webGalleryStatus', data.message || 'Stopped.');
}catch(err){
console.error('Error during stop web gallery:', err);
setText('webGalleryStatus', 'Error during stop web gallery.');
}finally{
setBusy(btnStart, false); setBusy(btnStop, false);
}
}
function clearURL(){ document.getElementById('galleryURL').value = ''; }
document.addEventListener('DOMContentLoaded', () => {
loadLastUsedData();
loadMediaSources();
});
</script>
</body>
</html>

View File

@ -36,7 +36,7 @@
.content-wrapper{ .content-wrapper{
position: relative; /* anchor for the underlay image */ position: relative; /* anchor for the underlay image */
max-width: 1100px; max-width: 980px;
margin: 0 auto; margin: 0 auto;
padding: 16px; padding: 16px;
z-index: 1; /* keep main content above the underlay */ z-index: 1; /* keep main content above the underlay */
@ -54,6 +54,73 @@
gap: 20px; gap: 20px;
} }
/* Tabs */
.tabs{ width:100%; max-width: 740px; margin: 0 auto; display:flex; flex-direction:column; align-items:center; }
/* When the Tools tab is active make the tabs area wider so the tools container can expand */
.tabs.tools-wide{ max-width: 880px; }
.tab-buttons{ display:flex; gap:8px; margin-bottom:12px; justify-content:center; }
.tab-button{
appearance: none;
border: 1px solid #d1d5db;
background: #f8fafc;
color: #111827;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
font-weight: 700;
}
.tab-button[aria-selected="true"]{
background: #1e40af;
color: #fff;
border-color: #1e40af;
}
.tab-panel{ display: none; }
.tab-panel[data-open="true"]{ display: block; }
/* Tools UI: 3-column layout where center column is fixed width and
left/right columns have both a minimum and maximum width so they
shrink and grow smoothly depending on available space. */
.tools-grid{
display: grid;
/* side columns: min 140px, max 360px; center column fixed at 160px */
grid-template-columns: minmax(140px, 360px) 160px minmax(140px, 360px);
gap: 12px;
align-items: start;
}
.tools-column{
display: flex;
flex-direction: column;
gap: 8px;
/* ensure columns can shrink but not collapse */
min-width: 140px;
max-width: 500px;
width: 100%;
}
/* Make the Tools tab container wider than the default small .container so both columns have space.
This changes only the container inside the Tools panel; textareas remain their own size. */
#tab-tools .container{
max-width: 1420px; /* wider than the default 420px */
width: 100%;
margin: 0 auto 16px;
padding: 16px;
}
.tools-list{ border:1px solid #d1d5db; border-radius:8px; padding:10px; height:220px; overflow:auto; background:#fff; }
.tools-textarea{ width:100%; height:80px; padding:8px; border:1px solid #d1d5db; border-radius:6px; overflow:auto; }
/* Center buttons column: fixed width and top-aligned. Padding-top is
initially 0; alignment can be adjusted dynamically by JS for pixel-perfect
alignment when needed. */
.tools-buttons{
width: 160px;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
justify-content: flex-start;
padding-top: 0;
box-sizing: border-box;
}
/* Ensure the main title spans full width and is centered above the panels */ /* Ensure the main title spans full width and is centered above the panels */
.content > h1{ .content > h1{
flex-basis: 100%; flex-basis: 100%;
@ -88,7 +155,7 @@
.container{ .container{
width: 100%; width: 100%;
max-width: 420px; max-width: 420px;
margin: 0 0 16px; margin: 0 auto 16px;
padding: 16px; padding: 16px;
border: 2px solid #ccc; border: 2px solid #ccc;
border-radius: 10px; border-radius: 10px;
@ -122,12 +189,34 @@
.checkbox-inline{ .checkbox-inline{
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 12px; /* more space between box and label */
font-weight: 600; font-weight: 600;
padding: 4px 6px; /* larger hit area for touch */
border-radius: 6px;
}
/* Larger, touch-friendly checkboxes while preserving native behavior */
.checkbox-inline input[type="checkbox"]{
/* scale is reliable across browsers for visually larger boxes */
transform: scale(1.3);
-webkit-transform: scale(1.3);
margin: 0; /* reset default margins so gap controls spacing */
/* align the scaled box nicely with text */
transform-origin: left center;
-webkit-transform-origin: left center;
width: 18px; /* keep a sensible layout size in some UAs */
height: 18px;
appearance: auto; /* keep native look (checked glyph) */
accent-color: #1e40af; /* modern browsers: use theme color for check mark */
} }
/* utility: push an inline checkbox/label to the far right inside a flex row */ /* utility: push an inline checkbox/label to the far right inside a flex row */
.checkbox-inline.right { margin-left: auto; } .checkbox-inline.right { margin-left: auto; }
/* Label with icon helper */
.label-icon{ display: inline-flex; align-items: flex-end; gap: 8px; font-weight: 700; }
/* Keep both icons same size and bottom-aligned with the label text */
.usb-icon, .pc-icon { width: 40px; height: 40px; object-fit: contain; display: inline-block; vertical-align: bottom; margin-bottom: 2px; }
.button-group{ .button-group{
display: flex; display: flex;
gap: 10px; gap: 10px;
@ -186,6 +275,94 @@
@media (max-width: 480px){ @media (max-width: 480px){
.side-img{ top: 140px; width: 190px; } .side-img{ top: 140px; width: 190px; }
} }
/* Strong override: ensure Tools container can expand even if other rules limit .container.
Uses !important to defeat competing rules when needed. */
#tab-tools .container{
max-width: 1100px !important;
width: 100% !important;
margin: 0 auto 16px !important;
padding: 24px !important;
min-height: 540px !important; /* make the tools container taller */
}
/* Make the checkbox lists taller so the Tools panel looks taller */
#toolsLeftList, #toolsRightList, .tools-list{
min-height: 420px !important;
height: 420px !important;
}
/* Responsive: stack the three columns on narrow screens */
@media (max-width: 599px){
.tools-grid{ display:flex; flex-direction:column; gap:12px; }
.tools-buttons{ width: auto; padding-top: 20px; }
.tools-column{ min-width: 0; }
/* slightly shorter lists on small screens so the stacked layout fits */
#toolsLeftList, #toolsRightList, .tools-list{ min-height: 280px !important; height: 280px !important; }
}
/* Tools footer: status text left, disk-usage bar right */
.tools-footer{
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 12px;
}
.tools-status-msg{
flex: 1 1 auto;
text-align: left;
font-size: 0.95rem;
color: #111827;
min-height: 1.2em;
}
.disk-usage-wrap{
display: flex;
align-items: center;
gap: 8px;
min-width: 220px;
justify-content: flex-end;
}
.disk-usage-bar{
width: 160px;
height: 14px;
background: #e5e7eb; /* gray-200 */
border-radius: 8px;
overflow: hidden;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.4);
}
.disk-usage-fill{
height: 100%;
width: 0%;
background: #10b981; /* green-500 */
transition: width 300ms ease, background 200ms ease;
}
.disk-usage-text{ font-weight: 600; font-size: 0.95rem; color: #111827; }
@media (max-width: 639px){
.disk-usage-wrap{ min-width: 140px; }
.disk-usage-bar{ width: 120px; }
}
/* Confirmation modal for delete action */
.confirm-overlay{
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
}
.confirm-box{
background: #fff;
padding: 18px;
border-radius: 8px;
max-width: 480px;
width: calc(100% - 48px);
box-shadow: 0 8px 24px rgba(15,23,42,0.2);
color: #111827;
}
.confirm-actions{ display:flex; gap:12px; justify-content:flex-end; margin-top:14px; }
.confirm-actions .btn{ min-width: 84px; }
.confirm-message{ font-weight:600; }
</style> </style>
</head> </head>
<body> <body>
@ -206,6 +383,14 @@
<div class="content"> <div class="content">
<h1>Media Dashboard</h1> <h1>Media Dashboard</h1>
<div class="tabs" role="tablist" aria-label="Main tabs">
<div class="tab-buttons">
<button class="tab-button" role="tab" id="tab-btn-dashboard" aria-controls="tab-dashboard" aria-selected="true">Dashboard</button>
<button class="tab-button" role="tab" id="tab-btn-tools" aria-controls="tab-tools" aria-selected="false">Tools</button>
</div>
<!-- Dashboard panel: existing controls moved here -->
<div id="tab-dashboard" class="tab-panel" data-open="true" role="tabpanel" aria-labelledby="tab-btn-dashboard">
<section class="container" aria-labelledby="playlist-heading"> <section class="container" aria-labelledby="playlist-heading">
<h2 id="playlist-heading">Playlist Loop</h2> <h2 id="playlist-heading">Playlist Loop</h2>
@ -216,9 +401,10 @@
</label> </label>
<label class="checkbox-inline" for="saveSettings"> <label class="checkbox-inline" for="saveSettings">
<input type="checkbox" id="saveSettings" name="saveSettings"> <input type="checkbox" id="saveSettings" name="saveSettings" title="Click to make sure your settings are saved permanently" aria-describedby="saveSettingsHint">
Save Save
</label> </label>
<span id="saveSettingsHint" class="sr-only">Click to make sure your settings are saved permanently</span>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -256,15 +442,16 @@
</label> </label>
<label class="checkbox-inline right" for="saveWebSettings"> <label class="checkbox-inline right" for="saveWebSettings">
<input type="checkbox" id="saveWebSettings" name="saveWebSettings"> <input type="checkbox" id="saveWebSettings" name="saveWebSettings" title="Click to make sure your settings are saved permanently" aria-describedby="saveWebSettingsHint">
Save Save
</label> </label>
<span id="saveWebSettingsHint" class="sr-only">Click to make sure your settings are saved permanently</span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="input-label" for="galleryURL">URL</label> <label class="input-label" for="galleryURL">URL</label>
<textarea id="galleryURL" name="galleryURL" rows="3" placeholder="https://yahoo.com"></textarea> <textarea id="galleryURL" name="galleryURL" rows="3" placeholder="https://ataphotobooths.com"></textarea>
</div> </div>
<div class="button-row" role="group" aria-label="Web gallery controls"> <div class="button-row" role="group" aria-label="Web gallery controls">
@ -286,6 +473,52 @@
<div class="status" id="webGalleryStatus" role="status" aria-live="polite">status</div> <div class="status" id="webGalleryStatus" role="status" aria-live="polite">status</div>
</section> </section>
</div> </div>
<!-- Tools panel -->
<div id="tab-tools" class="tab-panel" data-open="false" role="tabpanel" aria-labelledby="tab-btn-tools">
<section class="container" aria-labelledby="tools-heading">
<h2 id="tools-heading">Media Transfer</h2>
<div class="tools-grid">
<div class="tools-column">
<label for="toolsLeftTextarea" class="label-icon"><img src="../static/images/usb_blk.png" alt="" aria-hidden="true" class="usb-icon"><span>USB folders</span></label>
<label class="checkbox-inline" for="overwriteLeft" style="margin-top:6px;">
<input type="checkbox" id="overwriteLeft" name="overwriteLeft"> Allow Overwrite
</label>
<div id="toolsLeftList" class="tools-list" aria-label="USB items list"></div>
</div>
<div class="tools-buttons" aria-hidden="false">
<div style="height:100px; width:100%;"></div>
<button class="btn" type="button" title="Copy selected from USB to PC" onclick="moveSelected('left','right')">USB to PC →</button>
<button class="btn" type="button" title="Copy selected from PC to USB" onclick="moveSelected('right','left')">← PC to USB</button>
<button class="btn" type="button" onclick="refreshToolsLists()">Refresh Lists</button>
<div style="height:30px; width:100%;"></div>
<button class="btn" type="button" title="Delete selected PC folders" onclick="deleteSelectedPC()" style="background:#dc2626">Delete →</button>
</div>
<div class="tools-column">
<label for="toolsRightTextarea" class="label-icon"><img src="../static/images/helio-posh.png" alt="" aria-hidden="true" class="pc-icon"><span>PC folders</span></label>
<label class="checkbox-inline right" for="overwriteRight" style="margin-top:6px;">
<input type="checkbox" id="overwriteRight" name="overwriteRight"> Allow Overwrite
</label>
<div id="toolsRightList" class="tools-list" aria-label="PC items list"></div>
</div>
</div>
<!-- Footer area: left-aligned status message and right-aligned disk usage bar -->
<div class="tools-footer" role="status" aria-live="polite">
<div id="toolsStatusMessage" class="tools-status-msg">Ready.</div>
<div class="disk-usage-wrap" aria-hidden="false">
<div class="disk-usage-bar" title="Free space">
<div id="diskUsageFill" class="disk-usage-fill" style="width:0%"></div>
</div>
<div id="diskUsageText" class="disk-usage-text">-- free</div>
</div>
</div>
</section>
</div>
</div>
</div>
</main> </main>
<script> <script>
@ -445,7 +678,285 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadLastUsedData(); loadLastUsedData();
loadMediaSources(); loadMediaSources();
setupTabs();
// initial alignment of the buttons column
// initial disk usage fetch
try{ refreshDiskUsage(); }catch(e){}
}); });
/* Tab control */
function setupTabs(){
const btnDashboard = document.getElementById('tab-btn-dashboard');
const btnTools = document.getElementById('tab-btn-tools');
const panelDashboard = document.getElementById('tab-dashboard');
const panelTools = document.getElementById('tab-tools');
function select(panelToOpen, btn){
[panelDashboard, panelTools].forEach(p => p.setAttribute('data-open','false'));
panelToOpen.setAttribute('data-open','true');
[btnDashboard, btnTools].forEach(b => b.setAttribute('aria-selected','false'));
btn.setAttribute('aria-selected','true');
// if Tools panel opened, refresh disk usage and lists
if (panelToOpen === panelTools){
try{ refreshToolsLists(); }catch(e){}
try{ refreshDiskUsage(); }catch(e){}
}
}
btnDashboard.addEventListener('click', () => select(panelDashboard, btnDashboard));
btnTools.addEventListener('click', () => select(panelTools, btnTools));
// toggle a class on the .tabs wrapper so we can widen it when Tools is active
const tabsWrapper = document.querySelector('.tabs');
function updateTabsWideState(){
if (panelTools.getAttribute('data-open') === 'true') tabsWrapper.classList.add('tools-wide'); else tabsWrapper.classList.remove('tools-wide');
}
// watch clicks to update state
btnDashboard.addEventListener('click', updateTabsWideState);
btnTools.addEventListener('click', updateTabsWideState);
// and set initial state
updateTabsWideState();
}
// debounce helper for resize
function debounce(fn, wait){
let t;
return function(...args){ clearTimeout(t); t = setTimeout(() => fn.apply(this,args), wait); };
}
/* Tools UI: render items array into checkbox list */
function renderItems(listId, items){
const list = document.getElementById(listId);
if (!list) return;
list.innerHTML = '';
(items || []).forEach((it, idx) => {
const id = listId + '-item-' + idx;
const label = document.createElement('label');
label.className = 'checkbox-inline';
label.style.display = 'flex';
label.style.alignItems = 'center';
label.style.justifyContent = 'flex-start';
label.style.gap = '8px';
const cb = document.createElement('input');
cb.type = 'checkbox';
let value, text;
if (typeof it === 'string'){
value = it;
text = it;
} else if (it && typeof it === 'object'){
value = it.folder_path || it.path || it.value || JSON.stringify(it);
text = it.folder_name_display || it.name || it.label || value;
} else {
value = String(it);
text = String(it);
}
cb.value = value;
cb.id = id;
const span = document.createElement('span');
span.textContent = text;
label.appendChild(cb);
label.appendChild(span);
list.appendChild(label);
});
}
async function refreshToolsLists(){
try{
const res = await fetch('../get_media_lists');
if (!res.ok) throw new Error('Network response not ok');
const data = await res.json();
let usbItems = [];
if (Array.isArray(data.usb)) usbItems = data.usb;
else if (data.usb_folders){
if (Array.isArray(data.usb_folders.folders)) usbItems = data.usb_folders.folders;
else if (Array.isArray(data.usb_folders)) usbItems = data.usb_folders;
}
let pcItems = [];
if (Array.isArray(data.pc)) pcItems = data.pc;
else if (data.desk_folders){
if (Array.isArray(data.desk_folders.folders)) pcItems = data.desk_folders.folders;
else if (Array.isArray(data.desk_folders)) pcItems = data.desk_folders;
}
if (usbItems.length === 0 && Array.isArray(data.usb_folders)) usbItems = data.usb_folders;
if (pcItems.length === 0 && Array.isArray(data.desk_folders)) pcItems = data.desk_folders;
//console.log('Refreshing media lists:', { usbItems, pcItems });
renderItems('toolsLeftList', usbItems || []);
renderItems('toolsRightList', pcItems || []);
setText('toolsStatusMessage', 'Media lists refreshed.');
// refresh disk usage indicator shown in the Tools footer
try{ refreshDiskUsage(); }catch(e){ console.warn('refreshDiskUsage failed', e); }
}catch(err){
console.error('Error refreshing media lists:', err);
setText('toolsStatusMessage', 'Error refreshing media lists.');
}
}
/* Fetch disk usage from server and update the bar + text
Assumption: the server returns percent_used (0-100) and free_human. We color the bar red
when percent_used > 85 (i.e., disk is more than 85% full). The bar fill represents percent free. */
async function refreshDiskUsage(){
try{
const res = await fetch('../get_disk_usage');
if (!res.ok) throw new Error('Network response not ok');
const data = await res.json();
if (!data || data.status !== 'success') throw new Error('Bad disk usage response');
const total = Number(data.total) || 0;
const free = Number(data.free) || 0;
const percentUsed = Number(data.percent_used) || 0;
const percentFree = total > 0 ? Math.max(0, Math.min(100, (free / total) * 100)) : 0;
const fill = document.getElementById('diskUsageFill');
const text = document.getElementById('diskUsageText');
const statusMsg = document.getElementById('toolsStatusMessage');
if (fill) {
fill.style.width = percentFree.toFixed(1) + '%';
// color red when used > 85%
if (percentUsed > 85) fill.style.background = '#ef4444'; else fill.style.background = '#10b981';
}
if (text) text.textContent = (data.free_human || (free + ' B')) + ' free';
if (statusMsg){
statusMsg.textContent = `Disk: ${percentUsed.toFixed(1)}% used`;
}
}catch(err){
console.warn('Failed to refresh disk usage', err);
const statusMsg = document.getElementById('toolsStatusMessage');
if (statusMsg) statusMsg.textContent = 'Disk usage unavailable';
}
}
/* Confirmation modal utilities */
function showConfirmModal(message){
return new Promise((resolve) => {
let overlay = document.getElementById('confirmOverlay');
if (!overlay){
// create modal
overlay = document.createElement('div');
overlay.id = 'confirmOverlay';
overlay.className = 'confirm-overlay';
overlay.innerHTML = `
<div class="confirm-box" role="dialog" aria-modal="true" aria-labelledby="confirmTitle">
<div id="confirmTitle" class="confirm-message"></div>
<div style="margin-top:10px; font-size:0.95rem; color:#374151;">These folders and their contents will be permanently deleted.</div>
<div class="confirm-actions">
<button id="confirmCancelBtn" class="btn" type="button">Cancel</button>
<button id="confirmYesBtn" class="btn" type="button" style="background:#dc2626">Yes</button>
</div>
</div>`;
document.body.appendChild(overlay);
const cancelBtn = overlay.querySelector('#confirmCancelBtn');
const yesBtn = overlay.querySelector('#confirmYesBtn');
const titleEl = overlay.querySelector('#confirmTitle');
cancelBtn.addEventListener('click', () => {
overlay.style.display = 'none';
resolve(false);
});
yesBtn.addEventListener('click', () => {
overlay.style.display = 'none';
resolve(true);
});
}
overlay.querySelector('.confirm-message').textContent = message || 'Warning, these folders and their contents will be deleted';
overlay.style.display = 'flex';
// Focus Cancel by default (per requirement)
const cancelBtn = overlay.querySelector('#confirmCancelBtn');
if (cancelBtn){
cancelBtn.focus();
}
});
}
async function deleteSelectedPC(){
const rightList = document.getElementById('toolsRightList');
if (!rightList) return;
const selected = Array.from(rightList.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
if (!selected || selected.length === 0) return; // nothing selected
const ok = await showConfirmModal('Warning, these folders and their contents will be deleted');
if (!ok) return; // cancelled
// disable buttons while working
const btns = document.querySelectorAll('#tab-tools .tools-buttons .btn');
btns.forEach(b => b.disabled = true);
setText('toolsStatusMessage', 'Deleting...');
try{
const res = await fetch('../delete_pc_folders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: selected })
});
if (!res.ok) throw new Error('Network response not ok');
const data = await res.json();
if (data && data.success){
setText('toolsStatusMessage', 'Delete completed. Refreshing lists...');
await refreshToolsLists();
try{ refreshDiskUsage(); }catch(e){}
} else {
setText('toolsStatusMessage', (data && data.message) ? data.message : 'Delete failed');
}
}catch(err){
console.error('Error during delete:', err);
setText('toolsStatusMessage', 'Error during delete operation.');
}finally{
btns.forEach(b => b.disabled = false);
}
}
/* Move/transfer selected items from one list to the other by calling the server.
The server endpoints expected (GET) are: ../transfer_usb_to_pc and ../transfer_pc_to_usb
with URL params: items (JSON-encoded array) and overwrite (true|false).
On success the server should return JSON { success: true } and the client will refresh lists. */
async function moveSelected(from, to){
const fromList = document.getElementById(from === 'left' ? 'toolsLeftList' : 'toolsRightList');
if (!fromList) return;
const selected = Array.from(fromList.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value);
if (selected.length === 0) return; // nothing selected
// Determine overwrite checkbox depending on direction
const overwriteId = (from === 'left') ? 'overwriteLeft' : 'overwriteRight';
const overwrite = !!(document.getElementById(overwriteId) && document.getElementById(overwriteId).checked);
// choose endpoint
const endpoint = (from === 'left' && to === 'right') ? '../transfer_usb_to_pc' :
(from === 'right' && to === 'left') ? '../transfer_pc_to_usb' : null;
if (!endpoint) return;
// disable buttons while working
const btns = document.querySelectorAll('#tab-tools .tools-buttons .btn');
btns.forEach(b => b.disabled = true);
setText('toolsStatusMessage', 'Transferring...');
try{
const itemsParam = encodeURIComponent(JSON.stringify(selected));
const url = endpoint + '?items=' + itemsParam + '&overwrite=' + (overwrite ? 'true' : 'false');
const res = await fetch(url, { method: 'GET' });
if (!res.ok) throw new Error('Network response not ok');
const data = await res.json();
// Support both response shapes: { success: true } and { status: 'success' }
const okResponse = data && ((typeof data.success !== 'undefined' && data.success === true) || data.status === 'success');
if (okResponse){
setText('toolsStatusMessage', 'Transfer successful. Refreshing lists...');
await refreshToolsLists();
} else {
console.error('Transfer failed', data);
setText('toolsStatusMessage', (data && data.message) ? data.message : 'Transfer failed');
}
}catch(err){
console.error('Error during transfer:', err);
setText('toolsStatusMessage', 'Error during transfer.');
}finally{
btns.forEach(b => b.disabled = false);
}
}
</script> </script>
</body> </body>
</html> </html>