fixes
This commit is contained in:
parent
b75ffdb4f8
commit
9f30122846
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/__pycache__/version.cpython-310.pyc
Normal file
BIN
src/__pycache__/version.cpython-310.pyc
Normal file
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
@ -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__
|
||||
__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
@ -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__
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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__
|
||||
__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
3
src/version.py
Normal 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
@ -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__
|
||||
__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
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
468
templates/index copy.html
Normal 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>
|
||||
@ -36,7 +36,7 @@
|
||||
|
||||
.content-wrapper{
|
||||
position: relative; /* anchor for the underlay image */
|
||||
max-width: 1100px;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
z-index: 1; /* keep main content above the underlay */
|
||||
@ -54,6 +54,73 @@
|
||||
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 */
|
||||
.content > h1{
|
||||
flex-basis: 100%;
|
||||
@ -88,7 +155,7 @@
|
||||
.container{
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
margin: 0 0 16px;
|
||||
margin: 0 auto 16px;
|
||||
padding: 16px;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 10px;
|
||||
@ -122,12 +189,34 @@
|
||||
.checkbox-inline{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
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; }
|
||||
|
||||
/* 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{
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@ -186,6 +275,94 @@
|
||||
@media (max-width: 480px){
|
||||
.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>
|
||||
</head>
|
||||
<body>
|
||||
@ -206,6 +383,14 @@
|
||||
<div class="content">
|
||||
<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">
|
||||
<h2 id="playlist-heading">Playlist Loop</h2>
|
||||
|
||||
@ -216,9 +401,10 @@
|
||||
</label>
|
||||
|
||||
<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
|
||||
</label>
|
||||
<span id="saveSettingsHint" class="sr-only">Click to make sure your settings are saved permanently</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@ -256,15 +442,16 @@
|
||||
</label>
|
||||
|
||||
<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
|
||||
</label>
|
||||
<span id="saveWebSettingsHint" class="sr-only">Click to make sure your settings are saved permanently</span>
|
||||
</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>
|
||||
<textarea id="galleryURL" name="galleryURL" rows="3" placeholder="https://ataphotobooths.com"></textarea>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</section>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
@ -445,7 +678,285 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadLastUsedData();
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user