update 2026-07-27 22:20:34
No files matched your search
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="175" height="20" role="img" aria-label="OpenWrt: Compiler author"><title>OpenWrt: Compiler author</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="175" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="76" height="20" fill="#555"/><rect x="76" width="99" height="20" fill="#4c1"/><rect width="175" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJ3aGl0ZXNtb2tlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+R2l0SHViPC90aXRsZT48cGF0aCBkPSJNMTIgLjI5N2MtNi42MyAwLTEyIDUuMzczLTEyIDEyIDAgNS4zMDMgMy40MzggOS44IDguMjA1IDExLjM4NS42LjExMy44Mi0uMjU4LjgyLS41NzcgMC0uMjg1LS4wMS0xLjA0LS4wMTUtMi4wNC0zLjMzOC43MjQtNC4wNDItMS42MS00LjA0Mi0xLjYxQzQuNDIyIDE4LjA3IDMuNjMzIDE3LjcgMy42MzMgMTcuN2MtMS4wODctLjc0NC4wODQtLjcyOS4wODQtLjcyOSAxLjIwNS4wODQgMS44MzggMS4yMzYgMS44MzggMS4yMzYgMS4wNyAxLjgzNSAyLjgwOSAxLjMwNSAzLjQ5NS45OTguMTA4LS43NzYuNDE3LTEuMzA1Ljc2LTEuNjA1LTIuNjY1LS4zLTUuNDY2LTEuMzMyLTUuNDY2LTUuOTMgMC0xLjMxLjQ2NS0yLjM4IDEuMjM1LTMuMjItLjEzNS0uMzAzLS41NC0xLjUyMy4xMDUtMy4xNzYgMCAwIDEuMDA1LS4zMjIgMy4zIDEuMjMuOTYtLjI2NyAxLjk4LS4zOTkgMy0uNDA1IDEuMDIuMDA2IDIuMDQuMTM4IDMgLjQwNSAyLjI4LTEuNTUyIDMuMjg1LTEuMjMgMy4yODUtMS4yMy42NDUgMS42NTMuMjQgMi44NzMuMTIgMy4xNzYuNzY1Ljg0IDEuMjMgMS45MSAxLjIzIDMuMjIgMCA0LjYxLTIuODA1IDUuNjI1LTUuNDc1IDUuOTIuNDIuMzYuODEgMS4wOTYuODEgMi4yMiAwIDEuNjA2LS4wMTUgMi44OTYtLjAxNSAzLjI4NiAwIC4zMTUuMjEuNjkuODI1LjU3QzIwLjU2NSAyMi4wOTIgMjQgMTcuNTkyIDI0IDEyLjI5N2MwLTYuNjI3LTUuMzczLTEyLTEyLTEyIi8+PC9zdmc+"/><text aria-hidden="true" x="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">OpenWrt</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="490">OpenWrt</text><text aria-hidden="true" x="1245" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="890">Compiler author</text><text x="1245" y="140" transform="scale(.1)" fill="#fff" textLength="890">Compiler author</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="85" height="20" role="img" aria-label="JavaScript"><title>JavaScript</title><g shape-rendering="crispEdges"><rect width="0" height="20" fill="#555"/><rect x="0" width="85" height="20" fill="#323330"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" href="data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZjdkZjFlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+SmF2YVNjcmlwdDwvdGl0bGU+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6bTIyLjAzNCAxOC4yNzZjLS4xNzUtMS4wOTUtLjg4OC0yLjAxNS0zLjAwMy0yLjg3My0uNzM2LS4zNDUtMS41NTQtLjU4NS0xLjc5Ny0xLjE0LS4wOTEtLjMzLS4xMDUtLjUxLS4wNDYtLjcwNS4xNS0uNjQ2LjkxNS0uODQgMS41MTUtLjY2LjM5LjEyLjc1LjQyLjk3Ni45IDEuMDM0LS42NzYgMS4wMzQtLjY3NiAxLjc1NS0xLjEyNS0uMjctLjQyLS40MDQtLjYwMS0uNTg2LS43OC0uNjMtLjcwNS0xLjQ2OS0xLjA2NS0yLjgzNC0xLjAzNGwtLjcwNS4wODljLS42NzYuMTY1LTEuMzIuNTI1LTEuNzEgMS4wMDUtMS4xNCAxLjI5MS0uODExIDMuNTQxLjU2OSA0LjQ3MSAxLjM2NSAxLjAyIDMuMzYxIDEuMjQ0IDMuNjE2IDIuMjA1LjI0IDEuMTctLjg3IDEuNTQ1LTEuOTY2IDEuNDEtLjgxMS0uMTgtMS4yNi0uNTg2LTEuNzU1LTEuMzM2bC0xLjgzIDEuMDUxYy4yMS40OC40NS42ODkuODEgMS4xMDkgMS43NCAxLjc1NiA2LjA5IDEuNjY2IDYuODcxLTEuMDA0LjAyOS0uMDkuMjQtLjcwNS4wNzQtMS42NWwuMDQ2LjA2N3ptLTguOTgzLTcuMjQ1aC0yLjI0OGMwIDEuOTM4LS4wMDkgMy44NjQtLjAwOSA1LjgwNSAwIDEuMjMyLjA2MyAyLjM2My0uMTM4IDIuNzExLS4zMy42ODktMS4xOC42MDEtMS41NjYuNDgtLjM5Ni0uMTk2LS41OTctLjQ2Ni0uODMtLjg1NS0uMDYzLS4xMDUtLjExLS4xOTYtLjEyNy0uMTk2bC0xLjgyNSAxLjEyNWMuMzA1LjYzLjc1IDEuMTcyIDEuMzI0IDEuNTE3Ljg1NS41MSAyLjAwNC42NzUgMy4yMDcuNDA1Ljc4My0uMjI2IDEuNDU4LS42OTEgMS44MTEtMS40MTEuNTEtLjkzLjQwMi0yLjA3LjM5Ny0zLjM0Ni4wMTItMi4wNTQgMC00LjEwOSAwLTYuMTc5bC4wMDQtLjA1NnoiLz48L3N2Zz4="/><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="570">JavaScript</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="47" height="20" role="img" aria-label="Lua"><title>Lua</title><g shape-rendering="crispEdges"><rect width="0" height="20" fill="#555"/><rect x="0" width="47" height="20" fill="#2c2d72"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" href="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJ3aGl0ZSIgcm9sZT0iaW1nIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPkx1YTwvdGl0bGU+PHBhdGggZD0iTS4zOCAxMC4zNzdsLS4yNzItLjAzN2MtLjA0OC4zNDQtLjA4Mi42OTUtLjEwMSAxLjA0MWwuMjc1LjAxNmMuMDE4LS4zNC4wNTEtLjY4Mi4wOTgtMS4wMnpNNC4xMzYgMy4yODlsLS4xODQtLjIwNWMtLjI1OC4yMzItLjUwOS40OC0uNzQ2LjczNGwuMjAyLjE4OGMuMjMxLS4yNDguNDc2LS40OS43MjgtLjcxN3pNNS43NjkgMi4wNTlsLS4xNDYtLjIzNWMtLjI5Ni4xODYtLjU4Ni4zODUtLjg2My41OTRsLjE2Ni4yMTljLjI3LS4yMDMuNTU0LS4zOTkuODQzLS41Nzh6TTEuODI0IDE4LjM2OWMuMTg1LjI5Ny4zODQuNTg2LjU5My44NjNsLjIyLS4xNjRjLS4yMDUtLjI3MS0uMzk5LS41NTUtLjU4LS44NDRsLS4yMzMuMTQ1ek0xLjEyNyAxNi40MDJsLS4yNTUuMTA0Yy4xMjkuMzE4LjI3NC42MzUuNDMxLjk0M2wuMDA1LjAxLjI0NS0uMTI1LS4wMDUtLjAxYy0uMTUzLS4zMDEtLjI5NS0uNjExLS40MjEtLjkyMnpNLjI5OCA5LjMwOWwuMjY5LjA2M2MuMDc2LS4zMzIuMTY4LS42NjQuMjcyLS45ODZsLS4yNjEtLjA4N2MtLjEwOC4zMzItLjIwMi42NzItLjI4IDEuMDF6TS4yNzQgMTIuNDJsLS4yNzUuMDFjLjAxMi4zNDguMDQuNjk5LjA4MyAxLjA0M2wuMjczLS4wMzNjLS4wNDItLjMzNi0uMDY5LS42OC0uMDgxLTEuMDJ6TS4yNTYgMTQuNTA2Yy4wNzMuMzQuMTYyLjY4Mi4yNjQgMS4wMTRsLjI2My0uMDhjLS4xLS4zMjYtLjE4Ny0uNjU4LS4yNTgtLjk5bC0uMjY5LjA1NnpNMTEuNTczLjI3NUwxMS41NjMgMGMtLjM0OC4wMTItLjY5OS4wMzktMS4wNDQuMDgybC4wMzQuMjczYy4zMzgtLjA0MS42OC0uMDY4IDEuMDItLjA4ek0yMy4yMjEgOC41NjZjLjEuMzI2LjE4Ni42Ni4yNTYuOTkybC4yNy0uMDU5Yy0uMDcyLS4zNC0uMTYtLjY4Mi0uMjYyLTEuMDE0bC0uMjY0LjA4MXpNMTcuNjIxIDEuMzg5Yy0uMzA5LS4xNjQtLjYyNy0uMzE0LS45NDctLjQ0OWwtLjEwNy4yNTJjLjMxNC4xMzMuNjI1LjI4MS45MjYuNDM5bC4xMjgtLjI0MnpNMTUuNjkzLjU3MmMtLjMzMi0uMTA1LS42Ny0uMTk5LTEuMDEtLjI3N2wtLjA2My4yNjhjLjMzMi4wNzYuNjY0LjE2OC45ODguMjczbC4wODUtLjI2NHpNNi42NzQgMS41NDVjLjI5OC0uMTUuNjA2LS4yOTEuOTE2LS40MThMNy40ODYuODczYy0uMzE3LjEyNy0uNjMyLjI3Mi0uOTM3LjQyOGwtLjAxNS4wMDguMTI1LjI0NC4wMTUtLjAwOHpNMjMuNzI3IDExLjU4OGwuMjc1LS4wMWExMS43OTcgMTEuNzk3IDAgMCAwLS4wODItMS4wNDVsLS4yNzMuMDMzYy4wNDEuMzM4LjA2OC42ODIuMDggMS4wMjJ6TTEzLjY1NC4xMDVjLS4zNDYtLjA0Ny0uNjk2LS4wOC0xLjA0My0uMDk4bC0uMDE0LjI3M2MuMzM5LjAxOC42ODMuMDUxIDEuMDE5LjA5OGwuMDM4LS4yNzN6TTkuNTQ0LjUyN2wtLjA1OC0uMjdjLS4zNC4wNzItLjY4MS4xNi0xLjAxNC4yNjRsLjA4MS4yNjJjLjMyNS0uMDk5LjY1OS0uMTg1Ljk5MS0uMjU2ek0xLjkyMSA1LjQ2OWwuMjMxLjE1Yy4xODUtLjI4NS4zODQtLjU2Ni41OTItLjgzNGwtLjIxNy0uMTdjLS4yMTMuMjc2LS40MTcuNTYzLS42MDYuODU0ek0uOTQzIDcuMzE4bC4yNTMuMTA3Yy4xMzItLjMxMy4yOC0uNjI1LjQzOS0uOTI0bC0uMjQzLS4xMjhjLS4xNjMuMzA3LS4zMTQuNjI1LS40NDkuOTQ1ek0xOC4yMjMgMjEuOTQzbC4xNDUuMjM0Yy4yOTUtLjE4Ni41ODYtLjM4NS44NjMtLjU5NGwtLjE2NC0uMjE5Yy0uMjcyLjIwNC0uNTU3LjQtLjg0NC41Nzl6TTIxLjI0OCAxOS4yMTlsLjIxNy4xN2MuMjE1LS4yNzMuNDE4LS41NjEuNjA3LS44NTRsLS4yMy0uMTQ4Yy0uMTg2LjI4NS0uMzg1LjU2NC0uNTk0LjgzMnpNMTkuODU1IDIwLjcxNWwuMTg0LjIwM2MuMjU4LS4yMy41MS0uNDc5Ljc0Ni0uNzMybC0uMjAxLS4xODhjLS4yMy4yNDgtLjQ3Ny40ODgtLjcyOS43MTd6TTIyLjM1OSAxNy41MDRsLjI0NC4xMjljLjE2Mi0uMzA3LjMxNC0uNjI1LjQ0OS0uOTQ1bC0uMjU0LS4xMDdhMTEuMjcgMTEuMjcgMCAwIDEtLjQzOS45MjN6TTIzLjYxNyAxMy42MjlsLjI3My4wMzljLjA0OS0uMzQ2LjA4Mi0uNjk1LjEwMi0xLjA0M2wtLjI3NS0uMDE0Yy0uMDE4LjMzOC0uMDUxLjY4Mi0uMSAxLjAxOHpNMjMuMTU2IDE1LjYyMWwuMjY0LjA4NmMuMTA3LS4zMzIuMjAxLS42Ny4yNzktMS4wMWwtLjI2OC0uMDYzYy0uMDc3LjMzMy0uMTY5LjY2NS0uMjc1Ljk4N3pNMjIuNDUzIDYuNjcyYy4xNTQuMzAzLjI5Ny42MTcuNDI0LjkzMmwuMjU2LS4xMDRjLS4xMzEtLjMyMi0uMjc3LS42NDMtLjQzNi0uOTUzbC0uMjQ0LjEyNXpNOC4yOTYgMjMuNDE4Yy4zMzEuMTA3LjY3LjIwMSAxLjAwOS4yNzlsLjA2Mi0uMjY4Yy0uMzMxLS4wNzYtLjY2My0uMTY4LS45ODYtLjI3M2wtLjA4NS4yNjJ6TTEwLjMzNSAyMy44ODljLjM0NS4wNDkuNjk2LjA4MiAxLjA0My4xMDJsLjAxNC0uMjc1Yy0uMzM5LS4wMTgtLjY4Mi0uMDUxLTEuMDE5LS4wOThsLS4wMzguMjcxek0xNy4zMjYgMjIuNDQ5Yy0uMzAzLjE1NC0uNjEzLjI5Ny0uOTI2LjQyNGwuMTA0LjI1NmMuMzE4LS4xMzEuNjM5LS4yNzUuOTQ3LS40MzRsLjAwNC0uMDAyLS4xMjMtLjI0Ni0uMDA2LjAwMnpNNC42MTMgMjEuNDY3Yy4yNzQuMjEzLjU2Mi40MTguODU0LjYwNWwuMTQ5LS4yM2MtLjI4NS0uMTg0LS41NjUtLjM4NS0uODMzLS41OTJsLS4xNy4yMTd6TTEyLjQxNyAyMy43MjVsLjAwOS4yNzVjLjM0OC0uMDE0LjY5OS0uMDQxIDEuMDQ1LS4wODRsLS4wMzUtLjI3MWMtLjMzNi4wNDEtLjY4LjA2OC0xLjAxOS4wOHpNNi4zNyAyMi42MDRjLjMwNy4xNjIuNjI1LjMxNC45NDYuNDQ5bC4xMDctLjI1NGMtLjMxMy0uMTMzLS42MjQtLjI3OS0uOTI0LS40MzlsLS4xMjkuMjQ0ek0zLjA4MyAyMC4wNDFjLjIzMy4yNTguNDguNTEuNzM0Ljc0NmwuMTg4LS4yMDFjLS4yNDktLjIzLS40OS0uNDc3LS43MTctLjcyOWwtLjIwNS4xODR6TTE0LjQ0NSAyMy40NzVsLjA1OS4yN2MuMzQtLjA3NC42OC0uMTYyIDEuMDE0LS4yNjZsLS4wODItLjI2MmMtLjMyNS4wOTktLjY1OS4xODUtLjk5MS4yNTh6TTIxLjE4LjEyOUEyLjY4OSAyLjY4OSAwIDEgMCAyMS4xOCA1LjUwNyAyLjY4OSAyLjY4OSAwIDEgMCAyMS4xOC4xMjl6TTE1LjMyNCAxNS40NDdjMCAuNDcxLjMxNC42Ni44NTIuNjYuNjcgMCAxLjI5Ny0uMzk2IDEuMjk3LTEuMDE2di0uNjQ1Yy0uMjMuMTA3LS4zNzkuMTQxLTEuMTA3LjI0LS43MzUuMTA5LTEuMDQyLjMwNi0xLjA0Mi43NjF6TTEyIDIuODE4Yy01LjA3IDAtOS4xOCA0LjEwOS05LjE4IDkuMTggMCA1LjA2OCA0LjExIDkuMTggOS4xOCA5LjE4IDUuMDcgMCA5LjE4LTQuMTExIDkuMTgtOS4xOCAwLTUuMDctNC4xMS05LjE4LTkuMTgtOS4xOHptLTIuLine truncated
|
||||
|
Before Width: | Height: | Size: 5.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="187" height="20" role="img" aria-label="OpenWrt: flippy's homepage"><title>OpenWrt: flippy's homepage</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="187" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="76" height="20" fill="#555"/><rect x="76" width="111" height="20" fill="blueviolet"/><rect width="187" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJ3aGl0ZXNtb2tlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+R2l0SHViPC90aXRsZT48cGF0aCBkPSJNMTIgLjI5N2MtNi42MyAwLTEyIDUuMzczLTEyIDEyIDAgNS4zMDMgMy40MzggOS44IDguMjA1IDExLjM4NS42LjExMy44Mi0uMjU4LjgyLS41NzcgMC0uMjg1LS4wMS0xLjA0LS4wMTUtMi4wNC0zLjMzOC43MjQtNC4wNDItMS42MS00LjA0Mi0xLjYxQzQuNDIyIDE4LjA3IDMuNjMzIDE3LjcgMy42MzMgMTcuN2MtMS4wODctLjc0NC4wODQtLjcyOS4wODQtLjcyOSAxLjIwNS4wODQgMS44MzggMS4yMzYgMS44MzggMS4yMzYgMS4wNyAxLjgzNSAyLjgwOSAxLjMwNSAzLjQ5NS45OTguMTA4LS43NzYuNDE3LTEuMzA1Ljc2LTEuNjA1LTIuNjY1LS4zLTUuNDY2LTEuMzMyLTUuNDY2LTUuOTMgMC0xLjMxLjQ2NS0yLjM4IDEuMjM1LTMuMjItLjEzNS0uMzAzLS41NC0xLjUyMy4xMDUtMy4xNzYgMCAwIDEuMDA1LS4zMjIgMy4zIDEuMjMuOTYtLjI2NyAxLjk4LS4zOTkgMy0uNDA1IDEuMDIuMDA2IDIuMDQuMTM4IDMgLjQwNSAyLjI4LTEuNTUyIDMuMjg1LTEuMjMgMy4yODUtMS4yMy42NDUgMS42NTMuMjQgMi44NzMuMTIgMy4xNzYuNzY1Ljg0IDEuMjMgMS45MSAxLjIzIDMuMjIgMCA0LjYxLTIuODA1IDUuNjI1LTUuNDc1IDUuOTIuNDIuMzYuODEgMS4wOTYuODEgMi4yMiAwIDEuNjA2LS4wMTUgMi44OTYtLjAxNSAzLjI4NiAwIC4zMTUuMjEuNjkuODI1LjU3QzIwLjU2NSAyMi4wOTIgMjQgMTcuNTkyIDI0IDEyLjI5N2MwLTYuNjI3LTUuMzczLTEyLTEyLTEyIi8+PC9zdmc+"/><text aria-hidden="true" x="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">OpenWrt</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="490">OpenWrt</text><text aria-hidden="true" x="1305" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="1010">flippy's homepage</text><text x="1305" y="140" transform="scale(.1)" fill="#fff" textLength="1010">flippy's homepage</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="161" height="20" role="img" aria-label="Plugin: luci-app-amlogic"><title>Plugin: luci-app-amlogic</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="161" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="60" height="20" fill="#555"/><rect x="60" width="101" height="20" fill="#007ec6"/><rect width="161" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJ3aGl0ZXNtb2tlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+R2l0SHViPC90aXRsZT48cGF0aCBkPSJNMTIgLjI5N2MtNi42MyAwLTEyIDUuMzczLTEyIDEyIDAgNS4zMDMgMy40MzggOS44IDguMjA1IDExLjM4NS42LjExMy44Mi0uMjU4LjgyLS41NzcgMC0uMjg1LS4wMS0xLjA0LS4wMTUtMi4wNC0zLjMzOC43MjQtNC4wNDItMS42MS00LjA0Mi0xLjYxQzQuNDIyIDE4LjA3IDMuNjMzIDE3LjcgMy42MzMgMTcuN2MtMS4wODctLjc0NC4wODQtLjcyOS4wODQtLjcyOSAxLjIwNS4wODQgMS44MzggMS4yMzYgMS44MzggMS4yMzYgMS4wNyAxLjgzNSAyLjgwOSAxLjMwNSAzLjQ5NS45OTguMTA4LS43NzYuNDE3LTEuMzA1Ljc2LTEuNjA1LTIuNjY1LS4zLTUuNDY2LTEuMzMyLTUuNDY2LTUuOTMgMC0xLjMxLjQ2NS0yLjM4IDEuMjM1LTMuMjItLjEzNS0uMzAzLS41NC0xLjUyMy4xMDUtMy4xNzYgMCAwIDEuMDA1LS4zMjIgMy4zIDEuMjMuOTYtLjI2NyAxLjk4LS4zOTkgMy0uNDA1IDEuMDIuMDA2IDIuMDQuMTM4IDMgLjQwNSAyLjI4LTEuNTUyIDMuMjg1LTEuMjMgMy4yODUtMS4yMy42NDUgMS42NTMuMjQgMi44NzMuMTIgMy4xNzYuNzY1Ljg0IDEuMjMgMS45MSAxLjIzIDMuMjIgMCA0LjYxLTIuODA1IDUuNjI1LTUuNDc1IDUuOTIuNDIuMzYuODEgMS4wOTYuODEgMi4yMiAwIDEuNjA2LS4wMTUgMi44OTYtLjAxNSAzLjI4NiAwIC4zMTUuMjEuNjkuODI1LjU3QzIwLjU2NSAyMi4wOTIgMjQgMTcuNTkyIDI0IDEyLjI5N2MwLTYuNjI3LTUuMzczLTEyLTEyLTEyIi8+PC9zdmc+"/><text aria-hidden="true" x="395" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">Plugin</text><text x="395" y="140" transform="scale(.1)" fill="#fff" textLength="330">Plugin</text><text aria-hidden="true" x="1095" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="910">luci-app-amlogic</text><text x="1095" y="140" transform="scale(.1)" fill="#fff" textLength="910">luci-app-amlogic</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1,84 +0,0 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Theme-aware styles for luci-app-amlogic JS views.
|
||||
*/
|
||||
|
||||
/* --- Status text ------------------------------------ */
|
||||
.amlogic-status-ok { color: #1a7f37; }
|
||||
.amlogic-status-err { color: #c0392b; }
|
||||
.amlogic-status-info { color: #1f6feb; }
|
||||
|
||||
/* --- Spacious row table -----------------------------------------------
|
||||
* Used by views that lay out a single button + status text per row
|
||||
* (check / backup / upload). border-collapse:collapse avoids the visible
|
||||
* blank band that border-spacing would otherwise add around the table
|
||||
* edges; row breathing room comes purely from cell padding.
|
||||
* ---------------------------------------------------------------------- */
|
||||
.amlogic-row-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.amlogic-row-table > tbody > tr > td,
|
||||
.amlogic-row-table > tr > td {
|
||||
padding: 10px 12px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* --- Snapshot grid (cards) --------------------------------------------- */
|
||||
.amlogic-snap-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
.amlogic-snap-list:after,
|
||||
.amlogic-snap-list .amlogic-snap-item:not(:nth-child(3n)) {
|
||||
content: "";
|
||||
}
|
||||
.amlogic-snap-item {
|
||||
box-sizing: border-box;
|
||||
width: 150px;
|
||||
height: 130px;
|
||||
margin: 8px;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(127, 127, 127, 0.35);
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
.amlogic-snap-item:hover {
|
||||
background: rgba(127, 127, 127, 0.15);
|
||||
}
|
||||
.amlogic-snap-line {
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
/* --- Info-page logo / author tiles ------------------------------------- */
|
||||
.amlogic-info-logo {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* --- Dark mode overrides (system / browser preference) -----------------
|
||||
* Reads prefers-color-scheme from the system / browser. When the user has
|
||||
* picked a dark theme, switch to brighter colors to keep readable contrast.
|
||||
* LuCI itself only adjusts the background, so this stylesheet fills in the
|
||||
* status text and snapshot border colors that the upstream theme leaves
|
||||
* untouched.
|
||||
* ---------------------------------------------------------------------- */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.amlogic-status-ok { color: #4ade80; }
|
||||
.amlogic-status-err { color: #f87171; }
|
||||
.amlogic-status-info { color: #60a5fa; }
|
||||
.amlogic-snap-item {
|
||||
border-color: rgba(200, 200, 200, 0.25);
|
||||
}
|
||||
.amlogic-snap-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// CPU Freq Settings page
|
||||
//
|
||||
// Purpose: configure scaling governor and min/max frequency per CPU policy
|
||||
// (cluster). After save, the backend script fixcpufreq.pl is invoked so the
|
||||
// new values take effect immediately. The backing RPC is exposed by
|
||||
// /usr/share/rpcd/ucode/luci.amlogic.
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
|
||||
// Query CPU cluster info (each policy: core type, available governors, freqs).
|
||||
const callPolicies = rpc.declare({ object: 'luci.amlogic', method: 'cpu_policies',
|
||||
expect: { policies: [] } });
|
||||
// Tell backend to apply the new governor / frequency limits right after save.
|
||||
const callReloadCpu = rpc.declare({ object: 'luci.amlogic', method: 'reload_cpu' });
|
||||
|
||||
return view.extend({
|
||||
// Load in parallel: CPU policy info + UCI config (we will read/write it).
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
callPolicies(),
|
||||
uci.load('amlogic')
|
||||
]);
|
||||
},
|
||||
|
||||
// Render a tabbed form section per CPU policy (cluster), with options to
|
||||
// set the governor and min/max frequency. The form is bound to the amlogic.armcpu UCI section.
|
||||
render: function (data) {
|
||||
const policies = data[0] || [];
|
||||
|
||||
// Auto-create the armcpu section when missing, so that on a fresh
|
||||
// install/upgrade the user can save without first creating the section
|
||||
// manually.
|
||||
if (!uci.get('amlogic', 'armcpu')) {
|
||||
uci.add('amlogic', 'settings', 'armcpu');
|
||||
}
|
||||
|
||||
// Build CBI Map bound to the amlogic.armcpu NamedSection.
|
||||
const m = new form.Map('amlogic', _('CPU Freq Settings'),
|
||||
_('Set CPU Scaling Governor to Max Performance or Balance Mode'));
|
||||
const s = m.section(form.NamedSection, 'armcpu', 'settings');
|
||||
s.anonymous = true;
|
||||
|
||||
// Render an independent tab + form options per CPU cluster (big/little).
|
||||
policies.forEach(function (p) {
|
||||
const tabId = 'tab' + p.id;
|
||||
s.tab(tabId, p.name);
|
||||
|
||||
// Microarchitecture (read-only display, inline so it aligns with the label).
|
||||
const ct = s.taboption(tabId, form.DummyValue, 'core_type' + p.id,
|
||||
_('Microarchitectures:'));
|
||||
ct.cfgvalue = function () { return p.core_type; };
|
||||
ct.renderWidget = function (section_id) {
|
||||
return E('span', { style: 'line-height:2em; display:inline-block; vertical-align:middle' },
|
||||
this.cfgvalue(section_id) || '');
|
||||
};
|
||||
|
||||
// CPU scaling governor: list of governors exposed by the kernel
|
||||
// (performance / schedutil / etc).
|
||||
const gov = s.taboption(tabId, form.ListValue, 'governor' + p.id,
|
||||
_('CPU Scaling Governor:'));
|
||||
(p.governors || []).forEach(function (e) { gov.value(e, e.toUpperCase()); });
|
||||
gov.default = 'schedutil';
|
||||
gov.rmempty = false;
|
||||
|
||||
// Minimum running frequency (kHz).
|
||||
const minF = s.taboption(tabId, form.ListValue, 'minfreq' + p.id, _('Min Freq:'));
|
||||
(p.freqs || []).forEach(function (e) { minF.value(e); });
|
||||
minF.default = '500000';
|
||||
minF.rmempty = false;
|
||||
|
||||
// Maximum running frequency (kHz).
|
||||
const maxF = s.taboption(tabId, form.ListValue, 'maxfreq' + p.id, _('Max Freq:'));
|
||||
(p.freqs || []).forEach(function (e) { maxF.value(e); });
|
||||
maxF.default = '1512000';
|
||||
maxF.rmempty = false;
|
||||
});
|
||||
|
||||
// Reload CPU settings after the LuCI apply mechanism commits UCI to disk.
|
||||
// The standard apply flow (apply_rollback → ucitrack → init.d restart)
|
||||
// reads UCI before the new values are fully written, so we re-apply here.
|
||||
document.addEventListener('uci-applied', function () {
|
||||
callReloadCpu();
|
||||
});
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -1,261 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Backup Firmware Config + Snapshot management + (optional) KVM switch
|
||||
//
|
||||
// Purpose: pack current /etc config into openwrt_config.tar.gz and download it;
|
||||
// manage system snapshots (list / create / restore / delete); optionally show
|
||||
// KVM dual-partition switch when the platform reports has_kvm.
|
||||
// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (backup_create, snapshot_*, platform_info, kvm_switch).
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require fs';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// Generate one openwrt_config.tar.gz on the server and return its path so the
|
||||
// frontend can stream it back via fs.read_direct.
|
||||
const callBackup = rpc.declare({ object: 'luci.amlogic', method: 'backup_create' });
|
||||
// List existing snapshot names (e.g. etc-000 / etc-001 / etc-202401...).
|
||||
const callSnapList = rpc.declare({ object: 'luci.amlogic', method: 'snapshot_list',
|
||||
expect: { names: [] } });
|
||||
// Create a new snapshot (the backend chooses its name).
|
||||
const callSnapCreate = rpc.declare({ object: 'luci.amlogic', method: 'snapshot_create' });
|
||||
// Delete a snapshot by short name (without the etc- prefix).
|
||||
const callSnapDel = rpc.declare({ object: 'luci.amlogic', method: 'snapshot_delete',
|
||||
params: ['name'] });
|
||||
// Restore the given snapshot and reboot.
|
||||
const callSnapRest = rpc.declare({ object: 'luci.amlogic', method: 'snapshot_restore',
|
||||
params: ['name'] });
|
||||
// Read platform info (this page only consumes has_kvm to decide whether to
|
||||
// show the KVM switch row).
|
||||
const callPlatform = rpc.declare({ object: 'luci.amlogic', method: 'platform_info' });
|
||||
// Switch active KVM partition.
|
||||
const callKvmSwitch = rpc.declare({ object: 'luci.amlogic', method: 'kvm_switch' });
|
||||
|
||||
// The view object for the Backup & Snapshot page.
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
// Load in parallel: platform info + current snapshot list.
|
||||
load: function () {
|
||||
amlogicShared.ensureCss();
|
||||
return Promise.all([callPlatform(), callSnapList()]);
|
||||
},
|
||||
|
||||
// Render the page: backup config section + snapshot management section + (optional) KVM switch section.
|
||||
render: function (data) {
|
||||
const platform = data[0] || {};
|
||||
const snapNames = data[1] || [];
|
||||
const view = this;
|
||||
|
||||
const editListBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Open List'),
|
||||
// Navigate to the backuplist page to edit the backup file list.
|
||||
click: function () {
|
||||
location.href = L.url('admin/system/amlogic/backuplist');
|
||||
}
|
||||
});
|
||||
|
||||
// Download status hint; updated to success color after generation completes.
|
||||
const downloadStatus = E('span', { class: 'amlogic-status-err' });
|
||||
const downloadBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Download Backup'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Generating...');
|
||||
// Step 1: ask the server to pack /etc into openwrt_config.tar.gz.
|
||||
return callBackup().then(function (r) {
|
||||
if (!r || !r.ok) {
|
||||
downloadStatus.textContent = _("Couldn't open file:") + ' ' + (r ? r.path : '');
|
||||
return;
|
||||
}
|
||||
// Switch to success color before streaming the blob
|
||||
downloadStatus.className = 'amlogic-status-ok';
|
||||
downloadStatus.textContent = _('The file Will download automatically.') + ' ' + r.path;
|
||||
// Fetch the file blob via fs.read_direct and trigger browser download.
|
||||
return fs.read_direct(r.path, 'blob').then(function (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'openwrt_config.tar.gz';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
|
||||
});
|
||||
}).catch(function (e) {
|
||||
downloadStatus.textContent = _('Create upload file error.') +
|
||||
' ' + (e && e.message ? e.message : '');
|
||||
}).finally(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Download Backup');
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Navigate to the upload page where the user can upload a backup file to restore.
|
||||
const restoreBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Upload Backup'),
|
||||
click: function () {
|
||||
location.href = L.url('admin/system/amlogic/upload');
|
||||
}
|
||||
});
|
||||
|
||||
// Snapshot grid; styles adapt to the current theme via amlogic-snap-* classes.
|
||||
const snapDiv = E('div', { class: 'amlogic-snap-list' });
|
||||
|
||||
// Render snapshot cards; etc-000/001 are read-only (no Delete button).
|
||||
// Backend expects the short name without the "etc-" prefix.
|
||||
function renderSnapshots(names) {
|
||||
snapDiv.innerHTML = '';
|
||||
if (!names || !names.length) {
|
||||
snapDiv.appendChild(E('span', { class: 'amlogic-status-err' },
|
||||
_('Currently OpenWrt does not support the snapshot function.') +
|
||||
_('Please use this plugin to reinstall or upgrade OpenWrt to enable the snapshot function.')));
|
||||
return;
|
||||
}
|
||||
names.forEach(function (n) {
|
||||
let title;
|
||||
if (n === 'etc-000') title = _('Initialize Snapshot');
|
||||
else if (n === 'etc-001') title = _('Update Snapshot');
|
||||
else title = n;
|
||||
const restoreSnapBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-apply',
|
||||
value: _('Restore Snap'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
const short = n.replace(/^etc-/, '');
|
||||
if (!confirm(_('You selected a snapshot:') + ' [ ' + n + ' ] , ' +
|
||||
_('Confirm recovery and restart OpenWrt?')))
|
||||
return;
|
||||
// Capture btn before entering async chain; ev.currentTarget becomes null after await.
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Restoring...');
|
||||
return callSnapRest(short).then(function (r) {
|
||||
if (r && r.code == 0) {
|
||||
btn.value = _('Successfully Restored');
|
||||
ui.addNotification(null,
|
||||
E('p', { class: 'amlogic-status-err' },
|
||||
_('Snapshot restored. The system will reboot now.')));
|
||||
} else {
|
||||
btn.value = _('Restore Failed');
|
||||
}
|
||||
}).catch(function () { btn.value = _('Restore Failed'); });
|
||||
})
|
||||
});
|
||||
const item = E('div', {
|
||||
class: 'amlogic-snap-item',
|
||||
id: 'snapshots_div_' + n
|
||||
}, [
|
||||
E('div', { class: 'amlogic-snap-line' }, title),
|
||||
E('div', { class: 'amlogic-snap-line' }, restoreSnapBtn)
|
||||
]);
|
||||
if (n !== 'etc-000' && n !== 'etc-001') {
|
||||
const delBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-remove',
|
||||
value: _('Delete Snap'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
const short = n.replace(/^etc-/, '');
|
||||
if (!confirm(_('You selected a snapshot:') + ' [ ' + n + ' ] , ' +
|
||||
_('Confirm delete?')))
|
||||
return;
|
||||
// Capture btn before entering async chain; ev.currentTarget becomes null after await.
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Deleting...');
|
||||
return callSnapDel(short).then(function (r) {
|
||||
if (r && r.code == 0) {
|
||||
btn.value = _('Successfully Deleted');
|
||||
item.style.display = 'none';
|
||||
} else {
|
||||
btn.value = _('Delete Failed');
|
||||
}
|
||||
}).catch(function () { btn.value = _('Delete Failed'); });
|
||||
})
|
||||
});
|
||||
item.appendChild(E('div', { class: 'amlogic-snap-line' }, delBtn));
|
||||
}
|
||||
snapDiv.appendChild(item);
|
||||
});
|
||||
}
|
||||
renderSnapshots(snapNames);
|
||||
|
||||
// Create snapshot button above the grid.
|
||||
const createSnapBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Create Snapshot'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
return callSnapCreate().then(function () {
|
||||
return callSnapList();
|
||||
}).then(function (r) {
|
||||
renderSnapshots(Array.isArray(r) ? r : (r && r.names) || []);
|
||||
}).finally(function () { btn.disabled = false; });
|
||||
})
|
||||
});
|
||||
|
||||
// If the platform supports KVM dual partitions, show a button to trigger partition switch.
|
||||
const sections = [
|
||||
E('h2', _('Backup Firmware Config')),
|
||||
E('p', _('Backup OpenWrt config (openwrt_config.tar.gz). Use this file to restore the config in [Manually Upload Update].')),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('table', { class: 'amlogic-row-table' }, [
|
||||
E('tr', [
|
||||
E('td', { width: '20%', align: 'right' }, _('Edit List:')),
|
||||
E('td', { width: '80%', align: 'left' }, editListBtn)
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '20%', align: 'right' }, _('Backup Config:')),
|
||||
E('td', { width: '80%', align: 'left' }, [downloadBtn, ' ', downloadStatus])
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '20%', align: 'right' }, _('Restore Backup:')),
|
||||
E('td', { width: '80%', align: 'left' }, restoreBtn)
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('h2', _('Snapshot Management')),
|
||||
E('p', _('Create a snapshot of the current system configuration, or restore to a snapshot.')),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('div', { style: 'margin-bottom:10px' }, createSnapBtn),
|
||||
snapDiv
|
||||
])
|
||||
];
|
||||
|
||||
// If the platform supports KVM dual partitions, show a button to trigger partition switch.
|
||||
if (platform.has_kvm) {
|
||||
const kvmBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Switch System'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
if (!confirm(_('Are you sure you want to switch systems?'))) return;
|
||||
ev.currentTarget.disabled = true;
|
||||
ui.showModal(_('System is switching...'), [
|
||||
E('p', { class: 'spinning' }, _('Waiting for system switching...'))
|
||||
]);
|
||||
return callKvmSwitch().then(function () {
|
||||
if (ui.awaitReconnect) {
|
||||
ui.awaitReconnect();
|
||||
} else {
|
||||
// Fallback for older LuCI builds without awaitReconnect
|
||||
setTimeout(function () { location.reload(); }, 5000);
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
sections.push(E('h2', _('KVM dual system switching')));
|
||||
sections.push(E('p', _('You can freely switch between KVM dual partitions, using OpenWrt systems in different partitions.')));
|
||||
sections.push(E('div', { class: 'cbi-section' }, [kvmBtn]));
|
||||
}
|
||||
|
||||
return E('div', {}, sections);
|
||||
}
|
||||
});
|
||||
@@ -1,82 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Backup file list editor
|
||||
//
|
||||
// Purpose: edit /etc/amlogic_backup_list.conf which controls which files/dirs
|
||||
// the openwrt-backup script archives. If the file is missing or empty, the
|
||||
// backend seeds it from the default BACKUP_LIST in /usr/sbin/openwrt-backup.
|
||||
// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (save_backup_list, prime_backup_list).
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require fs';
|
||||
|
||||
// Persist the user-edited content back to /etc/amlogic_backup_list.conf.
|
||||
const callSave = rpc.declare({ object: 'luci.amlogic', method: 'save_backup_list',
|
||||
params: ['content'] });
|
||||
// Seed the list file from defaults embedded in openwrt-backup if missing or empty.
|
||||
const callPrime = rpc.declare({ object: 'luci.amlogic', method: 'prime_backup_list' });
|
||||
|
||||
// This page uses its own Save button; hide LuCI's default Save/Apply/Reset.
|
||||
return view.extend({
|
||||
// This page uses its own Save button; hide LuCI's default Save/Apply/Reset.
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
// Load the existing backup list content to pre-fill the editor. If the file is
|
||||
load: function () {
|
||||
// Run prime first (idempotent), then read the file content.
|
||||
return callPrime().then(function () {
|
||||
return fs.read('/etc/amlogic_backup_list.conf').catch(function () { return ''; });
|
||||
});
|
||||
},
|
||||
|
||||
// Render a full-width textarea pre-filled with the existing content, and a Save button
|
||||
// that persists the changes via RPC. Also include a Back button to return to the main Backup page.
|
||||
render: function (text) {
|
||||
// Full-width multi-line editor pre-filled with the existing list.
|
||||
const ta = E('textarea', {
|
||||
rows: '30',
|
||||
style: 'width:100%; font-family:monospace'
|
||||
}, text || '');
|
||||
|
||||
// Status hint area (saved successfully / failed).
|
||||
const status = E('span', { style: 'margin-left:1em' });
|
||||
const saveBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Save'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
return callSave(ta.value).then(function (r) {
|
||||
if (r && (r.ok || r.code == 0)) {
|
||||
status.textContent = _('Successfully saved.');
|
||||
// Navigate back after a short delay so the success message is visible.
|
||||
setTimeout(function () {
|
||||
location.href = L.url('admin/system/amlogic/backup');
|
||||
}, 700);
|
||||
} else {
|
||||
status.textContent = _('Save Failed');
|
||||
}
|
||||
}).catch(function (e) {
|
||||
status.textContent = _('Save Failed') + ': ' + (e && e.message ? e.message : String(e));
|
||||
}).finally(function () { btn.disabled = false; });
|
||||
})
|
||||
});
|
||||
// Return to the Backup main page (without saving).
|
||||
const backBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reset',
|
||||
value: _('Back'),
|
||||
click: function () { location.href = L.url('admin/system/amlogic/backup'); }
|
||||
});
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('Edit list of backup files')),
|
||||
E('p', _('In this list, you can manually add the file or directory list of files that need to be backed up. Please confirm that the format and content of the file are correct.')),
|
||||
E('div', { class: 'cbi-section' }, [ta]),
|
||||
E('div', { class: 'cbi-page-actions' }, [saveBtn, ' ', backBtn, status])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -1,471 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Online Download Update + Rescue Kernel
|
||||
//
|
||||
// Purpose: check / download / install plugin, kernel, and firmware updates online.
|
||||
// Log tail is polled at 1 Hz; the last log line drives a state machine
|
||||
// (idle → checking → button → installing → done). Terminal log lines contain
|
||||
// raw HTML <input> button strings which are parsed and rendered as DOM buttons.
|
||||
// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (state, check_*, start_*, read_log_tail).
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// ── RPCs ─────────────────────────────────────────────────────────────────────
|
||||
// Query system state (firmware / plugin / kernel versions).
|
||||
const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' });
|
||||
// Run plugin / kernel / firmware version check and write progress to the log.
|
||||
const callCheckPlugin = rpc.declare({ object: 'luci.amlogic', method: 'check_plugin', params: ['options'] });
|
||||
const callCheckKernel = rpc.declare({ object: 'luci.amlogic', method: 'check_kernel', params: ['options'] });
|
||||
const callCheckFirmware = rpc.declare({ object: 'luci.amlogic', method: 'check_firmware', params: ['options'] });
|
||||
const callStartPlugin = rpc.declare({ object: 'luci.amlogic', method: 'start_plugin' });
|
||||
const callStartKernel = rpc.declare({ object: 'luci.amlogic', method: 'start_kernel' });
|
||||
const callStartUpdate = rpc.declare({ object: 'luci.amlogic', method: 'start_update', params: ['amlogic_update_sel'] });
|
||||
const callStartRescue = rpc.declare({ object: 'luci.amlogic', method: 'start_rescue' });
|
||||
// Read the last line (up to 4096 bytes) of the named log file.
|
||||
const callLogTail = rpc.declare({ object: 'luci.amlogic', method: 'read_log_tail', params: ['name'], expect: { line: '' } });
|
||||
|
||||
// ── Log-line HTML button parser ──────────────────────────────────────────────
|
||||
// Parse an <input> button string from the log; return null for plain-text lines.
|
||||
// Returned object: { type, param, label } where type identifies the action.
|
||||
function parseButtonLine(line) {
|
||||
if (!line || line.indexOf('<input') === -1) return null;
|
||||
|
||||
// Extract value="..."
|
||||
var valueMatch = line.match(/value="([^"]*)"/);
|
||||
var onclickMatch = line.match(/onclick="([^"]*)"/);
|
||||
if (!valueMatch || !onclickMatch) return null;
|
||||
|
||||
var value = valueMatch[1];
|
||||
var onclick = onclickMatch[1];
|
||||
|
||||
// Text after the self-closing tag
|
||||
var labelMatch = line.match(/\/>(.*)$/);
|
||||
var label = labelMatch ? labelMatch[1].trim() : '';
|
||||
|
||||
// Plugin download: b_check_plugin(this, 'download_VERSION')
|
||||
if (value === 'Download' && onclick.indexOf('b_check_plugin') !== -1) {
|
||||
var m = onclick.match(/b_check_plugin\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
|
||||
return { type: 'plugin-download', param: m ? m[1] : null, label: label };
|
||||
}
|
||||
|
||||
// Firmware download: b_check_firmware(this, 'PARAM')
|
||||
if (value === 'Download' && onclick.indexOf('b_check_firmware') !== -1) {
|
||||
var m = onclick.match(/b_check_firmware\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
|
||||
return { type: 'firmware-download', param: m ? m[1] : null, label: label };
|
||||
}
|
||||
|
||||
// Kernel download: b_check_kernel(this, 'PARAM')
|
||||
if (value === 'Download' && onclick.indexOf('b_check_kernel') !== -1) {
|
||||
var m = onclick.match(/b_check_kernel\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
|
||||
return { type: 'kernel-download', param: m ? m[1] : null, label: label };
|
||||
}
|
||||
|
||||
// Firmware install: amlogic_update(this, 'PARAM')
|
||||
if (value === 'Update' && onclick.indexOf('amlogic_update') !== -1) {
|
||||
var m = onclick.match(/amlogic_update\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
|
||||
return { type: 'firmware-update', param: m ? m[1] : null, label: label };
|
||||
}
|
||||
|
||||
// Kernel install: amlogic_kernel(this)
|
||||
if (value === 'Update' && onclick.indexOf('amlogic_kernel') !== -1) {
|
||||
return { type: 'kernel-update', param: null, label: label };
|
||||
}
|
||||
|
||||
// Plugin install: amlogic_plugin(this)
|
||||
if (value === 'Update' && onclick.indexOf('amlogic_plugin') !== -1) {
|
||||
return { type: 'plugin-update', param: null, label: label };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Per-action state machine ──────────────────────────────────────────────────
|
||||
// States: 'idle' | 'checking' | 'button' | 'installing' | 'done'
|
||||
|
||||
// Action object factory: { name, statusEl, state, subBtn }
|
||||
function makeAction(name, statusEl) {
|
||||
return { name: name, statusEl: statusEl, state: 'idle', subBtn: null };
|
||||
}
|
||||
|
||||
// Update the status span with a text string (blue info style).
|
||||
function showInfo(el, text) {
|
||||
dom.content(el, E('span', { class: 'amlogic-status-info' }, text));
|
||||
}
|
||||
|
||||
// Update the status span with a success string (green).
|
||||
function showOk(el, text) {
|
||||
dom.content(el, E('span', { class: 'amlogic-status-ok' }, text));
|
||||
}
|
||||
|
||||
// Update the status span with an error string (red).
|
||||
function showErr(el, text) {
|
||||
dom.content(el, E('span', { class: 'amlogic-status-err' }, text));
|
||||
}
|
||||
|
||||
// Replace statusEl contents with a real DOM button + optional label text.
|
||||
// onClick is called when the user presses it.
|
||||
function showSubButton(el, btnLabel, labelText, onClick) {
|
||||
var btn = E('input', {
|
||||
type: 'button',
|
||||
class: 'cbi-button cbi-button-reload',
|
||||
value: btnLabel,
|
||||
click: onClick
|
||||
});
|
||||
var nodes = [btn];
|
||||
if (labelText) nodes.push(E('span', { class: 'amlogic-status-info' }, ' ' + labelText));
|
||||
dom.content(el, nodes);
|
||||
return btn;
|
||||
}
|
||||
|
||||
// ── Poll handler (called at 1 Hz) ────────────────────────────────────────────
|
||||
// Inspect the last non-empty log line and advance the action state machine.
|
||||
function handlePoll(action) {
|
||||
if (action.state === 'done') return;
|
||||
|
||||
return callLogTail(action.name).then(function (raw) {
|
||||
// Only inspect the last non-empty line to avoid false-positive
|
||||
// 'Failed'/'error' matches from intermediate opkg/apk output.
|
||||
var lines = (raw || '').split('\n');
|
||||
var line = '';
|
||||
for (var i = lines.length - 1; i >= 0; i--) {
|
||||
var l = lines[i].trim();
|
||||
if (l) { line = l; break; }
|
||||
}
|
||||
|
||||
if (action.state === 'idle') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.state === 'button') {
|
||||
// Waiting for user to click sub-button; don't re-render on every poll.
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.state === 'installing') {
|
||||
// Show progress lines; detect terminal keywords.
|
||||
if (!line) return;
|
||||
// Skip if line is the same HTML button (stale log from check phase).
|
||||
if (line.indexOf('<input') !== -1) return;
|
||||
if (line.indexOf('Successfully updated') !== -1 ||
|
||||
line.indexOf('Successful Update') !== -1 ||
|
||||
line.indexOf('Successful') !== -1) {
|
||||
showOk(action.statusEl, line);
|
||||
action.state = 'done';
|
||||
} else if (line.indexOf('Failed') !== -1 || line.indexOf('failed') !== -1 ||
|
||||
line.indexOf('Error') !== -1 || line.indexOf('error') !== -1) {
|
||||
showErr(action.statusEl, line);
|
||||
action.state = 'done';
|
||||
} else {
|
||||
// Show current progress line (may change rapidly).
|
||||
showInfo(action.statusEl, line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// state === 'checking': look for HTML button line or plain progress text.
|
||||
var parsed = parseButtonLine(line);
|
||||
if (parsed) {
|
||||
action.state = 'button';
|
||||
action._parsed = parsed;
|
||||
renderParsedButton(action);
|
||||
} else if (line) {
|
||||
showInfo(action.statusEl, line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render the sub-button parsed from the log line and wire up the second-phase RPC.
|
||||
function renderParsedButton(action) {
|
||||
var parsed = action._parsed;
|
||||
var el = action.statusEl;
|
||||
|
||||
switch (parsed.type) {
|
||||
|
||||
case 'plugin-download':
|
||||
showSubButton(el, _('Download'), parsed.label, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.value = _('Downloading...');
|
||||
action.state = 'checking';
|
||||
callCheckPlugin(parsed.param).then(function () {});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'firmware-download':
|
||||
showSubButton(el, _('Download'), parsed.label, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.value = _('Downloading...');
|
||||
action.state = 'checking';
|
||||
callCheckFirmware(parsed.param).then(function () {});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'kernel-download':
|
||||
showSubButton(el, _('Download'), parsed.label, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.value = _('Downloading...');
|
||||
action.state = 'checking';
|
||||
callCheckKernel(parsed.param).then(function () {});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'firmware-update':
|
||||
showSubButton(el, _('Update'), parsed.label, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.value = _('Updating...');
|
||||
// Transition to 'installing'; terminal result comes via log polling.
|
||||
action.state = 'installing';
|
||||
dom.content(el, E('span', { class: 'amlogic-status-info' }, _('Starting update...')));
|
||||
callStartUpdate(parsed.param).catch(function () {
|
||||
// Only show error if the trigger call itself fails (rare).
|
||||
showErr(el, _('Failed to start update'));
|
||||
action.state = 'done';
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'kernel-update':
|
||||
showSubButton(el, _('Update'), parsed.label, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.value = _('Updating...');
|
||||
action.state = 'installing';
|
||||
dom.content(el, E('span', { class: 'amlogic-status-info' }, _('Starting kernel update...')));
|
||||
callStartKernel().catch(function () {
|
||||
showErr(el, _('Failed to start kernel update'));
|
||||
action.state = 'done';
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'plugin-update':
|
||||
showSubButton(el, _('Update'), parsed.label, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.value = _('Updating...');
|
||||
action.state = 'installing';
|
||||
dom.content(el, E('span', { class: 'amlogic-status-info' }, _('Starting plugin update...')));
|
||||
callStartPlugin().catch(function () {
|
||||
showErr(el, _('Failed to start plugin update'));
|
||||
action.state = 'done';
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
showInfo(el, parsed.label || '');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main view ────────────────────────────────────────────────────────────────
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
render: function () {
|
||||
amlogicShared.ensureCss();
|
||||
|
||||
// Version display spans (left persistent, filled by state RPC)
|
||||
var verFirmware = E('span', _('Collecting data...'));
|
||||
var verPlugin = E('span', _('Collecting data...'));
|
||||
var verKernel = E('span', _('Collecting data...'));
|
||||
var verRescue = E('span', _('Collecting data...'));
|
||||
|
||||
// Per-action status spans (right side, updated by poll)
|
||||
var statusFw = E('span');
|
||||
var statusPl = E('span');
|
||||
var statusKn = E('span');
|
||||
var statusRs = E('span');
|
||||
|
||||
// State machines
|
||||
var actPlugin = makeAction('plugin', statusPl);
|
||||
var actKernel = makeAction('kernel', statusKn);
|
||||
var actFirmware = makeAction('firmware', statusFw);
|
||||
var actRescue = makeAction('rescue', statusRs);
|
||||
|
||||
// ── Action buttons (left column) ──────────────────────────────────
|
||||
|
||||
// Helper: reset an action back to idle so it can be re-run after 'done'.
|
||||
function resetAction(action, btn, idleLabel) {
|
||||
action.state = 'idle';
|
||||
action.subBtn = null;
|
||||
dom.content(action.statusEl, '');
|
||||
btn.disabled = false;
|
||||
btn.value = idleLabel;
|
||||
}
|
||||
|
||||
// Each button triggers the first-phase RPC to start the check/download/install process,
|
||||
// then the poll handler picks up log lines to advance the state machine and render sub-buttons as needed.
|
||||
var btnPlugin = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Only update Amlogic Service'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
var btn = ev.currentTarget;
|
||||
// Allow re-run after completion by resetting 'done' → 'idle'
|
||||
if (actPlugin.state === 'done')
|
||||
resetAction(actPlugin, btn, _('Only update Amlogic Service'));
|
||||
if (actPlugin.state !== 'idle') return;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Checking...');
|
||||
actPlugin.state = 'checking';
|
||||
dom.content(actPlugin.statusEl, '');
|
||||
return callCheckPlugin('check').then(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Only update Amlogic Service');
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Only update Amlogic Service');
|
||||
showErr(actPlugin.statusEl, _('Check Failed'));
|
||||
actPlugin.state = 'idle';
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// The kernel and firmware buttons have similar logic, just different RPCs and labels.
|
||||
var btnKernel = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Update system kernel only'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
var btn = ev.currentTarget;
|
||||
if (actKernel.state === 'done')
|
||||
resetAction(actKernel, btn, _('Update system kernel only'));
|
||||
if (actKernel.state !== 'idle') return;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Checking...');
|
||||
actKernel.state = 'checking';
|
||||
dom.content(actKernel.statusEl, '');
|
||||
return callCheckKernel('check').then(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Update system kernel only');
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Update system kernel only');
|
||||
showErr(actKernel.statusEl, _('Check Failed'));
|
||||
actKernel.state = 'idle';
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Firmware update may include both kernel and plugin updates, so it has a more generic label.
|
||||
var btnFirmware = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Complete system update'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
var btn = ev.currentTarget;
|
||||
if (actFirmware.state === 'done')
|
||||
resetAction(actFirmware, btn, _('Complete system update'));
|
||||
if (actFirmware.state !== 'idle') return;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Checking...');
|
||||
actFirmware.state = 'checking';
|
||||
dom.content(actFirmware.statusEl, '');
|
||||
return callCheckFirmware('check').then(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Complete system update');
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Complete system update');
|
||||
showErr(actFirmware.statusEl, _('Check Failed'));
|
||||
actFirmware.state = 'idle';
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Rescue button: triggers the rescue RPC which starts mutual recovery; the log will show progress and terminal status.
|
||||
var btnRescue = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Rescue the original system kernel'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
var btn = ev.currentTarget;
|
||||
if (actRescue.state === 'done')
|
||||
resetAction(actRescue, btn, _('Rescue the original system kernel'));
|
||||
if (actRescue.state !== 'idle') return;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Rescuing...');
|
||||
// Set installing state before the call so the poller picks up log lines.
|
||||
actRescue.state = 'installing';
|
||||
showInfo(actRescue.statusEl, _('Starting rescue...'));
|
||||
return callStartRescue().then(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Rescue the original system kernel');
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Rescue the original system kernel');
|
||||
showErr(actRescue.statusEl, _('Failed to start rescue'));
|
||||
actRescue.state = 'idle';
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// ── Layout ────────────────────────────────────────────────────────
|
||||
|
||||
var body = E('div', {}, [
|
||||
E('h2', _('Check Update')),
|
||||
E('p', _('Provide OpenWrt Firmware, Kernel and Plugin online check, download and update service.')),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('p', { style: 'text-align:center' }, [
|
||||
_('Update plugins first, then update the kernel or firmware. More options can be configured in [Plugin Settings].')
|
||||
]),
|
||||
E('table', { class: 'amlogic-row-table' }, [
|
||||
E('tr', [
|
||||
E('td', { width: '35%', align: 'right' }, btnPlugin),
|
||||
E('td', { width: '65%', align: 'left' }, [verPlugin, ' ', statusPl])
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '35%', align: 'right' }, btnKernel),
|
||||
E('td', { width: '65%', align: 'left' }, [verKernel, ' ', statusKn])
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '35%', align: 'right' }, btnFirmware),
|
||||
E('td', { width: '65%', align: 'left' }, [verFirmware, ' ', statusFw])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('h2', _('Rescue Kernel')),
|
||||
E('p', _('When a kernel update fails and causes the OpenWrt system to be unbootable, the kernel can be restored by mutual recovery from eMMC/NVMe/sdX.')),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('table', { class: 'amlogic-row-table' }, [
|
||||
E('tr', [
|
||||
E('td', { width: '35%', align: 'right' }, btnRescue),
|
||||
E('td', { width: '65%', align: 'left' }, [verRescue, ' ', statusRs])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
// ── Populate version spans ────────────────────────────────────────
|
||||
|
||||
callState().then(function (s) {
|
||||
dom.content(verFirmware, s && s.current_firmware_version
|
||||
? E('span', { class: 'amlogic-status-ok' }, _('Current Version') + ' [ ' + s.current_firmware_version + ' ] ')
|
||||
: E('span', { class: 'amlogic-status-err' }, _('Invalid value.')));
|
||||
dom.content(verPlugin, s && s.current_plugin_version
|
||||
? E('span', { class: 'amlogic-status-ok' }, _('Current Version') + ' [ ' + s.current_plugin_version + ' ] ')
|
||||
: E('span', { class: 'amlogic-status-err' }, _('Invalid value.')));
|
||||
dom.content(verKernel, s && s.current_kernel_version
|
||||
? E('span', { class: 'amlogic-status-ok' }, _('Current Version') + ' [ ' + s.current_kernel_version + ' ] ')
|
||||
: E('span', { class: 'amlogic-status-err' }, _('Invalid value.')));
|
||||
|
||||
// Rescue row: show same kernel version directly via the verRescue span
|
||||
dom.content(verRescue, s && s.current_kernel_version
|
||||
? E('span', { class: 'amlogic-status-ok' }, _('Current Version') + ' [ ' + s.current_kernel_version + ' ] ')
|
||||
: E('span', { class: 'amlogic-status-err' }, _('Invalid value.')));
|
||||
});
|
||||
|
||||
// ── 1 Hz poll ─────────────────────────────────────────────────────
|
||||
|
||||
poll.add(function () {
|
||||
return Promise.all([
|
||||
handlePoll(actPlugin),
|
||||
handlePoll(actKernel),
|
||||
handlePoll(actFirmware),
|
||||
handlePoll(actRescue)
|
||||
]);
|
||||
}, 1);
|
||||
|
||||
return body;
|
||||
}
|
||||
});
|
||||
@@ -1,161 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Plugin Settings
|
||||
//
|
||||
// Purpose: configure the `config` section in /etc/config/amlogic via form.Map,
|
||||
// covering firmware/kernel GitHub repos, tags, version branch, plugin branch,
|
||||
// keep-config flag, bootloader write flag, and shared partition fs type.
|
||||
// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (platform_info, state).
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
|
||||
// Get platform info; only used to display the device PLATFORM tag.
|
||||
const callPlatform = rpc.declare({ object: 'luci.amlogic', method: 'platform_info' });
|
||||
// Query runtime state; kernel_release is used to derive default kernel tag and branch.
|
||||
const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' });
|
||||
|
||||
// This page uses its own Save button; hide LuCI's default Save/Apply/Reset.
|
||||
return view.extend({
|
||||
// Load platform info + state in parallel, along with the UCI config (we will read/write it).
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
callPlatform(),
|
||||
callState(),
|
||||
uci.load('amlogic')
|
||||
]);
|
||||
},
|
||||
|
||||
// Render form options bound to the amlogic.config NamedSection, and pre-fill some defaults based on platform/state.
|
||||
render: function (data) {
|
||||
const platform = data[0] || {};
|
||||
const state = data[1] || {};
|
||||
|
||||
// Auto-create the section if missing
|
||||
if (!uci.get('amlogic', 'config')) {
|
||||
uci.add('amlogic', 'amlogic', 'config');
|
||||
}
|
||||
|
||||
// Build a form.Map bound to the amlogic.config NamedSection, with options for various plugin settings.
|
||||
const m = new form.Map('amlogic', _('Plugin Settings'),
|
||||
_('You can customize the github.com download repository of OpenWrt files and kernels in [Online Download Update].') +
|
||||
'<br />' + _('Tip: The same files as the current OpenWrt system\'s BOARD (such as rock5b) and kernel (such as 5.10) will be downloaded.'));
|
||||
const o = m.section(form.NamedSection, 'config', 'amlogic');
|
||||
o.anonymous = true;
|
||||
|
||||
// 1. Display device platform
|
||||
const dev = o.option(form.DummyValue, 'mydevice', _('Current Device:'),
|
||||
_('Display the PLATFORM classification of the device.'));
|
||||
dev.cfgvalue = function () { return 'PLATFORM: ' + (platform.platform || 'Unknown'); };
|
||||
|
||||
// 2. OpenWrt download repository
|
||||
const repo = o.option(form.Value, 'amlogic_firmware_repo',
|
||||
_('OpenWrt download repository:'),
|
||||
_('Set the OpenWrt files download repository on github.com in [Online Download Update].'));
|
||||
repo.default = 'https://github.com/breakingbadboy/OpenWrt';
|
||||
repo.rmempty = false;
|
||||
|
||||
// 3. Tags keyword
|
||||
const tag = o.option(form.Value, 'amlogic_firmware_tag',
|
||||
_('OpenWrt download tags keyword:'),
|
||||
_('Set the OpenWrt files download tags keyword for github.com in [Online Download Update].'));
|
||||
tag.default = 'ARMv8';
|
||||
tag.rmempty = false;
|
||||
|
||||
// 4. File suffix
|
||||
const suffix = o.option(form.ListValue, 'amlogic_firmware_suffix',
|
||||
_('OpenWrt files suffix:'),
|
||||
_('Set the OpenWrt files download suffix for github.com in [Online Download Update].'));
|
||||
['.7z', '.zip', '.img.gz', '.img.xz'].forEach(function (s) { suffix.value(s, _(s)); });
|
||||
suffix.default = '.img.gz';
|
||||
suffix.rmempty = false;
|
||||
|
||||
// 5. Kernel download repository
|
||||
const kpath = o.option(form.ListValue, 'amlogic_kernel_path',
|
||||
_('Kernel download repository:'),
|
||||
_('Set the kernel files download repository on github.com in [Online Download Update].'));
|
||||
kpath.value('https://github.com/breakingbadboy/OpenWrt');
|
||||
kpath.value('https://github.com/ophub/kernel');
|
||||
kpath.default = 'https://github.com/breakingbadboy/OpenWrt';
|
||||
kpath.rmempty = false;
|
||||
|
||||
// 6. Kernel tags; available tags depend on the selected kernel repo.
|
||||
// Default is auto-derived from kernel_release suffixes (-rk3588/-rk35xx).
|
||||
const currentKpath = uci.get('amlogic', 'config', 'amlogic_kernel_path') ||
|
||||
'https://github.com/breakingbadboy/OpenWrt';
|
||||
const knownTags = {
|
||||
kernel_rk3588: 'kernel_rk3588 [Rockchip RK3588 Kernel]',
|
||||
kernel_rk35xx: 'kernel_rk35xx [Rockchip RK35xx Kernel]',
|
||||
kernel_stable: 'kernel_stable [Mainline Stable Kernel]'
|
||||
};
|
||||
if (currentKpath.indexOf('ophub/kernel') >= 0) {
|
||||
knownTags.kernel_flippy = 'kernel_flippy [Mainline Stable Kernel by Flippy]';
|
||||
knownTags.kernel_beta = 'kernel_beta [Beta Kernel]';
|
||||
}
|
||||
// Determine default tag from saved config or kernel_release uname string.
|
||||
let kernelTagDefault = uci.get('amlogic', 'config', 'amlogic_kernel_tags') || '';
|
||||
if (!kernelTagDefault) {
|
||||
const u = state.kernel_release || '';
|
||||
if (u.indexOf('-rk3588') >= 0) kernelTagDefault = 'kernel_rk3588';
|
||||
else if (u.indexOf('-rk35xx') >= 0) kernelTagDefault = 'kernel_rk35xx';
|
||||
else if (u.indexOf('-beta') >= 0) kernelTagDefault = 'kernel_beta';
|
||||
else kernelTagDefault = 'kernel_stable';
|
||||
}
|
||||
const ktags = o.option(form.ListValue, 'amlogic_kernel_tags',
|
||||
_('Kernel download tags:'),
|
||||
_('Set the kernel files download tags on github.com in [Online Download Update].'));
|
||||
Object.keys(knownTags).forEach(function (k) { ktags.value(k, _(knownTags[k])); });
|
||||
if (!knownTags[kernelTagDefault]) ktags.value(kernelTagDefault, kernelTagDefault);
|
||||
ktags.default = kernelTagDefault;
|
||||
ktags.rmempty = false;
|
||||
|
||||
// 7. Kernel branch
|
||||
const kbranch = o.option(form.ListValue, 'amlogic_kernel_branch',
|
||||
_('Set version branch:'),
|
||||
_('Set the version branch of the OpenWrt files and kernel selected in [Online Download Update].'));
|
||||
['5.10', '5.15', '6.1', '6.6', '6.12', '6.18'].forEach(function (b) {
|
||||
kbranch.value(b, _(b));
|
||||
});
|
||||
const m2 = (state.kernel_release || '').match(/^(\d+\.\d+)/);
|
||||
kbranch.default = m2 ? m2[1] : '5.10';
|
||||
kbranch.rmempty = false;
|
||||
|
||||
// 8. Plugin branch
|
||||
const pbranch = o.option(form.ListValue, 'amlogic_plugin_branch',
|
||||
_('Set plugin branch:'),
|
||||
state.has_luci_js
|
||||
? _('Set the branch of the luci-app-amlogic plugin used in [Only update Amlogic Service]. Select main for JavaScript version or lua for Lua version.')
|
||||
: _('Set the branch of the luci-app-amlogic plugin used in [Only update Amlogic Service]. This system does not have JS LuCI, only the Lua branch is available.'));
|
||||
if (state.has_luci_js)
|
||||
pbranch.value('main', _('main [JavaScript version]'));
|
||||
pbranch.value('lua', _('lua [Lua version]'));
|
||||
pbranch.default = state.has_luci_js ? 'main' : 'lua';
|
||||
pbranch.rmempty = false;
|
||||
|
||||
// 9. Keep config
|
||||
const fcfg = o.option(form.Flag, 'amlogic_firmware_config',
|
||||
_('Keep config update:'),
|
||||
_('Set whether to keep the current config during [Online Download Update] and [Manually Upload Update].'));
|
||||
fcfg.default = '1';
|
||||
fcfg.rmempty = false;
|
||||
|
||||
// 9. Write bootloader
|
||||
const wb = o.option(form.Flag, 'amlogic_write_bootloader',
|
||||
_('Auto write bootloader:'),
|
||||
_('[Recommended choice] Set whether to auto write bootloader during install and update OpenWrt.'));
|
||||
wb.default = '0';
|
||||
wb.rmempty = false;
|
||||
|
||||
// 10. Shared partition fs type
|
||||
const fst = o.option(form.ListValue, 'amlogic_shared_fstype',
|
||||
_('Set the file system type:'),
|
||||
_('[Default ext4] Set the file system type of the shared partition (/mnt/mmcblk*p4) when install OpenWrt.'));
|
||||
['ext4', 'f2fs', 'btrfs', 'xfs'].forEach(function (s) { fst.value(s, _(s)); });
|
||||
fst.default = 'ext4';
|
||||
fst.rmempty = false;
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -1,163 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Plugin landing / info page
|
||||
//
|
||||
// Purpose: render the plugin logo, badge links, feature summary, and supported-box list.
|
||||
// Calls sync_menu on load to update sidebar menu flags from runtime platform detection.
|
||||
// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (author, sync_menu, state).
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// Extract OPENWRT_AUTHOR from /usr/sbin/openwrt-update-amlogic and return the author's GitHub repo URL.
|
||||
const callAuthor = rpc.declare({
|
||||
object: 'luci.amlogic',
|
||||
method: 'author',
|
||||
expect: { openwrt_author: 'Invalid value.' }
|
||||
});
|
||||
|
||||
// Sync menu_install / menu_armcpu UCI flags from runtime platform detection.
|
||||
const callSyncMenu = rpc.declare({ object: 'luci.amlogic', method: 'sync_menu' });
|
||||
|
||||
// Get runtime state to determine plugin_branch for the language badge.
|
||||
const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' });
|
||||
|
||||
// Static list of supported boxes (display only, not used in any logic).
|
||||
const SUPPORTED_BOXES = [
|
||||
_('Amlogic s922x --- [ Beelink, Beelink-Pro, Ugoos-AM6-Plus, ODROID-N2, Khadas-VIM3, Ali-CT2000 ]'),
|
||||
_('Amlogic s905x3 -- [ X96-Max+, HK1-Box, H96-Max-X3, Ugoos-X3, TX3, X96-Air, A95XF3-Air ]'),
|
||||
_('Amlogic s905x2 -- [ X96Max-4G, X96Max-2G, MECOOL-KM3-4G, Tanix-Tx5-Max, A95X-F2 ]'),
|
||||
_('Amlogic s912 ---- [ H96-Pro-Plus, Octopus-Planet, A1, A2, Z6-Plus, TX92, X92, TX8-MAX, TX9-Pro ]'),
|
||||
_('Amlogic s905x --- [ HG680P, B860H, TBee, T95, TX9, XiaoMI-3S, X96 ]'),
|
||||
_('Amlogic s905w --- [ X96-Mini, TX3-Mini, W95, X96W/FunTV, MXQ-Pro-4K ]'),
|
||||
_('Amlogic s905d --- [ Phicomm-N1, MECOOL-KI-Pro, SML-5442TW ]'),
|
||||
_('Amlogic s905l --- [ UNT402A, M201-S ]'),
|
||||
_('Amlogic s905l2 -- [ MGV2000, MGV3000, Wojia-TV-IPBS9505, M301A, E900v21E ]'),
|
||||
_('Amlogic s905l3 -- [ CM211-1, CM311-1, HG680-LC, M401A, UNT400G1, UNT402A, ZXV10-BV310 ]'),
|
||||
_('Amlogic s905l3a - [ E900V22C/D, CM311-1a-YST, M401A, M411A, UNT403A, UNT413A, IP112H ]'),
|
||||
_('Amlogic s905l3b - [ CM211-1, CM311-1, E900V22D, E900V21E, E900V22E, M302A/M304A ]'),
|
||||
_('Amlogic s905 ---- [ Beelink-Mini-MX-2G, Sunvell-T95M, MXQ-Pro+4K, SumaVision-Q5 ]'),
|
||||
_('Allwinner H6 ---- [ V-Plus Cloud ]'),
|
||||
_('Rockchip -------- [ BeikeYun, L1-Pro, FastRhino R66S/R68S, Radxa 5B/E25 ]'),
|
||||
_('Used in KVM ----- [ Can be used in KVM virtual machine of Armbian system. ]')
|
||||
];
|
||||
|
||||
// Open an external link in a new tab; fall back to the current window if the
|
||||
// browser blocks the popup.
|
||||
function openExternal(url) {
|
||||
const w = window.open(url, '_blank', 'noopener,noreferrer');
|
||||
if (!w) window.location.href = url;
|
||||
}
|
||||
|
||||
// This is a read-only info page, so we disable the top Save / Apply / Reset buttons by setting the handlers to null.
|
||||
return view.extend({
|
||||
// Read-only info page: disable the top Save / Apply / Reset buttons.
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
// On load, call sync_menu to update sidebar menu flags from runtime platform detection. If the sidebar nav is out of
|
||||
// sync with the server-side menu flags, reload once to pick up the new menu (guarded by #menu-synced hash).
|
||||
load: function () {
|
||||
// Call sync_menu; reload once when the sidebar nav disagrees with the
|
||||
// server-side menu flags in either direction (guarded by #menu-synced hash).
|
||||
const alreadyReloaded = window.location.hash === '#menu-synced';
|
||||
const syncMenuPromise = callSyncMenu().then(function (res) {
|
||||
if (alreadyReloaded || !res) return;
|
||||
// Check if the sidebar navigation already contains the install link.
|
||||
const installLink = document.querySelector('a[href*="amlogic/install"]');
|
||||
// Menu became visible on the server but is absent in the browser nav.
|
||||
const needShow = (res.show_install === 'yes' && !installLink);
|
||||
// Menu became hidden on the server but is still present in the browser nav.
|
||||
const needHide = (res.show_install === 'no' && installLink);
|
||||
if (needShow || needHide) {
|
||||
// Server-side index cache updated; reload to pick up new nav.
|
||||
window.location.replace(window.location.pathname + '#menu-synced');
|
||||
window.location.reload();
|
||||
}
|
||||
}).catch(function () {});
|
||||
return Promise.all([callAuthor(), syncMenuPromise]).then(function (r) { return r[0]; });
|
||||
},
|
||||
|
||||
// Render the plugin info: logo, badge links, feature summary, and supported-box list.
|
||||
render: function (authorUrl) {
|
||||
// Inject the theme stylesheet on first entry so amlogic-* classes work.
|
||||
amlogicShared.ensureCss();
|
||||
const res = L.resource('amlogic');
|
||||
|
||||
// Language badge image — default to javascript.svg (JS version),
|
||||
// will be confirmed/corrected after the state RPC resolves.
|
||||
var langsImg = E('img', {
|
||||
id: 'Langs',
|
||||
src: res + '/javascript.svg', alt: 'javascript',
|
||||
width: '86', height: '20',
|
||||
style: 'margin:0 5px; vertical-align:middle; border-radius:3px'
|
||||
});
|
||||
|
||||
// Fetch plugin_branch from state and update the language badge.
|
||||
callState().then(function (s) {
|
||||
var isLua = s && s.plugin_branch === 'lua';
|
||||
langsImg.src = res + (isLua ? '/lua.svg' : '/javascript.svg');
|
||||
langsImg.alt = isLua ? 'lua' : 'javascript';
|
||||
langsImg.setAttribute('width', isLua ? '46' : '86');
|
||||
}).catch(function () {});
|
||||
|
||||
// Flatten the supported-boxes list to [text, <br>, text, <br>, ...].
|
||||
const boxRows = [];
|
||||
for (let i = 0; i < SUPPORTED_BOXES.length; i++)
|
||||
boxRows.push(SUPPORTED_BOXES[i], E('br'));
|
||||
|
||||
return E('div', { class: 'cbi-section' }, [
|
||||
E('h2', { style: 'border-bottom:none' }, _('Amlogic Service')),
|
||||
E('p', { style: 'border:none; margin-bottom:0' }, _('Supports management of Amlogic s9xxx, Allwinner (V-Plus Cloud), and Rockchip (BeikeYun, Chainedbox L1 Pro) boxes.')),
|
||||
E('table', { class: 'amlogic-row-table' }, [
|
||||
E('tr', [
|
||||
E('td', { colspan: '2', style: 'text-align:center' }, [
|
||||
E('img', { src: res + '/logo.png', alt: 'Logo', width: '135' })
|
||||
])
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { colspan: '2', style: 'text-align:center' }, [
|
||||
E('img', {
|
||||
src: res + '/packit.svg', alt: 'Packit',
|
||||
width: '168', height: '20',
|
||||
style: 'cursor:pointer; margin:0 5px; vertical-align:middle; border-radius:3px',
|
||||
click: function () { openExternal('https://github.com/unifreq/openwrt_packit'); }
|
||||
}),
|
||||
E('img', {
|
||||
src: res + '/author.svg', alt: 'Author',
|
||||
width: '168', height: '20',
|
||||
style: 'cursor:pointer; margin:0 5px; vertical-align:middle; border-radius:3px',
|
||||
click: function () {
|
||||
callAuthor().then(function (repo) {
|
||||
let url = String(repo || '').trim();
|
||||
if (!url) return;
|
||||
if (url.indexOf('https') === -1)
|
||||
url = 'https://github.com/' + url;
|
||||
openExternal(url);
|
||||
});
|
||||
}
|
||||
}),
|
||||
E('img', {
|
||||
src: res + '/plugin.svg', alt: 'luci-app-amlogic',
|
||||
width: '160', height: '20',
|
||||
style: 'cursor:pointer; margin:0 5px; vertical-align:middle; border-radius:3px',
|
||||
click: function () { openExternal('https://github.com/ophub/luci-app-amlogic'); }
|
||||
}),
|
||||
langsImg
|
||||
])
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '20%', align: 'right' }, _('Supported functions:')),
|
||||
E('td', { width: '80%', align: 'left' },
|
||||
_('Provide services such as install to EMMC, Update Firmware and Kernel, Backup and Recovery Config, Snapshot management, etc.'))
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '20%', align: 'right', style: 'vertical-align:top; padding-top:4px' }, _('Supported Boxes:')),
|
||||
E('td', { width: '80%', align: 'left', style: 'line-height:1.8' }, boxRows)
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -1,156 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Install OpenWrt to EMMC
|
||||
//
|
||||
// Purpose: let the user select a device model (or enter dtb/soc/uboot manually),
|
||||
// then invoke start_install and poll the log tail until success or failure.
|
||||
// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (model_database, start_install, read_log_tail).
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// Read the model database (parsed by the backend from /usr/share/amlogic/model_database.txt).
|
||||
const callDB = rpc.declare({ object: 'luci.amlogic', method: 'model_database',
|
||||
expect: { entries: [] } });
|
||||
// Start install; parameter is the composed string "id@dtb:soc:uboot".
|
||||
const callStartInst = rpc.declare({ object: 'luci.amlogic', method: 'start_install',
|
||||
params: ['amlogic_install_sel'] });
|
||||
// Read the last line of the named log (here we use the 'install' log).
|
||||
const callLogTail = rpc.declare({ object: 'luci.amlogic', method: 'read_log_tail',
|
||||
params: ['name'], expect: { line: '' } });
|
||||
|
||||
// This page uses its own Install button; hide LuCI's default Save/Apply/Reset.
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function () {
|
||||
return callDB();
|
||||
},
|
||||
|
||||
// Render the plugin info: logo, badge links, feature summary, and supported-box list.
|
||||
render: function (entries) {
|
||||
amlogicShared.ensureCss();
|
||||
// Model dropdown: 0 = placeholder, 99 = manual entry, others from database.
|
||||
const sel = E('select', { style: 'width:auto', name: 'amlogic_soc', id: 'amlogic_soc' });
|
||||
sel.appendChild(E('option', { value: '0' }, _('Select List')));
|
||||
(entries || []).forEach(function (e) {
|
||||
sel.appendChild(E('option', { value: e.id },
|
||||
'[ ' + e.id + ' ] ' + e.name));
|
||||
});
|
||||
sel.appendChild(E('option', { value: '99' }, _('Enter the dtb file name')));
|
||||
|
||||
// The three manual fields are only required when the user picks 99.
|
||||
const dtbInput = E('input', { type: 'text', class: 'cbi-input-text',
|
||||
style: 'width:235px', id: 'amlogic_dtb' });
|
||||
const socInput = E('input', { type: 'text', class: 'cbi-input-text',
|
||||
style: 'width:235px', id: 'amlogic_socname' });
|
||||
const ubootInput = E('input', { type: 'text', class: 'cbi-input-text',
|
||||
style: 'width:235px', id: 'amlogic_uboot' });
|
||||
|
||||
const trDtb = E('tr', { style: 'display:none' }, [
|
||||
E('td', { width: '30%', align: 'right' }, _('Enter the dtb file name:')),
|
||||
E('td', { width: '70%', align: 'left' }, dtbInput)
|
||||
]);
|
||||
const trSoc = E('tr', { style: 'display:none' }, [
|
||||
E('td', { width: '30%', align: 'right' }, _('Enter the soc name:')),
|
||||
E('td', { width: '70%', align: 'left' }, socInput)
|
||||
]);
|
||||
const trUboot = E('tr', { style: 'display:none' }, [
|
||||
E('td', { width: '30%', align: 'right' }, _('Enter the uboot_overload name:')),
|
||||
E('td', { width: '70%', align: 'left' }, ubootInput)
|
||||
]);
|
||||
|
||||
sel.addEventListener('change', function () {
|
||||
// On selection change, dynamically show/hide the three manual rows.
|
||||
const show = (sel.value === '99') ? '' : 'none';
|
||||
trDtb.style.display = show;
|
||||
trSoc.style.display = show;
|
||||
trUboot.style.display = show;
|
||||
});
|
||||
|
||||
// Status / log line shown next to the install button while it runs.
|
||||
// 'installing' tracks whether the background install is in progress.
|
||||
const logSpan = E('span');
|
||||
var installing = false;
|
||||
var installDone = false;
|
||||
const btn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Install'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
if (installing) return;
|
||||
const text = sel.options[sel.selectedIndex].text;
|
||||
// Confirm dialog; install proceeds only after user confirms.
|
||||
if (!confirm(_('You have chosen:') + ' ' + text + ', ' + _('Start install?')))
|
||||
return;
|
||||
// dtb falls back to auto_dtb when the manual field is blank.
|
||||
const dtbVal = dtbInput.value || 'auto_dtb';
|
||||
const socVal = socInput.value || '';
|
||||
const ubVal = ubootInput.value || '';
|
||||
// Compose the "id@dtb:soc:uboot" string the backend expects.
|
||||
const composed = sel.value + '@' + dtbVal + ':' + socVal + ':' + ubVal;
|
||||
const target = ev.currentTarget;
|
||||
target.disabled = true;
|
||||
target.value = _('Installing...');
|
||||
installing = true;
|
||||
installDone = false;
|
||||
// Fire the background install — result comes via log polling.
|
||||
return callStartInst(composed).catch(function () {
|
||||
target.value = _('Install Failed');
|
||||
target.disabled = false;
|
||||
installing = false;
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Poll the install log once per second; detect success/failure keywords.
|
||||
poll.add(function () {
|
||||
return callLogTail('install').then(function (line) {
|
||||
if (!line || line === '\n') {
|
||||
dom.content(logSpan, '');
|
||||
return;
|
||||
}
|
||||
dom.content(logSpan, E('span', { class: 'amlogic-status-info' }, ' ' + line));
|
||||
if (installing && !installDone) {
|
||||
if (line.indexOf('Successfully') !== -1 || line.indexOf('successful') !== -1) {
|
||||
installDone = true;
|
||||
installing = false;
|
||||
btn.value = _('Successful Install');
|
||||
btn.disabled = false;
|
||||
dom.content(logSpan, E('span', { class: 'amlogic-status-ok' }, ' ' + line));
|
||||
} else if (line.indexOf('Failed') !== -1 || line.indexOf('failed') !== -1 ||
|
||||
line.indexOf('Error') !== -1 || line.indexOf('error') !== -1) {
|
||||
installDone = true;
|
||||
installing = false;
|
||||
btn.value = _('Install Failed');
|
||||
btn.disabled = false;
|
||||
dom.content(logSpan, E('span', { class: 'amlogic-status-err' }, ' ' + line));
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1);
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('Install OpenWrt')),
|
||||
E('p', _('Install OpenWrt to EMMC, Please select the device model, Or enter the dtb file name.')),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('table', { class: 'amlogic-row-table' }, [
|
||||
E('tr', [
|
||||
E('td', { width: '35%', align: 'right' }, _('Select the device model:')),
|
||||
E('td', { width: '65%', align: 'left' }, sel)
|
||||
]),
|
||||
trDtb, trSoc, trUboot,
|
||||
E('tr', [
|
||||
E('td', { width: '30%', align: 'right' }, _('Install OpenWrt:')),
|
||||
E('td', { width: '70%', align: 'left' }, [btn, ' ', logSpan])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Service log viewer
|
||||
//
|
||||
// Purpose: read the full amlogic service log into a read-only textarea and
|
||||
// refresh it every 2 seconds; provide controls to pause/resume polling, clear
|
||||
// the log, and download the current contents as a plain-text file.
|
||||
// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (read_log_full, del_log).
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require poll';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// Read the full log content (entire text).
|
||||
const callLogFull = rpc.declare({ object: 'luci.amlogic', method: 'read_log_full',
|
||||
params: ['name'], expect: { content: '' } });
|
||||
// Clear the service main log.
|
||||
const callDelLog = rpc.declare({ object: 'luci.amlogic', method: 'del_log' });
|
||||
|
||||
// This page uses its own Start/Stop Refresh buttons and does not need the default Save/Apply/Reset buttons,
|
||||
// so we disable them by setting the handlers to null.
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
render: function () {
|
||||
amlogicShared.ensureCss();
|
||||
// Read-only, no-wrap, monospace log display area.
|
||||
const ta = E('textarea', {
|
||||
rows: '20', readonly: 'readonly', wrap: 'off',
|
||||
style: 'width:100%; font-family:monospace; white-space:pre'
|
||||
});
|
||||
// Status hint: Running / Stopped.
|
||||
const status = E('span', { class: 'amlogic-status-ok', style: 'margin-left:1em' });
|
||||
// Polling is enabled by default when the page opens.
|
||||
let polling = true;
|
||||
|
||||
// Refresh the full log and scroll to the bottom.
|
||||
function refresh() {
|
||||
return callLogFull('main').then(function (txt) {
|
||||
ta.value = txt || '';
|
||||
ta.scrollTop = ta.scrollHeight;
|
||||
});
|
||||
}
|
||||
const view = this;
|
||||
|
||||
const stopBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reset',
|
||||
value: _('Stop Refresh'),
|
||||
click: function () { polling = false; status.textContent = _('Stopped'); }
|
||||
});
|
||||
const startBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-apply',
|
||||
value: _('Start Refresh'),
|
||||
click: function () { polling = true; status.textContent = _('Running'); refresh(); }
|
||||
});
|
||||
const cleanBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-remove',
|
||||
value: _('Clean Log'),
|
||||
click: ui.createHandlerFn(view, function () {
|
||||
return callDelLog().then(refresh);
|
||||
})
|
||||
});
|
||||
const dlBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Download Log'),
|
||||
click: function () {
|
||||
// Wrap textarea content in a Blob and trigger a browser download via a temporary <a>.
|
||||
const blob = new Blob([ta.value || ''], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'amlogic_service.log';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
|
||||
// Polling: only refresh while polling is true; runs once every 2s.
|
||||
poll.add(function () { if (polling) return refresh(); }, 2);
|
||||
refresh();
|
||||
status.textContent = _('Running');
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('Service Log')),
|
||||
E('p', _('Display the log of plug-in service operation.')),
|
||||
E('div', { class: 'cbi-section' }, [ta]),
|
||||
E('div', { class: 'cbi-page-actions' },
|
||||
[stopBtn, ' ', startBtn, ' ', cleanBtn, ' ', dlBtn, status])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Safe Power Off page
|
||||
//
|
||||
// Purpose: call the backend poweroff RPC then run a 5-second on-page countdown;
|
||||
// switch status to "Powered off" and show a power-off icon when done.
|
||||
// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (poweroff).
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// Trigger the backend poweroff RPC.
|
||||
const callPoweroff = rpc.declare({ object: 'luci.amlogic', method: 'poweroff' });
|
||||
|
||||
// This page uses its own PowerOff button and does not need the default Save/Apply/Reset buttons,
|
||||
// so we disable them by setting the handlers to null.
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
render: function () {
|
||||
amlogicShared.ensureCss();
|
||||
// Status text; amlogic-status-err color adapts to dark mode automatically.
|
||||
const status = E('span', { class: 'amlogic-status-err', style: 'margin-left:1em' });
|
||||
// Power-off icon, hidden by default; shown only after the countdown ends.
|
||||
const icon = E('img', {
|
||||
src: L.resource('amlogic/poweroff.png'),
|
||||
width: '32', height: '32',
|
||||
style: 'width:32px; height:32px; max-width:32px; display:none; margin-left:1em; vertical-align:middle'
|
||||
});
|
||||
|
||||
// PowerOff button click handler: confirm twice, then call RPC and start countdown on success.
|
||||
const btn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-remove',
|
||||
value: _('Perform PowerOff'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
// Confirm twice to avoid accidental clicks.
|
||||
if (!confirm(_('Are you sure you want to power off the device?')))
|
||||
return;
|
||||
ev.currentTarget.disabled = true;
|
||||
return callPoweroff().then(function () {
|
||||
// Show a 5-second countdown; replace with "Powered off" and icon when done.
|
||||
status.textContent = _('Powering off, please wait...');
|
||||
let n = 5;
|
||||
const t = setInterval(function () {
|
||||
if (--n <= 0) {
|
||||
clearInterval(t);
|
||||
status.textContent = _('Powered off.');
|
||||
icon.style.display = 'inline-block';
|
||||
} else {
|
||||
status.textContent = _('Powering off, please wait...') + ' ' + n;
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('Power Off')),
|
||||
E('p', _('Click the button below to safely power off the device.')),
|
||||
E('div', { class: 'cbi-section' }, [btn, status, icon])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Shared helper module for all luci-app-amlogic JS views.
|
||||
//
|
||||
// Purpose: inject the theme-aware stylesheet amlogic.css into <head> on first
|
||||
// page load so every view can use .amlogic-status-* / .amlogic-snap-* classes
|
||||
// and prefers-color-scheme dark mode overrides without duplicating inline styles.
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
return baseclass.extend({
|
||||
// Inject amlogic.css once; guarded by element id to prevent duplicates.
|
||||
ensureCss: function () {
|
||||
if (document.getElementById('amlogic-shared-css')) return;
|
||||
const link = document.createElement('link');
|
||||
link.id = 'amlogic-shared-css';
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = L.resource('view/amlogic/amlogic.css');
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
});
|
||||
@@ -1,298 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Manually Upload Update
|
||||
//
|
||||
// Purpose: upload local files (firmware / kernel / ipk / config backup) to the server,
|
||||
// list them, and trigger install / restore / update actions. Chunked upload uses
|
||||
// a custom ubus RPC instead of cgi-upload to avoid session ACL issues.
|
||||
// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (upload_path, list_uploads, upload_chunk,
|
||||
// delete_upload, install_upload, start_update, start_kernel, read_log_tail, state).
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require fs';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// Query the upload directory the backend allows (default /tmp/upload/, the backend ensures it exists).
|
||||
const callUploadPath = rpc.declare({ object: 'luci.amlogic', method: 'upload_path', expect: { path: '/tmp/upload/' } });
|
||||
// List files in the upload dir plus flags about installable firmware/kernel/cfg.
|
||||
const callList = rpc.declare({ object: 'luci.amlogic', method: 'list_uploads' });
|
||||
// Delete a named file in the upload dir.
|
||||
const callDelete = rpc.declare({ object: 'luci.amlogic', method: 'delete_upload', params: ['name'] });
|
||||
// Install one uploaded file (ipk) or restore a config backup; the backend dispatches by suffix.
|
||||
const callInstall = rpc.declare({ object: 'luci.amlogic', method: 'install_upload', params: ['name'] });
|
||||
// Chunked upload via our own RPC (sidesteps cgi-upload's session ACL flow).
|
||||
const callUploadChunk = rpc.declare({ object: 'luci.amlogic', method: 'upload_chunk', params: ['name', 'data', 'append'] });
|
||||
// Run the full firmware update flow with parameters auto@updated@/tmp.
|
||||
const callStartUpd = rpc.declare({ object: 'luci.amlogic', method: 'start_update', params: ['amlogic_update_sel'] });
|
||||
// Run the kernel-only replace flow.
|
||||
const callStartKnl = rpc.declare({ object: 'luci.amlogic', method: 'start_kernel' });
|
||||
// Tail a named log to surface progress during install/update.
|
||||
const callLogTail = rpc.declare({ object: 'luci.amlogic', method: 'read_log_tail', params: ['name'], expect: { line: '' } });
|
||||
// Read system state (this page only uses current_firmware_version).
|
||||
const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' });
|
||||
|
||||
// This page uses its own Upload/Install buttons and does not need the default Save/Apply/Reset buttons,
|
||||
// so we disable them by setting the handlers to null.
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
// On load, ensure the CSS is injected and load the upload path from the backend.
|
||||
load: function () {
|
||||
amlogicShared.ensureCss();
|
||||
return callUploadPath();
|
||||
},
|
||||
|
||||
// Render the upload page: file input + upload button, table of existing uploads with Remove + Install/Restore buttons,
|
||||
// and firmware/kernel update buttons if applicable. Also start polling the install/update log tails.
|
||||
render: function (path) {
|
||||
const view = this;
|
||||
|
||||
const tableContainer = E('div');
|
||||
const descSpan = E('span', { class: 'amlogic-status-ok', style: 'font-weight:bold' });
|
||||
const fwBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Update OpenWrt firmware'), style: 'display:none',
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
return doButton(ev.currentTarget,
|
||||
_('Updating...'), _('Update Failed'),
|
||||
function () { return callStartUpd('auto@updated@/tmp'); });
|
||||
})
|
||||
});
|
||||
const knBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Replace OpenWrt Kernel'), style: 'display:none; margin-left:5px',
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
return doButton(ev.currentTarget,
|
||||
_('Updating...'), _('Update Failed'),
|
||||
function () { return callStartKnl(); });
|
||||
})
|
||||
});
|
||||
const fwLog = E('span');
|
||||
const knLog = E('span');
|
||||
const fwVer = E('span', _('Collecting data...'));
|
||||
|
||||
function doButton(btn, busyText, fail, fn) {
|
||||
// Shared button state machine: disabled → RPC → busyText or fail label.
|
||||
// On success (code==0) the button stays in busyText state (device rebooting);
|
||||
// on failure, re-enable so the user can retry.
|
||||
btn.disabled = true; btn.value = busyText;
|
||||
return Promise.resolve(fn()).then(function (r) {
|
||||
if (!r || r.code !== 0) {
|
||||
btn.value = fail;
|
||||
btn.disabled = false;
|
||||
}
|
||||
// code == 0 → keep busyText + disabled (update in progress)
|
||||
}).catch(function () {
|
||||
btn.value = fail;
|
||||
btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function refreshList() {
|
||||
// Refresh the file table; show/hide firmware/kernel update buttons
|
||||
// based on has_firmware / has_kernel flags from the backend.
|
||||
return callList().then(function (info) {
|
||||
dom.content(tableContainer, buildTable(info.items || [], info.path));
|
||||
let parts = [];
|
||||
if (info.has_config)
|
||||
parts.push(_('There are config file in the upload directory, and you can restore the config. '));
|
||||
if (info.has_kernel)
|
||||
parts.push(_('There are kernel files in the upload directory, and you can replace the kernel.'));
|
||||
if (info.has_firmware)
|
||||
parts.push(_('There are openwrt firmware file in the upload directory, and you can update the openwrt.'));
|
||||
descSpan.textContent = parts.length ? ' Tip: ' + parts.join('') : '';
|
||||
fwBtn.style.display = info.has_firmware ? '' : 'none';
|
||||
knBtn.style.display = info.has_kernel ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to build the file table with Remove + Install/Restore buttons per row.
|
||||
function buildTable(items, dir) {
|
||||
// File table: expose Remove + Install (ipk) / Restore (config backup) per row.
|
||||
const tbl = E('table', { class: 'table cbi-section-table' }, [
|
||||
E('tr', { class: 'tr cbi-section-table-titles' }, [
|
||||
E('th', { class: 'th' }, _('File name')),
|
||||
E('th', { class: 'th' }, _('Modify time')),
|
||||
E('th', { class: 'th' }, _('Attributes')),
|
||||
E('th', { class: 'th' }, _('Size')),
|
||||
E('th', { class: 'th' }, _('Remove')),
|
||||
E('th', { class: 'th' }, _('Install'))
|
||||
])
|
||||
]);
|
||||
items.forEach(function (it) {
|
||||
const removeBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-remove',
|
||||
value: _('Remove'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
return callDelete(it.name).then(refreshList);
|
||||
})
|
||||
});
|
||||
let installCell;
|
||||
if (it.ipk) {
|
||||
// ipk: "Install" calls install_upload to install one package.
|
||||
installCell = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-apply',
|
||||
value: _('Install'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
return callInstall(it.name).then(function (r) {
|
||||
if (r && r.code !== 0 && r.output)
|
||||
ui.addNotification(null,
|
||||
E('p', { class: 'amlogic-status-err' }, [
|
||||
r.output, E('br'),
|
||||
E('b', _('Please refresh the page to see the changes.'))
|
||||
]));
|
||||
else if (r && r.code === 0)
|
||||
ui.addNotification(null,
|
||||
E('p', { class: 'amlogic-status-ok' },
|
||||
_('Package installed successfully. Please refresh the page to see the changes.')));
|
||||
return refreshList();
|
||||
});
|
||||
})
|
||||
});
|
||||
} else if (it.cfg) {
|
||||
// Config backup (openwrt-config.tar.gz): "Restore" makes the
|
||||
// backend roll the config back and reboot.
|
||||
installCell = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-apply',
|
||||
value: _('Restore'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
ui.addNotification(null,
|
||||
E('p', { class: 'amlogic-status-ok' },
|
||||
_('Tip: The config is being restored, and it will automatically restart after completion.')));
|
||||
return callInstall(it.name);
|
||||
})
|
||||
});
|
||||
} else {
|
||||
installCell = E('span', '-');
|
||||
}
|
||||
tbl.appendChild(E('tr', { class: 'tr cbi-rowstyle-1' }, [
|
||||
E('td', { class: 'td' }, it.name),
|
||||
E('td', { class: 'td' }, it.mtime),
|
||||
E('td', { class: 'td' }, it.modestr),
|
||||
E('td', { class: 'td' }, it.size),
|
||||
E('td', { class: 'td' }, removeBtn),
|
||||
E('td', { class: 'td' }, installCell)
|
||||
]));
|
||||
});
|
||||
if (!items.length)
|
||||
tbl.appendChild(E('tr', [E('td', { class: 'td', colspan: '6' }, _('No specify upload file.'))]));
|
||||
return tbl;
|
||||
}
|
||||
|
||||
// Chunked file upload via luci.amlogic.upload_chunk RPC (avoids cgi-upload session ACL).
|
||||
// 32 KB raw per chunk → ~44 KB base64, safely under the ubus per-request size limit.
|
||||
const CHUNK_SIZE = 32 * 1024;
|
||||
const uploadStatus = E('span');
|
||||
const uploadFileName = E('span', { style: 'margin-right:1em' });
|
||||
|
||||
// Encode a Uint8Array as base64 in 32 KB slices to avoid call-stack limits.
|
||||
function bytesToB64(buf) {
|
||||
let s = '';
|
||||
for (let i = 0; i < buf.length; i += 0x8000)
|
||||
s += String.fromCharCode.apply(null, buf.subarray(i, i + 0x8000));
|
||||
return btoa(s);
|
||||
}
|
||||
|
||||
// Upload a file in chunks; on success, refresh the file list to show the new upload.
|
||||
function uploadFile(file, target, basename) {
|
||||
const total = file.size;
|
||||
let offset = 0;
|
||||
let first = true;
|
||||
function step() {
|
||||
if (offset >= total) {
|
||||
uploadStatus.textContent = _('File saved to') + ' ' + target;
|
||||
return refreshList();
|
||||
}
|
||||
const slice = file.slice(offset, offset + CHUNK_SIZE);
|
||||
return slice.arrayBuffer().then(function (buf) {
|
||||
const b64 = bytesToB64(new Uint8Array(buf));
|
||||
const append = !first;
|
||||
first = false;
|
||||
return callUploadChunk(basename, b64, append).then(function (r) {
|
||||
if (!r || !r.ok)
|
||||
throw new Error((r && r.error) || _('upload chunk failed'));
|
||||
offset += CHUNK_SIZE;
|
||||
const pct = Math.min(100, (offset / total) * 100);
|
||||
uploadStatus.textContent = _('Uploading file…') +
|
||||
' ' + pct.toFixed(1) + '%';
|
||||
return step();
|
||||
});
|
||||
});
|
||||
}
|
||||
return step();
|
||||
}
|
||||
|
||||
// Hidden file input to trigger the file picker dialog; on file selection, start the chunked upload.
|
||||
const hiddenFileInput = E('input', {
|
||||
type: 'file', style: 'display:none',
|
||||
change: ui.createHandlerFn(this, function (ev) {
|
||||
const file = ev.target.files && ev.target.files[0];
|
||||
if (!file) return;
|
||||
const basename = file.name.replace(/^.*[\\\/]/, '');
|
||||
const target = path.replace(/\/+$/, '') + '/' + basename;
|
||||
uploadFileName.textContent = basename;
|
||||
uploadStatus.textContent = _('Uploading file…') + ' 0%';
|
||||
return uploadFile(file, target, basename).catch(function (e) {
|
||||
uploadStatus.textContent = _('Create upload file error.') +
|
||||
' ' + (e && e.message ? e.message : e);
|
||||
}).finally(function () {
|
||||
hiddenFileInput.value = '';
|
||||
});
|
||||
})
|
||||
});
|
||||
const uploadBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-apply',
|
||||
value: _('Upload file...'),
|
||||
click: function () { hiddenFileInput.click(); }
|
||||
});
|
||||
|
||||
// Polling: pull firmware and kernel log tails once per second.
|
||||
poll.add(function () {
|
||||
return Promise.all([
|
||||
callLogTail('firmware').then(function (l) {
|
||||
dom.content(fwLog, l && l !== '\n'
|
||||
? E('span', { class: 'amlogic-status-info' }, ' ' + l) : '');
|
||||
}),
|
||||
callLogTail('kernel').then(function (l) {
|
||||
dom.content(knLog, l && l !== '\n'
|
||||
? E('span', { class: 'amlogic-status-info' }, ' ' + l) : '');
|
||||
})
|
||||
]);
|
||||
}, 1);
|
||||
callState().then(function (s) {
|
||||
dom.content(fwVer, s.current_firmware_version
|
||||
? E('span', { class: 'amlogic-status-ok' }, _('Current Version') + ' [ ' + s.current_firmware_version + ' ] ')
|
||||
: E('span', { class: 'amlogic-status-err' }, _('Invalid value.')));
|
||||
});
|
||||
refreshList();
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('Upload')),
|
||||
E('p', _('Update plugins first, then update the kernel or firmware.')),
|
||||
E('p', _('After uploading [Firmware], [Kernel], [IPK] or [Backup Config], the operation buttons will be displayed.')),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('label', { style: 'display:inline-block;width:150px' },
|
||||
_('Choose local file:')),
|
||||
uploadBtn, ' ', uploadFileName, hiddenFileInput,
|
||||
E('br'), uploadStatus
|
||||
]),
|
||||
E('h3', _('Upload file list')),
|
||||
E('div', { class: 'cbi-section' }, [descSpan, tableContainer]),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('p', { style: 'text-align:center' },
|
||||
_('After uploading firmware (.img/.img.gz/.img.xz/.7z suffix) or kernel files (3 kernel files), the update button will be displayed.')),
|
||||
// Single centered cell: when fwBtn / knBtn are hidden the row would otherwise show an empty left column.
|
||||
E('div', { style: 'text-align:center' },
|
||||
[fwBtn, knBtn, ' ', fwVer, ' ', fwLog, knLog])
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||