mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 06:45:13 +08:00
⛄ Sync 2026-08-12 20:59:19
This commit is contained in:
1 parent
d5f9df4a19
commit
5c244874df
62 files changed
+2165
-809
No files matched your search
+2
-2
@@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_BAIDUDRIVE:=$(ARCH)
|
||||
|
||||
PKG_NAME:=baidudrive
|
||||
PKG_VERSION:=baidudrive-runtime-v1.0.5
|
||||
PKG_RELEASE:=8
|
||||
PKG_VERSION:=linkeasefull-runtime-v3.0.6
|
||||
PKG_RELEASE:=9
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/baidudrive-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkease-common-bin
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=1.7.5
|
||||
PKG_RELEASE:=4
|
||||
PKG_RELEASE:=5
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=b5383a43aa18185cc32c759d2b8536c235fe54aa
|
||||
PKG_SOURCE_VERSION:=9f351503d4320bb3767c300bc677a85c5685c9ba
|
||||
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkease
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=1.7.5
|
||||
PKG_RELEASE:=7
|
||||
PKG_RELEASE:=8
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=b5383a43aa18185cc32c759d2b8536c235fe54aa
|
||||
PKG_SOURCE_VERSION:=9f351503d4320bb3767c300bc677a85c5685c9ba
|
||||
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||
|
||||
@@ -11,7 +11,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkeasefull
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=3.0.5
|
||||
PKG_RELEASE:=6
|
||||
PKG_RELEASE:=7
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
|
||||
PKG_HASH:=skip
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=b5383a43aa18185cc32c759d2b8536c235fe54aa
|
||||
PKG_SOURCE_VERSION:=9f351503d4320bb3767c300bc677a85c5685c9ba
|
||||
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
|
||||
|
||||
@@ -15,7 +15,7 @@ LUCI_TITLE:=Nezha Agent is the client version 2 of the Nezha Panel.
|
||||
LUCI_DEPENDS:=+nezha-agentv1
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_VERSION:=30.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
PKG_MAINTAINER:=<https://github.com/2018nuoyan/openwrt_nezha/>
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
module("luci.controller.nezha-agent", package.seeall)
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/nezha-agent") then
|
||||
return
|
||||
end
|
||||
entry({"admin", "services", "nezha-agent"}, cbi("nezha-agent"), _("Nezha Agent"), 300).dependent = true
|
||||
entry({"admin","services","nezha-agent","status"},call("act_status")).leaf=true
|
||||
if not nixio.fs.access("/etc/config/nezha-agent") then
|
||||
return
|
||||
end
|
||||
local page = entry({"admin", "services", "nezha-agent"}, cbi("nezha-agent"), nil, 300)
|
||||
page.dependent = true
|
||||
page.hidden = true
|
||||
page.index = false -- 不在菜单索引中显示
|
||||
entry({"admin","services","nezha-agent","status"},call("act_status")).leaf=true
|
||||
end
|
||||
|
||||
function trim(s)
|
||||
|
||||
+666
-18
@@ -1,26 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (c) 2025-2026 10000ge10000
|
||||
Project: luci-app-openclaw — OpenClaw AI Gateway LuCI Plugin for OpenWrt
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
Preamble
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
The full text of the GNU General Public License version 3 is available at:
|
||||
https://www.gnu.org/licenses/gpl-3.0.html
|
||||
SOFTWARE.
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
This project integrates with OpenClaw (https://github.com/nicepkg/openclaw),
|
||||
which is licensed under its own terms. Node.js runtime downloaded during setup
|
||||
is subject to the Node.js license (https://github.com/nodejs/node/blob/main/LICENSE).
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-openclaw
|
||||
PKG_VERSION:=$(strip $(shell cat $(CURDIR)/VERSION 2>/dev/null || echo "1.0.0"))
|
||||
PKG_RELEASE:=23
|
||||
PKG_RELEASE:=24
|
||||
|
||||
PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com>
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.8.11
|
||||
PKG_RELEASE:=214
|
||||
PKG_VERSION:=26.8.12
|
||||
PKG_RELEASE:=215
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -1246,7 +1246,7 @@ function add_shunt_rule()
|
||||
if add_name then
|
||||
local has = uci:get(appname, uid)
|
||||
if has then
|
||||
http_write_json_error({ message = "This ID already exists." })
|
||||
http_write_json_error({ message = i18n.translate("This ID already exists.") })
|
||||
return
|
||||
end
|
||||
else
|
||||
|
||||
@@ -245,11 +245,12 @@ table td, .table .td {
|
||||
btn.disabled = false;
|
||||
}
|
||||
if (x && x.status === 200) {
|
||||
origin_group_shunt_rule_order[group] = get_shunt_rule_order(group);
|
||||
// origin_group_shunt_rule_order[group] = get_shunt_rule_order(group);
|
||||
alert("<%:Saved current page order successfully.%>");
|
||||
if (btn) {
|
||||
btn.style.display = "none";
|
||||
}
|
||||
// if (btn) {
|
||||
// btn.style.display = "none";
|
||||
// }
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert("<%:Save failed!%>");
|
||||
}
|
||||
|
||||
@@ -1142,6 +1142,9 @@ msgstr "分流规则组"
|
||||
msgid "Please note attention to the priority, the higher the order, the higher the priority."
|
||||
msgstr "请注意优先级问题,排序越上面优先级越高。"
|
||||
|
||||
msgid "This ID already exists."
|
||||
msgstr "该 ID 已存在。"
|
||||
|
||||
msgid "Update..."
|
||||
msgstr "更新中"
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ FWI=$(uci -q get firewall.passwall.path 2>/dev/null)
|
||||
FAKE_IP="198.18.0.0/15"
|
||||
FAKE_IP_6="fc00::/18"
|
||||
|
||||
USE_GEOVIEW=0
|
||||
|
||||
factor() {
|
||||
if [ -z "$1" ] || [ -z "$2" ]; then
|
||||
echo ""
|
||||
@@ -392,6 +394,28 @@ load_acl() {
|
||||
shunt6_set_name="psw_${sid}_shunt6"
|
||||
ipset -! create $shunt_set_name nethash maxelem 1048576 timeout 172800
|
||||
ipset -! create $shunt6_set_name nethash family inet6 maxelem 1048576 timeout 172800
|
||||
# 预加载分流规则 ip 到 ipset
|
||||
local GEOIP_CODE=""
|
||||
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
|
||||
local shunt_group shunt_id
|
||||
if [ "${use_shunt_tcp}" = "1" ]; then
|
||||
shunt_group=$(config_n_get $tcp_node shunt_group)
|
||||
elif [ "${use_shunt_udp}" = "1" ]; then
|
||||
shunt_group=$(config_n_get $udp_node shunt_group)
|
||||
fi
|
||||
for shunt_id in $shunt_ids; do
|
||||
[ "${shunt_group}" != "$(config_n_get ${shunt_id} group)" ] && continue
|
||||
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | grep -v "^#" | sed -e "/^$/d" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | sed -e "s/^/add $shunt_set_name &/g" -e "s/$/ timeout 0/g" | ipset -! -R
|
||||
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | grep -v "^#" | sed -e "/^$/d" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "s/^/add $shunt6_set_name &/g" -e "s/$/ timeout 0/g" | ipset -! -R
|
||||
[ "$USE_GEOVIEW" = "1" ] && {
|
||||
local geoip_code=$(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
|
||||
[ -n "$geoip_code" ] && GEOIP_CODE="${GEOIP_CODE:+$GEOIP_CODE,}$geoip_code"
|
||||
}
|
||||
done
|
||||
if [ -n "$GEOIP_CODE" ]; then
|
||||
get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | sed -e "s/^/add $shunt_set_name &/g" -e "s/$/ timeout 0/g" | ipset -! -R
|
||||
get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "s/^/add $shunt6_set_name &/g" -e "s/$/ timeout 0/g" | ipset -! -R
|
||||
fi
|
||||
}
|
||||
}
|
||||
[ -n "${dns_redirect_port}" ] && dns_redirect=${dns_redirect_port}
|
||||
@@ -891,7 +915,7 @@ add_firewall_rule() {
|
||||
local USE_BLOCK_LIST_ALL=${USE_BLOCK_LIST}
|
||||
local _TCP_NODE=$(config_t_get global tcp_node)
|
||||
local _UDP_NODE=$(config_t_get global udp_node)
|
||||
local USE_GEOVIEW=$(config_t_get global_rules enable_geoview)
|
||||
USE_GEOVIEW=$(config_t_get global_rules enable_geoview)
|
||||
[ -z "$(first_type $(config_t_get global_app geoview_file) geoview)" ] && USE_GEOVIEW=0
|
||||
|
||||
[ -n "$_TCP_NODE" ] && [ "$(config_n_get $_TCP_NODE protocol)" = "_shunt" ] && USE_SHUNT_TCP=1 && USE_SHUNT_NODE=1
|
||||
@@ -957,7 +981,7 @@ add_firewall_rule() {
|
||||
[ "$USE_SHUNT_NODE" = "1" ] && {
|
||||
local GEOIP_CODE=""
|
||||
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
|
||||
local shunt_group
|
||||
local shunt_group shunt_id
|
||||
if [ "${USE_SHUNT_TCP}" = "1" ]; then
|
||||
shunt_group=$(config_n_get $_TCP_NODE shunt_group)
|
||||
elif [ "${USE_SHUNT_UDP}" = "1" ]; then
|
||||
|
||||
@@ -52,6 +52,8 @@ FWI=$(uci -q get firewall.passwall.path 2>/dev/null)
|
||||
FAKE_IP="198.18.0.0/15"
|
||||
FAKE_IP_6="fc00::/18"
|
||||
|
||||
USE_GEOVIEW=0
|
||||
|
||||
factor() {
|
||||
local ports="$1"
|
||||
if [ -z "$1" ] || [ -z "$2" ] || [ "$ports" = "1:65535" ]; then
|
||||
@@ -453,6 +455,28 @@ load_acl() {
|
||||
gen_nftset $shunt_set_name_static ipv4_addr 0
|
||||
gen_nftset $shunt6_set_name ipv6_addr "2d"
|
||||
gen_nftset $shunt6_set_name_static ipv6_addr 0
|
||||
# 预加载分流规则 ip 到 nftset
|
||||
local GEOIP_CODE=""
|
||||
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
|
||||
local shunt_group shunt_id
|
||||
if [ "${use_shunt_tcp}" = "1" ]; then
|
||||
shunt_group=$(config_n_get $tcp_node shunt_group)
|
||||
elif [ "${use_shunt_udp}" = "1" ]; then
|
||||
shunt_group=$(config_n_get $udp_node shunt_group)
|
||||
fi
|
||||
for shunt_id in $shunt_ids; do
|
||||
[ "${shunt_group}" != "$(config_n_get ${shunt_id} group)" ] && continue
|
||||
config_n_get $shunt_id ip_list | sed 's/#.*//' | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $shunt_set_name_static
|
||||
config_n_get $shunt_id ip_list | sed 's/#.*//' | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $shunt6_set_name_static
|
||||
[ "$USE_GEOVIEW" = "1" ] && {
|
||||
local geoip_code=$(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
|
||||
[ -n "$geoip_code" ] && GEOIP_CODE="${GEOIP_CODE:+$GEOIP_CODE,}$geoip_code"
|
||||
}
|
||||
done
|
||||
if [ -n "$GEOIP_CODE" ]; then
|
||||
get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $shunt_set_name_static
|
||||
get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $shunt6_set_name_static
|
||||
fi
|
||||
}
|
||||
}
|
||||
[ -n "${dns_redirect_port}" ] && dns_redirect=${dns_redirect_port}
|
||||
@@ -954,7 +978,8 @@ add_firewall_rule() {
|
||||
if [ -f $RULES_PATH/chnroute.nft ] && [ -s $RULES_PATH/chnroute.nft ] && [ $(awk 'END{print NR}' $RULES_PATH/chnroute.nft) -ge 8 ]; then
|
||||
nft -f $RULES_PATH/chnroute.nft
|
||||
else
|
||||
cat $RULES_PATH/chnroute | tr -s '\n' | sed 's/#.*//' | gen_nftset $NFTSET_CHN_STATIC ipv4_addr 0
|
||||
gen_nftset $NFTSET_CHN_STATIC ipv4_addr 0
|
||||
cat $RULES_PATH/chnroute | tr -s '\n' | sed 's/#.*//' | insert_nftset $NFTSET_CHN_STATIC
|
||||
fi
|
||||
gen_nftset $NFTSET_BLACK ipv4_addr "2d"
|
||||
gen_nftset $NFTSET_BLACK_STATIC ipv4_addr 0
|
||||
@@ -975,7 +1000,8 @@ add_firewall_rule() {
|
||||
#echolog "使用缓存加载chnroute6..."
|
||||
nft -f $RULES_PATH/chnroute6.nft
|
||||
else
|
||||
cat $RULES_PATH/chnroute6 | tr -s '\n' | sed 's/#.*//' | gen_nftset $NFTSET_CHN6_STATIC ipv6_addr 0
|
||||
gen_nftset $NFTSET_CHN6_STATIC ipv6_addr 0
|
||||
cat $RULES_PATH/chnroute6 | tr -s '\n' | sed 's/#.*//' | insert_nftset $NFTSET_CHN6_STATIC
|
||||
fi
|
||||
gen_nftset $NFTSET_BLACK6 ipv6_addr "2d"
|
||||
gen_nftset $NFTSET_BLACK6_STATIC ipv6_addr 0
|
||||
@@ -993,7 +1019,7 @@ add_firewall_rule() {
|
||||
local USE_BLOCK_LIST_ALL=${USE_BLOCK_LIST}
|
||||
local _TCP_NODE=$(config_t_get global tcp_node)
|
||||
local _UDP_NODE=$(config_t_get global udp_node)
|
||||
local USE_GEOVIEW=$(config_t_get global_rules enable_geoview)
|
||||
USE_GEOVIEW=$(config_t_get global_rules enable_geoview)
|
||||
[ -z "$(first_type $(config_t_get global_app geoview_file) geoview)" ] && USE_GEOVIEW=0
|
||||
|
||||
[ -n "$_TCP_NODE" ] && [ "$(config_n_get $_TCP_NODE protocol)" = "_shunt" ] && USE_SHUNT_TCP=1 && USE_SHUNT_NODE=1
|
||||
@@ -1059,7 +1085,7 @@ add_firewall_rule() {
|
||||
[ "$USE_SHUNT_NODE" = "1" ] && {
|
||||
local GEOIP_CODE=""
|
||||
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
|
||||
local shunt_group
|
||||
local shunt_group shunt_id
|
||||
if [ "${USE_SHUNT_TCP}" = "1" ]; then
|
||||
shunt_group=$(config_n_get $_TCP_NODE shunt_group)
|
||||
elif [ "${USE_SHUNT_UDP}" = "1" ]; then
|
||||
|
||||
@@ -17,15 +17,15 @@ LUCI_NAME:=luci-theme-footstrap
|
||||
FOOTSTRAP_VERSION?=
|
||||
ifneq ($(FOOTSTRAP_VERSION),)
|
||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||
PKG_RELEASE:=13
|
||||
PKG_RELEASE:=14
|
||||
endif
|
||||
|
||||
LUCI_TITLE:=Footstrap Theme
|
||||
# +luci-base is the WHOLE list. The self-updater used to hard-require `curl`, which is NOT in
|
||||
# OpenWrt's default package set (the base image ships uclient-fetch), so the update button
|
||||
# failed on a stock router; it falls back to uclient-fetch rather than pull curl onto an 8 MB
|
||||
# device for one API call. jsonfilter and sha256sum ARE in the base image. Do not add a
|
||||
# runtime dep for a convenience tool — fall back instead.
|
||||
# +luci-base is the WHOLE list, and keeping it that way is the constraint. Nothing this package
|
||||
# ships fetches anything at run time any more (the self-updater is retired), so there is no second
|
||||
# name to justify — and the rule that got it here stands: the one convenience tool that ever wanted
|
||||
# a dep was `curl`, which is NOT in OpenWrt's default set, on an 8 MB device, for one API call. Fall
|
||||
# back to what the base image has (uclient-fetch, jsonfilter, sha256sum) or do without.
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
LUCI_PKGARCH:=all
|
||||
# Else luci.mk defaults these to LuCI's own, and the package claims MAINTAINER "OpenWrt LuCI
|
||||
@@ -159,7 +159,8 @@ endef
|
||||
# build-css.sh reads it from $(CURDIR) and writes into the build tree. cat/awk only, no host
|
||||
# toolchain: that is why it runs on an OpenWrt buildbot.
|
||||
# 2. Stamp the git-derived version into the theme JS (fs-version.js — the string the Appearance tab
|
||||
# shows). Skipped when PKG_VERSION is empty, leaving '0.0.0-dev'.
|
||||
# shows). Unconditional: with PKG_VERSION empty it falls back to PKG_SRC_VERSION, which luci.mk
|
||||
# always defines, so only an un-built source checkout keeps the literal '0.0.0-dev'.
|
||||
#
|
||||
# THE CATALOGUE IS luci.mk's BUSINESS, and nothing here compiles it. `po/` is the directory
|
||||
# LUCI_LANGUAGES globs, so luci.mk emits one `luci-i18n-footstrap-<lang>` package per language the
|
||||
@@ -190,9 +191,9 @@ define Build/Prepare/luci-theme-footstrap
|
||||
$(SHELL) $(CURDIR)/mangle-tokens.sh \
|
||||
$(PKG_BUILD_DIR)/htdocs/luci-static/footstrap/cascade.css \
|
||||
$(CURDIR)/htdocs/luci-static/resources $(CURDIR)/ucode
|
||||
# Templates are 58% comments and shipped them all. jsmin/terser strip the JS, build-css.sh the
|
||||
# Templates are mostly comments and shipped them all. jsmin/terser strip the JS, build-css.sh the
|
||||
# CSS; the .ut files were simply never included in that trade. Only `{# … #}` goes — see the
|
||||
# script for why the ucode-code `/* … */` deliberately stays. -16 KB of 39, git keeps every word,
|
||||
# script for why the ucode-code `/* … */` deliberately stays. -22 KB of 60, git keeps every word,
|
||||
# and dev-sync.sh does NOT run this: a router you are debugging on keeps its comments.
|
||||
$(SHELL) $(CURDIR)/strip-templates.sh $(PKG_BUILD_DIR)/ucode
|
||||
# …and the same for the shell under root/ (71% and 95% comment lines). Whole-line `#` only.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
# Build luci-theme-footstrap as an OpenWrt .apk via the SDK.
|
||||
# The theme is noarch (CSS/JS/templates/fonts only), so the package installs on any
|
||||
# The theme is noarch (CSS, JS and templates only — it ships no fonts), so the package installs on any
|
||||
# router of that release whatever its CPU architecture.
|
||||
#
|
||||
# ./build-apk.sh # download SDK if needed, then build
|
||||
@@ -13,9 +13,15 @@
|
||||
# has never heard of owfeed — and this is the only way to find out that it does.
|
||||
set -e
|
||||
|
||||
REL="${OPENWRT_RELEASE:-25.12.2}"
|
||||
# The point release and the compiler are both baked into the SDK's filename, so this default goes
|
||||
# stale twice over: downloads.openwrt.org keeps only the CURRENT point release of a branch (older
|
||||
# ones move to archive.openwrt.org and the fetch 404s), and a toolchain bump changes gcc-14.3.0.
|
||||
# Kept in step with owfeed.lock's `point:` for the 25.12 branch — that file is what the release
|
||||
# actually builds against. Override for any other: OPENWRT_RELEASE=24.10.8 sh build-apk.sh, and
|
||||
# OPENWRT_SDK_FILE=… when the compiler string differs too.
|
||||
REL="${OPENWRT_RELEASE:-25.12.5}"
|
||||
SDK_BASE="https://downloads.openwrt.org/releases/${REL}/targets/mediatek/filogic"
|
||||
SDK_FILE="openwrt-sdk-${REL}-mediatek-filogic_gcc-14.3.0_musl.Linux-x86_64.tar.zst"
|
||||
SDK_FILE="${OPENWRT_SDK_FILE:-openwrt-sdk-${REL}-mediatek-filogic_gcc-14.3.0_musl.Linux-x86_64.tar.zst}"
|
||||
SDK_URL="$SDK_BASE/$SDK_FILE"
|
||||
# MUST be a case-sensitive fs (ext4/…), NOT an NTFS/9p Windows mount.
|
||||
BUILD_DIR="${BUILD_DIR:-/tmp/ow-footstrap-build}"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# order within a dir. A later layer beats an earlier one whatever the specificity, so
|
||||
# nothing needs !important to override base (declared in styles/00-header.css).
|
||||
#
|
||||
# Minified unless --dev: ~287 KB of source -> ~111 KB (-61%). uhttpd serves CSS with NO
|
||||
# Minified unless --dev: ~468 KB of source -> ~136 KB (-71%). uhttpd serves CSS with NO
|
||||
# gzip, so every byte is a wire byte. Comments and whitespace go; a selector or a
|
||||
# declaration is never rewritten, which is why LuCI's csstidy stays off (it mangles
|
||||
# :has()/color-mix()). Needs only cat/awk, so an OpenWrt buildbot can run it.
|
||||
|
||||
@@ -22,9 +22,9 @@ scp -q "$D"/ucode/template/themes/$N/*.ut "$R":/usr/share/ucode/luci/templ
|
||||
ssh "$R" "mkdir -p /usr/share/ucode/luci/template/themes/$N/partials"
|
||||
scp -q "$D"/ucode/template/themes/$N/partials/*.ut "$R":/usr/share/ucode/luci/template/themes/$N/partials/
|
||||
|
||||
# shared static (cascade.css, fonts, logo). The doodle wallpapers are NOT here and not in the
|
||||
# package: they are downloaded on demand from GitHub (fs-prefs.js installWallpaper), so the dev
|
||||
# router deliberately starts without them — that is the state to test the download dialog in.
|
||||
# shared static: cascade.css and the two logo files, which is all this directory holds. No
|
||||
# wallpapers — the doodles this theme once downloaded from GitHub are gone, and nothing in the
|
||||
# package fetches anything at run time; the Background axis paints an SVG the admin uploads.
|
||||
scp -qr "$D"/htdocs/luci-static/$N/* "$R":/www/luci-static/$N/
|
||||
|
||||
# EVERY resource JS, by GLOB — never by name. This used to list the four files
|
||||
@@ -41,17 +41,27 @@ tar -C "$D/htdocs/luci-static/resources" -cf - . | ssh "$R" "mkdir -p /www/luci-
|
||||
# Build/Prepare) so the Footstrap tab shows a real version instead of "(dev)". The FILE NAME is part
|
||||
# of the contract: FS_VERSION lives in fs-version.js and moving it means changing every sed.
|
||||
FS_V="$(git -C "$D" describe --tags --always 2>/dev/null | sed 's/^v//')"
|
||||
# if-form, not `[ -n ] && ssh`: under set -e a failed &&-list aborts the whole sync when git
|
||||
# describe yields nothing (a copied tree without .git). expr refuses a tag with
|
||||
# sed/shell-special characters rather than interpolating it.
|
||||
# if-form, not `[ -n ] && ssh`: under `set -e` a bare `&&`-list whose LAST command fails is what
|
||||
# aborts, so `[ -n "$FS_V" ] && ssh …` would take the sync down whenever git describe yielded
|
||||
# nothing (a copied tree without .git). An `&&` used to guard an ASSIGNMENT is a different shape and
|
||||
# is safe — stage.sh:41 and update-po.sh:39 rely on that. expr refuses a tag with sed/shell-special
|
||||
# characters rather than interpolating it.
|
||||
if [ -n "$FS_V" ] && expr "$FS_V" : '[0-9A-Za-z._-]*$' >/dev/null; then
|
||||
ssh "$R" "sed -i \"s#const FS_VERSION *= *'[^']*'#const FS_VERSION = '$FS_V'#\" /www/luci-static/resources/fs-version.js"
|
||||
fi
|
||||
|
||||
# The catalogue, which the PACKAGE compiles in Build/Prepare. po2lmo is a luci-base host tool,
|
||||
# absent from a dev machine by default, so this is best-effort: without it the strings stay
|
||||
# English and the sync still succeeds. (Build it once from the pinned luci commit: cc -o po2lmo
|
||||
# po2lmo.c lib/lmo.c lib/plural_formula.c.) Same basename as the package — see the Makefile.
|
||||
# The catalogue. Nothing in Build/Prepare compiles it, and the RELEASE does not come from luci.mk
|
||||
# either: owfeed bundles the catalogues inside the theme package (owfeed.yml), so a dev router gets
|
||||
# them from nowhere unless this sync makes them. po2lmo is a luci-base host tool, absent from a dev
|
||||
# machine by default, so this is best-effort: without it the strings stay English and the sync still
|
||||
# succeeds. (Build it once from the pinned luci checkout: make -C modules/luci-base/src po2lmo —
|
||||
# plural_formula.c is GENERATED by contrib/lemon, so a bare cc over the .c files cannot work.)
|
||||
#
|
||||
# THE BASENAME IS `footstrap-theme`, matching owfeed.yml and NOT the po file's own name: lmo_load_
|
||||
# catalog globs *.<lang>.lmo so any basename loads, and a v0.8.4 router still owns footstrap.ru.lmo
|
||||
# through the old luci-i18n package, where the same path would be a file conflict. Sync under a
|
||||
# different name and the dev router carries two archives for the same msgids, with glob order
|
||||
# deciding which one the page shows.
|
||||
if command -v po2lmo >/dev/null 2>&1; then
|
||||
ssh "$R" "mkdir -p /usr/lib/lua/luci/i18n"
|
||||
for po in "$D"/po/*/*.po; do
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
*
|
||||
* WHERE IT LIVES. It used to be a popover hanging off a button in the chrome; it is now a TAB on
|
||||
* System -> System (admin/system/system), beside General Settings / Logging / Time Synchronization
|
||||
* / Language and Style — the page an admin already opens to set the things that are not network. The axes had outgrown a floating panel — eighteen of them, nine
|
||||
* carrying a colour field, a swatch and a contrast readout — and a dialog that has to trap Tab,
|
||||
* / Language and Style — the page an admin already opens to set the things that are not network. The axes had outgrown a floating panel — twenty-one of
|
||||
* them, nine carrying a colour field, a swatch and a contrast readout — and a dialog that has to trap Tab,
|
||||
* place itself against a viewport edge and stay inside a 320px column is the wrong container for
|
||||
* that. Keeping BOTH would have meant every axis rendered twice, which is the failure this file's
|
||||
* own history is made of.
|
||||
@@ -46,9 +46,12 @@
|
||||
* the current look to the ROUTER for other browsers. That distinction is the whole model
|
||||
* (docs/design-system.md) and it is why this page has no Save/Reset footer of LuCI's own. */
|
||||
function render() {
|
||||
/* still a promise: the view awaits it, and keeping the shape means a future asynchronous step
|
||||
* (or the caller) needs no change. */
|
||||
return Promise.resolve(build());
|
||||
/* A promise, and the build runs INSIDE it rather than as its argument: `Promise.resolve(build())`
|
||||
* evaluates build() synchronously, so a throw in it unwound out of render() before mount() could
|
||||
* attach .catch/.finally — leaving mount()'s _building flag set for the life of the document. The
|
||||
* tab then never built again on that page and nothing was logged, which is both channels silent
|
||||
* at once. Deferred like this, the same throw lands in the .catch that exists for it. */
|
||||
return Promise.resolve().then(build);
|
||||
}
|
||||
|
||||
function build() {
|
||||
@@ -219,15 +222,15 @@ function build() {
|
||||
/* the STRENGTH half of the Tint — how strong the hue reads. Only meaningful in hue mode: a
|
||||
* hex canvas IS the colour asked for, with no chroma of ours to scale. CSS hides it in the
|
||||
* other two states (no tint at all, or a hex one). */
|
||||
/* NOT "Density": that is the UI-density segment above, and this string is both the visible
|
||||
/* NOT "Density": that is the UI-density select above, and this string is both the visible
|
||||
* caption AND the control's aria-label, so two rows would read "Density" and a screen reader
|
||||
* would announce "Density, radio group" and "Density, slider" with nothing to tell them
|
||||
* would announce "Density, combo box" and "Density, slider" with nothing to tell them
|
||||
* apart. */
|
||||
group(_('Tint strength', 'footstrap'),
|
||||
(label) => sliderCtl(prefs.currentTintStrength(), 0, 200, bump(repaint(prefs.applyTintStrength)), label, {
|
||||
step: 5,
|
||||
fmt: (v) => v + '%'
|
||||
}), { cls: 'fs-ap-tint fs-ap-density' }),
|
||||
unit: '%'
|
||||
}), { cls: 'fs-ap-tint fs-ap-tintstr' }),
|
||||
|
||||
/* recolours the accented CONTROLS (buttons/toggles/sliders/focus rings), not the canvas the
|
||||
* way Tint does. Measured as TEXT on a card, which is the use that fails first: as a fill it
|
||||
@@ -487,8 +490,21 @@ function build() {
|
||||
const saveErr = E('div', { 'class': 'fs-ap-err', 'role': 'alert', 'hidden': '' });
|
||||
|
||||
/* the Save button IS the status: match -> disabled "Saved as default", diverged -> enabled
|
||||
* "Save as default". Called after every axis change (via bump). */
|
||||
* "Save as default". Called after every axis change (via bump).
|
||||
*
|
||||
* Unless this browser refuses storage, in which case the status would be a lie in both halves:
|
||||
* nothing was written, so every current*() reads the ROUTER default back and the comparison is
|
||||
* true however far the page has been dragged from it. Say what is actually true instead — the
|
||||
* axes apply and are forgotten on reload — and leave the button enabled, because pushing this
|
||||
* browser's look to the router is the one thing that still works here. */
|
||||
function refreshSave() {
|
||||
if (prefs.storageBroken()) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = _('Save as default', 'footstrap');
|
||||
saveErr.textContent = _('This browser is not storing preferences (site data is blocked), so a change here lasts until you reload. Saving as default still works and applies to every browser.', 'footstrap');
|
||||
saveErr.hidden = false;
|
||||
return;
|
||||
}
|
||||
const saved = prefs.matchesSavedDefault();
|
||||
saveBtn.disabled = saved;
|
||||
saveBtn.textContent = saved ? _('Saved as default', 'footstrap') : _('Save as default', 'footstrap');
|
||||
@@ -615,8 +631,9 @@ function build() {
|
||||
open = !open;
|
||||
prefs.lsSet(key, open ? 'on' : 'off');
|
||||
paint();
|
||||
/* the colour controls read the COMPUTED cascade, and a hidden element computes nothing
|
||||
* useful — so the readouts are filled when the group becomes visible, not before */
|
||||
/* Refreshed on OPEN because the axes below it may have moved while it was collapsed —
|
||||
* a palette switch or a preset changes what every readout says. Cheap and skipped
|
||||
* while closed: nothing in that fold is on screen to be wrong. */
|
||||
if (open) refreshColours();
|
||||
});
|
||||
paint();
|
||||
@@ -637,10 +654,11 @@ function build() {
|
||||
section(_('Defaults', 'footstrap'), defaults)
|
||||
]);
|
||||
|
||||
/* The colour controls read the COMPUTED cascade, which needs them in the document — so the first
|
||||
* fill cannot happen while the tree is still being assembled above. The view appends this
|
||||
* element synchronously on return, so a microtask is late enough and early enough: late enough
|
||||
* that the probe resolves, early enough that nothing has painted a blank field. */
|
||||
/* The first fill, deferred one microtask so the tree above is finished being assembled. It does
|
||||
* NOT wait for the form to be in the document, and does not need to: every value a readout shows
|
||||
* comes back through widgets.probeColor(), which keeps its own hidden probe attached to <body>
|
||||
* and resolves the cascade there — so this runs while the form is still detached (mount() appends
|
||||
* it a microtask later) and still reads the live palette. */
|
||||
Promise.resolve().then(refreshColours);
|
||||
return page;
|
||||
}
|
||||
@@ -666,7 +684,9 @@ function takeReturn() {
|
||||
return true;
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
const MARK = 'fs-ap'; /* the built form's own class; also how mount() knows it is already there */
|
||||
const MARK = 'fs-ap';
|
||||
/* how long the stock view gets to render its tabs before a missing group counts as a failure */
|
||||
const TAB_DEADLINE = 5000; /* the built form's own class; also how mount() knows it is already there */
|
||||
const TAB = 'fs-appearance'; /* the pane's data-tab, which ui.tabs' click handler matches on */
|
||||
|
||||
let _routeObserver = null, _viewObserver = null, _observedView = null, _building = false;
|
||||
@@ -692,11 +712,31 @@ function stopWatch() {
|
||||
* yet is a page still rendering, not a page without tabs.
|
||||
*
|
||||
* The flag and the sibling are the whole test, deliberately: the panes themselves are NOT required
|
||||
* to be found here. On the stock System page they are not direct children of the marked element at
|
||||
* all — the map nests them a level deeper — so a `:scope > .cbi-tabcontainer` check found none and
|
||||
* the tab was never added, silently, on a page that plainly has tabs. */
|
||||
* to be found here, and a check for them by CLASS is what failed. A modern pane carries no class at
|
||||
* all — form.js gives it `data-tab` and `data-tab-title` and nothing else, and `.cbi-tabcontainer`
|
||||
* is luci-compat vocabulary from the Lua CBI — so `:scope > .cbi-tabcontainer` matched nothing on a
|
||||
* page that plainly has tabs, and the tab was never added, silently. ui.tabs itself marks
|
||||
* `panes[0].parentNode`, so the panes ARE this element's children; they are simply not identifiable
|
||||
* that way. */
|
||||
/* Tab groups that belong to the page we just LEFT. The router stamps body[data-page] before the
|
||||
* incoming view renders, and on a warm route #view still holds the outgoing page's DOM at that
|
||||
* moment — so mount() found ITS tab strip and appended the whole Appearance form, plus a live,
|
||||
* clickable "Footstrap" <li>, to another page's tabs. Measured arriving at System -> System from
|
||||
* Network -> DHCP: two builds for one arrival, and for 66 ms on localhost (an RTT or more on a real
|
||||
* router, since the window is the incoming view's load()) the tab sat on the DHCP strip and opened
|
||||
* all 24 Appearance rows when clicked. Every group present at the moment of the stamp is therefore
|
||||
* disqualified; the incoming view's own group is a fresh element and is not in this set. */
|
||||
const _staleGroups = new WeakSet();
|
||||
function disqualifyCurrentGroups() {
|
||||
const view = document.getElementById('view');
|
||||
if (!view) return;
|
||||
for (const g of view.querySelectorAll('[data-initialized="true"]'))
|
||||
_staleGroups.add(g);
|
||||
}
|
||||
|
||||
function tabGroup(view) {
|
||||
for (const g of view.querySelectorAll('[data-initialized="true"]')) {
|
||||
if (_staleGroups.has(g)) continue;
|
||||
const menu = g.previousElementSibling;
|
||||
if (menu?.classList.contains('cbi-tabmenu'))
|
||||
return { group: g, menu };
|
||||
@@ -739,8 +779,10 @@ function mount() {
|
||||
const title = 'Footstrap';
|
||||
/* data-tab-active is deliberately absent: the stock page opens on whichever tab it
|
||||
* opened on before, and a theme has no business taking that over. */
|
||||
/* The same shape a stock pane has, which is `data-tab` + `data-tab-title` and no class:
|
||||
* ui.tabs.switchTab reads the attributes, and the `cbi-tabcontainer` class this used to
|
||||
* carry is luci-compat's, styled by no rule this theme ships. */
|
||||
t.group.appendChild(E('div', {
|
||||
'class': 'cbi-tabcontainer',
|
||||
'data-tab': TAB,
|
||||
'data-tab-title': title
|
||||
}, [ form ]));
|
||||
@@ -764,13 +806,32 @@ function watch() {
|
||||
_viewObserver = new MutationObserver(mount);
|
||||
_viewObserver.observe(view, { childList: true, subtree: true });
|
||||
mount();
|
||||
/* A DEADLINE on the one failure that is otherwise perfectly silent. tabGroup() reads three
|
||||
* private ui.tabs facts — the `data-initialized` marker, the `cbi-tabmenu` class on the menu it
|
||||
* inserts, and `cbi-tab-disabled` on the items — and one of those has already moved between
|
||||
* 24.10 and 25.12 (`data-tab-group` was dropped with no announcement). If any of the three we do
|
||||
* read goes the same way, mount() simply returns early on every mutation: the stock page renders
|
||||
* perfectly, nothing throws, and every Appearance axis becomes unreachable. Say it once, after
|
||||
* the page has had time to render. */
|
||||
window.setTimeout(() => {
|
||||
const v = document.getElementById('view');
|
||||
if (!onPage() || !v || v.querySelector('.' + MARK) || _building) return;
|
||||
console.error('footstrap: the Appearance tab could not be attached — this page has tabs, but '
|
||||
+ 'ui.tabs no longer marks them the way fs-appearance.js looks for. Every Appearance axis '
|
||||
+ 'is unreachable until that is updated.');
|
||||
}, TAB_DEADLINE);
|
||||
}
|
||||
|
||||
/* Called by menu-footstrap-common's init, once. Everything route-dependent hangs off the data-page
|
||||
* observer inside. */
|
||||
function wire() {
|
||||
if (_routeObserver || !document.body) return;
|
||||
_routeObserver = new MutationObserver(() => (onPage() ? watch() : stopWatch()));
|
||||
_routeObserver = new MutationObserver(() => {
|
||||
/* BEFORE deciding anything: whatever is in #view at the moment data-page changes belongs to
|
||||
* the page being left (see _staleGroups). */
|
||||
disqualifyCurrentGroups();
|
||||
return onPage() ? watch() : stopWatch();
|
||||
});
|
||||
_routeObserver.observe(document.body, { attributes: true, attributeFilter: [ 'data-page' ] });
|
||||
if (onPage()) watch();
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ function resolveLen(token, dflt) {
|
||||
* stylesheet's widths in two different places. Reaching for these means the measurement failed. */
|
||||
const GEOM_DFLT = { contentMin: 500, sidebarW: 224, railW: 68, contentPad: 56 };
|
||||
|
||||
let _geom = null, _geomDensity = null;
|
||||
let _geom = null, _geomDensity = null, _geomWarned = false;
|
||||
function shellGeometry() {
|
||||
const density = document.documentElement.getAttribute('data-density') || '';
|
||||
if (_geom && _geomDensity === density) return _geom;
|
||||
@@ -170,7 +170,19 @@ function shellGeometry() {
|
||||
* absent token reports 0 for all four (an abs-positioned empty div with `width:auto` shrinks to
|
||||
* 0, and 0 is finite, so the per-read fallback above never fires). Neither can be seen in the
|
||||
* numbers one at a time; the RELATION between them is what gives it away. */
|
||||
_geom = (g.railW > 0 && g.railW < g.sidebarW && g.contentMin > 0) ? g : Object.assign({}, GEOM_DFLT);
|
||||
const sane = (g.railW > 0 && g.railW < g.sidebarW && g.contentMin > 0);
|
||||
/* SAY SO when it fires. The fallback keeps the chrome laid out, which is right — but it lays it
|
||||
* out on the literals CLAUDE.md forbids copying into JS, so from there on the sidebar folds at a
|
||||
* width nobody chose while the page still LOOKS correct. Silent, that is how a renamed token
|
||||
* ships green; one console line is the difference between a mystery and a grep. Once per
|
||||
* document — this runs on every resize and every mutation. */
|
||||
if (!sane && !_geomWarned) {
|
||||
_geomWarned = true;
|
||||
console.error('footstrap: the chrome widths did not read back from the stylesheet (got '
|
||||
+ JSON.stringify(g) + ') — falling back to the built-in defaults. A --fs-* width token was '
|
||||
+ 'renamed, or a foreign sheet is reaching the measurement probe.');
|
||||
}
|
||||
_geom = sane ? g : Object.assign({}, GEOM_DFLT);
|
||||
return _geom;
|
||||
}
|
||||
|
||||
@@ -236,8 +248,8 @@ function fitChrome() {
|
||||
}
|
||||
|
||||
/* The cluster's own escalation, for EVERY bar — the top layout at any width, and the sidebar
|
||||
* layout once fitShell has stamped data-narrow. It runs after the menu's, because on the top
|
||||
* layout .fs-bar-stack has by then given the menu its own row and taken it out of the sum. */
|
||||
* layout once fitShell has stamped data-narrow. It runs after the menu's, so that when the menu
|
||||
* did NOT fit, .fs-bar-stack has already given it a row of its own. */
|
||||
if (bar && (topBar || document.documentElement.hasAttribute('data-narrow')))
|
||||
fitCluster(bar, menu);
|
||||
}
|
||||
@@ -268,8 +280,12 @@ function fitCluster(bar, menu) {
|
||||
|
||||
/* Add the widths up rather than read positions: the bar is align-items:center over children of
|
||||
* differing heights, so offsetTop differs even on ONE row — the same trap the menu measurement
|
||||
* documents above. The menu is excluded because it owns a full-width row of its own in every bar
|
||||
* (`ul.nav { flex: 1 1 100% }`), so it is never what the cluster competes with. */
|
||||
* documents above. The menu is excluded either way. Where it wraps to a row of its own it plainly
|
||||
* is not competing (`ul.nav { flex: 1 1 100% }` — the sidebar-layout bar, and the top bar once
|
||||
* .fs-bar-stack is set); where it does share the brand's row, un-stacked at `flex: 1 1 auto`
|
||||
* (theme/50-toplayout.css), it is the one child that SHRINKS, so counting its current width would
|
||||
* report the cluster as not fitting whenever the menu happened to be wide. What the cluster has to
|
||||
* fit beside is the brand and the other actions. */
|
||||
function clusterFitsBrandRow(bar, menu) {
|
||||
const cs = getComputedStyle(bar);
|
||||
const gap = parseFloat(cs.columnGap) || 0;
|
||||
@@ -292,8 +308,7 @@ function clusterFitsBrandRow(bar, menu) {
|
||||
* MutationObserver already watches — and it re-fits SYNCHRONOUSLY (rule 2), where the copy here
|
||||
* deferred through fit.schedule(), i.e. the duplicate was strictly the slower path into the same
|
||||
* work. #tabmenu is a sibling of #view rather than inside it, but nothing writes it except
|
||||
* renderChrome(), which schedules a fit itself. Resize is fs-fit's ResizeObserver on #view (with a
|
||||
* window-resize fallback where there is no RO). */
|
||||
* renderChrome(), which schedules a fit itself. Resize is fs-fit's ResizeObserver on #view. */
|
||||
|
||||
/* modes -> #modemenu; drives the injected renderMainMenu for the active mode */
|
||||
function renderModeMenu(node, renderMainMenu) {
|
||||
|
||||
@@ -44,13 +44,11 @@ function schedule() {
|
||||
/* Watch an element's size. Any change re-fits everything — the fitters are cheap and few. */
|
||||
function watch(el) {
|
||||
if (!el) return;
|
||||
if (!_ro) {
|
||||
if (!window.ResizeObserver) { /* no RO: fall back to the window */
|
||||
window.addEventListener('resize', schedule);
|
||||
return;
|
||||
}
|
||||
_ro = new ResizeObserver(schedule);
|
||||
}
|
||||
/* No feature test: the shipped CSS needs :has() and container queries, both years younger than
|
||||
* ResizeObserver in every engine, so a browser that can render this theme at all has it. The
|
||||
* window-resize fallback that used to sit here was worse than nothing anyway — it cannot see a
|
||||
* rail collapse or a layout toggle, which is the pair this file uses an observer FOR. */
|
||||
if (!_ro) _ro = new ResizeObserver(schedule);
|
||||
_ro.observe(el);
|
||||
}
|
||||
|
||||
|
||||
@@ -138,8 +138,8 @@ function readonlyForSegs(segs) {
|
||||
}
|
||||
|
||||
/* The view class a menu node instantiates, or null if the node isn't SPA-able. The Status→Overview
|
||||
* `template` node maps to view.status.index (its server template just instantiates that — see
|
||||
* ensureOverviewHelpers in fs-router.js). Shared by navigate() and the hover prefetch. */
|
||||
* `template` node maps to view.status.index (its server template just instantiates that — the
|
||||
* globals that template also defines are fs-overview.js's, see ensureOverviewHelpers there). Shared by navigate() and the hover prefetch. */
|
||||
function viewClassFor(node) {
|
||||
if (!node || !node.action || node.satisfied === false)
|
||||
return null;
|
||||
|
||||
@@ -19,15 +19,13 @@
|
||||
* silenced it, but only after it had already been downloaded and run — a theme package reaching
|
||||
* into another module's namespace, which is exactly what this project refuses to do to third-party
|
||||
* apps (docs/conventions.md, the three zones). It is a chrome module now, loaded by the chrome, so a router on
|
||||
* another theme never sees it at all.
|
||||
* another theme never sees it at all — which is why nothing below re-checks `L.env.media`: the file
|
||||
* cannot be reached except through this theme's own footer partial, and a gate for that is a gate
|
||||
* for a state that cannot occur.
|
||||
*
|
||||
* WHAT THAT COST, and why the code below looks the way it does: the old location bought two timing
|
||||
* guarantees for free, because LuCI evaluated the file INSIDE index.load(). Both had to be paid for
|
||||
* explicitly — see ensureOverviewHelpers() and patchOverview() at the bottom. */
|
||||
function isFootstrapTheme() {
|
||||
return String(L.env.media || '').indexOf('footstrap') >= 0;
|
||||
}
|
||||
|
||||
/* section title -> grid role. _() with NO msgctxt on purpose: these must resolve to exactly what
|
||||
* luci-mod-status resolves to, or the titles stop matching. Built once — it used to cost an
|
||||
* allocation plus three _() lookups per poll tick. */
|
||||
@@ -39,7 +37,17 @@ function sectionTitle(sec) {
|
||||
* grid never applied on 24.10 at all — measured on the dev container: every section tagged
|
||||
* `none`, `.fs-ovl` never built, silently. */
|
||||
const h = sec.querySelector('.cbi-title h3, :scope > h3');
|
||||
return (h && h.firstChild) ? String(h.firstChild.nodeValue || '').trim() : '';
|
||||
if (!h) return '';
|
||||
/* The first non-empty TEXT node, not `firstChild`. That heading is not text-only any more: 25.12
|
||||
* appends a hide/show <span> inside the same <h3>, and `firstChild` lands on the title today only
|
||||
* because upstream happened to put the words first. One reordering there and every section reads
|
||||
* as an empty title — the grid silently stops being built, with the page still rendering fine. */
|
||||
for (const n of h.childNodes) {
|
||||
if (n.nodeType !== 3) continue;
|
||||
const t = String(n.nodeValue || '').trim();
|
||||
if (t) return t;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/* the wrapper we built, so the poll-tick fast path costs one property read */
|
||||
@@ -126,7 +134,7 @@ function watch() {
|
||||
* The empty `.cbi-section` wrapper stock used to build around this include is gone with it, and so
|
||||
* is the `.fs-ovl-marker` element that existed only to let CSS hide that wrapper. */
|
||||
function wire() {
|
||||
if (_routeObserver || !isFootstrapTheme() || !document.body)
|
||||
if (_routeObserver || !document.body)
|
||||
return;
|
||||
_routeObserver = new MutationObserver(() => {
|
||||
if ((document.body.getAttribute('data-page') || '') === 'admin-status-overview')
|
||||
@@ -188,11 +196,21 @@ function fillSection(inc, container, res) {
|
||||
}
|
||||
|
||||
let _inflight = null;
|
||||
/* WHICH containers the in-flight run is filling. The guard below is module-level because the
|
||||
* duplicate load it kills is module-level, but the frames are per RENDER — so joining a run blindly
|
||||
* joined one that fills SOMEBODY ELSE'S frames. Reproduced: double-click Status → Overview 100 ms
|
||||
* apart, or leave and return inside the first-load window, and the second arrival's sections stayed
|
||||
* at their birth `display:none` for a full poll interval (measured 5.9 s against 0.4 s on a single
|
||||
* arrival) — it had "joined" a run that was filling the frames the content swap had just detached.
|
||||
* Stock LuCI has no such guard and each render fetches its own data, so the blank page is ours. */
|
||||
let _inflightFor = null;
|
||||
|
||||
function pollProgressive(includes, containers, first_load) {
|
||||
/* A run is already fetching exactly this data — join it instead of starting a second
|
||||
* stampede of the same RPCs. This is what kills the duplicate load. */
|
||||
if (_inflight)
|
||||
/* A run is already fetching exactly this data, FOR THESE FRAMES — join it instead of starting a
|
||||
* second stampede of the same RPCs. This is what kills the duplicate load. A run for older
|
||||
* frames is left to finish into the detached nodes it owns, which costs nothing and is simpler
|
||||
* than cancelling RPCs that are already in flight. */
|
||||
if (_inflight && _inflightFor === containers)
|
||||
return first_load ? Promise.resolve() : _inflight;
|
||||
|
||||
const run = network.flushCache().then(() => Promise.all(
|
||||
@@ -214,7 +232,11 @@ function pollProgressive(includes, containers, first_load) {
|
||||
if (ssi) { ssi.style.display = ''; ssi.classList.add('fade-in'); }
|
||||
});
|
||||
|
||||
_inflight = run.finally(() => { _inflight = null; });
|
||||
_inflight = run.finally(() => {
|
||||
/* only if it is still OURS: a newer render may have replaced it while this one was running */
|
||||
if (_inflightFor === containers) { _inflight = null; _inflightFor = null; }
|
||||
});
|
||||
_inflightFor = containers;
|
||||
/* NOBODY AWAITS THIS ON THE FIRST LOAD — the line below hands the caller a fresh
|
||||
* Promise.resolve() so index.render() can return at once — so a rejection here has no handler
|
||||
* and surfaces as an unhandled rejection in the console. `run` rejects for one ordinary reason:
|
||||
|
||||
@@ -27,9 +27,19 @@
|
||||
* tint back off). Every axis therefore records the chosen value, including the off/default one, the
|
||||
* way `layout` always has. lsDel is reserved for resetToSaved(), which drops back to the router
|
||||
* default on purpose. */
|
||||
function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
|
||||
function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
|
||||
function lsDel(k) { try { localStorage.removeItem(k); } catch (e) {} }
|
||||
/* A browser can REFUSE storage outright — "block all cookies" for this address in Chrome/Safari,
|
||||
* dom.storage.enabled=false in Firefox, a partitioned WebView — and then every access throws. The
|
||||
* three helpers below still swallow it, because an axis that cannot be remembered must still be
|
||||
* allowed to APPLY; what they no longer do is keep quiet about it. Without this flag the page told a
|
||||
* flat lie: each control took effect, nothing was written, current*() then read null and fell back
|
||||
* to the router default, so matchesSavedDefault() was true and the Save button sat there DISABLED
|
||||
* reading "Saved as default" while the page was painted in three axes the router default does not
|
||||
* carry — and a reload dropped all of them. The Appearance tab asks this and says so once. */
|
||||
let _lsBroken = false;
|
||||
function storageBroken() { return _lsBroken; }
|
||||
function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { _lsBroken = true; return null; } }
|
||||
function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) { _lsBroken = true; } }
|
||||
function lsDel(k) { try { localStorage.removeItem(k); } catch (e) { _lsBroken = true; } }
|
||||
|
||||
/* A stored JSON ARRAY, or [] — the shape the two REMEMBERED LISTS use (the search palette's recent
|
||||
* paths, the menu's open sections). lsGet above owns the try/catch around localStorage itself; this
|
||||
@@ -45,7 +55,7 @@ function lsGetArr(k) {
|
||||
}
|
||||
|
||||
/* the router-wide defaults the server stamped (head.ut). Read at RUNTIME so current*() reports the
|
||||
* effective default when this browser has no localStorage — the popover's controls then show what
|
||||
* effective default when this browser has no localStorage — the Appearance tab's controls then show what
|
||||
* the page is actually painted as, not a phantom "auto". */
|
||||
function sd(k) { try { return (window.__fsSD || {})[k]; } catch (e) { return undefined; } }
|
||||
|
||||
@@ -371,7 +381,7 @@ function applyPalette(val) {
|
||||
* The list is what VALIDATES a stored value, so a value added to the CSS and not to this array is
|
||||
* one head.ut pre-paints and the live applier then rejects: the page would paint it and the first
|
||||
* touch of any other control would take it away. Adding one means this line, the head.ut whitelist,
|
||||
* the segmented control in fs-appearance.js and the rules in 15-wallpaper.css.
|
||||
* the Wallpaper select in fs-appearance.js and the rules in 15-wallpaper.css.
|
||||
*
|
||||
* A router upgrading from a version with the downloaded `cats`/`dinos` doodles reads its stored
|
||||
* value here, finds it is not in the list, and falls to 'off' — the files those named are not in
|
||||
@@ -576,8 +586,8 @@ function applyAutoCollapse(val) {
|
||||
document.dispatchEvent(new CustomEvent('fs-autocollapse', { detail: { on } }));
|
||||
}
|
||||
|
||||
/* The sidebar rail's collapsed flag. The BUTTON that flips it is chrome (fs-chrome.js): only the
|
||||
* stored state belongs to the preference layer, and fs-update.js needs the ls* helpers anyway.
|
||||
/* The sidebar rail's collapsed flag. The BUTTON that flips it is chrome (fs-chrome.js); only the
|
||||
* stored state belongs to the preference layer.
|
||||
* NOT part of the router-wide defaults — it is a transient chrome collapse, not an appearance
|
||||
* choice, so it is absent from snapshotAxes()/resetToSaved() below. */
|
||||
function applyRail(on) {
|
||||
@@ -609,7 +619,10 @@ const AXIS_KEYS = [
|
||||
/* Tint density: the STRENGTH of the router-identity Tint (the hue washed onto --fs-bg), a per-browser
|
||||
* axis paired with the Tint hue — the hue picks the colour, this picks how strong it reads.
|
||||
* --fs-tint-strength is a multiplier on the tint chroma (03-palettes.css): 100% = the designed
|
||||
* strength, 0% = none, up to 200%. It only bites while a Tint hue is set (data-tint), and it is
|
||||
* strength, up to 200%. 0 is not quite "no tint": the palette's canvas carries a slight cast of its
|
||||
* own and the relative colour that applies the tint replaces chroma outright, so 0 leaves a neutral
|
||||
* canvas at the same lightness rather than the untinted one (03-palettes.css measures it). Clearing
|
||||
* the Tint hue is the real off. It only bites while a Tint hue is set (data-tint), and it is
|
||||
* hidden and moot under the File wallpaper, where the tint resets to neutral (the photo covers the
|
||||
* canvas). A normal per-browser axis: localStorage ?? router default ?? built-in; head.ut pre-paints
|
||||
* it; stored explicitly (incl. the 100 default) so it can override a router default, like the hues.
|
||||
@@ -700,7 +713,7 @@ function snapshotAxes() {
|
||||
};
|
||||
}
|
||||
/* The RESOLVED router default (UCI value if set, else the built-in), in snapshotAxes() string form,
|
||||
* so the popover can grey the Save button out when this browser already shows exactly it. Seeded
|
||||
* so the Appearance tab can grey the Save button out when this browser already shows exactly it. Seeded
|
||||
* from window.__fsSD at load and replaced with the just-saved snapshot after saveAsDefault(), so a
|
||||
* save flips the match to true without a reload.
|
||||
*
|
||||
@@ -745,7 +758,7 @@ function matchesSavedDefault() {
|
||||
return Object.keys(cur).every((k) => cur[k] === _savedDefault[k]);
|
||||
}
|
||||
|
||||
/* ---- /etc/config/footstrap is written by Save-as-default AND BY NOTHING ELSE -------------------
|
||||
/* ---- no AXIS reaches /etc/config/footstrap except through Save-as-default ----------------------
|
||||
* EVERY axis is per-browser and reaches the router only through this button. Two of them used to
|
||||
* write through the moment they changed — `wallpaper` on every pick, `photo_dim` on every drag —
|
||||
* on the argument that the File photo is router-side, so "which wallpaper shows it" and "how dim"
|
||||
@@ -789,10 +802,11 @@ function resetToSaved() {
|
||||
|
||||
/* The built-in defaults, written through the ordinary appliers so each one validates its own value
|
||||
* and stamps :root the way it always does. Stated here rather than derived: a "default" is only a
|
||||
* default because it is the value a bare :root paints, and the three that already have a named
|
||||
* const (rounding, tint strength, photo dim) use it, so the numbers cannot drift from the CSS. */
|
||||
* default because it is the value a bare :root paints, and the five that already have a named
|
||||
* const (rounding, tint strength, photo dim, pattern size, pattern strength) use it, so the numbers
|
||||
* cannot drift from the CSS. */
|
||||
function resetToBuiltin() {
|
||||
/* TOP, not sidebar: the bar is what a bare :root paints (header.ut stamps it when uci says
|
||||
/* TOP, not sidebar: the bar is what a bare :root paints (head.ut stamps it when uci says
|
||||
* nothing), so it is what "the theme as it ships" means. This said 'sidebar' after the default
|
||||
* flipped and nothing caught it — the button quietly reset to a layout that is no longer the
|
||||
* default, which is the one thing this button must not do. */
|
||||
@@ -955,6 +969,7 @@ function uploadPattern(file) {
|
||||
* as making every other device paint it, which is the wallpaper axis and Save-as-default. */
|
||||
.then(() => _uciSet('footstrap', 'settings', { pattern: tok }))
|
||||
.then(() => _uciCommit('footstrap'))
|
||||
.catch((e) => _rollbackUpload(PAT_PATH, e))
|
||||
.then(() => {
|
||||
/* switch THIS browser onto it — the ordinary axis path, localStorage only */
|
||||
applyWallpaper('pattern');
|
||||
@@ -967,8 +982,7 @@ function uploadPattern(file) {
|
||||
/* Remove: delete the file, blank the token (uci `set` to '', not delete — the scoped ACL grants
|
||||
* set/commit only), clear the tile live. */
|
||||
function removePattern() {
|
||||
return _fileRemove(PAT_PATH)
|
||||
.catch(() => null) /* already gone is success — still blank the token below */
|
||||
return _removeServed(PAT_PATH)
|
||||
.then(() => _uciSet('footstrap', 'settings', { pattern: '' }))
|
||||
.then(() => _uciCommit('footstrap'))
|
||||
.then(() => { _applyPattern(''); });
|
||||
@@ -990,11 +1004,33 @@ const BG_SERVE = '/luci-static/footstrap/bg'; /* the uhttpd symlink to BG_PATH (
|
||||
const BG_MAX_SIDE = 1920; /* cap the longest side — a router serves this off flash with no gzip, and 1080p covers the screens LuCI is actually admin'd from; still crisp full-screen, far fewer flash/wire bytes */
|
||||
const BG_QUALITY = 0.9;
|
||||
const BG_SRC_MAX = 25 * 1024 * 1024; /* refuse a source this big before decoding (decode-bomb guard) */
|
||||
const _fileRemove = rpc.declare({ object: 'file', method: 'remove', params: [ 'path' ], reject: true });
|
||||
/* NO `reject: true` here, unlike every other declare in this file, and that is the whole point: with
|
||||
* it the caller gets an Error whose only account of WHY is a sentence with the ubus code inside it,
|
||||
* so "the file was already gone" and "the router refused to delete it" arrive indistinguishable —
|
||||
* and both callers below used to swallow the rejection whole and report success. Without it the
|
||||
* promise resolves with the ubus status as a NUMBER (rpc.js hands back `msg.result[0]`), which is a
|
||||
* signal this code can actually branch on. */
|
||||
const _fileRemoveStatus = rpc.declare({ object: 'file', method: 'remove', params: [ 'path' ] });
|
||||
|
||||
/* Delete, treating "not found" as done. Anything else is a real refusal — a read-only or full
|
||||
* overlay, an immutable flag, a path replaced by a non-empty directory — and it must NOT be reported
|
||||
* as a removal: the file stays on flash and stays fetchable WITHOUT A SESSION through the /www
|
||||
* symlink, which is precisely what an admin removing a background for privacy reasons believes they
|
||||
* have just stopped. */
|
||||
const UBUS_NOT_FOUND = 4;
|
||||
function _removeServed(path) {
|
||||
return _fileRemoveStatus(path).then((res) => {
|
||||
const code = (typeof res === 'number') ? res : parseInt(res, 10);
|
||||
if (code === 0 || code === UBUS_NOT_FOUND || isNaN(code)) return;
|
||||
return Promise.reject(new Error(
|
||||
_('The router refused to delete the file (ubus status %d).', 'footstrap').format(code)));
|
||||
});
|
||||
}
|
||||
/* cgi-upload writes the file mode 0600, and uhttpd refuses to SERVE a file that is not
|
||||
* world-readable (measured: 0600 -> 403, 0644 -> 200), so make it 0644 before it can be fetched. The
|
||||
* rpcd ACL grants exec on exactly `/bin/chmod 644 /etc/footstrap/login-bg` — one fixed command, no
|
||||
* argument the caller controls. */
|
||||
* rpcd ACL grants exec on exactly two fixed commands — `/bin/chmod 644 /etc/footstrap/login-bg` and
|
||||
* `/bin/chmod 644 /etc/footstrap/pattern.svg`, the two files this module uploads — with no argument
|
||||
* the caller controls. */
|
||||
const _fileExec = rpc.declare({ object: 'file', method: 'exec', params: [ 'command', 'params' ], reject: true });
|
||||
/* …and the ubus status is only half of it: `file.exec` reports the COMMAND's exit status inside the
|
||||
* payload, so a chmod that ran and failed still comes back as a successful call. Unchecked, the
|
||||
@@ -1015,7 +1051,7 @@ function _chmodServeable(path) {
|
||||
const BG_TOKEN_RE = /^[a-f0-9]{6,64}$/;
|
||||
|
||||
/* the token the server last saved (window.__fsSD.login_bg), validated to the same hex charset the
|
||||
* head.ut sanitiser and pre-paint use — so the popover shows the current background and builds a
|
||||
* head.ut sanitiser and pre-paint use — so the Appearance tab shows the current background and builds a
|
||||
* cache-busted preview src. '' = none. */
|
||||
function currentLoginBg() {
|
||||
const t = sd('login_bg');
|
||||
@@ -1041,10 +1077,10 @@ function _applyLoginBg(tok) {
|
||||
* sits in — it escapes as an uncaught error and leaves the promise pending FOREVER. Two real ways
|
||||
* out of `onload`: `getContext('2d')` answers null when the canvas cannot be backed (out of memory
|
||||
* on a low-RAM box is the case this decodes a 25 MB source on), and drawImage/toBlob can throw on
|
||||
* their own. The caller is fs-appearance's file picker, which disables "Choose image" and relabels
|
||||
* their own. The caller is the Appearance tab's file picker, which disables "Choose image" and relabels
|
||||
* it "Uploading…" before the call and restores both in a `.finally()` — so a pending promise means
|
||||
* that button stays disabled and lying for the life of the page. The popover is built ONCE, in
|
||||
* init(), so nothing short of a reload gets it back. */
|
||||
* that button stays disabled and lying until the form is rebuilt, which mount() does only when the
|
||||
* stock view re-renders: the next arrival at System -> System, not this one. */
|
||||
function _downscale(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
@@ -1069,6 +1105,23 @@ function _downscale(file) {
|
||||
});
|
||||
}
|
||||
|
||||
/* An upload that has landed but could not be RECORDED must not stay on the router. The two paths
|
||||
* below write the file first (cgi-upload) and the token second (uci), and the second half can fail
|
||||
* on its own: no `settings` section yet, a narrowed uci ACL, ubus busy. The page then showed the
|
||||
* rpc error — honest as far as it went — while the image sat in /etc/footstrap at mode 0644 and was
|
||||
* served to ANYONE at /luci-static/footstrap/bg, because the /www symlink does not depend on the
|
||||
* token. Worse, Remove is hidden exactly when the token is empty, so the page offered no way to
|
||||
* delete what it had just published. Roll the file back instead, and report the failure that
|
||||
* started it — a rollback that itself fails is appended, because at that point the admin has to
|
||||
* know the file is there. */
|
||||
function _rollbackUpload(path, cause) {
|
||||
return _removeServed(path).then(
|
||||
() => Promise.reject(cause),
|
||||
() => Promise.reject(new Error(String((cause && cause.message) || cause) + ' — '
|
||||
+ _('the uploaded file could not be removed either; it is still on the router.', 'footstrap')))
|
||||
);
|
||||
}
|
||||
|
||||
/* Upload flow: validate -> canvas re-encode -> multipart POST to cgi-io's cgi-upload (the same
|
||||
* endpoint L.ui.uploadFile uses; session carried in the `sessionid` FIELD, path in `filename`, bytes
|
||||
* in `filedata`) -> take the md5 `checksum` from the JSON reply as the cache-bust token -> save it in
|
||||
@@ -1100,6 +1153,7 @@ function uploadLoginBg(file) {
|
||||
* here would re-point every other device's default from one admin's upload, silently. */
|
||||
.then(() => _uciSet('footstrap', 'settings', { login_bg: tok }))
|
||||
.then(() => _uciCommit('footstrap'))
|
||||
.catch((e) => _rollbackUpload(BG_PATH, e))
|
||||
.then(() => {
|
||||
/* switch THIS browser to the photo — the ordinary axis path, localStorage only */
|
||||
applyWallpaper('file');
|
||||
@@ -1112,8 +1166,7 @@ function uploadLoginBg(file) {
|
||||
/* Remove: delete the file, blank the token (uci `set` to '', not delete — the scoped ACL grants
|
||||
* set/commit only), clear the background live. */
|
||||
function removeLoginBg() {
|
||||
return _fileRemove(BG_PATH)
|
||||
.catch(() => null) /* already gone is success — still blank the token below */
|
||||
return _removeServed(BG_PATH)
|
||||
.then(() => _uciSet('footstrap', 'settings', { login_bg: '' }))
|
||||
.then(() => _uciCommit('footstrap'))
|
||||
.then(() => { _applyLoginBg(''); });
|
||||
@@ -1122,7 +1175,7 @@ function removeLoginBg() {
|
||||
|
||||
return baseclass.extend({
|
||||
/* the storage helpers */
|
||||
lsGet, lsSet, lsDel, lsGetArr,
|
||||
lsGet, lsSet, lsDel, lsGetArr, storageBroken,
|
||||
|
||||
currentMode, applyMode, guardDarkStamp,
|
||||
currentPalette, applyPalette,
|
||||
|
||||
@@ -47,9 +47,34 @@ const _viewIntervals = (window.__fsViewIntervals || (window.__fsViewIntervals =
|
||||
};
|
||||
})();
|
||||
function clearViewIntervals() {
|
||||
const keep = (L.Poll && L.Poll.timer) || null;
|
||||
/* `L.Poll.timer` is the id of LuCI's OWN 1 s tick, and it is private state — `add`/`remove`/
|
||||
* `start`/`stop`/`active` are the documented surface, and upstream has already marked the whole
|
||||
* `L.Poll` alias deprecated (`'require poll'` is its replacement, and neither 24.10 nor 25.12
|
||||
* ships poll.js yet, so the alias is still the only way in). If that field ever goes, `keep`
|
||||
* becomes null and this sweep would clear LuCI's tick along with the view's timers — every poll
|
||||
* on every later page silently dead, from a rename we did not notice. So the missing field is
|
||||
* not a null: it is a reason to do NOTHING, once, loudly. A view's leftover interval outliving
|
||||
* its page costs a wasted RPC; killing the global tick costs the router's live data. */
|
||||
/* Asked through the DOCUMENTED half first: active() says whether LuCI's tick is running at all,
|
||||
* and `timer` is deleted by stop() — so an absent field is the ordinary "nothing to protect"
|
||||
* case on a page with no pollers, not a sign that upstream moved it. The anomaly worth reporting
|
||||
* is the pair disagreeing: a tick that is running while the id it runs on has no name we know.
|
||||
* Then do NOTHING, once, loudly: a view's leftover interval outliving its page costs a wasted
|
||||
* RPC, whereas clearing LuCI's own tick costs every live value on every later page. */
|
||||
const running = (typeof L.Poll.active === 'function') ? L.Poll.active() : (L.Poll.timer != null);
|
||||
if (running && L.Poll.timer == null) {
|
||||
if (!_pollTimerWarned) {
|
||||
_pollTimerWarned = true;
|
||||
console.error('footstrap: LuCI is polling but L.Poll.timer is not readable — leaving view '
|
||||
+ 'intervals alone rather than risking its tick. fs-router.js needs updating for this '
|
||||
+ 'luci-base.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const keep = running ? L.Poll.timer : null;
|
||||
_viewIntervals.forEach((id) => { if (id !== keep) window.clearInterval(id); });
|
||||
}
|
||||
let _pollTimerWarned = false;
|
||||
|
||||
let _wired = false;
|
||||
/* The pathname whose view is CURRENTLY rendered — popstate compares against it to tell a real
|
||||
@@ -64,7 +89,7 @@ let _navGen = 0;
|
||||
* The two layouts scroll different elements: the sidebar layout pins .fs-shell to 100dvh and gives
|
||||
* overflow-y to .fs-main (#maincontent), the top layout lets the document scroll. A browser restores
|
||||
* an INNER scrollable region only across full loads, never on a same-document traversal — measured
|
||||
* (docs/spa-router.md §2): Back opened the incoming page at 0, because the swap empties #view,
|
||||
* (docs/spa-router.md, "Scroll"): Back opened the incoming page at 0, because the swap empties #view,
|
||||
* scrollHeight collapses and the browser clamps scrollTop.
|
||||
*
|
||||
* The DOCUMENT scroller was left to the browser's own scrollRestoration ('auto') on the grounds that
|
||||
@@ -116,28 +141,75 @@ function saveScroll() {
|
||||
}
|
||||
|
||||
/* Put the scrollers back where the entry left them — but only once the incoming view has grown that
|
||||
* much height (docs/spa-router.md §5: restoring before the content exists is clamped to 0 and reads as
|
||||
* much height (docs/spa-router.md, "Scroll": restoring before the content exists is clamped to 0 and reads as
|
||||
* "worked"). The view renders behind an RPC, so poll by frame; a newer navigation cancels via the
|
||||
* generation, and a page that never reaches the old height again is simply left at the top. Each
|
||||
* offset is waited for on its OWN scroller, so a layout switched between the two entries restores
|
||||
* whichever half it can rather than blocking on the half that no longer scrolls. */
|
||||
function restoreScroll(pos, gen) {
|
||||
if (!pos || (!pos.win && !pos.main)) return;
|
||||
let tries = 300; /* ~5 s at 60 fps — outlasts a slow RPC without polling forever */
|
||||
/* A DEADLINE, not a frame count: this was 300 frames called "~5 s", which holds only at 60 Hz —
|
||||
* the same budget is 10 s on a 30 Hz panel. Frames stay the tick (they are when a paint could
|
||||
* have changed the height); time decides when to stop waiting for an RPC that is not coming. */
|
||||
const until = Date.now() + 5000;
|
||||
|
||||
/* THE USER OUTRANKS THE SAVED POSITION. Waiting up to five seconds for a slow view to grow means
|
||||
* the reader may have started using the page in the meantime — and jumping them somewhere else
|
||||
* two seconds after they began reading is worse than opening at the top, which is what a full
|
||||
* load does anyway. So any sign that the scroll is THEIRS cancels the restore for good.
|
||||
*
|
||||
* Two kinds of sign, because neither covers the other. The three input events are intent even
|
||||
* when nothing moves yet (a wheel tick on a page too short to scroll, a touch that becomes a
|
||||
* drag); `scroll` is the catch-all for everything they cannot see — a scrollbar drag, a
|
||||
* trackpad fling, Find-in-page, an anchor jump, assistive tech. `scroll` also fires for OUR
|
||||
* OWN writes, asynchronously, so a flag around the write would still be false by the time it
|
||||
* arrives: the position we last wrote is remembered instead, and a scroll that lands exactly
|
||||
* there is ours. Landing there by hand is indistinguishable and costs nothing — it is the
|
||||
* position we were restoring to.
|
||||
*
|
||||
* Passive listeners: this must never sit in front of the scroll it is watching for. */
|
||||
let cancelled = false, wroteWin = -1, wroteMain = -1;
|
||||
const stop = () => { cancelled = true; off(); };
|
||||
const onScroll = (ev) => {
|
||||
const t = ev.target;
|
||||
const now = (t === document || t === document.documentElement || t === document.body)
|
||||
? Math.round(window.scrollY) : (t && t.scrollTop);
|
||||
if (now === wroteWin || now === wroteMain) return; /* our own write coming back */
|
||||
stop();
|
||||
};
|
||||
/* the keys that scroll, and only those: typing in a field must not cancel anything */
|
||||
const SCROLL_KEYS = new Set([ 'PageUp', 'PageDown', 'Home', 'End', 'ArrowUp', 'ArrowDown', ' ', 'Spacebar' ]);
|
||||
const onKey = (ev) => { if (SCROLL_KEYS.has(ev.key)) stop(); };
|
||||
const opts = { passive: true, capture: true };
|
||||
function off() {
|
||||
window.removeEventListener('wheel', stop, opts);
|
||||
window.removeEventListener('touchstart', stop, opts);
|
||||
window.removeEventListener('keydown', onKey, opts);
|
||||
window.removeEventListener('scroll', onScroll, opts);
|
||||
}
|
||||
window.addEventListener('wheel', stop, opts);
|
||||
window.addEventListener('touchstart', stop, opts);
|
||||
window.addEventListener('keydown', onKey, opts);
|
||||
/* capture, so the inner scroller (#maincontent in the sidebar layout) is seen too: `scroll`
|
||||
* does not bubble from an element, but it does travel down the capture phase. */
|
||||
window.addEventListener('scroll', onScroll, opts);
|
||||
|
||||
(function tick() {
|
||||
if (gen !== _navGen || --tries < 0) return;
|
||||
if (cancelled) return;
|
||||
if (gen !== _navGen || Date.now() > until) { off(); return; }
|
||||
const de = document.documentElement;
|
||||
const sc = document.getElementById('maincontent');
|
||||
let pending = false;
|
||||
if (pos.main) {
|
||||
if (sc && sc.scrollHeight - sc.clientHeight >= pos.main) sc.scrollTop = pos.main;
|
||||
if (sc && sc.scrollHeight - sc.clientHeight >= pos.main) { wroteMain = pos.main; sc.scrollTop = pos.main; }
|
||||
else pending = true;
|
||||
}
|
||||
if (pos.win) {
|
||||
if (de.scrollHeight - de.clientHeight >= pos.win) window.scrollTo(0, pos.win);
|
||||
if (de.scrollHeight - de.clientHeight >= pos.win) { wroteWin = pos.win; window.scrollTo(0, pos.win); }
|
||||
else pending = true;
|
||||
}
|
||||
if (pending) requestAnimationFrame(tick);
|
||||
else off();
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -276,38 +348,43 @@ function pragmaDeps(src) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/* SIX CLASS NAMES HAVE NO FILE, and fetching one is a guaranteed 404 in the user's console. luci.js
|
||||
* seeds its class registry with them at load — `const classes = { baseclass: Class, dom: DOM,
|
||||
* poll: Poll, request: Request, session: Session, view: View }` — so require() answers them from
|
||||
* memory and never asks the network, while `ls /www/luci-static/resources` has no baseclass.js,
|
||||
* dom.js, poll.js, request.js or session.js at all. Every view file's pragmas name `view` and
|
||||
* `baseclass`, so a dependency walk hits this on its very first step: measured, warming the recents
|
||||
* at idle put 404s for view.js and poll.js in the console of every page load, and the first hover
|
||||
* added baseclass.js. A require() that 404s is noise this theme refuses to make, and the same
|
||||
* standard applies here.
|
||||
/* WHICH CLASS NAMES ARE WORTH A PREFETCH, and the answer is the DOTTED ones — that is the whole
|
||||
* rule now, and it used to be a hardcoded list of the six names luci.js seeds its registry with
|
||||
* (`baseclass`, `dom`, `poll`, `request`, `session`, `view`). Those have no file, so fetching one is
|
||||
* a guaranteed 404 in the user's console — measured before the list existed: warming the recents at
|
||||
* idle put 404s for view.js and poll.js into every page load, and the first hover added baseclass.js.
|
||||
* The list worked, and it was a literal copied out of luci.js rather than a guess about it, but it
|
||||
* was still a copy: a seventh built-in would cost one 404 per name per session until this file
|
||||
* caught up with a release of somebody else's software.
|
||||
*
|
||||
* A future LuCI adding a seventh built-in would cost one 404 per session — `_prefetched` makes it
|
||||
* at most one — and the list is a literal from luci.js, not a guess about it. */
|
||||
const BUILTIN_CLASSES = new Set([ 'baseclass', 'dom', 'poll', 'request', 'session', 'view' ]);
|
||||
|
||||
/* Is this class already instantiated? require() attaches its singleton to the LuCI prototype
|
||||
* (`ptr[parts[idx]] = instance`), so a SINGLE-segment name reads back as L[name] — which covers the
|
||||
* libs a loaded page has already pulled in (ui, form, uci, rpc, fs, validation, and network/firewall
|
||||
* once some network page has been open). Skipping them spends no request on a cache hit nobody needs.
|
||||
* `instanceof L.Class` rather than a truthiness test: L.env, L.url and L.get are members too, and a
|
||||
* dep whose name collided with one of those would otherwise be skipped without ever being loaded —
|
||||
* which is why this cannot be the guard for the six above either: they are seeded as CONSTRUCTORS
|
||||
* (and under other names — L.Poll, L.Request, L.Class), so no `instanceof` probe sees them. A DOTTED
|
||||
* name (tools.network) is not attached unless its parent already exists, so it is simply fetched —
|
||||
* and those are the ones the win comes from. */
|
||||
* The shape of the names answers it without the copy. A LuCI class name is a path — `tools.widgets`
|
||||
* is tools/widgets.js — so a name with no dot is either one of those six virtual classes or one of
|
||||
* the flat libraries (`ui`, `form`, `network`, `uci`, `rpc`, `fs`, `validation`), and those are
|
||||
* already loaded by the time any prefetch runs: this theme's own chrome requires `network` for the
|
||||
* overview grid, which drags in firewall/uci/rpc/validation, and `ui` comes with the widgets.
|
||||
* Measured on the stand from three different landing pages, including the lightest one there is
|
||||
* (System -> Reboot): all eight flat libraries were already instances on arrival, and driving the
|
||||
* prefetch walk over seven pages fetched 10 files, every one of them nested. So the flat half of the
|
||||
* namespace is worth nothing to prefetch and can never be worth a 404 — declining it outright costs
|
||||
* nothing measurable, needs no list, and a future built-in is covered before it ships.
|
||||
*
|
||||
* The dotted half is still asked properly: require() attaches a class at its path (`ptr[parts[i]] =
|
||||
* instance`), so `tools.widgets` reads back as L.tools.widgets once some form page has pulled it,
|
||||
* and a second navigation there spends no request at all. `instanceof L.Class` rather than a
|
||||
* truthiness test: L.env, L.url and L.get are members too. */
|
||||
function classLoaded(name) {
|
||||
if (BUILTIN_CLASSES.has(name)) return true;
|
||||
try { return name.indexOf('.') < 0 && window.L[name] instanceof window.L.Class; }
|
||||
if (name.indexOf('.') < 0) return true;
|
||||
try {
|
||||
let ptr = window.L;
|
||||
for (const part of name.split('.')) {
|
||||
ptr = ptr[part];
|
||||
if (ptr == null) return false;
|
||||
}
|
||||
return ptr instanceof window.L.Class;
|
||||
}
|
||||
catch (e) { return false; }
|
||||
}
|
||||
|
||||
/* view classes already required, i.e. the ones LuCI has an instance cached for. A class NOT in
|
||||
* here is rendered by the require() itself (see navigate). */
|
||||
const _seen = new Set();
|
||||
const _prefetched = new Set();
|
||||
/* className -> the promise of ITS OWN body being in the HTTP cache; navigate() waits on that.
|
||||
@@ -490,7 +567,7 @@ function navigate(pathname, push, kbd) {
|
||||
* when View.__init__ replaces it there is nothing to see, and the string arrives already
|
||||
* translated in the ~40 languages this theme ships no catalogue for (the chrome rule — a
|
||||
* context-free msgid is how we inherit luci-base's translation). */
|
||||
if (!_seen.has(tree.viewClassFor(node))) {
|
||||
if (!_seen.has(className)) {
|
||||
const vp = document.getElementById('view');
|
||||
if (vp) {
|
||||
const s = document.createElement('div');
|
||||
@@ -541,7 +618,11 @@ function navigate(pathname, push, kbd) {
|
||||
try { fn(rsegs); }
|
||||
catch (e) { console.error('footstrap: a navigation callback threw', e); }
|
||||
}
|
||||
try { if (typeof ui.hideModal === 'function') ui.hideModal(); } catch (e) {}
|
||||
/* ui hard-requires into this module and ui.js defines hideModal unconditionally, so there is no
|
||||
* feature to test; what is caught is a modal's own teardown throwing, which must not take the
|
||||
* navigation with it. Reported, not swallowed — the same rule as the loop above. */
|
||||
try { ui.hideModal(); }
|
||||
catch (e) { console.error('footstrap: hideModal threw during a navigation', e); }
|
||||
|
||||
/* point the runtime env at the new node so views, tabs and highlighting read the right
|
||||
* path. For a fully-matched leaf, request == dispatch path. */
|
||||
@@ -579,8 +660,8 @@ function navigate(pathname, push, kbd) {
|
||||
* correctly returns — one dead Back press per stray click. A full load has no such trap. */
|
||||
if (push) {
|
||||
const same = pathname === window.location.pathname;
|
||||
if (_curId == null) _curId = newEntryId();
|
||||
/* a NEW entry gets a new id; re-navigating in place keeps the entry and therefore its id */
|
||||
/* a NEW entry gets a new id; re-navigating in place keeps the entry and therefore its id
|
||||
* (seed() adopted one before wire() made this function reachable, so there is always one) */
|
||||
if (!same) _curId = newEntryId();
|
||||
history[same ? 'replaceState' : 'pushState']({ fsnav: true, fsid: _curId }, '', pathname);
|
||||
}
|
||||
@@ -601,7 +682,7 @@ function navigate(pathname, push, kbd) {
|
||||
*
|
||||
* A popstate replay resets nothing on purpose: BOTH scrollers are restored there from _scrollMem —
|
||||
* see restoreScroll(). scrollRestoration is left at 'auto': the UA's own attempt lands before the
|
||||
* swap and is undone by it (§2), so it neither helps nor hurts, and 'manual' would only take away
|
||||
* swap and is undone by it (see restoreScroll above), so it neither helps nor hurts, and 'manual' would only take away
|
||||
* the case that does work — a genuine full load. */
|
||||
if (push) {
|
||||
window.scrollTo(0, 0);
|
||||
@@ -613,7 +694,8 @@ function navigate(pathname, push, kbd) {
|
||||
* renderChrome() has just done `#topmenu.innerHTML = ''`, so the very <a> the user activated with
|
||||
* Enter no longer exists: focus falls back to <body>, the next Tab restarts at the skip link, and
|
||||
* nothing says the page changed — URL, title and #view all moved in silence. So do what a real
|
||||
* navigation would, and where matters (Sutton's five-prototype study, docs/spa-router.md §3): a KEYBOARD
|
||||
* navigation would, and where matters (Sutton's five-prototype study, docs/spa-router.md,
|
||||
* "Accessibility of a route change"): a KEYBOARD
|
||||
* activation (ev.detail === 0) moves focus to the skip link — a small target whose :focus overlay
|
||||
* tells a sighted keyboard user where they are, with Enter jumping straight to the content; its
|
||||
* text differs from the live region's announcement below, so the double announcement complements
|
||||
@@ -664,7 +746,15 @@ function navigate(pathname, push, kbd) {
|
||||
* repairStaleRender() a mess that only exists because a require in flight cannot be stopped. */
|
||||
if (gen !== _navGen) return null;
|
||||
_seen.add(className);
|
||||
return RT.require(className);
|
||||
/* NAME THE OWNER for the length of this require. On a first visit the require IS the render,
|
||||
* so any <style> the module injects belongs to THIS page — even if a newer navigation has
|
||||
* stamped data-page by the time it lands, which is exactly the case repairStaleRender() below
|
||||
* exists for. Without it fs-sheets credited such a sheet to the page that superseded this one
|
||||
* and bound it there for the life of the document (measured: luci-app-filemanager's
|
||||
* `.cbi-button-save { display: none !important }` disabled on its own page and live on
|
||||
* System -> System, across return visits). */
|
||||
sheets.attributeTo(rsegs);
|
||||
return RT.require(className).finally(() => sheets.attributeTo(null));
|
||||
}).then((view) => {
|
||||
if (view == null) return;
|
||||
if (!(view instanceof RT.view))
|
||||
@@ -818,7 +908,8 @@ function wireRouter() {
|
||||
* take back what that one just painted. */
|
||||
document.addEventListener('poll-stop', () => {
|
||||
if (L.Poll && L.Poll.queue && L.Poll.queue.length === 0) {
|
||||
try { ui.hideIndicator('poll-status'); } catch (e) {}
|
||||
try { ui.hideIndicator('poll-status'); }
|
||||
catch (e) { console.error('footstrap: hideIndicator threw on poll-stop', e); }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -844,7 +935,7 @@ function wireVisibility() {
|
||||
else if (wasActive) {
|
||||
L.Poll.start();
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) { console.error('footstrap: the poll pause/resume threw', e); }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,11 @@
|
||||
|
||||
/* ---- the index ---------------------------------------------------------- */
|
||||
|
||||
/* admin/<section>/<page>/<tab> — four levels is every path LuCI's dispatcher renders. */
|
||||
/* How deep below the MODE the walk goes. `depth` counts recursion levels and the walk starts at 1
|
||||
* on a node that already has two segments, so a node at depth N carries N+1 of them: 4 admits
|
||||
* admin/<section>/<page>/<tab>/<subtab>. That is one more than LuCI's own dispatcher renders, and
|
||||
* deliberately so — a third-party node nested deeper is worth finding, and the depth term in the
|
||||
* score below already ranks it last. */
|
||||
const MAX_DEPTH = 4;
|
||||
|
||||
let _index = null;
|
||||
@@ -99,6 +103,13 @@ function buildIndex() {
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Built once per DOCUMENT, and deliberately not re-derived per open: it is a projection of the
|
||||
* client menu tree, which `ui.menu.load()` itself caches for the life of the document. So the
|
||||
* palette is exactly as fresh as the sidebar beside it — a package installed or removed without a
|
||||
* reload is missing from, or still listed in, BOTH. Invalidating only this half would make the two
|
||||
* disagree, which is worse than either being stale; the reload the package manager already prompts
|
||||
* for is what refreshes them together. (Cheap enough to rebuild if that ever changes: 238 nodes on
|
||||
* the dev router.) */
|
||||
function index() {
|
||||
if (!_index) _index = buildIndex();
|
||||
return _index;
|
||||
@@ -190,9 +201,9 @@ function warmRecent() {
|
||||
const paths = _recent.filter((p) => p !== here).slice(0, RECENT_WARM);
|
||||
if (!paths.length) return;
|
||||
/* Nothing waits on this, so it belongs after the page has settled — at idle, with a timeout for a
|
||||
* page that never goes idle (a busy poll). The fallback is deliberately a LONG timeout, unlike
|
||||
* fs-appearance's ~1 ms one: that wires a button the user may click immediately, this competes
|
||||
* with the view's own module fetches and RPCs and must lose that race on purpose. */
|
||||
* page that never goes idle (a busy poll). The fallback is deliberately a LONG timeout: nothing
|
||||
* on screen waits for this, so it competes with the view's own module fetches and RPCs and must
|
||||
* lose that race on purpose. */
|
||||
const go = () => paths.forEach((p) => router.prefetchSegs(p.split('/')));
|
||||
if (typeof window.requestIdleCallback === 'function')
|
||||
window.requestIdleCallback(go, { timeout: 4000 });
|
||||
@@ -379,8 +390,7 @@ function wire() {
|
||||
/* Back and Forward are navigations no listener here can see: every close above is a user act on
|
||||
* the document (Escape, the scrim, the trigger, picking a result). An open palette therefore rode
|
||||
* a popstate onto the next page, aria-modal and Tab-trapped, while the router moved focus behind
|
||||
* it. Same reasoning as the Appearance popover's — see fs-appearance.js. returnFocus=false because
|
||||
* the router places focus itself on a navigation. */
|
||||
* it. returnFocus=false because the router places focus itself on a navigation. */
|
||||
router.onNavigate(() => close(false));
|
||||
|
||||
/* Ctrl/Cmd+K is the shortcut every command palette has taught users, and `/` is the one every
|
||||
|
||||
@@ -87,7 +87,7 @@ function enhance(sel) {
|
||||
if (sel.dataset.fsSelect || sel.disabled) return; /* disabled: NOT marked — it may be enabled later */
|
||||
/* `multiple` and "not in a CBI field" are permanent, so mark it and stop re-testing on
|
||||
* every scan */
|
||||
if (sel.multiple || !sel.closest('.cbi-value-field, .td.cbi-value-field, .cbi-value')) {
|
||||
if (sel.multiple || !sel.closest('.cbi-value-field, .cbi-value')) {
|
||||
sel.dataset.fsSelect = 'skip';
|
||||
return;
|
||||
}
|
||||
@@ -100,7 +100,14 @@ function enhance(sel) {
|
||||
sort: false,
|
||||
optional: Object.prototype.hasOwnProperty.call(choices, '')
|
||||
});
|
||||
} catch (e) { return; }
|
||||
} catch (e) {
|
||||
/* Marked, not merely returned from: without the mark the same select is re-selected by
|
||||
* scan()'s :not([data-fs-select]) on every mutation frame and throws again, forever and
|
||||
* silently. One loud failure, then left as the stock <select> it already is. */
|
||||
sel.dataset.fsSelect = 'skip';
|
||||
console.error('footstrap: a select could not be enhanced', e);
|
||||
return;
|
||||
}
|
||||
|
||||
const node = dd.render();
|
||||
const ac = new AbortController();
|
||||
@@ -210,12 +217,19 @@ function labelCells(t, head) {
|
||||
for (const row of t.querySelectorAll('.tr, tbody tr')) {
|
||||
if (row === head) continue;
|
||||
const cells = row.children;
|
||||
/* COLUMN cursor, not the cell index: a cell that spans N columns occupies N of the header's
|
||||
* slots while advancing the cell index by one, so keying titles off `i` captioned every cell
|
||||
* AFTER a spanning one with the heading of the column to its left — "Hostname" over an IP
|
||||
* address, and nothing to say the mapping was guessed. Only the spanning cell itself is left
|
||||
* uncaptioned, because it has no single heading to take. (A rowspan reaching down from an
|
||||
* earlier row would shift this too; no LuCI table emits one, and a wrong caption is worse
|
||||
* than none, so that shape stays unhandled rather than approximated.) */
|
||||
let col = 0;
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
if (i >= titles.length || !titles[i]) continue;
|
||||
if (cells[i].hasAttribute('data-title')) continue;
|
||||
/* a cell that spans columns has no single heading to take */
|
||||
if (cells[i].colSpan > 1) continue;
|
||||
cells[i].setAttribute('data-title', titles[i]);
|
||||
const span = (cells[i].colSpan > 1) ? cells[i].colSpan : 1;
|
||||
if (span === 1 && col < titles.length && titles[col] && !cells[i].hasAttribute('data-title'))
|
||||
cells[i].setAttribute('data-title', titles[col]);
|
||||
col += span;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,8 +257,11 @@ const STACKABLE = '#view .table.fs-dt';
|
||||
* unreadable ribbon. Do NOT give the cells a min-width so that "cramped" MANUFACTURES an
|
||||
* overflow: tried, and it carded the firewall's zone table at 1420px and still overflowed by
|
||||
* 39px once carded — a floor big enough to force the overflow is big enough to break the card. */
|
||||
const CRAMPED = 568; /* stock LuCI cards its tables at a 600px viewport; below the 767px
|
||||
* tier .fs-content pads 16px a side, so 600 -> 568 of room */
|
||||
const CRAMPED = 568; /* stock LuCI cards its tables at a 600px viewport; below the 767px tier
|
||||
* .fs-content pads var(--fs-space-4) a side, 16px at the default density,
|
||||
* so 600 -> 568 of room. A fixed number and not a re-read of that token on
|
||||
* purpose: the threshold is the DESIGN judgement above, and Compact density
|
||||
* shrinking the gutter to 10px is not a reason to keep a table wider. */
|
||||
|
||||
/* The ribbon has one more shape, and CRAMPED cannot see it: the table has room by the number
|
||||
* above and still shreds its FIRST column, because auto table layout hands width out by what
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
*
|
||||
* `invasiveSheet()` is that test; its universe is read back from cascade.css itself (same-origin,
|
||||
* so `cssRules` is readable) rather than a hand-written list, so it tracks the theme. 0.3 ms per
|
||||
* nav. Exempt: `[data-fs-shell]` (the one <style> the server emits — marked, not guessed at) and
|
||||
* nav. Exempt: anything the server marked `[data-fs-shell]` — the shell's own two links and two
|
||||
* styles, marked rather than guessed at (partials/head.ut) — and
|
||||
* anything inside `#view` (dies with the content swap); LuCI core injects no <style> at runtime at
|
||||
* all (checked: luci.js, ui.js, cbi.js). If cascade.css cannot be read, EVERY view sheet counts as
|
||||
* invasive: fail to the slow path, never the broken one. */
|
||||
@@ -328,7 +329,8 @@ function outlivesPage(el) {
|
||||
* is real.
|
||||
*
|
||||
* WHOLE PATH, NOT A SUFFIX. head.ut prints `{{ resource }}/{{ dispatched.css }}?v=…`, and the base in
|
||||
* that line is the SAME value the runtime holds: header.ut hands `resource` to `new LuCI({…})`, and
|
||||
* that line is the SAME value the runtime holds: luci-base's own header.ut hands `resource` to
|
||||
* `new LuCI({…})`, and
|
||||
* `L.resource()` joins it back exactly, so the server's href is reconstructable rather than guessable
|
||||
* — only the cache key has to come off. A suffix match is what a guess costs: anchored at nothing but
|
||||
* a `/`, `custom.css` matches any sheet ending in that filename, and two in-tree apps append exactly
|
||||
@@ -339,9 +341,13 @@ function outlivesPage(el) {
|
||||
* through Adblock → Feeds, and the router would swap into a page whose stylesheet was never linked —
|
||||
* the one outcome the guard exists to prevent.
|
||||
*
|
||||
* Equality also keeps the failure safe: L.path() drops a part that leaves its charset, so a malformed
|
||||
* `css` yields the bare base and matches no href at all — a full load, which is the correct answer for
|
||||
* a value nobody can serve. */
|
||||
* Equality also keeps the failure safe, in the direction that costs only speed. L.path() keeps a part
|
||||
* only if it matches /^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/ — no `+ ~ ( ) @ ! ' $ &`, nothing
|
||||
* non-ASCII, no leading `/` — and drops it otherwise, while head.ut interpolates `css` raw and uhttpd
|
||||
* serves those names without complaint. So a third-party `"css": "view/foo/style(dark).css"` does get
|
||||
* linked and rendered; `want` collapses to the bare base, matches no href, and every entry into that
|
||||
* page is a full load. A suffix compare covered those names, and losing them is what closing the false
|
||||
* positive above costs. No in-tree node sets `css` at all, so the shape is third-party only. */
|
||||
function documentCarries(path) {
|
||||
const want = L.resource(String(path));
|
||||
for (const link of document.querySelectorAll('link[rel~="stylesheet"][href]')) {
|
||||
@@ -456,8 +462,14 @@ function documentPoisoned() {
|
||||
const CHROME_FENCE = ':where(:not([data-fs-chrome],[data-fs-chrome] *))';
|
||||
|
||||
function fenceSelector(part) {
|
||||
/* the getter always normalises a pseudo-element to `::`, incl. legacy `:before` */
|
||||
const i = part.indexOf('::');
|
||||
/* The getter always normalises a pseudo-element to `::`, incl. legacy `:before` — and the split
|
||||
* point is found on the MASK and sliced out of the ORIGINAL, exactly as selectorParts() does and
|
||||
* for the same reason: `::` is legal inside a quoted attribute VALUE (an IPv6 literal is the real
|
||||
* case — `[data-addr*="::"]`, `a[href*="[::1]"]`). Read raw, the fence went INSIDE the quotes and
|
||||
* the app's rule came back matching a value it never wrote — still valid CSS, so the setter
|
||||
* reported success and nothing looked wrong afterwards, which this file's header calls the worse
|
||||
* half of the two failures. */
|
||||
const i = maskStrings(part).indexOf('::');
|
||||
return i < 0 ? part + CHROME_FENCE : part.slice(0, i) + CHROME_FENCE + part.slice(i);
|
||||
}
|
||||
|
||||
@@ -481,8 +493,14 @@ function fenceRules(rules, names) {
|
||||
|
||||
/* An @import's rules live in a sheet that is fetched separately, so they are not there the moment
|
||||
* the shim is inserted — retry until they are, then fence. Bounded: a sheet that never becomes
|
||||
* readable (404, cross-origin) simply stays unfenced, which is where we already were. */
|
||||
function fenceImported(styleEl, names, tries) {
|
||||
* readable (404, cross-origin) simply stays unfenced, which is where we already were.
|
||||
*
|
||||
* The bound is a DEADLINE in ms, not a frame count. It was 60 frames described as "~1 s", which is
|
||||
* only true at 60 Hz: the same 60 frames is 2 s on a 30 Hz panel and longer still on a throttled
|
||||
* clock, so the budget moved with the display. Frames remain the retry TICK (they are what the
|
||||
* browser gives for free and a cache hit lands on the first one); only the giving-up point is
|
||||
* measured in time. */
|
||||
function fenceImported(styleEl, names, until) {
|
||||
/* no initialiser: every path below assigns (the try, or the catch's null), so `= null`
|
||||
* here was a dead store — eslint 10 puts no-useless-assignment in recommended and said so. */
|
||||
let rules;
|
||||
@@ -491,7 +509,15 @@ function fenceImported(styleEl, names, tries) {
|
||||
rules = first && first.styleSheet && first.styleSheet.cssRules;
|
||||
} catch (e) { rules = null; }
|
||||
if (rules) { fenceRules(rules, names); return; }
|
||||
if (tries > 0) requestAnimationFrame(() => fenceImported(styleEl, names, tries - 1));
|
||||
if (Date.now() < until) { requestAnimationFrame(() => fenceImported(styleEl, names, until)); return; }
|
||||
/* GIVING UP IS A REPORTABLE EVENT. The irreversible half already happened — the app's original
|
||||
* is silenced, the shim owns the page — so a fence that never lands leaves that app's rules
|
||||
* reaching the chrome while every later pass skips the sheet as handled and documentPoisoned()
|
||||
* calls the document clean. It is also not only the slow-router case: rAF does not fire in a
|
||||
* background tab, so a page opened in one and left there passes the deadline without a single
|
||||
* retry. Rare, silent and it looks like a CSS bug, which is exactly what a console line is for. */
|
||||
console.error('footstrap: could not read the re-hosted @import within the deadline — the sheet '
|
||||
+ 'stays unfenced and may repaint the chrome on this page.', styleEl);
|
||||
}
|
||||
|
||||
/* What a sheet IS, as text: the rules that are APPLYING, not the markup that may or may not have
|
||||
@@ -553,11 +579,7 @@ const _silenced = new WeakSet();
|
||||
*/
|
||||
function silence(el) {
|
||||
_silenced.add(el);
|
||||
el.disabled = true;
|
||||
if (el.sheet) { el.sheet.disabled = true; return; }
|
||||
/* no sheet yet: re-assert once there is one. `once` — the element is marked fsLayered, so this
|
||||
* never re-arms, and a sheet that never loads has nothing to silence. */
|
||||
el.addEventListener('load', () => { if (el.sheet) el.sheet.disabled = true; }, { once: true });
|
||||
setEnabled(el, false);
|
||||
}
|
||||
|
||||
/* ---- PAGE OWNERSHIP: contain an invasive sheet instead of spending the document ----
|
||||
@@ -590,7 +612,7 @@ function silence(el) {
|
||||
* Disabling is reversible, which is the whole difference.
|
||||
*
|
||||
* OWNER = body[data-page] when the sheet was re-hosted. The order that makes this sound is in
|
||||
* fs-router.js: it stamps data-page (line ~371) BEFORE require()ing the view class (~450), so at the
|
||||
* fs-router.js: navigate() stamps data-page BEFORE it require()s the view class, so at the
|
||||
* moment a view module evaluates and appends its <style>, the attribute already names ITS page. On a
|
||||
* full load the server stamped it. Either way "now" is the sheet's own page.
|
||||
*
|
||||
@@ -643,6 +665,28 @@ function appKey(segs) {
|
||||
* /admin/status renders the card as `inline-block 240px 96px`; System -> General and back leaves
|
||||
* it `block 966px` — every interface card on the Overview stacked full-width. Keyed on the
|
||||
* dispatch path: 240px before and after. */
|
||||
/* WHO A SHEET INJECTED RIGHT NOW BELONGS TO, when that is not the page the chrome is showing.
|
||||
*
|
||||
* On a FIRST visit the require() of a view IS its render, and a require in flight cannot be
|
||||
* stopped: click a page whose module injects CSS, click away before it lands, and the app's <style>
|
||||
* appears after the router has already stamped data-page for the page that superseded it. Credited
|
||||
* to currentKey() that sheet is bound to the wrong page for the life of the document — disabled on
|
||||
* its own page and ENABLED on one it has no business painting, which for luci-app-filemanager means
|
||||
* `.cbi-button-save { display: none !important }` on somebody else's config form (reproduced on the
|
||||
* stand: the System page came back with no Save button, and stayed that way across return visits).
|
||||
*
|
||||
* So the router names the owner for the duration of such a require (see fs-router.js), and this is
|
||||
* that hint. It is a plain value rather than a stack: two cold requires cannot overlap, because the
|
||||
* second navigation supersedes the first before it starts one. */
|
||||
let _ownerHint = null;
|
||||
function attributeTo(segs) {
|
||||
_ownerHint = (segs == null) ? null : appKey(segs);
|
||||
}
|
||||
|
||||
function ownerKey() {
|
||||
return (_ownerHint !== null) ? _ownerHint : currentKey();
|
||||
}
|
||||
|
||||
function currentKey() {
|
||||
if (_curKey !== null) return _curKey;
|
||||
const dp = L.env && L.env.dispatchpath;
|
||||
@@ -652,9 +696,14 @@ function currentKey() {
|
||||
return appKey(p ? p.split('/') : []);
|
||||
}
|
||||
|
||||
/* Both halves, for the reason silence() documents: el.disabled is the ELEMENT's flag and
|
||||
* el.sheet.disabled is what decides whether the CSS paints, and a still-loading <link> has no
|
||||
* .sheet for the assignment to reach. */
|
||||
/* BOTH HALVES, for the reason the paragraph above silence() measured: el.disabled is the ELEMENT's
|
||||
* flag and el.sheet.disabled is what decides whether the CSS paints, and a still-loading <link> has
|
||||
* no .sheet for the assignment to reach — so a switch-off also re-asserts once the bytes arrive.
|
||||
* `once` — the element is marked fsLayered by then, so this never re-arms, and a sheet that never
|
||||
* loads has nothing to silence.
|
||||
*
|
||||
* silence() is this with the element remembered, and says so by calling it: the two used to write
|
||||
* the rule out separately, which is one copy of a JUDGEMENT more than this file allows itself. */
|
||||
function setEnabled(el, on) {
|
||||
el.disabled = !on;
|
||||
if (el.sheet) el.sheet.disabled = !on;
|
||||
@@ -686,8 +735,8 @@ function rehostIntoThemeLayer(el, universe) {
|
||||
el.dataset.fsLayered = '1';
|
||||
el.after(s); /* keep source order: ties inside the layer still resolve as they did */
|
||||
silence(el);
|
||||
_owner.set(s, currentKey()); /* the shim paints; the original is silenced for good */
|
||||
fenceImported(s, universe.names, 60); /* ~1s of frames; a cache hit lands on the first */
|
||||
_owner.set(s, ownerKey()); /* the shim paints; the original is silenced for good */
|
||||
fenceImported(s, universe.names, Date.now() + 1000); /* a cache hit lands on the first frame */
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -703,7 +752,7 @@ function rehostIntoThemeLayer(el, universe) {
|
||||
* unpinned all over again and a second pass appends a second fence. The mark is the only thing
|
||||
* that says the work is done, so it has to be set for every path below, wrapped or not. */
|
||||
el.dataset.fsLayered = '1';
|
||||
_owner.set(el, currentKey()); /* a <style> is re-hosted IN PLACE, so it paints itself */
|
||||
_owner.set(el, ownerKey()); /* a <style> is re-hosted IN PLACE, so it paints itself */
|
||||
|
||||
/* Wrap only if the text still IS the sheet (see textIsSheet). When it is not, the sheet stays
|
||||
* unlayered — Zone 2 exactly where it already was, which is a trade — rather than lose rules,
|
||||
@@ -887,6 +936,7 @@ function watchViewSheets() {
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
attributeTo,
|
||||
documentCarries,
|
||||
documentPoisoned,
|
||||
scopeToCurrentPage,
|
||||
|
||||
@@ -34,6 +34,6 @@ return baseclass.extend({
|
||||
* API paths from, and nothing has needed the bare `owner/name` since. */
|
||||
REPO_URL: 'https://github.com/VizzleTF/luci-theme-footstrap',
|
||||
isReal,
|
||||
/* what the popover's version row prints */
|
||||
/* what the Appearance tab's version row prints */
|
||||
label: () => (isReal() ? ('Footstrap v' + FS_VERSION) : 'Footstrap (dev)')
|
||||
});
|
||||
@@ -1,11 +1,15 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs-prefs as prefs';
|
||||
|
||||
/* The theme's own UI primitives: the disclosure pair the menu is built on, the two controls the
|
||||
* Appearance popover is built on, and the popup placement both hand-placed popups share. Nothing
|
||||
* here knows what it is being used FOR — that is the point, and it is why the menu and the popover
|
||||
* can each take what they need without either requiring the other. */
|
||||
/* The theme's own UI primitives: the one inline-SVG wrapper, the disclosure pair the menu is built
|
||||
* on, and the colour control the Appearance tab's nine colour axes are built on. Nothing here knows
|
||||
* what it is being used FOR — that is the point, and it is why the menu and the Appearance tab can
|
||||
* each take what they need without either requiring the other.
|
||||
*
|
||||
* A popup PLACEMENT helper lived here too, shared by the menu's dropdown clamp and the Appearance
|
||||
* panel when that was a floating popover on <body>. The panel is a tab on System -> System now
|
||||
* (fs-appearance.js) and places nothing, so the helper had one caller left and went with it; the
|
||||
* edge gap it clamped against belongs to the menu, which is the only thing still clamping. */
|
||||
|
||||
/* The chrome's ONE inline-SVG wrapper. Every icon this theme draws is the same 24x24 stroked
|
||||
* outline and differs only in its path data, but the wrapper was written out per call site — in the
|
||||
@@ -112,15 +116,47 @@ function probeColor(expr) {
|
||||
return getComputedStyle(_probe).color;
|
||||
}
|
||||
|
||||
/* A computed colour string -> [r,g,b] 0..255, or null. Engines do not agree on the notation: both
|
||||
* `rgb(9, 105, 218)` and the space-separated `rgb(9 105 218 / .5)` are current, and a colour outside
|
||||
* sRGB (which oklch() readily produces) comes back from some engines as `color(srgb .03 .41 .85)`,
|
||||
* i.e. 0..1 floats. Take the first three numbers and scale by whether the form is the 0..1 one —
|
||||
* the alpha after the slash is deliberately ignored, since every token this reads is opaque. */
|
||||
/* A computed colour -> [r,g,b] 0..255, or null. RASTERISED, never parsed out of the string, and that
|
||||
* is not a preference: a computed `color` keeps the SPACE it was authored in, so an axis in hue mode
|
||||
* comes back as `oklch(0.54 0.19 300)`. Read as numbers that is [0.54, 0.19, 300] — three values in
|
||||
* the wrong units, clamped into a colour nobody chose — and every consumer here believed it: the hex
|
||||
* field, the `<input type="color">` swatch beside it and the WCAG readout. Measured on the stand with
|
||||
* `tint=120, accent=300`: a near-white green canvas whose real text contrast is ~16:1 reported #010078 and "Too
|
||||
* faint to read", and confirming the swatch would have written that navy over the admin's own hue.
|
||||
*
|
||||
* Painting one pixel and reading it back asks the engine to do the conversion instead. It is the
|
||||
* same method tools/export-tier.mjs uses, and for the same reason its header gives: "a color-mix()
|
||||
* computes to whatever space it was written in, and `oklch(L C H)` has three numbers that parse
|
||||
* perfectly well — silently and wrongly — as an rgb() triple". The parse survives only as the
|
||||
* fallback for an engine with no 2D context, where the legacy `rgb()`/`color(srgb …)` forms are all
|
||||
* that can appear anyway. */
|
||||
let _cx = null;
|
||||
function rasterCtx() {
|
||||
if (_cx !== null) return _cx;
|
||||
try {
|
||||
const cv = document.createElement('canvas');
|
||||
cv.width = cv.height = 1;
|
||||
_cx = cv.getContext('2d', { willReadFrequently: true }) || false;
|
||||
} catch (e) { _cx = false; }
|
||||
return _cx;
|
||||
}
|
||||
function parseColor(s) {
|
||||
const nums = String(s || '').match(/[\d.]+/g);
|
||||
const str = String(s || '');
|
||||
const cx = rasterCtx();
|
||||
if (cx) {
|
||||
/* fillStyle KEEPS the last value it could parse, so a colour this engine rejects would report
|
||||
* the previous one as a fresh reading — the same trap probeColor() clears its own style for. */
|
||||
cx.fillStyle = '#000';
|
||||
cx.fillStyle = str;
|
||||
cx.clearRect(0, 0, 1, 1);
|
||||
cx.fillRect(0, 0, 1, 1);
|
||||
const d = cx.getImageData(0, 0, 1, 1).data;
|
||||
if (d[3] === 255) return [ d[0], d[1], d[2] ];
|
||||
/* translucent: composite over nothing is meaningless for a readout, so fall through */
|
||||
}
|
||||
const nums = str.match(/[\d.]+/g);
|
||||
if (!nums || nums.length < 3) return null;
|
||||
const unit = (/^color\(/i).test(String(s)) ? 255 : 1;
|
||||
const unit = (/^color\(/i).test(str) ? 255 : 1;
|
||||
return nums.slice(0, 3).map((n) => Math.max(0, Math.min(255, parseFloat(n) * unit)));
|
||||
}
|
||||
|
||||
@@ -258,36 +294,6 @@ function colorControl(current, onPick, label, opts) {
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/* How close a popup may come to the viewport edge before it is nudged back in. Read by BOTH popups
|
||||
* the theme places by hand — the Appearance popover (fs-appearance.js) and the menu's dropdown
|
||||
* edge-clamp (menu-footstrap.js) — which had each written their own `8`. */
|
||||
const EDGE_GAP = 8;
|
||||
|
||||
/* Place the popover next to its trigger and keep it inside the viewport. It is position:fixed on
|
||||
* <body> because the sidebar is `overflow-y: auto` (which computes overflow-x to `auto` too), so
|
||||
* an absolutely-positioned popover parented to the Appearance row was clipped off the sidebar
|
||||
* edge. The top bar opens downward from the button's right edge, the sidebar sideways out of the
|
||||
* rail; both are then clamped. */
|
||||
function placePopover(btn, pop) {
|
||||
const gap = EDGE_GAP, r = btn.getBoundingClientRect();
|
||||
const w = pop.offsetWidth, h = pop.offsetHeight;
|
||||
const vw = document.documentElement.clientWidth;
|
||||
const vh = document.documentElement.clientHeight;
|
||||
const top_layout = prefs.isTopLayout();
|
||||
|
||||
let left = top_layout ? (r.right - w) : (r.right + gap);
|
||||
let top = top_layout ? (r.bottom + gap) : (r.bottom - h);
|
||||
|
||||
/* sidebar: if there is no room to the right, fall back above the trigger */
|
||||
if (!top_layout && left + w > vw - gap) {
|
||||
left = r.left;
|
||||
top = r.top - h - gap;
|
||||
}
|
||||
|
||||
pop.style.left = Math.max(gap, Math.min(left, vw - w - gap)) + 'px';
|
||||
pop.style.top = Math.max(gap, Math.min(top, vh - h - gap)) + 'px';
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
svgIcon,
|
||||
setOpen,
|
||||
@@ -295,7 +301,5 @@ return baseclass.extend({
|
||||
wireDismiss,
|
||||
colorControl,
|
||||
probeColor,
|
||||
toHex,
|
||||
EDGE_GAP,
|
||||
placePopover
|
||||
toHex
|
||||
});
|
||||
@@ -16,7 +16,7 @@
|
||||
*
|
||||
* fs-menutree path <-> menu node, alias/firstchild resolution (a port of dispatcher.uc)
|
||||
* fs-prefs the Appearance axes and their localStorage
|
||||
* fs-widgets disclosure primitives, the seg/slider controls, popup placement
|
||||
* fs-widgets the inline-SVG wrapper, the disclosure primitives, the colour control
|
||||
* fs-chrome mode menu, section tabs, the rail toggle, the "does it still fit" measurements
|
||||
* fs-router the SPA client router (docs/spa-router.md)
|
||||
* fs-sheets the guard against a view's injected CSS repainting every later page
|
||||
@@ -76,7 +76,7 @@ return baseclass.extend({
|
||||
router.wire();
|
||||
router.wireVisibility();
|
||||
/* fs-chrome's renderTabMenu warns about exactly this, and the root chain was left bare: a
|
||||
* throw anywhere in the calls above took out the menu, the router and the Appearance popover
|
||||
* throw anywhere in the calls above took out the menu, the router and the Appearance tab
|
||||
* together, silently. It still fails — there is no sane partial recovery — but loudly. */
|
||||
}).catch((e) => console.error('footstrap: chrome init failed', e));
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
/* The theme's ONE menu renderer: a vertical #topmenu the CSS also turns into the top bar and the
|
||||
* rail flyouts — same markup, no second renderer (docs/chrome.md). The disclosure primitives it builds
|
||||
* sections on come from fs-widgets and the auto-collapse preference from fs-prefs; the rest of the
|
||||
* chrome (mode menu, tabs, rail, router, popover) is bootstrapped by menu-footstrap-common, which
|
||||
* chrome (mode menu, tabs, rail, router, Appearance tab) is bootstrapped by menu-footstrap-common, which
|
||||
* this file composes with by injecting renderMainMenu into common.init — a callback, not an
|
||||
* override, because a required LuCI module is a singleton and cannot be subclassed (docs/conventions.md).
|
||||
* Spec: docs/chrome.md */
|
||||
@@ -67,9 +67,8 @@ const OPEN_LI = '#topmenu > li.open';
|
||||
* item near the right edge would push its panel past the viewport. Nudge it back inside. Runs at
|
||||
* ANY width now that the top bar is measured (no 768 floor); the rail flies panels out sideways
|
||||
* and needs a different placement, so it is excluded below. */
|
||||
/* the viewport edge gap, defined once in fs-widgets.js — the Appearance popover keeps a popup off
|
||||
* the edge by the same amount, and the two used to state it separately */
|
||||
const EDGE_GAP = widgets.EDGE_GAP;
|
||||
/* how close the panel may come to the viewport edge before it is nudged back in */
|
||||
const EDGE_GAP = 8;
|
||||
/* Is this panel a BAR dropdown (anchored under its item) rather than a rail flyout (anchored
|
||||
* beside it)? Ask what the STYLESHEET asks, exactly as flyoutMode() does: `data-narrow` is what
|
||||
* turns the sidebar layout into a bar, and it also disables the rail (the rail's rules are all
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
# ./mangle-tokens.sh <cascade.css> <reserved-source-dir>...
|
||||
#
|
||||
# Run from the package Makefile over $(PKG_BUILD_DIR) only — NEVER over a dev build. The names are
|
||||
# 16.1% of the sheet (21640 B over 1770 occurrences, 123 distinct) and mean nothing to a browser;
|
||||
# 16.6% of the sheet (22586 B over 1824 occurrences, 137 distinct) and mean nothing to a browser;
|
||||
# this is the same trade terser already makes for the JS, where top-level identifiers are mangled
|
||||
# because a LuCI resource file is function-scoped. Measured: 134240 -> ~119600, i.e. -14.6 KB, and
|
||||
# because a LuCI resource file is function-scoped. Measured: 135655 -> 123935, i.e. -11.7 KB, and
|
||||
# uhttpd serves /www with no compression, so those are wire bytes on every cold load as well as
|
||||
# flash bytes.
|
||||
#
|
||||
@@ -16,12 +16,13 @@
|
||||
# luci-app reads a `--fs-` name.
|
||||
# * The RESERVED set is DERIVED, not listed: every `--fs-` name that appears in the theme's JS or
|
||||
# in a .ut template crosses a seam (fs-prefs.js writes `--fs-tint`, head.ut's pre-paint writes
|
||||
# `--fs-radius-base`, fs-chrome.js reads `--fs-sidebar-w` …) and keeps its name. 15 of 123 today.
|
||||
# `--fs-radius-base`, fs-chrome.js reads `--fs-sidebar-w` …) and keeps its name. 35 of 137 today.
|
||||
# A new one cannot be forgotten, because nothing here names them.
|
||||
# * Point the dirs at the SOURCE tree, not at $(PKG_BUILD_DIR). In CI the build tree's JS has
|
||||
# already been through terser and its comments are gone, so five names that only appear in a
|
||||
# comment stopped being reserved — the same source produced a different sheet depending on who
|
||||
# built it (10 reserved via CI, 15 via a plain SDK build). Reading the source over-reserves by
|
||||
# built it (measured at the time: 10 reserved via CI against 15 via a plain SDK build, of a
|
||||
# smaller reserved set than today's 35). Reading the source over-reserves by
|
||||
# about a kilobyte, and over-reserving is the direction that cannot break anything.
|
||||
# * The scan is STRING-AWARE and reads the WHOLE identifier before deciding. A prefix match would
|
||||
# be a silent corruption: `--fs-space-2` is a prefix of `--fs-space-2-5`.
|
||||
|
||||
@@ -11,145 +11,145 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 3.9\n"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:232
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:238
|
||||
msgctxt "footstrap"
|
||||
msgid "Accent"
|
||||
msgstr "Acento"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:357
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:361
|
||||
msgctxt "footstrap"
|
||||
msgid "As in file"
|
||||
msgstr "Como en el archivo"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:37
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:70
|
||||
msgid "Authorization Required"
|
||||
msgstr "Se requiere autorización"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:169
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:172
|
||||
msgctxt "footstrap"
|
||||
msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:199
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:205
|
||||
msgctxt "footstrap"
|
||||
msgid "Auto-collapse"
|
||||
msgstr "Colapso automático"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:615
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:653
|
||||
msgctxt "footstrap"
|
||||
msgid "Background"
|
||||
msgstr "Fondo"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:223
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:259
|
||||
msgctxt "footstrap"
|
||||
msgid "Barely visible %s"
|
||||
msgstr "Apenas visible %s"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:288
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:294
|
||||
msgctxt "footstrap"
|
||||
msgid "Borders"
|
||||
msgstr "Bordes"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:269
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:275
|
||||
msgctxt "footstrap"
|
||||
msgid "Cards"
|
||||
msgstr "Tarjetas"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:342
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:331
|
||||
msgctxt "footstrap"
|
||||
msgid "Choose SVG"
|
||||
msgstr "Elegir SVG"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:309
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:311
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:324
|
||||
msgctxt "footstrap"
|
||||
msgid "Choose image"
|
||||
msgstr "Elegir imagen"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:222
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:258
|
||||
msgctxt "footstrap"
|
||||
msgid "Clearly visible %s"
|
||||
msgstr "Bien visible %s"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:369
|
||||
#: ucode/template/themes/footstrap/header.ut:133
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:372
|
||||
#: ucode/template/themes/footstrap/header.ut:144
|
||||
msgid "Collapse menu"
|
||||
msgstr "Contraer menú"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:347
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:612
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:344
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:650
|
||||
msgctxt "footstrap"
|
||||
msgid "Colours"
|
||||
msgstr "Colores"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:182
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:188
|
||||
msgctxt "footstrap"
|
||||
msgid "Compact"
|
||||
msgstr "Compacto"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:508
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:545
|
||||
msgctxt "footstrap"
|
||||
msgid "Confirm reset"
|
||||
msgstr "Confirmar restablecimiento"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:232
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:268
|
||||
msgctxt "footstrap"
|
||||
msgid "Contrast %s:1 (WCAG AA wants %s:1 here)"
|
||||
msgstr "Contraste %s:1 (WCAG AA exige aquí %s:1)"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:275
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:281
|
||||
msgctxt "footstrap"
|
||||
msgid "Controls"
|
||||
msgstr "Controles"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1014
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1016
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1099
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1101
|
||||
msgctxt "footstrap"
|
||||
msgid "Could not process the image."
|
||||
msgstr "No se pudo procesar la imagen."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:484
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:521
|
||||
msgctxt "footstrap"
|
||||
msgid "Could not save the default. Reload the page and try again."
|
||||
msgstr ""
|
||||
"No se pudo guardar la configuración predeterminada. Recargue la página e "
|
||||
"inténtelo de nuevo."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:250
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:256
|
||||
msgctxt "footstrap"
|
||||
msgid "Danger"
|
||||
msgstr "Peligro"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:171
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:174
|
||||
msgctxt "footstrap"
|
||||
msgid "Dark"
|
||||
msgstr "Oscuro"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:616
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:654
|
||||
msgctxt "footstrap"
|
||||
msgid "Defaults"
|
||||
msgstr "Predeterminados"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:181
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:187
|
||||
msgctxt "footstrap"
|
||||
msgid "Density"
|
||||
msgstr "Densidad"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:318
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:341
|
||||
msgctxt "footstrap"
|
||||
msgid "Dim"
|
||||
msgstr "Oscurecimiento"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:225
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:261
|
||||
msgctxt "footstrap"
|
||||
msgid "Easy to read %s"
|
||||
msgstr "Se lee bien %s"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:369
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:372
|
||||
msgid "Expand menu"
|
||||
msgstr "Expandir menú"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:413
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:366
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:451
|
||||
msgctxt "footstrap"
|
||||
msgid "File"
|
||||
msgstr "Archivo"
|
||||
@@ -170,22 +170,22 @@ msgstr "Ir a la actualización del firmware..."
|
||||
msgid "Go to password configuration..."
|
||||
msgstr "Ir a la configuración de contraseñas..."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:238
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:244
|
||||
msgctxt "footstrap"
|
||||
msgid "Good"
|
||||
msgstr "Correcto"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:227
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:263
|
||||
msgctxt "footstrap"
|
||||
msgid "Hard to read %s — large text only"
|
||||
msgstr "Se lee mal %s: solo texto grande"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:611
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:649
|
||||
msgctxt "footstrap"
|
||||
msgid "Interface"
|
||||
msgstr "Interfaz"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:32
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:60
|
||||
msgid "Invalid username and/or password! Please try again."
|
||||
msgstr "¡Nombre de usuario y/o contraseña incorrectos! Inténtelo de nuevo."
|
||||
|
||||
@@ -193,49 +193,49 @@ msgstr "¡Nombre de usuario y/o contraseña incorrectos! Inténtelo de nuevo."
|
||||
msgid "JavaScript required!"
|
||||
msgstr "¡Se requiere JavaScript!"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:198
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:204
|
||||
msgctxt "footstrap"
|
||||
msgid "Keep open"
|
||||
msgstr "Mantener abierto"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:184
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:190
|
||||
msgctxt "footstrap"
|
||||
msgid "Large"
|
||||
msgstr "Grande"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:163
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:166
|
||||
msgctxt "footstrap"
|
||||
msgid "Layout"
|
||||
msgstr "Diseño"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:170
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:173
|
||||
msgctxt "footstrap"
|
||||
msgid "Light"
|
||||
msgstr "Claro"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-router.js:466
|
||||
#: htdocs/luci-static/resources/fs-router.js:501
|
||||
msgid "Loading view…"
|
||||
msgstr "Cargando vista…"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:58
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:142
|
||||
msgid "Log in"
|
||||
msgstr "Iniciar sesión"
|
||||
|
||||
#: ucode/template/themes/footstrap/partials/logout.ut:11
|
||||
#: ucode/template/themes/footstrap/partials/logout.ut:13
|
||||
msgid "Logout"
|
||||
msgid "Log out"
|
||||
msgstr "Cerrar sesión"
|
||||
|
||||
#: ucode/template/themes/footstrap/partials/footer.ut:27
|
||||
msgid "Lua compatibility mode active"
|
||||
msgstr "Modo de compatibilidad Lua activo"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-overview.js:34
|
||||
#: htdocs/luci-static/resources/fs-overview.js:32
|
||||
msgid "Memory"
|
||||
msgstr "Memoria"
|
||||
|
||||
#: ucode/template/themes/footstrap/header.ut:130
|
||||
#: ucode/template/themes/footstrap/header.ut:141
|
||||
#: ucode/template/themes/footstrap/header.ut:152
|
||||
msgid "Menu"
|
||||
msgstr "Menú"
|
||||
|
||||
@@ -248,7 +248,7 @@ msgstr ""
|
||||
"reiniciar el sistema. Este modo solo debe usarse para instalar una "
|
||||
"actualización de firmware"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:316
|
||||
#: htdocs/luci-static/resources/fs-search.js:320
|
||||
msgctxt "footstrap"
|
||||
msgid "No pages found"
|
||||
msgstr "No se encontraron páginas"
|
||||
@@ -257,155 +257,157 @@ msgstr "No se encontraron páginas"
|
||||
msgid "No password set!"
|
||||
msgstr "¡No se ha establecido ninguna contraseña!"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:183
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:189
|
||||
msgctxt "footstrap"
|
||||
msgid "Normal"
|
||||
msgstr "Normal"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:411
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:449
|
||||
msgctxt "footstrap"
|
||||
msgid "Off"
|
||||
msgstr "Apagado"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:233
|
||||
#: htdocs/luci-static/resources/fs-search.js:237
|
||||
msgctxt "footstrap"
|
||||
msgid "Pages"
|
||||
msgstr "Páginas"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:174
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:180
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:177
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:216
|
||||
msgctxt "footstrap"
|
||||
msgid "Palette"
|
||||
msgstr "Paleta"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:49
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:82
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:412
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:352
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:450
|
||||
msgctxt "footstrap"
|
||||
msgid "Pattern"
|
||||
msgstr "Patrón"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:882
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:884
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:945
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:947
|
||||
msgctxt "footstrap"
|
||||
msgid "Please choose an SVG file."
|
||||
msgstr "Elija un archivo SVG."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1029
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1131
|
||||
msgctxt "footstrap"
|
||||
msgid "Please choose an image file."
|
||||
msgstr "Por favor, seleccione un archivo de imagen."
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:39
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:72
|
||||
msgid "Please enter your username and password."
|
||||
msgstr "Por favor, ingrese su nombre de usuario y contraseña."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:315
|
||||
#: htdocs/luci-static/resources/fs-search.js:319
|
||||
msgctxt "footstrap"
|
||||
msgid "Recently visited"
|
||||
msgstr "Visitado recientemente"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:310
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:344
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:326
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:333
|
||||
msgctxt "footstrap"
|
||||
msgid "Remove"
|
||||
msgstr "Eliminar"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:459
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:519
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:483
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:556
|
||||
msgctxt "footstrap"
|
||||
msgid "Reset to default"
|
||||
msgstr "Restablecer a valores predeterminados"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:458
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:518
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:478
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:555
|
||||
msgctxt "footstrap"
|
||||
msgid "Reset to saved"
|
||||
msgstr "Restablecer a lo guardado"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:187
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:193
|
||||
msgctxt "footstrap"
|
||||
msgid "Rounding"
|
||||
msgstr "Redondeo"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:532
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:569
|
||||
msgctxt "footstrap"
|
||||
msgid "Router default"
|
||||
msgstr "Configuración predeterminada del enrutador"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:452
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:473
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:472
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:503
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:510
|
||||
msgctxt "footstrap"
|
||||
msgid "Save as default"
|
||||
msgstr "Guardar como predeterminado"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:473
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:510
|
||||
msgctxt "footstrap"
|
||||
msgid "Saved as default"
|
||||
msgstr "Guardado como predeterminado"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:345
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:342
|
||||
msgctxt "footstrap"
|
||||
msgid "Scale"
|
||||
msgstr "Escala"
|
||||
|
||||
#: ucode/template/themes/footstrap/partials/search.ut:14
|
||||
#: ucode/template/themes/footstrap/partials/search.ut:16
|
||||
#: ucode/template/themes/footstrap/partials/search.ut:18
|
||||
msgid "Search"
|
||||
msgstr "Buscar"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:227
|
||||
#: htdocs/luci-static/resources/fs-search.js:254
|
||||
#: htdocs/luci-static/resources/fs-search.js:231
|
||||
#: htdocs/luci-static/resources/fs-search.js:258
|
||||
msgctxt "footstrap"
|
||||
msgid "Search pages"
|
||||
msgstr "Buscar páginas"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:228
|
||||
#: htdocs/luci-static/resources/fs-search.js:232
|
||||
msgctxt "footstrap"
|
||||
msgid "Search pages…"
|
||||
msgstr "Buscar páginas…"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:164
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:167
|
||||
msgctxt "footstrap"
|
||||
msgid "Sidebar"
|
||||
msgstr "Barra lateral"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:281
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:287
|
||||
msgctxt "footstrap"
|
||||
msgid "Sidebar and bar"
|
||||
msgstr "Barra lateral y superior"
|
||||
|
||||
#: ucode/template/themes/footstrap/header.ut:109
|
||||
#: ucode/template/themes/footstrap/header.ut:120
|
||||
msgid "Skip to content"
|
||||
msgstr "Saltar al contenido"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:316
|
||||
#: htdocs/luci-static/resources/fs-search.js:320
|
||||
msgctxt "footstrap"
|
||||
msgid "Start typing to find a page"
|
||||
msgstr "Empiece a escribir para encontrar una página"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-overview.js:34
|
||||
#: htdocs/luci-static/resources/fs-overview.js:32
|
||||
msgid "Storage"
|
||||
msgstr "Almacenamiento"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:346
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:343
|
||||
msgctxt "footstrap"
|
||||
msgid "Strength"
|
||||
msgstr "Intensidad"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:196
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:202
|
||||
msgctxt "footstrap"
|
||||
msgid "Submenus"
|
||||
msgstr "Submenús"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:613
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:651
|
||||
msgctxt "footstrap"
|
||||
msgid "Surfaces"
|
||||
msgstr "Superficies"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-overview.js:34
|
||||
#: htdocs/luci-static/resources/fs-overview.js:32
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
@@ -413,7 +415,7 @@ msgstr "Sistema"
|
||||
msgid "System running in recovery (initramfs) mode."
|
||||
msgstr "El sistema se está ejecutando en modo de recuperación (initramfs)."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:830
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:893
|
||||
msgctxt "footstrap"
|
||||
msgid ""
|
||||
"That SVG contains script or external references, which this theme will not "
|
||||
@@ -421,34 +423,39 @@ msgid ""
|
||||
msgstr ""
|
||||
"Ese SVG contiene scripts o referencias externas, y este tema no lo instalará."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:854
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:917
|
||||
msgctxt "footstrap"
|
||||
msgid "That file could not be read."
|
||||
msgstr "No se ha podido leer el archivo."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1018
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1103
|
||||
msgctxt "footstrap"
|
||||
msgid "That file is not a readable image."
|
||||
msgstr "Ese archivo no es una imagen legible."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:826
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:829
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:889
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:892
|
||||
msgctxt "footstrap"
|
||||
msgid "That file is not an SVG image."
|
||||
msgstr "Ese archivo no es una imagen SVG."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:885
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:948
|
||||
msgctxt "footstrap"
|
||||
msgid "That file is too large."
|
||||
msgstr "Ese archivo es demasiado grande."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1031
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1133
|
||||
msgctxt "footstrap"
|
||||
msgid "That image is too large."
|
||||
msgstr "Esa imagen es demasiado grande."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:168
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:356
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1026
|
||||
msgctxt "footstrap"
|
||||
msgid "The router refused to delete the file (ubus status %d)."
|
||||
msgstr "El router se negó a eliminar el archivo (estado ubus %d)."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:171
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:360
|
||||
msgctxt "footstrap"
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
@@ -461,51 +468,56 @@ msgstr ""
|
||||
"Este enrutador no tiene contraseña. Configure una contraseña de "
|
||||
"administrador para proteger la interfaz web."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:208
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:504
|
||||
msgctxt "footstrap"
|
||||
msgid "This browser is not storing preferences (site data is blocked), so a change here lasts until you reload. Saving as default still works and applies to every browser."
|
||||
msgstr "Este navegador no guarda preferencias (los datos del sitio están bloqueados), así que un cambio aquí dura hasta que recargues. Guardar como predeterminado sigue funcionando y se aplica a todos los navegadores."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:214
|
||||
msgctxt "footstrap"
|
||||
msgid "Tint (router identification)"
|
||||
msgstr "Tinte (identificación del enrutador)"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:223
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:229
|
||||
msgctxt "footstrap"
|
||||
msgid "Tint strength"
|
||||
msgstr "Intensidad del tinte"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:228
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:264
|
||||
msgctxt "footstrap"
|
||||
msgid "Too faint to read %s"
|
||||
msgstr "Demasiado tenue para leerse %s"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:165
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:168
|
||||
msgctxt "footstrap"
|
||||
msgid "Top"
|
||||
msgstr "Arriba"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:897
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:900
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:958
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1042
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1045
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:960
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:963
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1043
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1144
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1147
|
||||
msgctxt "footstrap"
|
||||
msgid "Upload failed."
|
||||
msgstr "Error al cargar el archivo."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:383
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:422
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:402
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:432
|
||||
msgctxt "footstrap"
|
||||
msgid "Uploading…"
|
||||
msgstr "Cargando…"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:43
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:76
|
||||
msgid "Username"
|
||||
msgstr "Nombre de usuario"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:303
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:447
|
||||
msgctxt "footstrap"
|
||||
msgid "Wallpaper"
|
||||
msgstr "Fondo de pantalla"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:244
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:250
|
||||
msgctxt "footstrap"
|
||||
msgid "Warning"
|
||||
msgstr "Advertencia"
|
||||
@@ -516,31 +528,39 @@ msgid ""
|
||||
msgstr ""
|
||||
"Debe activar JavaScript en su navegador o LuCI no funcionará correctamente."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:284
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:290
|
||||
msgctxt "footstrap"
|
||||
msgid "in the sidebar"
|
||||
msgstr "en la barra lateral"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:235
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:241
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:247
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:253
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:272
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:291
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:259
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:278
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:297
|
||||
msgctxt "footstrap"
|
||||
msgid "on a card"
|
||||
msgstr "en una tarjeta"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:278
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:284
|
||||
msgctxt "footstrap"
|
||||
msgid "on a control"
|
||||
msgstr "en un control"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:213
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:219
|
||||
msgctxt "footstrap"
|
||||
msgid "on the canvas"
|
||||
msgstr "en el lienzo"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1121
|
||||
msgctxt "footstrap"
|
||||
msgid "the uploaded file could not be removed either; it is still on the router."
|
||||
msgstr "tampoco se pudo eliminar el archivo subido; sigue en el router."
|
||||
|
||||
#~ msgid "Logout"
|
||||
#~ msgstr "Cerrar sesión"
|
||||
|
||||
#~ msgctxt "footstrap"
|
||||
#~ msgid "Amber"
|
||||
#~ msgstr "Ámbar"
|
||||
|
||||
@@ -7,143 +7,143 @@ msgstr ""
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:232
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:238
|
||||
msgctxt "footstrap"
|
||||
msgid "Accent"
|
||||
msgstr "Акцент"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:357
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:361
|
||||
msgctxt "footstrap"
|
||||
msgid "As in file"
|
||||
msgstr "Как в файле"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:37
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:70
|
||||
msgid "Authorization Required"
|
||||
msgstr "Требуется авторизация"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:169
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:172
|
||||
msgctxt "footstrap"
|
||||
msgid "Auto"
|
||||
msgstr "Авто"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:199
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:205
|
||||
msgctxt "footstrap"
|
||||
msgid "Auto-collapse"
|
||||
msgstr "Сворачивать"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:615
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:653
|
||||
msgctxt "footstrap"
|
||||
msgid "Background"
|
||||
msgstr "Фон"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:223
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:259
|
||||
msgctxt "footstrap"
|
||||
msgid "Barely visible %s"
|
||||
msgstr "Едва заметна %s"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:288
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:294
|
||||
msgctxt "footstrap"
|
||||
msgid "Borders"
|
||||
msgstr "Границы"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:269
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:275
|
||||
msgctxt "footstrap"
|
||||
msgid "Cards"
|
||||
msgstr "Карточки"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:342
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:331
|
||||
msgctxt "footstrap"
|
||||
msgid "Choose SVG"
|
||||
msgstr "Выбрать SVG"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:309
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:311
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:324
|
||||
msgctxt "footstrap"
|
||||
msgid "Choose image"
|
||||
msgstr "Выбрать изображение"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:222
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:258
|
||||
msgctxt "footstrap"
|
||||
msgid "Clearly visible %s"
|
||||
msgstr "Хорошо видна %s"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:369
|
||||
#: ucode/template/themes/footstrap/header.ut:133
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:372
|
||||
#: ucode/template/themes/footstrap/header.ut:144
|
||||
msgid "Collapse menu"
|
||||
msgstr "Свернуть меню"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:347
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:612
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:344
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:650
|
||||
msgctxt "footstrap"
|
||||
msgid "Colours"
|
||||
msgstr "Цвета"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:182
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:188
|
||||
msgctxt "footstrap"
|
||||
msgid "Compact"
|
||||
msgstr "Компактно"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:508
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:545
|
||||
msgctxt "footstrap"
|
||||
msgid "Confirm reset"
|
||||
msgstr "Подтвердить сброс"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:232
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:268
|
||||
msgctxt "footstrap"
|
||||
msgid "Contrast %s:1 (WCAG AA wants %s:1 here)"
|
||||
msgstr "Контраст %s:1 (WCAG AA требует здесь %s:1)"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:275
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:281
|
||||
msgctxt "footstrap"
|
||||
msgid "Controls"
|
||||
msgstr "Элементы форм"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1014
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1016
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1099
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1101
|
||||
msgctxt "footstrap"
|
||||
msgid "Could not process the image."
|
||||
msgstr "Не удалось обработать изображение."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:484
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:521
|
||||
msgctxt "footstrap"
|
||||
msgid "Could not save the default. Reload the page and try again."
|
||||
msgstr "Не удалось сохранить умолчание. Обновите страницу и повторите."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:250
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:256
|
||||
msgctxt "footstrap"
|
||||
msgid "Danger"
|
||||
msgstr "Опасность"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:171
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:174
|
||||
msgctxt "footstrap"
|
||||
msgid "Dark"
|
||||
msgstr "Тёмная"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:616
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:654
|
||||
msgctxt "footstrap"
|
||||
msgid "Defaults"
|
||||
msgstr "Умолчания"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:181
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:187
|
||||
msgctxt "footstrap"
|
||||
msgid "Density"
|
||||
msgstr "Плотность"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:318
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:341
|
||||
msgctxt "footstrap"
|
||||
msgid "Dim"
|
||||
msgstr "Затемнение"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:225
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:261
|
||||
msgctxt "footstrap"
|
||||
msgid "Easy to read %s"
|
||||
msgstr "Хорошо читается %s"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:369
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:372
|
||||
msgid "Expand menu"
|
||||
msgstr "Развернуть меню"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:413
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:366
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:451
|
||||
msgctxt "footstrap"
|
||||
msgid "File"
|
||||
msgstr "Файл"
|
||||
@@ -164,22 +164,22 @@ msgstr "Перейти к обновлению прошивки..."
|
||||
msgid "Go to password configuration..."
|
||||
msgstr "Перейти к настройке пароля..."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:238
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:244
|
||||
msgctxt "footstrap"
|
||||
msgid "Good"
|
||||
msgstr "Норма"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:227
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:263
|
||||
msgctxt "footstrap"
|
||||
msgid "Hard to read %s — large text only"
|
||||
msgstr "Плохо читается %s — только крупный текст"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:611
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:649
|
||||
msgctxt "footstrap"
|
||||
msgid "Interface"
|
||||
msgstr "Интерфейс"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:32
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:60
|
||||
msgid "Invalid username and/or password! Please try again."
|
||||
msgstr "Неверное имя пользователя и/или пароль! Попробуйте ещё раз."
|
||||
|
||||
@@ -187,49 +187,49 @@ msgstr "Неверное имя пользователя и/или пароль!
|
||||
msgid "JavaScript required!"
|
||||
msgstr "Требуется JavaScript!"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:198
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:204
|
||||
msgctxt "footstrap"
|
||||
msgid "Keep open"
|
||||
msgstr "Не сворачивать"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:184
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:190
|
||||
msgctxt "footstrap"
|
||||
msgid "Large"
|
||||
msgstr "Крупно"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:163
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:166
|
||||
msgctxt "footstrap"
|
||||
msgid "Layout"
|
||||
msgstr "Раскладка"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:170
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:173
|
||||
msgctxt "footstrap"
|
||||
msgid "Light"
|
||||
msgstr "Светлая"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-router.js:466
|
||||
#: htdocs/luci-static/resources/fs-router.js:501
|
||||
msgid "Loading view…"
|
||||
msgstr "Загрузка представления…"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:58
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:142
|
||||
msgid "Log in"
|
||||
msgstr "Войти"
|
||||
|
||||
#: ucode/template/themes/footstrap/partials/logout.ut:11
|
||||
#: ucode/template/themes/footstrap/partials/logout.ut:13
|
||||
msgid "Logout"
|
||||
msgstr "Выход"
|
||||
msgid "Log out"
|
||||
msgstr "Выйти"
|
||||
|
||||
#: ucode/template/themes/footstrap/partials/footer.ut:27
|
||||
msgid "Lua compatibility mode active"
|
||||
msgstr "Включён режим совместимости с Lua"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-overview.js:34
|
||||
#: htdocs/luci-static/resources/fs-overview.js:32
|
||||
msgid "Memory"
|
||||
msgstr "Память"
|
||||
|
||||
#: ucode/template/themes/footstrap/header.ut:130
|
||||
#: ucode/template/themes/footstrap/header.ut:141
|
||||
#: ucode/template/themes/footstrap/header.ut:152
|
||||
msgid "Menu"
|
||||
msgstr "Меню"
|
||||
|
||||
@@ -241,7 +241,7 @@ msgstr ""
|
||||
"Изменения настроек не сохраняются и будут потеряны после перезагрузки. Этот "
|
||||
"режим следует использовать только для установки обновления прошивки"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:316
|
||||
#: htdocs/luci-static/resources/fs-search.js:320
|
||||
msgctxt "footstrap"
|
||||
msgid "No pages found"
|
||||
msgstr "Страницы не найдены"
|
||||
@@ -250,155 +250,157 @@ msgstr "Страницы не найдены"
|
||||
msgid "No password set!"
|
||||
msgstr "Пароль не установлен!"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:183
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:189
|
||||
msgctxt "footstrap"
|
||||
msgid "Normal"
|
||||
msgstr "Обычно"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:411
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:449
|
||||
msgctxt "footstrap"
|
||||
msgid "Off"
|
||||
msgstr "Выкл."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:233
|
||||
#: htdocs/luci-static/resources/fs-search.js:237
|
||||
msgctxt "footstrap"
|
||||
msgid "Pages"
|
||||
msgstr "Страницы"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:174
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:180
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:177
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:216
|
||||
msgctxt "footstrap"
|
||||
msgid "Palette"
|
||||
msgstr "Палитра"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:49
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:82
|
||||
msgid "Password"
|
||||
msgstr "Пароль"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:412
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:352
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:450
|
||||
msgctxt "footstrap"
|
||||
msgid "Pattern"
|
||||
msgstr "Узор"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:882
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:884
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:945
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:947
|
||||
msgctxt "footstrap"
|
||||
msgid "Please choose an SVG file."
|
||||
msgstr "Выберите файл SVG."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1029
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1131
|
||||
msgctxt "footstrap"
|
||||
msgid "Please choose an image file."
|
||||
msgstr "Выберите файл изображения."
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:39
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:72
|
||||
msgid "Please enter your username and password."
|
||||
msgstr "Введите имя пользователя и пароль."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:315
|
||||
#: htdocs/luci-static/resources/fs-search.js:319
|
||||
msgctxt "footstrap"
|
||||
msgid "Recently visited"
|
||||
msgstr "Недавние"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:310
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:344
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:326
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:333
|
||||
msgctxt "footstrap"
|
||||
msgid "Remove"
|
||||
msgstr "Убрать"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:459
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:519
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:483
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:556
|
||||
msgctxt "footstrap"
|
||||
msgid "Reset to default"
|
||||
msgstr "Сбросить к умолчанию"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:458
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:518
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:478
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:555
|
||||
msgctxt "footstrap"
|
||||
msgid "Reset to saved"
|
||||
msgstr "Сбросить к сохранённым"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:187
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:193
|
||||
msgctxt "footstrap"
|
||||
msgid "Rounding"
|
||||
msgstr "Скругление"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:532
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:569
|
||||
msgctxt "footstrap"
|
||||
msgid "Router default"
|
||||
msgstr "По умолчанию для роутера"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:452
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:473
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:472
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:503
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:510
|
||||
msgctxt "footstrap"
|
||||
msgid "Save as default"
|
||||
msgstr "Сохранить по умолчанию"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:473
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:510
|
||||
msgctxt "footstrap"
|
||||
msgid "Saved as default"
|
||||
msgstr "Сохранено по умолчанию"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:345
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:342
|
||||
msgctxt "footstrap"
|
||||
msgid "Scale"
|
||||
msgstr "Масштаб"
|
||||
|
||||
#: ucode/template/themes/footstrap/partials/search.ut:14
|
||||
#: ucode/template/themes/footstrap/partials/search.ut:16
|
||||
#: ucode/template/themes/footstrap/partials/search.ut:18
|
||||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:227
|
||||
#: htdocs/luci-static/resources/fs-search.js:254
|
||||
#: htdocs/luci-static/resources/fs-search.js:231
|
||||
#: htdocs/luci-static/resources/fs-search.js:258
|
||||
msgctxt "footstrap"
|
||||
msgid "Search pages"
|
||||
msgstr "Поиск по страницам"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:228
|
||||
#: htdocs/luci-static/resources/fs-search.js:232
|
||||
msgctxt "footstrap"
|
||||
msgid "Search pages…"
|
||||
msgstr "Поиск по страницам…"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:164
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:167
|
||||
msgctxt "footstrap"
|
||||
msgid "Sidebar"
|
||||
msgstr "Сбоку"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:281
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:287
|
||||
msgctxt "footstrap"
|
||||
msgid "Sidebar and bar"
|
||||
msgstr "Боковая панель и шапка"
|
||||
|
||||
#: ucode/template/themes/footstrap/header.ut:109
|
||||
#: ucode/template/themes/footstrap/header.ut:120
|
||||
msgid "Skip to content"
|
||||
msgstr "Перейти к содержимому"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:316
|
||||
#: htdocs/luci-static/resources/fs-search.js:320
|
||||
msgctxt "footstrap"
|
||||
msgid "Start typing to find a page"
|
||||
msgstr "Начните вводить название страницы"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-overview.js:34
|
||||
#: htdocs/luci-static/resources/fs-overview.js:32
|
||||
msgid "Storage"
|
||||
msgstr "Накопитель"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:346
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:343
|
||||
msgctxt "footstrap"
|
||||
msgid "Strength"
|
||||
msgstr "Насыщенность"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:196
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:202
|
||||
msgctxt "footstrap"
|
||||
msgid "Submenus"
|
||||
msgstr "Подменю"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:613
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:651
|
||||
msgctxt "footstrap"
|
||||
msgid "Surfaces"
|
||||
msgstr "Поверхности"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-overview.js:34
|
||||
#: htdocs/luci-static/resources/fs-overview.js:32
|
||||
msgid "System"
|
||||
msgstr "Система"
|
||||
|
||||
@@ -406,7 +408,7 @@ msgstr "Система"
|
||||
msgid "System running in recovery (initramfs) mode."
|
||||
msgstr "Система работает в режиме восстановления (initramfs)."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:830
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:893
|
||||
msgctxt "footstrap"
|
||||
msgid ""
|
||||
"That SVG contains script or external references, which this theme will not "
|
||||
@@ -414,34 +416,39 @@ msgid ""
|
||||
msgstr ""
|
||||
"В этом SVG есть скрипт или внешние ссылки — тема не станет его устанавливать."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:854
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:917
|
||||
msgctxt "footstrap"
|
||||
msgid "That file could not be read."
|
||||
msgstr "Не удалось прочитать файл."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1018
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1103
|
||||
msgctxt "footstrap"
|
||||
msgid "That file is not a readable image."
|
||||
msgstr "Не удалось прочитать файл как изображение."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:826
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:829
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:889
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:892
|
||||
msgctxt "footstrap"
|
||||
msgid "That file is not an SVG image."
|
||||
msgstr "Этот файл — не изображение SVG."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:885
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:948
|
||||
msgctxt "footstrap"
|
||||
msgid "That file is too large."
|
||||
msgstr "Файл слишком большой."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1031
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1133
|
||||
msgctxt "footstrap"
|
||||
msgid "That image is too large."
|
||||
msgstr "Изображение слишком большое."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:168
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:356
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1026
|
||||
msgctxt "footstrap"
|
||||
msgid "The router refused to delete the file (ubus status %d)."
|
||||
msgstr "Роутер отказался удалить файл (статус ubus %d)."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:171
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:360
|
||||
msgctxt "footstrap"
|
||||
msgid "Theme"
|
||||
msgstr "Тема"
|
||||
@@ -454,51 +461,56 @@ msgstr ""
|
||||
"На этом роутере не задан пароль. Установите пароль root, чтобы защитить веб-"
|
||||
"интерфейс."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:208
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:504
|
||||
msgctxt "footstrap"
|
||||
msgid "This browser is not storing preferences (site data is blocked), so a change here lasts until you reload. Saving as default still works and applies to every browser."
|
||||
msgstr "Этот браузер не хранит настройки (данные сайта заблокированы), поэтому изменение здесь действует до перезагрузки страницы. «Сохранить как умолчание» по-прежнему работает и применяется ко всем браузерам."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:214
|
||||
msgctxt "footstrap"
|
||||
msgid "Tint (router identification)"
|
||||
msgstr "Оттенок (опознание роутера)"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:223
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:229
|
||||
msgctxt "footstrap"
|
||||
msgid "Tint strength"
|
||||
msgstr "Насыщенность оттенка"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:228
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:264
|
||||
msgctxt "footstrap"
|
||||
msgid "Too faint to read %s"
|
||||
msgstr "Слишком блёкло, не читается %s"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:165
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:168
|
||||
msgctxt "footstrap"
|
||||
msgid "Top"
|
||||
msgstr "Сверху"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:897
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:900
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:958
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1042
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1045
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:960
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:963
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1043
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1144
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1147
|
||||
msgctxt "footstrap"
|
||||
msgid "Upload failed."
|
||||
msgstr "Не удалось загрузить."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:383
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:422
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:402
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:432
|
||||
msgctxt "footstrap"
|
||||
msgid "Uploading…"
|
||||
msgstr "Загрузка…"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:43
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:76
|
||||
msgid "Username"
|
||||
msgstr "Имя пользователя"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:303
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:447
|
||||
msgctxt "footstrap"
|
||||
msgid "Wallpaper"
|
||||
msgstr "Обои"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:244
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:250
|
||||
msgctxt "footstrap"
|
||||
msgid "Warning"
|
||||
msgstr "Предупреждение"
|
||||
@@ -509,31 +521,39 @@ msgid ""
|
||||
msgstr ""
|
||||
"Включите JavaScript в браузере, иначе LuCI не будет работать корректно."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:284
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:290
|
||||
msgctxt "footstrap"
|
||||
msgid "in the sidebar"
|
||||
msgstr "в боковой панели"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:235
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:241
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:247
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:253
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:272
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:291
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:259
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:278
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:297
|
||||
msgctxt "footstrap"
|
||||
msgid "on a card"
|
||||
msgstr "на карточке"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:278
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:284
|
||||
msgctxt "footstrap"
|
||||
msgid "on a control"
|
||||
msgstr "на элементе формы"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:213
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:219
|
||||
msgctxt "footstrap"
|
||||
msgid "on the canvas"
|
||||
msgstr "на холсте"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1121
|
||||
msgctxt "footstrap"
|
||||
msgid "the uploaded file could not be removed either; it is still on the router."
|
||||
msgstr "загруженный файл тоже не удалось удалить — он остался на роутере."
|
||||
|
||||
#~ msgid "Logout"
|
||||
#~ msgstr "Выход"
|
||||
|
||||
#~ msgctxt "footstrap"
|
||||
#~ msgid "Amber"
|
||||
#~ msgstr "Янтарь"
|
||||
|
||||
@@ -1,143 +1,143 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:232
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:238
|
||||
msgctxt "footstrap"
|
||||
msgid "Accent"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:357
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:361
|
||||
msgctxt "footstrap"
|
||||
msgid "As in file"
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:37
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:70
|
||||
msgid "Authorization Required"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:169
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:172
|
||||
msgctxt "footstrap"
|
||||
msgid "Auto"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:199
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:205
|
||||
msgctxt "footstrap"
|
||||
msgid "Auto-collapse"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:615
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:653
|
||||
msgctxt "footstrap"
|
||||
msgid "Background"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:223
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:259
|
||||
msgctxt "footstrap"
|
||||
msgid "Barely visible %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:288
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:294
|
||||
msgctxt "footstrap"
|
||||
msgid "Borders"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:269
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:275
|
||||
msgctxt "footstrap"
|
||||
msgid "Cards"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:342
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:331
|
||||
msgctxt "footstrap"
|
||||
msgid "Choose SVG"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:309
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:311
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:324
|
||||
msgctxt "footstrap"
|
||||
msgid "Choose image"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:222
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:258
|
||||
msgctxt "footstrap"
|
||||
msgid "Clearly visible %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:369
|
||||
#: ucode/template/themes/footstrap/header.ut:133
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:372
|
||||
#: ucode/template/themes/footstrap/header.ut:144
|
||||
msgid "Collapse menu"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:347
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:612
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:344
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:650
|
||||
msgctxt "footstrap"
|
||||
msgid "Colours"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:182
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:188
|
||||
msgctxt "footstrap"
|
||||
msgid "Compact"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:508
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:545
|
||||
msgctxt "footstrap"
|
||||
msgid "Confirm reset"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:232
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:268
|
||||
msgctxt "footstrap"
|
||||
msgid "Contrast %s:1 (WCAG AA wants %s:1 here)"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:275
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:281
|
||||
msgctxt "footstrap"
|
||||
msgid "Controls"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1014
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1016
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1099
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1101
|
||||
msgctxt "footstrap"
|
||||
msgid "Could not process the image."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:484
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:521
|
||||
msgctxt "footstrap"
|
||||
msgid "Could not save the default. Reload the page and try again."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:250
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:256
|
||||
msgctxt "footstrap"
|
||||
msgid "Danger"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:171
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:174
|
||||
msgctxt "footstrap"
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:616
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:654
|
||||
msgctxt "footstrap"
|
||||
msgid "Defaults"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:181
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:187
|
||||
msgctxt "footstrap"
|
||||
msgid "Density"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:318
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:341
|
||||
msgctxt "footstrap"
|
||||
msgid "Dim"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:225
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:261
|
||||
msgctxt "footstrap"
|
||||
msgid "Easy to read %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:369
|
||||
#: htdocs/luci-static/resources/fs-chrome.js:372
|
||||
msgid "Expand menu"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:413
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:366
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:451
|
||||
msgctxt "footstrap"
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
@@ -156,22 +156,22 @@ msgstr ""
|
||||
msgid "Go to password configuration..."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:238
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:244
|
||||
msgctxt "footstrap"
|
||||
msgid "Good"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:227
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:263
|
||||
msgctxt "footstrap"
|
||||
msgid "Hard to read %s — large text only"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:611
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:649
|
||||
msgctxt "footstrap"
|
||||
msgid "Interface"
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:32
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:60
|
||||
msgid "Invalid username and/or password! Please try again."
|
||||
msgstr ""
|
||||
|
||||
@@ -179,49 +179,49 @@ msgstr ""
|
||||
msgid "JavaScript required!"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:198
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:204
|
||||
msgctxt "footstrap"
|
||||
msgid "Keep open"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:184
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:190
|
||||
msgctxt "footstrap"
|
||||
msgid "Large"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:163
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:166
|
||||
msgctxt "footstrap"
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:170
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:173
|
||||
msgctxt "footstrap"
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-router.js:466
|
||||
#: htdocs/luci-static/resources/fs-router.js:501
|
||||
msgid "Loading view…"
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:58
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:142
|
||||
msgid "Log in"
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/partials/logout.ut:11
|
||||
#: ucode/template/themes/footstrap/partials/logout.ut:13
|
||||
msgid "Logout"
|
||||
msgid "Log out"
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/partials/footer.ut:27
|
||||
msgid "Lua compatibility mode active"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-overview.js:34
|
||||
#: htdocs/luci-static/resources/fs-overview.js:32
|
||||
msgid "Memory"
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/header.ut:130
|
||||
#: ucode/template/themes/footstrap/header.ut:141
|
||||
#: ucode/template/themes/footstrap/header.ut:152
|
||||
msgid "Menu"
|
||||
msgstr ""
|
||||
|
||||
@@ -231,7 +231,7 @@ msgid ""
|
||||
"mode should only be used to install a firmware upgrade"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:316
|
||||
#: htdocs/luci-static/resources/fs-search.js:320
|
||||
msgctxt "footstrap"
|
||||
msgid "No pages found"
|
||||
msgstr ""
|
||||
@@ -240,155 +240,157 @@ msgstr ""
|
||||
msgid "No password set!"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:183
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:189
|
||||
msgctxt "footstrap"
|
||||
msgid "Normal"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:411
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:449
|
||||
msgctxt "footstrap"
|
||||
msgid "Off"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:233
|
||||
#: htdocs/luci-static/resources/fs-search.js:237
|
||||
msgctxt "footstrap"
|
||||
msgid "Pages"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:174
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:180
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:177
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:216
|
||||
msgctxt "footstrap"
|
||||
msgid "Palette"
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:49
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:82
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:412
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:352
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:450
|
||||
msgctxt "footstrap"
|
||||
msgid "Pattern"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:882
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:884
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:945
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:947
|
||||
msgctxt "footstrap"
|
||||
msgid "Please choose an SVG file."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1029
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1131
|
||||
msgctxt "footstrap"
|
||||
msgid "Please choose an image file."
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:39
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:72
|
||||
msgid "Please enter your username and password."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:315
|
||||
#: htdocs/luci-static/resources/fs-search.js:319
|
||||
msgctxt "footstrap"
|
||||
msgid "Recently visited"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:310
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:344
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:326
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:333
|
||||
msgctxt "footstrap"
|
||||
msgid "Remove"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:459
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:519
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:483
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:556
|
||||
msgctxt "footstrap"
|
||||
msgid "Reset to default"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:458
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:518
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:478
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:555
|
||||
msgctxt "footstrap"
|
||||
msgid "Reset to saved"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:187
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:193
|
||||
msgctxt "footstrap"
|
||||
msgid "Rounding"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:532
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:569
|
||||
msgctxt "footstrap"
|
||||
msgid "Router default"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:452
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:473
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:472
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:503
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:510
|
||||
msgctxt "footstrap"
|
||||
msgid "Save as default"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:473
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:510
|
||||
msgctxt "footstrap"
|
||||
msgid "Saved as default"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:345
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:342
|
||||
msgctxt "footstrap"
|
||||
msgid "Scale"
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/partials/search.ut:14
|
||||
#: ucode/template/themes/footstrap/partials/search.ut:16
|
||||
#: ucode/template/themes/footstrap/partials/search.ut:18
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:227
|
||||
#: htdocs/luci-static/resources/fs-search.js:254
|
||||
#: htdocs/luci-static/resources/fs-search.js:231
|
||||
#: htdocs/luci-static/resources/fs-search.js:258
|
||||
msgctxt "footstrap"
|
||||
msgid "Search pages"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:228
|
||||
#: htdocs/luci-static/resources/fs-search.js:232
|
||||
msgctxt "footstrap"
|
||||
msgid "Search pages…"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:164
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:167
|
||||
msgctxt "footstrap"
|
||||
msgid "Sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:281
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:287
|
||||
msgctxt "footstrap"
|
||||
msgid "Sidebar and bar"
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/header.ut:109
|
||||
#: ucode/template/themes/footstrap/header.ut:120
|
||||
msgid "Skip to content"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-search.js:316
|
||||
#: htdocs/luci-static/resources/fs-search.js:320
|
||||
msgctxt "footstrap"
|
||||
msgid "Start typing to find a page"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-overview.js:34
|
||||
#: htdocs/luci-static/resources/fs-overview.js:32
|
||||
msgid "Storage"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:346
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:343
|
||||
msgctxt "footstrap"
|
||||
msgid "Strength"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:196
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:202
|
||||
msgctxt "footstrap"
|
||||
msgid "Submenus"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:613
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:651
|
||||
msgctxt "footstrap"
|
||||
msgid "Surfaces"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-overview.js:34
|
||||
#: htdocs/luci-static/resources/fs-overview.js:32
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
|
||||
@@ -396,41 +398,46 @@ msgstr ""
|
||||
msgid "System running in recovery (initramfs) mode."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:830
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:893
|
||||
msgctxt "footstrap"
|
||||
msgid ""
|
||||
"That SVG contains script or external references, which this theme will not "
|
||||
"install."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:854
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:917
|
||||
msgctxt "footstrap"
|
||||
msgid "That file could not be read."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1018
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1103
|
||||
msgctxt "footstrap"
|
||||
msgid "That file is not a readable image."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:826
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:829
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:889
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:892
|
||||
msgctxt "footstrap"
|
||||
msgid "That file is not an SVG image."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:885
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:948
|
||||
msgctxt "footstrap"
|
||||
msgid "That file is too large."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1031
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1133
|
||||
msgctxt "footstrap"
|
||||
msgid "That image is too large."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:168
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:356
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1026
|
||||
msgctxt "footstrap"
|
||||
msgid "The router refused to delete the file (ubus status %d)."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:171
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:360
|
||||
msgctxt "footstrap"
|
||||
msgid "Theme"
|
||||
msgstr ""
|
||||
@@ -441,51 +448,59 @@ msgid ""
|
||||
"protect the web interface."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:208
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:504
|
||||
msgctxt "footstrap"
|
||||
msgid ""
|
||||
"This browser is not storing preferences (site data is blocked), so a change "
|
||||
"here lasts until you reload. Saving as default still works and applies to "
|
||||
"every browser."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:214
|
||||
msgctxt "footstrap"
|
||||
msgid "Tint (router identification)"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:223
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:229
|
||||
msgctxt "footstrap"
|
||||
msgid "Tint strength"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:228
|
||||
#: htdocs/luci-static/resources/fs-widgets.js:264
|
||||
msgctxt "footstrap"
|
||||
msgid "Too faint to read %s"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:165
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:168
|
||||
msgctxt "footstrap"
|
||||
msgid "Top"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:897
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:900
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:958
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1042
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1045
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:960
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:963
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1043
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1144
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1147
|
||||
msgctxt "footstrap"
|
||||
msgid "Upload failed."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:383
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:422
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:402
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:432
|
||||
msgctxt "footstrap"
|
||||
msgid "Uploading…"
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:43
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:76
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:303
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:447
|
||||
msgctxt "footstrap"
|
||||
msgid "Wallpaper"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:244
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:250
|
||||
msgctxt "footstrap"
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
@@ -495,27 +510,33 @@ msgid ""
|
||||
"You must enable JavaScript in your browser or LuCI will not work properly."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:284
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:290
|
||||
msgctxt "footstrap"
|
||||
msgid "in the sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:235
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:241
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:247
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:253
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:272
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:291
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:259
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:278
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:297
|
||||
msgctxt "footstrap"
|
||||
msgid "on a card"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:278
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:284
|
||||
msgctxt "footstrap"
|
||||
msgid "on a control"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:213
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:219
|
||||
msgctxt "footstrap"
|
||||
msgid "on the canvas"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-prefs.js:1121
|
||||
msgctxt "footstrap"
|
||||
msgid ""
|
||||
"the uploaded file could not be removed either; it is still on the router."
|
||||
msgstr ""
|
||||
@@ -6,16 +6,20 @@
|
||||
# upgrade branch: on a real router the guard was DEAD, and an upgrade that found an empty
|
||||
# mediaurlbase would activate this theme behind the user's back. The marker file is the state that
|
||||
# really tells the two apart — it exists iff this script has completed at least once.
|
||||
# `${PKG_UPGRADE:-}`, not `$PKG_UPGRADE`: the variable is unset on every real path (that is the
|
||||
# point of the paragraph above), and this file is `sh`-run by the Makefile today but sourced by
|
||||
# whatever runs uci-defaults tomorrow — under `set -u` a bare expansion aborts the script here,
|
||||
# the theme is never registered, and the Makefile's `>/dev/null 2>&1` swallows the reason.
|
||||
FS_MARKER=/usr/share/luci-theme-footstrap/.installed
|
||||
if [ "$PKG_UPGRADE" = 1 ] || [ -f "$FS_MARKER" ]; then
|
||||
if [ "${PKG_UPGRADE:-}" = 1 ] || [ -f "$FS_MARKER" ]; then
|
||||
PKG_UPGRADE=1
|
||||
else
|
||||
PKG_UPGRADE=0
|
||||
fi
|
||||
|
||||
# Every theme name this package has EVER registered — delete them all; the ONE surviving entry
|
||||
# is re-added below (layout, mode and palette are client-side toggles in the Appearance popover,
|
||||
# so they are not theme entries). The Makefile's postrm must delete the same set (it runs when
|
||||
# is re-added below (layout, mode and palette are client-side axes on the Footstrap tab of
|
||||
# System -> System, so they are not theme entries). The Makefile's postrm must delete the same set (it runs when
|
||||
# this file is already gone, so it cannot source it) and the two lists had already drifted into a
|
||||
# different ORDER — which is how one of them would quietly lose a name. Only the assignment is
|
||||
# pinned: the Makefile writes `$$t` where this writes `$t`.
|
||||
@@ -26,8 +30,19 @@ for t in $LEGACY_THEMES; do
|
||||
uci -q delete "luci.themes.$t"
|
||||
done
|
||||
|
||||
# WHETHER THE REGISTRATION ACTUALLY LANDED. Every uci WRITE below is checked into this flag, and a
|
||||
# run that ends with it set refuses to write the marker and exits non-zero — which is what makes the
|
||||
# failure survivable: OpenWrt's default_postinst deletes a uci-defaults script only when it exits 0
|
||||
# (`( . "$i" ) && rm -f "$i"`), so a non-zero run is retried on the next boot. Without this the
|
||||
# script reported success on a full or read-only overlay, on a locked /etc/config/luci and mid-
|
||||
# upgrade: the theme was never registered, the script was deleted so nothing retried it, AND the
|
||||
# marker was written anyway — so the next install took the upgrade branch, which never activates a
|
||||
# theme. One unwritable file turned into a permanently unthemed router, silently. Deletes are NOT
|
||||
# counted: `uci -q delete` on an absent entry exits 1 and that is the normal case here.
|
||||
fail=0
|
||||
|
||||
# The ONE theme entry: sidebar and top bar are one template morphed by :root[data-layout].
|
||||
uci set luci.themes.Footstrap=/luci-static/footstrap
|
||||
uci set luci.themes.Footstrap=/luci-static/footstrap || fail=1
|
||||
|
||||
# Normalise the active theme. Every legacy media path below is now neither registered in
|
||||
# luci.themes nor on disk, so an install left pointing at one is stranded: the Design page does
|
||||
@@ -45,7 +60,7 @@ case "$cur" in
|
||||
# was on the top-nav theme (incl. its legacy dark/light variants) -> keep the bar
|
||||
/luci-static/footstrap-top*)
|
||||
[ -n "$(uci -q get luci.main.footstrap_layout)" ] || \
|
||||
uci set luci.main.footstrap_layout=top
|
||||
uci set luci.main.footstrap_layout=top || fail=1
|
||||
new=/luci-static/footstrap ;;
|
||||
# any other footstrap path (incl. the legacy -dark/-light) -> the built-in default (the bar):
|
||||
# this branch deliberately sets no footstrap_layout, and absent means top (head.ut)
|
||||
@@ -73,9 +88,9 @@ case "$new" in
|
||||
;;
|
||||
esac
|
||||
|
||||
[ "$new" = "$cur" ] || uci set luci.main.mediaurlbase="$new"
|
||||
[ "$new" = "$cur" ] || uci set luci.main.mediaurlbase="$new" || fail=1
|
||||
|
||||
uci commit luci
|
||||
uci commit luci || fail=1
|
||||
|
||||
# Appearance "Save as default" writes the router-wide axis defaults into /etc/config/footstrap
|
||||
# through the scoped uci ACL (config 'footstrap' only). The rpcd uci `set` backend the browser
|
||||
@@ -84,8 +99,8 @@ uci commit luci
|
||||
# to CREATE the config file (only the section within one), so touch it first. Idempotent.
|
||||
[ -f /etc/config/footstrap ] || touch /etc/config/footstrap
|
||||
if ! uci -q get footstrap.settings >/dev/null 2>&1; then
|
||||
uci set footstrap.settings=footstrap
|
||||
uci commit footstrap
|
||||
uci set footstrap.settings=footstrap || fail=1
|
||||
uci commit footstrap || fail=1
|
||||
fi
|
||||
|
||||
# Login/page background upload (Appearance -> Wallpaper -> File). cgi-upload writes the image to
|
||||
@@ -111,6 +126,13 @@ mkdir -p /etc/footstrap 2>/dev/null
|
||||
# woff2 files. `-n` does not help — measured on a 25.12 router: exit 0, link created inside.
|
||||
# So clear the path first unless it is already the symlink we want. What is being deleted can only
|
||||
# be ours, and its contents are package files of a version that no longer exists.
|
||||
#
|
||||
# DO NOT DELETE THIS AS "an old migration". It is needed for every router whose last footstrap was
|
||||
# v0.12.0 or older, because that is where the package still SHIPPED files in that directory — and
|
||||
# there is no floor on how old an installed footstrap can be. Without it `ln -sfn` exits 0 with the
|
||||
# link planted one level down, the admin's webfonts stop serving, and footstrap.settings.fonts
|
||||
# points at a 404 with nothing reporting anything. The doodle cleanup below is the opposite case:
|
||||
# dropping it costs 200 kB of flash and nothing else.
|
||||
if [ -d /www/luci-static/footstrap ]; then
|
||||
[ ! -e /www/luci-static/footstrap/fonts ] || [ -L /www/luci-static/footstrap/fonts ] ||
|
||||
rm -rf /www/luci-static/footstrap/fonts
|
||||
@@ -128,6 +150,12 @@ rm -rf /tmp/luci-modulecache 2>/dev/null
|
||||
|
||||
# Write the marker LAST, so a run that dies half-way is still a fresh install next
|
||||
# time rather than an upgrade.
|
||||
if [ "$fail" = 1 ]; then
|
||||
echo "luci-theme-footstrap: a uci write failed — not marking this install as complete;" >&2
|
||||
echo " this script stays in /etc/uci-defaults and runs again on the next boot." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$FS_MARKER")" 2>/dev/null
|
||||
: > "$FS_MARKER" 2>/dev/null
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
# ./strip-shell.sh <dir>
|
||||
#
|
||||
# Same trade as strip-templates.sh and build-css.sh: the reader gets the comments from git, the
|
||||
# router gets the bytes. `root/etc/uci-defaults/30_luci-theme-footstrap` is 71% comment lines and
|
||||
# `root/lib/upgrade/keep.d/luci-theme-footstrap` is 95% (six lines of reasoning above one path).
|
||||
# router gets the bytes. `root/etc/uci-defaults/30_luci-theme-footstrap` is 51% comment lines and
|
||||
# `root/lib/upgrade/keep.d/luci-theme-footstrap` is 77% (ten lines of reasoning above three paths).
|
||||
#
|
||||
# ONLY a line whose first non-blank character is `#`, and never the shebang. That rule is safe here
|
||||
# and the reason is checked, not assumed: sh has no block-comment syntax, so the only way a `#` can
|
||||
@@ -21,37 +21,55 @@ set -e
|
||||
DIR="${1:-}"
|
||||
[ -n "$DIR" ] && [ -d "$DIR" ] || { echo "usage: strip-shell.sh <dir>" >&2; exit 1; }
|
||||
|
||||
if find "$DIR" -type f -exec grep -l '<<' {} + 2>/dev/null | grep -q .; then
|
||||
echo "strip-shell: a heredoc appeared under $DIR — refusing (a '#' line inside one is DATA, not a comment)" >&2
|
||||
exit 1
|
||||
fi
|
||||
# ONE trap, covering the file list and whatever temp the loop is holding when it dies — the same
|
||||
# shape build-css.sh, mangle-tokens.sh and update-po.sh already use. Under `set -e` an awk failure
|
||||
# used to leave `<file>.tmp<pid>` behind, and stage.sh runs this over the staged payload, so the
|
||||
# leftover would be copied into the package: a `30_luci-theme-footstrap.tmp1234` under
|
||||
# /etc/uci-defaults is a file the ROUTER executes and deletes on its next boot.
|
||||
LIST=$(mktemp)
|
||||
CUR=""
|
||||
trap 'rm -f "$LIST" ${CUR:+"$CUR"}' EXIT INT TERM
|
||||
|
||||
# The list goes through a file rather than `for f in $(find …)`: that splits on whitespace, so a
|
||||
# checkout under a path with a space in it fed this loop two halves of a directory name and the
|
||||
# script reported "no shell file" while pointing at the tree instead of at the path.
|
||||
find "$DIR" -type f | sort > "$LIST"
|
||||
|
||||
before=0
|
||||
after=0
|
||||
found=0
|
||||
for f in $(find "$DIR" -type f | sort); do
|
||||
# shell only: a shebang, or the two comment-only manifests this package ships
|
||||
while IFS= read -r f; do
|
||||
# shell only: a shebang, or the one comment-only manifest this package ships (keep.d). The other
|
||||
# two files under root/ — the uci config and the rpcd ACL — are deliberately left alone.
|
||||
case "$(head -c 2 "$f")" in
|
||||
'#!') ;;
|
||||
*) case "$f" in */keep.d/*) ;; *) continue ;; esac ;;
|
||||
esac
|
||||
found=$((found + 1))
|
||||
b=$(wc -c < "$f")
|
||||
awk 'NR == 1 && /^#!/ { print; next }
|
||||
/^[ \t]*#/ { next }
|
||||
{ print }' "$f" > "$f.tmp$$"
|
||||
# never leave a file that lost its shebang or came out empty
|
||||
if [ ! -s "$f.tmp$$" ]; then
|
||||
echo "strip-shell: $f came out empty — refusing" >&2
|
||||
rm -f "$f.tmp$$"
|
||||
# The heredoc refusal, over the files this loop actually REWRITES. It used to run over the whole
|
||||
# tree, so a `<<` inside the rpcd ACL or the uci config — neither of which is ever touched here —
|
||||
# aborted the package build for a hazard that cannot apply to them.
|
||||
if grep -q '<<' "$f"; then
|
||||
echo "strip-shell: a heredoc in $f — refusing (a '#' line inside one is DATA, not a comment)" >&2
|
||||
exit 1
|
||||
fi
|
||||
cat "$f.tmp$$" > "$f" # cat, not mv: keep the original mode (uci-defaults must stay +x)
|
||||
rm -f "$f.tmp$$"
|
||||
found=$((found + 1))
|
||||
b=$(wc -c < "$f")
|
||||
CUR="$f.tmp$$"
|
||||
awk 'NR == 1 && /^#!/ { print; next }
|
||||
/^[ \t]*#/ { next }
|
||||
{ print }' "$f" > "$CUR"
|
||||
# never leave a file that lost its shebang or came out empty
|
||||
if [ ! -s "$CUR" ]; then
|
||||
echo "strip-shell: $f came out empty — refusing" >&2
|
||||
exit 1
|
||||
fi
|
||||
cat "$CUR" > "$f" # cat, not mv: keep the original mode (uci-defaults must stay +x)
|
||||
rm -f "$CUR"
|
||||
CUR=""
|
||||
a=$(wc -c < "$f")
|
||||
before=$((before + b))
|
||||
after=$((after + a))
|
||||
done
|
||||
done < "$LIST"
|
||||
|
||||
[ "$found" -gt 0 ] || { echo "strip-shell: no shell file under $DIR" >&2; exit 1; }
|
||||
echo "strip-shell: $found file(s), $before -> $after bytes (-$((before - after)))"
|
||||
@@ -6,7 +6,7 @@
|
||||
# Run from the package Makefile (Build/Prepare) over $(PKG_BUILD_DIR), never over the source tree:
|
||||
# git keeps every word, the router does not need any of them. Same trade this project already makes
|
||||
# for JS (jsmin/terser) and CSS (build-css.sh) — templates were simply never included in it, and
|
||||
# they are 58% comments. `{# … #}` alone is 16112 of 39426 bytes (41%).
|
||||
# they are mostly comments: `{# … #}` is 22633 of 60227 bytes (38%).
|
||||
#
|
||||
# sh + awk only, like build-css.sh: an OpenWrt buildbot has no node and this must not become the
|
||||
# reason a build needs one.
|
||||
@@ -23,21 +23,32 @@
|
||||
# strings. Not worth 6.8 KB.
|
||||
#
|
||||
# Whitespace control is EMULATED, not ignored: `{#- …` also eats the whitespace before the comment
|
||||
# and `… -#}` the whitespace after it, which is how ucode itself renders them. Every .ut here opens
|
||||
# with a licence block closing `-#}` to swallow the newline before <!DOCTYPE html>; dropping the
|
||||
# comment without the trim would put that newline back.
|
||||
# and `… -#}` the whitespace after it, which is how ucode itself renders them. Nearly every .ut here
|
||||
# opens with a licence block closing `-#}` (footer.ut is one `{% include %}` line and has none);
|
||||
# head.ut's is the one that matters, swallowing the newline before its <!DOCTYPE html>, and dropping
|
||||
# the comment without the trim would put that newline back.
|
||||
set -e
|
||||
|
||||
DIR="${1:-}"
|
||||
[ -n "$DIR" ] && [ -d "$DIR" ] || { echo "usage: strip-templates.sh <dir>" >&2; exit 1; }
|
||||
|
||||
# ONE trap over the file list and whatever temp the loop holds, and the list goes through a file
|
||||
# rather than `for f in $(find …)`: the same two fixes as strip-shell.sh, for the same reasons — a
|
||||
# checkout path with a space in it was split into words, and an awk failure under `set -e` left a
|
||||
# `<file>.tmp<pid>` inside the staged payload, which stage.sh then copies into the package.
|
||||
LIST=$(mktemp)
|
||||
CUR=""
|
||||
trap 'rm -f "$LIST" ${CUR:+"$CUR"}' EXIT INT TERM
|
||||
find "$DIR" -name '*.ut' -type f | sort > "$LIST"
|
||||
|
||||
before=0
|
||||
after=0
|
||||
found=0
|
||||
|
||||
for f in $(find "$DIR" -name '*.ut' -type f | sort); do
|
||||
while IFS= read -r f; do
|
||||
found=$((found + 1))
|
||||
b=$(wc -c < "$f")
|
||||
CUR="$f.tmp$$"
|
||||
awk '
|
||||
BEGIN { RS = "^$" } # slurp the whole file
|
||||
{
|
||||
@@ -60,12 +71,13 @@ for f in $(find "$DIR" -name '*.ut' -type f | sort); do
|
||||
}
|
||||
printf "%s", out
|
||||
}
|
||||
' "$f" > "$f.tmp$$"
|
||||
mv "$f.tmp$$" "$f"
|
||||
' "$f" > "$CUR"
|
||||
mv "$CUR" "$f"
|
||||
CUR=""
|
||||
a=$(wc -c < "$f")
|
||||
before=$((before + b))
|
||||
after=$((after + a))
|
||||
done
|
||||
done < "$LIST"
|
||||
|
||||
[ "$found" -gt 0 ] || { echo "strip-templates: no .ut under $DIR" >&2; exit 1; }
|
||||
echo "strip-templates: $found file(s), $before -> $after bytes (-$((before - after)))"
|
||||
@@ -42,8 +42,8 @@
|
||||
* measures 1.15 / 1.27 / 1.17 / 1.29 : 1 across palette x mode. That is a GLOW, not an
|
||||
* indicator: WCAG 1.4.11 wants 3:1 for the thing that tells you where focus is. It is
|
||||
* legitimate only BESIDE a second channel — every field here also flips border-color to
|
||||
* --fs-accent, and that change measures 4.88-6.52:1, so the border is the indicator and
|
||||
* the tint is the halo around it.
|
||||
* --fs-accent, and that change measures 4.57-7.04:1 across the three palettes x both modes
|
||||
* x the three surfaces, so the border is the indicator and the tint is the halo around it.
|
||||
*
|
||||
* -solo is for a control where the ring is the WHOLE indicator: nothing else about it
|
||||
* changes on focus. Three groups were relying on the tint alone and therefore showed
|
||||
@@ -51,7 +51,8 @@
|
||||
* buttons (10-chrome.css), the section tabs (40-tabs.css) and both range sliders. The
|
||||
* 2px of surface is what lets it read against a FILLED control too (an accent-filled
|
||||
* button or the active tab pill would otherwise take an accent ring on an accent box);
|
||||
* solid accent then measures 5.19 / 5.39 / 6.52 / 8.52 on panel, 4.88-9.14 on the page.
|
||||
* solid accent then measures 5.19 / 5.39 / 5.22 / 7.04 / 5.81 / 5.24 on panel across the six
|
||||
* palette x mode blocks, and 4.61-7.55 on the canvas.
|
||||
* It had been written out as a literal twice (the filled buttons, the active pill) before
|
||||
* it had a name — the drift shape the ladder exists to prevent.
|
||||
*
|
||||
@@ -127,9 +128,9 @@
|
||||
--fs-blur: blur(12px);
|
||||
|
||||
/* the circular-arrows refresh glyph, as a mask so its colour follows the palette
|
||||
* (--fs-good active / --fs-dim paused). ONE copy: the poll-status pill collapses to it
|
||||
* both in the rail (20-shell.css) and in the top bar before it stacks (50-toplayout.css),
|
||||
* and inlining the SVG twice is exactly the drift css-dup exists to catch. STATIC glyph
|
||||
* (--fs-good active / --fs-dim paused). ONE copy: the poll-status pill collapses to it both
|
||||
* in the collapsed rail and in the bar's compact cluster (20-shell.css owns both rules), and
|
||||
* inlining the SVG twice is exactly the drift css-dup exists to catch. STATIC glyph
|
||||
* on purpose — see the rail note; a spinner makes an idle poll look busy and moves the
|
||||
* click target that pauses it. */
|
||||
/* OURS. It was derived from Lucide's `refresh-cw` (ISC) — `M21 3v5h-5` was byte-identical,
|
||||
@@ -158,9 +159,11 @@
|
||||
* `filter` recolours the TEXT too, and a light-mode solid button is a saturated fill
|
||||
* with WHITE ink, which cannot get brighter — on --fs-accent (#0969da) white is
|
||||
* 5.19:1 at rest, brightness(1.08) drops it to 4.59:1 and a visible 1.15 to 4.08:1,
|
||||
* an AA failure caused by hovering. So light DARKENS (0.90 -> 6.16:1: bigger cue AND
|
||||
* better contrast); dark, whose fill is light and ink dark, brightens (1.15 -> ~8:1).
|
||||
* Both move the button away from the page it sits on. */
|
||||
* an AA failure caused by hovering. So light DARKENS: .90 moves fill and ink together
|
||||
* and lands at 4.94:1 — a hair under the resting 5.19 rather than the 6.16 you get by
|
||||
* darkening the fill alone, and still clear of the 4.5 floor that brightening breaks.
|
||||
* Dark, whose fill is light and ink dark, brightens instead (1.15). Both move the button
|
||||
* away from the page it sits on, which is what the cue has to do. */
|
||||
--fs-hover-lift: brightness(.90);
|
||||
|
||||
/* ---- MOTION — four durations, because the UI does four things. There used to be
|
||||
@@ -215,9 +218,10 @@
|
||||
* localStorage `fs-radius`, pre-painted inline on :root by partials/head.ut before
|
||||
* first paint); the three semantic radii scale from it proportionally so every
|
||||
* surface rounds in step, and pills stay fully round. At base 12: card 12, control
|
||||
* 10, chip 8. Every border-radius in theme/pages reads one of these — the only bare
|
||||
* px left are 0 / 50% / hairline insets and two that are not a card's corner
|
||||
* (theme/60-inputs.css 8px = half the 16px scrollbar; theme/70-modal.css 4px). */
|
||||
* 10, chip 8. Every border-radius in theme/pages reads one of these — the only bare px left
|
||||
* are 0, 50%, and three that are not a card's corner at all: the checkbox tick box's 2px
|
||||
* (theme/60-inputs.css) and the 1px on each of the two chevron glyphs (pages/20-overview.css,
|
||||
* pages/80-appearance.css), which are 7px squares rotated 45°. */
|
||||
--fs-radius-base: 12px;
|
||||
--fs-radius-lg: var(--fs-radius-base); /* cards, panels, modals, table frame, popovers */
|
||||
--fs-radius: calc(var(--fs-radius-base) * 5 / 6); /* controls: inputs, buttons, dropdowns, tabs (10 @12) */
|
||||
@@ -267,7 +271,7 @@
|
||||
* between a heading and the text under it, where before there was one pixel.
|
||||
* Ratio is roughly 1.25 per step. Anything that needs a size not on this list is making a
|
||||
* design decision and should say so in a comment. */
|
||||
/* ---- DENSITY: one axis, two multipliers, and it only works because of the scales ----
|
||||
/* ---- DENSITY: one axis, three multipliers, and it only works because of the scales ----
|
||||
*
|
||||
* Compact / Normal / Large is a CLIENT preference (Appearance), stamped as
|
||||
* :root[data-density] — absent means Normal, so the default costs no attribute and no
|
||||
@@ -276,16 +280,20 @@
|
||||
* THREE multipliers, because three different kinds of size want different treatment and one
|
||||
* knob gets all three wrong:
|
||||
*
|
||||
* --fs-density-type TEXT. Compact does not touch it at all: "compact" is a request for
|
||||
* less air, not for smaller writing, and shrinking the text is how a
|
||||
* density control turns into an accessibility problem.
|
||||
* --fs-density-type TEXT, and it moves LEAST: .9 at Compact, 1.15 at Large. "Compact" is
|
||||
* a request for less air, not for smaller writing, so the type gives up
|
||||
* a tenth where the air gives up a third — at .9 the base is 12px and
|
||||
* the eyebrow 10px, which is the floor this file is willing to go to,
|
||||
* and only because the user asked for the mode by name.
|
||||
* --fs-density-space pure AIR — padding, margin, gap. This is what actually buys the room,
|
||||
* so it moves the most (.65 at Compact).
|
||||
* so it moves the most (.65 at Compact). It does NOT grow at Large: the
|
||||
* point there is bigger text, and inflating the air with it just reads
|
||||
* as zoom, which is the browser's job.
|
||||
* --fs-density-box boxes that must HOLD text or an icon: the sidebar column, the rail,
|
||||
* the bar height, the square icon buttons, the control-height floors.
|
||||
* These cannot follow the air: with type unchanged, a sidebar cut by
|
||||
* 35% would no longer fit the menu labels it exists to show. They move
|
||||
* gently (.85), which is roughly what their own padding gave up.
|
||||
* These cannot follow the air: the type only gives up a tenth, so a
|
||||
* sidebar cut by 35% would no longer fit the menu labels it exists to
|
||||
* show. They move gently (.85 / 1.15).
|
||||
*
|
||||
* This is only expressible because every size in the theme now reads one of these tokens:
|
||||
* before that, a density axis would have meant hunting ~240 literals and would have gone
|
||||
@@ -312,11 +320,21 @@
|
||||
--fs-ctl-h: calc(38px * var(--fs-density-box)); /* the default control */
|
||||
--fs-ctl-h-lg: calc(44px * var(--fs-density-box)); /* prominent: .btn.large, the login submit */
|
||||
|
||||
--fs-type-xs: calc(round(11px * var(--fs-density-type), 1px)); /* eyebrow, badges, tooltips, dense metadata */
|
||||
--fs-type: calc(round(13px * var(--fs-density-type), 1px)); /* body, tables, fields, menu items — the base */
|
||||
--fs-type-lg: calc(round(16px * var(--fs-density-type), 1px)); /* section titles (h3/legend), alert headings */
|
||||
--fs-type-xl: calc(round(20px * var(--fs-density-type), 1px)); /* page titles */
|
||||
--fs-type-2xl: calc(round(26px * var(--fs-density-type), 1px)); /* h1 — only a third-party app emits a bare one */
|
||||
/* UNROUNDED HERE, ROUNDED IN THE @supports BELOW, and the split is not style: it is the
|
||||
* difference between a soft failure and no page. `round()` is the YOUNGEST thing this theme
|
||||
* depends on (Chromium 125, Firefox 118, Safari 15.4 — years after :has(), color-mix() and
|
||||
* @layer), and a custom property holds any token stream, so an engine without it PARSES these
|
||||
* fine and fails at SUBSTITUTION: `font-size: var(--fs-type)` is then invalid at
|
||||
* computed-value time and computes to `unset`. Measured by serving a sheet with `round(`
|
||||
* renamed: 5881 of 5912 elements on Overview change, body type goes 13px -> 16px, 5880
|
||||
* font-size and 5733 line-height declarations are lost, and --fs-control-h takes every
|
||||
* field's min-height with it because it reads --fs-type. Declared this way such an engine
|
||||
* keeps the whole ladder and pays a fractional pixel at the two non-default densities. */
|
||||
--fs-type-xs: calc(11px * var(--fs-density-type)); /* eyebrow, badges, tooltips, dense metadata */
|
||||
--fs-type: calc(13px * var(--fs-density-type)); /* body, tables, fields, menu items — the base */
|
||||
--fs-type-lg: calc(16px * var(--fs-density-type)); /* section titles (h3/legend), alert headings */
|
||||
--fs-type-xl: calc(20px * var(--fs-density-type)); /* page titles */
|
||||
--fs-type-2xl: calc(26px * var(--fs-density-type)); /* h1 — only a third-party app emits a bare one */
|
||||
/* ---- WEIGHT: there are only ever three, and they had six spellings ----------
|
||||
* The theme ships no font at all — the stacks name Manrope and JetBrains Mono first and
|
||||
* fall through to the system faces — so these are not a ramp to pick from: they are the
|
||||
@@ -383,8 +401,9 @@
|
||||
--fs-space-6: calc(24px * var(--fs-density-space));
|
||||
--fs-space-7: calc(28px * var(--fs-density-space));
|
||||
/* The top of the ladder is sparse on purpose — these three are gutters and insets, not
|
||||
* rhythm, and each has exactly one or two users (the login card's bottom margin, the gauge's
|
||||
* right gutter, the room a field leaves for its own chevron). They are named so that no
|
||||
* rhythm, and each has two or three users: the sidebar accordion's indent and the search
|
||||
* palette's bottom padding (-7), the room a field leaves for its own chevron (-8), the login
|
||||
* card's bottom margin and the gauge's two right gutters (-10). They are named so that no
|
||||
* padding in this theme is an unexplained number, not because a fourth caller is expected. */
|
||||
--fs-space-8: calc(32px * var(--fs-density-space));
|
||||
--fs-space-10: calc(40px * var(--fs-density-space));
|
||||
@@ -400,12 +419,12 @@
|
||||
|
||||
/* One control height for every field that is a single line — input, select, .cbi-select,
|
||||
* .cbi-dropdown, and the checkbox/radio row that must line up beside them. DERIVED, not
|
||||
* measured by hand: the text box (--fs-text x --fs-leading = 19.5px) plus the vertical
|
||||
* measured by hand: the text box (--fs-type x --fs-leading = 19.5px) plus the vertical
|
||||
* inset those fields carry (2 x --fs-space-1) plus both 1px borders = 29.5 -> ceil 32.
|
||||
* It was a bare 30px in four rules, which was upstream's same arithmetic over upstream's
|
||||
* 18px leading — so re-scaling the type left every control the wrong height for its own
|
||||
* text, silently. Stated once here so the two cannot drift again. */
|
||||
--fs-control-h: calc(round(up, calc(var(--fs-type) * var(--fs-leading) + var(--fs-space-1) * 2 + 2px), 4px));
|
||||
--fs-control-h: calc(var(--fs-type) * var(--fs-leading) + var(--fs-space-1) * 2 + 2px);
|
||||
|
||||
/* HOW WIDE AN ELASTIC CONTROL GETS IN A FORM ROW. A `.cbi-value-field` is `flex: 1`, so
|
||||
* anything asking for 100% takes the whole card — and a form whose every control ends at a
|
||||
@@ -536,15 +555,39 @@
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* ---- the rounded half of the type ladder, for engines that can round -------------
|
||||
*
|
||||
* A fractional font size renders soft, so every size here is meant to land on a whole pixel —
|
||||
* but `round()` in calc() is younger than everything else this theme needs, and a token that
|
||||
* uses it on an engine that does not is not a fallback, it is a DELETED token that takes every
|
||||
* rule reading it (the note by the ladder above has the measurement). So the ladder is declared
|
||||
* plainly up there and rounded here, where the at-rule can be asked first. --fs-control-h is
|
||||
* derived from --fs-type and rounds UP to the 4px control ladder for the same reason.
|
||||
*
|
||||
* `@supports (width: round(1px, 1px))` and not a `calc()` probe: the function has to be tested
|
||||
* where a length is expected, and `width` is the plainest property that takes one. */
|
||||
@supports (width: round(1px, 1px)) {
|
||||
:root {
|
||||
--fs-type-xs: calc(round(11px * var(--fs-density-type), 1px));
|
||||
--fs-type: calc(round(13px * var(--fs-density-type), 1px));
|
||||
--fs-type-lg: calc(round(16px * var(--fs-density-type), 1px));
|
||||
--fs-type-xl: calc(round(20px * var(--fs-density-type), 1px));
|
||||
--fs-type-2xl: calc(round(26px * var(--fs-density-type), 1px));
|
||||
--fs-control-h: calc(round(up, calc(var(--fs-type) * var(--fs-leading) + var(--fs-space-1) * 2 + 2px), 4px));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- the DENSITY axis, and it is exactly two declarations per step ----------------
|
||||
*
|
||||
* Normal is a bare :root — no attribute, no override — so the default path costs nothing and a
|
||||
* browser that has never touched the control renders identically to before this existed.
|
||||
*
|
||||
* Space moves further than type in both directions (0.8/1.25 against 0.92/1.15), because that is
|
||||
* what the two settings are actually for: Compact is a request for less AIR, not for smaller
|
||||
* writing — matching them would put body text at 10.4px, under the floor the rest of this file
|
||||
* defends. At Compact the base is 12px and at Large 15px; the eyebrow never drops below 10px.
|
||||
* Compact moves the air furthest (space .65 against type .9), because that is what the setting is
|
||||
* actually for: less AIR, not smaller writing — matching the type to the air would put the body
|
||||
* at 8.5px, far under the floor the rest of this file defends. Large is the mirror image and not
|
||||
* the symmetric one: type and box grow to 1.15 while the air stays at 1, so the mode reads as
|
||||
* bigger text rather than as zoom. At Compact the base is 12px and at Large 15px; the eyebrow
|
||||
* never drops below 10px.
|
||||
*
|
||||
* These multiply the LADDERS, not the rules, so a size that is on a ladder follows the axis for
|
||||
* free and one that is not silently ignores it. That is the whole reason the ladders came first. */
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
* THE INKS ARE PER PALETTE AND PER MODE, and that is not a style choice. A single global
|
||||
* `--fs-on-accent: #fff` used to sit on every accent/good/warn/danger fill (solid buttons,
|
||||
* the active tab pill, the toggle knob, .fs-seg, the login submit). White holds up on the
|
||||
* DARK fills of a LIGHT palette and nowhere else: on footstrap dark it measured 2.83–3.70:1
|
||||
* DARK fills of a LIGHT palette and nowhere else: on footstrap dark it measured 2.77–2.80:1
|
||||
* and on hicontrast dark --fs-good 1.69:1, where WCAG AA wants 4.5:1 for text and 3:1 for a
|
||||
* UI shape like the toggle knob. A dark palette has LIGHT fills, so its ink must be DARK.
|
||||
* Check a new colourway's inks against all four of its own fills: every pair below clears
|
||||
@@ -74,8 +74,17 @@
|
||||
* tint was invisible on blue hues and obvious on green ones, i.e. the cue's strength
|
||||
* depended on WHICH colour you picked. So instead: give the canvas the user's hue at a
|
||||
* chroma we choose, leave its LIGHTNESS alone (one relative-colour declaration).
|
||||
* ONE HONEST KNOB --fs-tint-c is the strength, in chroma, and behaves the same on
|
||||
* every hue — no gamut clipping at these values, no hue surprise.
|
||||
* ONE HONEST KNOB --fs-tint-c is the strength, in chroma, and in DARK mode it behaves the
|
||||
* same on every hue: no gamut clipping at these values, no hue surprise.
|
||||
* Light mode has less headroom and does clip at the top of the wheel
|
||||
* (measured: footstrap light hue 0 wants 1.011/0.955/0.970 of sRGB and is
|
||||
* clamped to 255,244,247), which costs a few units of the cue's strength but
|
||||
* not its direction — every hue still moves the canvas its own way. The one
|
||||
* surface where that stops being true is a canvas of pure white: on the
|
||||
* bootstrap colourway's #ffffff, 180° and 258° land 1/255 apart, so the cue
|
||||
* is nearly hue-blind there. That is the price of copying a stock theme's
|
||||
* surfaces exactly, and it is why the tint is designed against the default
|
||||
* palette's off-white rather than against white.
|
||||
* CONTRAST IS FREE `l` is copied through, so AA margins barely move. The mix dragged
|
||||
* lightness toward the anchor: an early attempt cost
|
||||
* --primary-color-high on --fs-bg its whole margin in hicontrast
|
||||
@@ -137,9 +146,18 @@
|
||||
--fs-good-h: 0;
|
||||
--fs-warn-h: 0;
|
||||
--fs-danger-h: 0;
|
||||
/* Tint DENSITY (Appearance → Density), a multiplier on the tint chroma below: 1 = the designed
|
||||
* strength, 0 = none, up to 2. fs-prefs.js/head.ut set it inline (value/100) when the user
|
||||
* drags the slider; paired with the hue, it is the second half of the router-identity Tint. */
|
||||
/* Tint strength (Appearance → Tint strength — NOT the Density select, which is the UI-density
|
||||
* axis and owns that word), a multiplier on the tint chroma below: 1 = the designed strength,
|
||||
* up to 2. fs-prefs.js/head.ut set it inline (value/100) when the user drags the slider;
|
||||
* paired with the hue, it is the second half of the router-identity Tint.
|
||||
*
|
||||
* 0 IS NOT "the palette back". The rule below is a relative colour that REPLACES chroma, so a
|
||||
* multiplier of 0 also strips the cast the canvas already had — every palette's canvas is a
|
||||
* blue-grey (hue 248-264, chroma .0153 dark / .0034 light, measured below), and at 0 it goes
|
||||
* neutral at the same lightness: footstrap dark rgb(28,33,40) paints as rgb(33,33,33). Small,
|
||||
* but it is not the untinted page, and it cannot be made into one here — a conditional would
|
||||
* have to sit on :root itself, which is the one element a style query cannot reach. The true
|
||||
* off is clearing the Tint hue, which drops data-tint and this rule with it. */
|
||||
--fs-tint-strength: 1;
|
||||
/* The uploaded login/page background (Appearance → Wallpaper → File). --fs-login-bg-url is
|
||||
* the url() head.ut/fs-prefs.js stamp inline; `none` here is the same stand-on-its-own
|
||||
@@ -209,9 +227,9 @@
|
||||
* WHY THE INK IS DERIVED, AND ONLY IN HEX MODE. --fs-on-accent is the ink ON the accent
|
||||
* fill — a solid button's label, the toggle knob, the active tab pill. The palettes pick it
|
||||
* per mode against fills they were measured against (white over footstrap light's #0969da is
|
||||
* 5.19:1; over the dark palette's light fills white collapses to 2.85:1, hence #10151c
|
||||
* 5.19:1; over the dark palette's light fills white collapses to 2.79:1, hence #10151c
|
||||
* there). A user-entered colour is measured against nothing, so keeping the palette's ink
|
||||
* would put white on #ffd400 — 1.2:1, an unreadable button, on the one control whose whole
|
||||
* would put white on #ffd400 — 1.43:1, an unreadable button, on the one control whose whole
|
||||
* job is to be pressed.
|
||||
*
|
||||
* So in hex mode the ink follows the LIGHTNESS of whatever was entered:
|
||||
@@ -275,7 +293,7 @@
|
||||
--fs-good-base: #197c36;
|
||||
--fs-warn-base: #946300;
|
||||
--fs-danger-base: #cf222e;
|
||||
/* ink ON the fills above — white clears AA on all four: 5.19/5.08/4.87/5.36 */
|
||||
/* ink ON the fills above — white clears AA on all four: 5.19/5.28/5.19/5.36 */
|
||||
--fs-on-accent: #fff;
|
||||
--fs-on-good: #fff;
|
||||
--fs-on-warn: #fff;
|
||||
@@ -291,15 +309,15 @@
|
||||
--fs-text: #adbac7;
|
||||
--fs-dim: #8f9cab;
|
||||
--fs-faint: #959ca5;
|
||||
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%238c99a8" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
|
||||
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%238f9cab" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
|
||||
--fs-accent-base: #569df5;
|
||||
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent);
|
||||
--fs-track: #373e47;
|
||||
--fs-good-base: #5aad5d;
|
||||
--fs-warn-base: #c79128;
|
||||
--fs-danger-base: #ea7a74;
|
||||
/* dark ink: these fills are LIGHT. White measured 2.85/2.85/2.83/3.70 — a fail on
|
||||
* all four. #10151c (a shade of --fs-bg) gives 6.43/6.44/6.47/4.95. */
|
||||
/* dark ink: these fills are LIGHT. White measures 2.79/2.77/2.80/2.79 — a fail on
|
||||
* all four. #10151c (a shade of --fs-bg) gives 6.58/6.61/6.55/6.57. */
|
||||
--fs-on-accent: #10151c;
|
||||
--fs-on-good: #10151c;
|
||||
--fs-on-warn: #10151c;
|
||||
@@ -364,7 +382,7 @@
|
||||
--fs-on-danger: #0a0d13;
|
||||
}
|
||||
|
||||
/* ---- classic — the colours of luci-theme-bootstrap, the LuCI default (opt-in) ----
|
||||
/* ---- bootstrap — the colours of luci-theme-bootstrap, the LuCI default (opt-in) ----
|
||||
*
|
||||
* The one palette named after another PACKAGE, because that is what it is: the surfaces,
|
||||
* greys and semantic colours of the stock theme, so an admin who wants the look they already
|
||||
@@ -400,7 +418,7 @@
|
||||
--fs-border-base: #cccccc;
|
||||
/* THE INK IS THE ONE PLACE THIS COLOURWAY LEAVES BOOTSTRAP'S OWN VALUES BEHIND, and it is
|
||||
* the same decision the default palette made: bootstrap's #404040 body and #6a6a6a muted
|
||||
* tier measure 10.4:1 and 5.0:1 on white, and the muted one carries LuCI's page
|
||||
* tier measure 10.4:1 and 5.4:1 on white, and the muted one carries LuCI's page
|
||||
* descriptions and every field title — help text you have to lean into. Matched to the
|
||||
* default palette's light ramp instead, ratio for ratio: 15.7 / 11.7 / 10.0 against its
|
||||
* 15.8 / 11.8 / 10.0. Neutral greys, so the colourway still reads as bootstrap's — what is
|
||||
@@ -434,14 +452,19 @@
|
||||
--fs-panel2-base: #303030;
|
||||
--fs-border-base: #444444;
|
||||
--fs-text: #bfbfbf;
|
||||
/* faint == dim here, and that is the deviation this palette allows itself: bootstrap's dark
|
||||
* has no third ink, and the export tier's -low now follows --fs-faint (02-tokens.css), so
|
||||
* the #969696 this used to carry landed at 4.46:1 on a tinted --fs-panel2 — under AA for a
|
||||
* name apps print text in. The step above it is bootstrap's own, so the fainter one gives
|
||||
* way rather than the ramp. */
|
||||
--fs-dim: #9a9a9a;
|
||||
/* Bootstrap's dark has no third ink — #bfbfbf body, #7f7f7f muted — so the two quiet tiers
|
||||
* are this palette's own, and AA decides where the fainter one can sit: the export tier's
|
||||
* -low follows --fs-faint (02-tokens.css), and on a tinted --fs-panel2 the #969696 this
|
||||
* used to carry measures 4.46:1, under the floor for a name apps print text in. #9a9a9a is
|
||||
* the first step that clears it, so the FLOOR pins --fs-faint and the step between the
|
||||
* tiers has to come from above: --fs-dim moves up by the same distance the hicontrast dark
|
||||
* ramp puts between its own two (12 channel steps), leaving 191 / 166 / 154. Bootstrap's
|
||||
* muted grey is darker than either, so both are already a deviation; one step more of it
|
||||
* is cheaper than collapsing --text-color-medium and -low onto one colour, which is the
|
||||
* thing this revision separates everywhere else. */
|
||||
--fs-dim: #a6a6a6;
|
||||
--fs-faint: #9a9a9a;
|
||||
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%239a9a9a" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
|
||||
--fs-select-chevron: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="%23a6a6a6" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>');
|
||||
--fs-accent-base: #53a4c2;
|
||||
--fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent);
|
||||
--fs-track: #333333;
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
flex: 0 0 180px;
|
||||
/* `end`, not `right`: the label is aligned TOWARDS its field, which is a logical intent.
|
||||
* NOT the same as base/95-luci.css's `.left/.right/.center` — those are LuCI's forcing
|
||||
* utilities, emitted 59 times by form.js, and a class named `right` means right in any
|
||||
* direction. Never sweep those onto start/end. */
|
||||
* utilities (form.js hands them out at four call sites, eight across luci-base), and a
|
||||
* class named `right` means right in any direction. Never sweep those onto start/end. */
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
@@ -190,7 +190,6 @@
|
||||
button[disabled],
|
||||
select[disabled],
|
||||
textarea[disabled],
|
||||
.cbi-select[disabled]::before,
|
||||
.cbi-dropdown[disabled]:not(.btn):not(.cbi-button),
|
||||
input[type="checkbox"][disabled]::before,
|
||||
input[type="checkbox"][disabled]::after,
|
||||
|
||||
@@ -37,10 +37,11 @@
|
||||
|
||||
/* AN UNBREAKABLE VALUE MUST NOT LEAVE THE CARD, in ANY .table. Stock has always had this
|
||||
* (`overflow-wrap: break-word` on every cell); the theme replaced it with a rule that first
|
||||
* asks what KIND of table it is — theme/30-tables.css breaks long values only for
|
||||
* `#view .table[id] .td, table.fs-dt .td`, i.e. a table carrying an id or a
|
||||
* `.tr.table-titles` header row. A third-party status page rendering a key/value
|
||||
* `<div class="table">` with neither matched nothing, so its cells kept
|
||||
* asked what KIND of table it is — it broke long values only for `#view .table[id] .td,
|
||||
* table.fs-dt .td`, i.e. a table carrying an id or a `.tr.table-titles` header row. That
|
||||
* narrowing is gone (theme/30-tables.css records its removal) and this rule is the only owner
|
||||
* now. A third-party status page rendering a key/value `<div class="table">` with neither
|
||||
* matched nothing, so its cells kept
|
||||
* `overflow-wrap: normal` and one unbroken token — an ICCID, a firmware string, an APN, a
|
||||
* base64 key, a DUID — held the column open and pushed the table outside its card.
|
||||
* Measured against luci-app-3ginfo-lite's own markup (`admin/modem`, three such tables).
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
.cbi-page-actions .cbi-button-apply + .cbi-button-save,
|
||||
.cbi-page-actions .cbi-button-negative + .cbi-button-save {
|
||||
--on-color: var(--fs-good);
|
||||
border-color: var(--on-color);
|
||||
/* neutral fill: theme/55-buttons.css — absorbed */
|
||||
/* fill + border: theme/55-buttons.css — absorbed */
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,16 @@
|
||||
* rules' job in theme/30-tables.css, next to the real cells it has to line up with. (Upstream
|
||||
* declares the border-top here; ours would draw the stray line above the header that issue #7
|
||||
* removed from every other cell.) */
|
||||
/* SPLIT OFF THE :has() HALF, and the split is the point: an engine that cannot parse `:has()`
|
||||
* throws away the WHOLE rule, siblings included — so a Firefox older than 121 lost the two
|
||||
* plain selectors above with it, and stacked tables came up with no card labels at all. Same
|
||||
* declarations, two rules, and the half that needs nothing new survives on its own. */
|
||||
.cbi-section-table-titles.named[data-title]::before,
|
||||
.cbi-section-table-titles.named[data-title] ~ .cbi-section-table-descr.named::before,
|
||||
.cbi-section-table-titles.named[data-title] ~ .cbi-section-table-descr.named::before {
|
||||
content: attr(data-title) " ";
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.cbi-section-table-row[data-title]:not(:has(> .cbi-section-table-titles))::before {
|
||||
content: attr(data-title) " ";
|
||||
display: table-cell;
|
||||
@@ -379,8 +387,11 @@
|
||||
|
||||
#modal_overlay > .modal.login > button.important {
|
||||
/* no !important needed: the ID makes this (1,3,1), which no button rule can reach */
|
||||
/* the login dialog's one action, a step up the scale — it was a bare 120% of whatever the
|
||||
* button happened to be. */
|
||||
/* Coverage vocabulary, not a live control: nothing in luci-base gives a modal the `login`
|
||||
* class (ui.showModal takes classes and no call site passes one), and this theme's own
|
||||
* login is a server-rendered form, not a modal. It is inherited from luci-theme-bootstrap,
|
||||
* which carries the same selector, so an app that emits it gets a themed result — a step up
|
||||
* the scale, where it was a bare 120% of whatever the button happened to be. */
|
||||
font-size: var(--fs-type-lg);
|
||||
margin-top: var(--fs-space-2);
|
||||
width: 100%;
|
||||
|
||||
@@ -25,20 +25,28 @@
|
||||
justify-content: center;
|
||||
border-radius: var(--fs-radius-sm);
|
||||
}
|
||||
/* The glyph itself is the Appearance fold's, to the byte — one chevron for "this opens", drawn
|
||||
* in two page files because the two hosts are a stock element keyed by attribute and one of
|
||||
* ours keyed by class, which no single selector list in one layer can carry. Pinned instead:
|
||||
* tools/mirror.mjs fails the build if the copies drift (styles/pages/80-appearance.css). */
|
||||
.cbi-title .label[data-indicator="poll-status"]::after {
|
||||
/* @mirror chevron/closed */
|
||||
content: "";
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-right: 2px solid currentColor;
|
||||
border-bottom: 2px solid currentColor;
|
||||
border-radius: 1px;
|
||||
transform: rotate(45deg); /* default: chevron down (collapsed / Show) */
|
||||
transform: rotate(45deg); /* closed: chevron down */
|
||||
margin-top: -2px; /* optical centring of the down chevron */
|
||||
transition: transform var(--fs-dur-move);
|
||||
/* @endmirror */
|
||||
}
|
||||
.cbi-title .label[data-indicator="poll-status"][data-style="inactive"]::after {
|
||||
transform: rotate(-135deg); /* expanded / Hide -> chevron up */
|
||||
/* @mirror chevron/open */
|
||||
transform: rotate(-135deg); /* open: chevron up */
|
||||
margin-top: var(--fs-space-0-5);
|
||||
/* @endmirror */
|
||||
}
|
||||
|
||||
/* Stock overview layout: fs-overview.js wraps the stock System / Memory / Storage
|
||||
|
||||
@@ -59,15 +59,23 @@
|
||||
* -5 -4 -3 -2 -1
|
||||
*
|
||||
* so: the hostname, the IPv6 list and DUID may wrap; IPv4/MAC/IAID and the two short trailing
|
||||
* columns keep the nowrap above. `.leases` vs `.lases` separates the v4 tables by release on its
|
||||
* own. The v6 table carries the SAME class on both, so it is split on column COUNT — 24.10 has
|
||||
* exactly five, 25.12+ has six or seven. That count test is wrapped in `:where()` so it costs
|
||||
* NOTHING: `:has()` otherwise takes its argument's specificity, and `.tr.table-titles >
|
||||
* :nth-child(6)` alone pushed the selector to [1,8,1], past the sheet's [1,7,0] ceiling. */
|
||||
#view table.fs-dt.leases:not(.fs-stacked) .td:is(:nth-last-child(7), :nth-last-child(4)),
|
||||
* columns keep the nowrap above. BOTH tables are split on column COUNT, never on the release and
|
||||
* never on the class name: 24.10 emits five columns in each, 25.12 eight in the v4 table and
|
||||
* seven in the v6 one. The v4 table is also where upstream's `lases` typo lives — fixed on
|
||||
* 25.12, still there on 24.10 and on ImmortalWrt's fork of it — so keying the shapes off that
|
||||
* name meant a one-character upstream fix would hand the 24.10 table the 25.12 column plan and
|
||||
* silently un-fix issue #7. Both spellings are accepted here and the MARKUP decides, which is
|
||||
* what base/95-luci.css states as the rule for release-conditioned styling.
|
||||
*
|
||||
* The count test is wrapped in `:where()` so it costs NOTHING: `:has()` otherwise takes its
|
||||
* argument's specificity, and `.tr.table-titles > :nth-child(6)` alone pushed the selector to
|
||||
* [1,8,1], past the sheet's [1,7,0] ceiling. */
|
||||
#view table.fs-dt:is(.leases, .lases):has(:where(.tr.table-titles > :nth-child(6))):not(.fs-stacked)
|
||||
.td:is(:nth-last-child(7), :nth-last-child(4)),
|
||||
#view table.fs-dt.leases6:has(:where(.tr.table-titles > :nth-child(6))):not(.fs-stacked)
|
||||
.td:is(:nth-last-child(6), :nth-last-child(5), :nth-last-child(4)),
|
||||
#view table.fs-dt.lases:not(.fs-stacked) .td:nth-last-child(5),
|
||||
#view table.fs-dt:is(.leases, .lases):not(:has(:where(.tr.table-titles > :nth-child(6)))):not(.fs-stacked)
|
||||
.td:nth-last-child(5),
|
||||
#view table.fs-dt.leases6:not(:has(:where(.tr.table-titles > :nth-child(6)))):not(.fs-stacked)
|
||||
.td:is(:nth-last-child(5), :nth-last-child(4), :nth-last-child(3)) {
|
||||
/* `white-space` is the whole rule now: it is what re-permits wrapping for these columns
|
||||
|
||||
@@ -30,8 +30,9 @@
|
||||
button.fs-ap-fold:hover h4 { color: var(--fs-accent); }
|
||||
button.fs-ap-fold:focus-visible { outline: none; box-shadow: var(--fs-focus-ring-solo); border-radius: var(--fs-radius-sm); }
|
||||
/* The chevron: the same 26px box with a two-border ::after that the overview's card toggles
|
||||
* use (styles/pages/20-overview.css) — one chevron for "this panel opens", drawn once here and
|
||||
* once there rather than an SVG in one place and borders in the other.
|
||||
* use (styles/pages/20-overview.css) — one chevron for "this panel opens", drawn here and there
|
||||
* rather than an SVG in one place and borders in the other, with the glyph itself pinned as
|
||||
* @mirror chevron/* so the two copies cannot drift.
|
||||
*
|
||||
* It turns on the SAME attribute that tells a screen reader the state, so the two cannot
|
||||
* disagree. Rotated with `transform` and not the individual `rotate` property, because that is
|
||||
@@ -42,17 +43,23 @@
|
||||
color: var(--fs-dim); border-radius: var(--fs-radius-sm);
|
||||
}
|
||||
.fs-ap-chev::after {
|
||||
/* @mirror chevron/closed */
|
||||
content: "";
|
||||
width: 7px; height: 7px;
|
||||
border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-right: 2px solid currentColor;
|
||||
border-bottom: 2px solid currentColor;
|
||||
border-radius: 1px;
|
||||
transform: rotate(45deg); /* closed: chevron down */
|
||||
margin-top: -2px; /* optical centring of the down chevron */
|
||||
transition: transform var(--fs-dur-move);
|
||||
/* @endmirror */
|
||||
}
|
||||
button.fs-ap-fold[aria-expanded="true"] .fs-ap-chev::after {
|
||||
/* @mirror chevron/open */
|
||||
transform: rotate(-135deg); /* open: chevron up */
|
||||
margin-top: var(--fs-space-0-5);
|
||||
/* @endmirror */
|
||||
}
|
||||
button.fs-ap-fold:hover .fs-ap-chev { color: var(--fs-accent); }
|
||||
/* Stock `.cbi-value` rows stack one per line and carry their own hairline, so a folded group needs
|
||||
@@ -173,8 +180,11 @@
|
||||
:root[data-wallpaper="file"] .fs-ap-tint { display: none; }
|
||||
/* and the strength is a multiplier on a chroma WE choose, so it only means anything in hue mode:
|
||||
* with the tint off there is nothing to scale, and on a hex canvas the colour is the one the
|
||||
* admin entered, with no chroma of ours in it. (0,3,0) out-ranks the stock row's own display. */
|
||||
:root:not([data-tint="hue"]) .fs-ap-density { display: none; }
|
||||
* admin entered, with no chroma of ours in it. (0,3,0) out-ranks the stock row's own display.
|
||||
* The class was `fs-ap-density` while this axis was called Tint density; the name moved with the
|
||||
* label, because Density is the UI-density select four rows above and one name for two rows is
|
||||
* how the label itself went wrong. */
|
||||
:root:not([data-tint="hue"]) .fs-ap-tintstr { display: none; }
|
||||
|
||||
/* the version line at the foot of the page */
|
||||
.fs-ap-footer {
|
||||
@@ -189,7 +199,7 @@
|
||||
/* THE SEGMENTED CONTROL AND THE RANGE SLIDER had their rules here — .fs-seg, .fs-rangewrap,
|
||||
* .fs-range and its readout. Both controls are gone from fs-widgets.js: the page draws its enums
|
||||
* with `ui.Select` and its numbers with `ui.RangeSlider`, which this theme already styles for
|
||||
* every other LuCI form (`select` in base/30-forms.css, `.cbi-range-slider` and
|
||||
* every other LuCI form (`select` in theme/60-inputs.css, `.cbi-range-slider` and
|
||||
* `input[type="range"]` in theme/60-inputs.css). Two copies of a slider, one of them ours, was
|
||||
* the thing to delete — not a third. */
|
||||
|
||||
|
||||
@@ -15,12 +15,24 @@
|
||||
* normal hostname still breaks at its dots), and it lets the flex item's min-content size
|
||||
* shrink — which is what stops a long name pushing the menu out of the bar. It used to be
|
||||
* nowrap + ellipsis, which truncated the one string that identifies the router. */
|
||||
.fs-wordmark { overflow-wrap: anywhere; min-width: 0; }
|
||||
/* AND A CEILING ON THE LINES, because the width cap alone bounds the wrong axis. Measured in
|
||||
* the top bar at 1280px by growing the string: 8 characters give the ordinary 46px bar, 63 (the
|
||||
* kernel's nodename limit, i.e. the realistic worst case) give 73px, and a hand-set 4000
|
||||
* character hostname gave a 4273px tall bar with the page content starting below it. Three lines
|
||||
* is past every real hostname and still a bar; the rest is clipped rather than allowed to push
|
||||
* the page down. `-webkit-line-clamp` is the only cross-engine spelling of this today and needs
|
||||
* its two companions, which is why they are here rather than a plain max-height: a max-height cut
|
||||
* would slice a line in half instead of ending it. */
|
||||
.fs-wordmark {
|
||||
overflow-wrap: anywhere; min-width: 0;
|
||||
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;
|
||||
}
|
||||
/* collapse the sidebar to an icon rail. The toggle only EXISTS in the vertical sidebar,
|
||||
* so its `display` is declared there, not here, or it would just be shadowed. */
|
||||
.fs-railtoggle {
|
||||
align-items: center; justify-content: center;
|
||||
width: 28px; height: 28px; padding: 0; flex: 0 0 auto;
|
||||
width: calc(28px * var(--fs-density-box)); height: calc(28px * var(--fs-density-box));
|
||||
padding: 0; flex: 0 0 auto;
|
||||
border: 1px solid transparent; border-radius: var(--fs-radius-sm);
|
||||
background: transparent; color: var(--fs-faint); cursor: pointer;
|
||||
transition: background var(--fs-dur), border-color var(--fs-dur), color var(--fs-dur);
|
||||
@@ -554,9 +566,15 @@
|
||||
* pre-paint, while data-narrow is written later by fitShell() in an async module. So on a
|
||||
* <521px phone a rail user had every OTHER rail rule excluded by the floor while these
|
||||
* two matched, and the "Refreshing" pill painted as a bare 30px green glyph inside the
|
||||
* phone bar until the modules landed — permanently if they failed. */
|
||||
* phone bar until the modules landed — permanently if they failed.
|
||||
*
|
||||
* The box is --fs-ctl-h-sm and not --fs-btn-size: one step under the bar's compact pill,
|
||||
* because the rail is 68px wide and the glyph sits in a column of icons rather than in a
|
||||
* row of equal squares. It was a bare 30px, which is the same size at the default density
|
||||
* and no size at all at the other two — every other chrome box scales with the Density
|
||||
* axis, and a literal silently opts out of it (02-tokens.css). */
|
||||
:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar #indicators [data-indicator="poll-status"] {
|
||||
font-size: 0; width: 30px; height: 30px; padding: 0;
|
||||
font-size: 0; width: var(--fs-ctl-h-sm); height: var(--fs-ctl-h-sm); padding: 0;
|
||||
border: 0; box-shadow: none; background: var(--fs-good);
|
||||
/* @mirror poll-glyph/mask */
|
||||
-webkit-mask: var(--fs-icon-refresh) center / var(--fs-icon-refresh-sz) no-repeat;
|
||||
@@ -579,7 +597,7 @@
|
||||
* Its label is prose and the rail is 68px: measured on the router, that pill wants 86px,
|
||||
* wraps onto three lines and hangs 34px past the rail's edge over the content (issue #14).
|
||||
* The pill is not ours to shorten, so it becomes a square in the poll glyph's box and shows
|
||||
* the COUNT alone — the only part that changes, lifted into data-fs-count by fs-chrome.js
|
||||
* the COUNT alone — the only part that changes, lifted into data-fs-badge by fs-chrome.js
|
||||
* because a text node cannot be reached by a selector. `font-size: 0` kills the prose
|
||||
* without removing it: the label stays in the DOM for a screen reader, and in the `title`
|
||||
* for the pointer.
|
||||
@@ -595,7 +613,7 @@
|
||||
/* min-width, not width: the count is unbounded (a big config can queue three digits) and
|
||||
* a fixed box would clip it. It grows from the poll glyph's square and stops at the
|
||||
* rail's inner width. */
|
||||
font-size: 0; min-width: 30px; max-width: 100%; height: 30px;
|
||||
font-size: 0; min-width: var(--fs-ctl-h-sm); max-width: 100%; height: var(--fs-ctl-h-sm);
|
||||
box-sizing: border-box; padding: 0 var(--fs-space-1); overflow: hidden;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
}
|
||||
|
||||
@@ -48,21 +48,37 @@
|
||||
* `-140.0 dBm`, half again as wide as the `100% (1.2 GiB)` of a table cell. The Overview and the
|
||||
* package manager keep the value above the bar — their meters sit in a `.td` and a bare
|
||||
* `.controls` div, which is neither shape. */
|
||||
.cbi-value-field > .cbi-progressbar,
|
||||
.cbi-section .table:has(.tr.table-titles) .td .cbi-progressbar {
|
||||
/* ONE RULE PER SHAPE, not one selector list for both, and the split is not cosmetic: the table
|
||||
* half needs `:has()`, and an engine that cannot parse a selector discards the WHOLE rule —
|
||||
* siblings included. Grouped, the form row (which needs nothing new) lost its placement on such
|
||||
* an engine along with the cell that was unreachable there anyway. The declarations they share
|
||||
* are @mirror-pinned instead, so the two copies cannot drift. */
|
||||
.cbi-value-field > .cbi-progressbar {
|
||||
/* @mirror meter/beside */
|
||||
margin-inline-end: var(--fs-meter-value);
|
||||
/* @endmirror */
|
||||
--fs-meter-value: calc(var(--fs-space-10) * 2);
|
||||
}
|
||||
.cbi-value-field > .cbi-progressbar::after,
|
||||
.cbi-section .table:has(.tr.table-titles) .td .cbi-progressbar::after {
|
||||
.cbi-value-field > .cbi-progressbar::after {
|
||||
/* @mirror meter/beside-value */
|
||||
inset-inline: calc(100% + var(--fs-space-2)) auto; bottom: auto; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
/* @endmirror */
|
||||
}
|
||||
.cbi-value-field > .cbi-progressbar { --fs-meter-value: calc(var(--fs-space-10) * 2); }
|
||||
.cbi-section .table:has(.tr.table-titles) .td .cbi-progressbar {
|
||||
/* @mirror meter/beside */
|
||||
margin-inline-end: var(--fs-meter-value);
|
||||
/* @endmirror */
|
||||
--fs-meter-value: var(--fs-space-10);
|
||||
/* narrower than this it stops reading as a meter at all; the column gives up the rest. */
|
||||
min-width: 60px;
|
||||
}
|
||||
.cbi-section .table:has(.tr.table-titles) .td .cbi-progressbar::after {
|
||||
/* @mirror meter/beside-value */
|
||||
inset-inline: calc(100% + var(--fs-space-2)) auto; bottom: auto; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
/* @endmirror */
|
||||
}
|
||||
|
||||
/* Progressbar rows in a section table (luci-app-cpu-status): keep the native label | bar
|
||||
* row, just centre the cells vertically. */
|
||||
|
||||
@@ -65,11 +65,11 @@
|
||||
* one place a field cannot claim the width it wants: it lives in a table HEADER cell, so the
|
||||
* column is as wide as the DATA under it, not as wide as the box. The rule above already makes
|
||||
* it fill that cell — what did not fit was the TEXT. LuCI writes the placeholder as `Filter ` +
|
||||
* the column title, and the theme's field is 13px type inside 11px of side padding, so on a
|
||||
* the column title, and the theme's field is 13px type inside 12px of side padding, so on a
|
||||
* Russian firewall page "Фильтр Внутризональная пересылка" ran out of box and Firefox sliced it
|
||||
* mid-glyph (issue #12). The flag column is the worse half: form.js writes `width: 30px` INLINE
|
||||
* there (its filter takes one character), of which the theme's padding ate 22 — six pixels for
|
||||
* "0/1", which rendered as a single clipped digit.
|
||||
* there (its filter takes one character), of which the theme's padding ate 24 — leaving less
|
||||
* than one glyph of room for "0/1", which rendered as a single clipped digit.
|
||||
*
|
||||
* Denser box and denser type buy back ~20px per column and make the 30px one legible; the
|
||||
* ellipsis is what changes the FAILURE: a placeholder too long for its column now ENDS, instead
|
||||
|
||||
@@ -144,8 +144,9 @@
|
||||
* DANGER role, not the accent one: Reset discards your unsaved changes. (An opaque
|
||||
* --fs-panel2 was the first attempt and weighed MORE than Save's translucent 10-15% tint: a
|
||||
* solid surface and a tint do not read as the same fill, whatever their lightness.)
|
||||
* Label is --fs-text for the same reason as Save's: --fs-dim on --fs-danger-soft measured
|
||||
* 4.06:1 and failed AA. The role lives in the fill and the border. */
|
||||
* Label is --fs-text for the same reason as Save's: --fs-dim on --fs-danger-soft measures
|
||||
* 4.49:1 on --fs-panel in footstrap dark, the worst of the six palette x mode blocks — under
|
||||
* AA by a hundredth, which is still under it. The role lives in the fill and the border. */
|
||||
.cbi-page-actions .cbi-button-reset {
|
||||
background: var(--fs-danger-soft);
|
||||
border: 1px solid var(--fs-danger-line);
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
transition: border-color var(--fs-dur), box-shadow var(--fs-dur);
|
||||
}
|
||||
|
||||
/* keep base's padding-top: 6px — it is tuned for base's align-items: baseline. */
|
||||
/* keep base's padding-top (--fs-space-1) — it is tuned for base's align-items: baseline. */
|
||||
.cbi-value-title {
|
||||
color: var(--fs-dim); font-size: var(--fs-type); font-weight: var(--fs-weight);
|
||||
/* stated, not inherited: the control rule above hands every label `line-height: normal`,
|
||||
@@ -65,7 +65,8 @@
|
||||
|
||||
/* A text field in a CBI form was a fixed 210px — base's width, straight out of bootstrap's
|
||||
* cascade. The same 210px holds far LESS here: the field is monospaced (the rule above) and
|
||||
* padded 11px a side instead of 4, so Attended Sysupgrade's server URL was clipped mid-domain
|
||||
* padded --fs-space-3 a side (12px at the default density) instead of 4, so Attended
|
||||
* Sysupgrade's server URL was clipped mid-domain
|
||||
* — while the .cbi-dynlist directly under it on that same page is 440px wide, which is what
|
||||
* makes the mismatch the thing you actually see (issue #5). Elastic now, with the dynlist's own
|
||||
* ceiling so the two line up. Number/date/time keep the fixed width: they hold short values,
|
||||
@@ -513,10 +514,6 @@
|
||||
line-height: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* The ▾ plate is gone: the chevron is a background on the shell, exactly as on `select`. Hidden
|
||||
* rather than deleted because base/30-forms.css still names `.cbi-select[disabled]::before` in
|
||||
* its disabled list — dropping the rule here would leave that one to paint a bare glyph. */
|
||||
.cbi-select::before { display: none; }
|
||||
|
||||
/* spinner / loading */
|
||||
.spinning, .cbi-value em, em.spinning { color: var(--fs-dim); }
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
@layer theme {
|
||||
/* Generic .dropdown-menu popup — ui.js's split "Save & Apply" menu, plus whatever a third-party
|
||||
* luci-app-* emits. NOT the theme's nav menu (that is a plain nested <ul>; see 10-chrome.css).
|
||||
/* Generic .dropdown-menu popup. NOTHING in luci-base emits it — the split "Save & Apply" button
|
||||
* is a `.cbi-dropdown`, a different widget — so this is coverage vocabulary: stock
|
||||
* luci-theme-bootstrap styles it, a third-party luci-app-* may therefore emit it and expect a
|
||||
* themed result (the same contract 10-chrome.css states for the same class). NOT the theme's
|
||||
* nav menu (that is a plain nested <ul>; see 10-chrome.css).
|
||||
* Hover text is ACCENT to match the open cbi-dropdown list: this is a form control's menu. */
|
||||
.dropdown-menu { background-color: var(--fs-panel); border-color: var(--fs-border); }
|
||||
.dropdown-menu a { color: var(--fs-text); text-shadow: none; }
|
||||
@@ -284,7 +287,7 @@
|
||||
font-size: 0; line-height: 0; border: 0;
|
||||
position: relative; padding: 0;
|
||||
align-self: stretch;
|
||||
min-width: 34px; min-height: var(--fs-ctl-h-sm);
|
||||
min-width: var(--fs-btn-size); min-height: var(--fs-ctl-h-sm);
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,12 @@
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
|
||||
/* Must stay INSIDE this @media: written unscoped it never took effect (base's
|
||||
* .cbi-dropdown:not(.btn):not(.cbi-button) is more specific and kept its 400px cap), and
|
||||
* with base in its own layer it would now cap every dropdown on the desktop too. A
|
||||
* dropdown must never exceed its field, on a page or in a dialog. */
|
||||
/* Must stay INSIDE this @media, and the reason is specificity within ONE layer: the 400px
|
||||
* cap lives in theme/65-dropdown.css (absorbed there from base) on
|
||||
* `.cbi-dropdown:not(.btn):not(.cbi-button)` — (0,3,0) against this rule's (0,1,0) — so
|
||||
* unscoped this would lose to it everywhere and cap nothing at all. Inside the phone tier
|
||||
* it is what the narrow width needs: a dropdown must never exceed its field, on a page or
|
||||
* in a dialog. */
|
||||
.cbi-dropdown { min-width: 0; max-width: 100%; }
|
||||
|
||||
/* Software page (luci-app-package-manager): stack each labeled control; base gives its
|
||||
|
||||
@@ -53,8 +53,15 @@
|
||||
* text's contrast; pull it toward --fs-text. The role hairlines (-line/-line-hi, a 40/55%
|
||||
* tint of their role) go to the FULL role colour — a tint exists to be quiet, which is
|
||||
* the opposite of what this user asked for.
|
||||
* - secondary text: --fs-dim toward --fs-text, and --fs-faint (10-11px eyebrow labels)
|
||||
* up to dim's level — small + faint is where AA erodes first.
|
||||
* - secondary text: --fs-dim toward --fs-text, and --fs-faint most of the way there too
|
||||
* (10-11px eyebrow labels: small + faint is where AA erodes first). NOT all the way onto
|
||||
* --fs-dim, which is what this used to say: the two are the export tier's --text-color-
|
||||
* medium and -low (02-tokens.css), so collapsing them here republished one colour twice to
|
||||
* every app reading the gradation — the exact flattening that file spends twelve lines
|
||||
* undoing. 82% against dim's 85% is inside a narrow window, and export-tier.mjs now measures
|
||||
* this media query rather than taking it on trust: below 80% the fainter ink drops under AA
|
||||
* on footstrap dark's --fs-panel2 (4.51:1 at 80), above 82% the high-to-low ramp falls under
|
||||
* the 0.10 spread the tier promises (0.098 at 84). 82 clears both: 4.68:1 and 0.110.
|
||||
* - the focus ring: --fs-focus-ring is a tint of the accent (--fs-accent-soft: 10% light,
|
||||
* 15% dark — the one ladder member whose strength is per MODE, so not the ladder's 12%);
|
||||
* at "more contrast" it carries the accent at 60%. Its sibling --fs-focus-ring-solo is
|
||||
@@ -64,7 +71,7 @@
|
||||
:root {
|
||||
--fs-border: color-mix(in srgb, var(--fs-text) 45%, var(--fs-bg));
|
||||
--fs-dim: color-mix(in srgb, var(--fs-text) 85%, var(--fs-bg));
|
||||
--fs-faint: var(--fs-dim);
|
||||
--fs-faint: color-mix(in srgb, var(--fs-text) 82%, var(--fs-bg));
|
||||
--fs-focus-ring: 0 0 0 3px color-mix(in srgb, var(--fs-accent) 60%, transparent);
|
||||
--fs-accent-line: var(--fs-accent); --fs-accent-line-hi: var(--fs-accent);
|
||||
--fs-good-line: var(--fs-good); --fs-good-line-hi: var(--fs-good);
|
||||
@@ -106,6 +113,16 @@
|
||||
background: Highlight;
|
||||
forced-color-adjust: none; /* keep the fill: it IS the state */
|
||||
}
|
||||
/* WHICH PAGE AM I ON. The menu's active item carries its state in exactly the two things
|
||||
* forced colors drops: a 10%-alpha accent background and the box-shadow rail. Measured with
|
||||
* the query emulated — all twelve #topmenu links reported the same weight, the same absent
|
||||
* shadow, the same absent border and the same LinkText colour, so the current page was not
|
||||
* marked at all. `aria-current="page"` is on the link (menu-footstrap.js), so this was a
|
||||
* sighted-user loss only. The rail comes back as a real border, which forced colors keeps. */
|
||||
#topmenu li.active > a {
|
||||
border-inline-start: 3px solid Highlight;
|
||||
padding-inline-start: calc(var(--fs-space-3) - 3px);
|
||||
}
|
||||
/* the toggle switch is a bare rounded box in forced colors — give the knob and
|
||||
* the track an edge so on/off is distinguishable at all */
|
||||
.cbi-checkbox > label { border: 1px solid ButtonBorder; }
|
||||
|
||||
@@ -14,9 +14,13 @@
|
||||
|
||||
So every label in the Appearance page carries the `footstrap` msgctxt (fs-appearance.js), while
|
||||
the CHROME's own strings — Skip to content, Menu, Collapse menu, Search, Log out — are
|
||||
deliberately msgctxt-FREE: inheriting luci-base's translation is a feature in the ~40 languages
|
||||
this theme ships no catalogue for. Same for the login/notice sentences and for
|
||||
System/Memory/Storage in fs-overview.js, which MATCH the stock headings on purpose.
|
||||
deliberately msgctxt-FREE, because a bare msgid is how a string INHERITS a translation in the ~40
|
||||
languages this theme ships no catalogue for. Two of the five collect on that today: Skip to
|
||||
content and Log out are luci-base msgids verbatim (the latter was `Logout`, which is nobody's
|
||||
msgid, so it rendered in English everywhere our own catalogue does not reach). Menu, Collapse menu
|
||||
and Search are ours alone for now and stay bare so a future luci-base string can adopt them. Same
|
||||
for the login/notice sentences and for System/Memory/Storage in fs-overview.js, which MATCH the
|
||||
stock headings on purpose.
|
||||
-#}
|
||||
|
||||
{%
|
||||
@@ -74,8 +78,14 @@
|
||||
fs_defaults[k] = fsd[k] || '';
|
||||
/* the one value with a fallback of its own: the legacy migration seed
|
||||
luci.main.footstrap_layout=top, which uci-defaults sets for a router upgraded from the old
|
||||
top-nav theme (a shell script cannot write localStorage). */
|
||||
fs_defaults.layout = fsd.layout || config.main.footstrap_layout || '';
|
||||
top-nav theme (a shell script cannot write localStorage).
|
||||
|
||||
`config.main?.` and not `config.main.`: ucode raises a hard reference error on a property read
|
||||
through null and this template is the ONE that renders every page, so a dispatcher that stopped
|
||||
defaulting that section would answer 500 everywhere. It defaults it today — dispatcher.uc has
|
||||
`main: uci.get_all('luci', 'main') ?? {}` on both release branches — and the CI gate cannot see
|
||||
the difference either way: `ucode -T -c` COMPILES the template, it never runs it. */
|
||||
fs_defaults.layout = fsd.layout || config.main?.footstrap_layout || '';
|
||||
|
||||
http.prepare_content('text/html; charset=UTF-8');
|
||||
-%}
|
||||
|
||||
@@ -12,8 +12,12 @@
|
||||
break the inline script nor inject markup (defence in depth — writing that file already needs
|
||||
root). Absent/invalid becomes '' (or 0, or -1 for rounding whose 0 is a real value), which the
|
||||
JS reads as "no router default". The client's own localStorage overrides every one of these
|
||||
before paint — the browser choice always wins (see fs-prefs.js). */
|
||||
fs_defaults = fs_defaults ?? {};
|
||||
before paint — the browser choice always wins (see fs-prefs.js).
|
||||
|
||||
`fs_defaults` is a required include parameter, not an optional one: header.ut is the only
|
||||
caller and builds it as an object literal before the include, so there is nothing to default
|
||||
it to — a `?? {}` here only hid that contract from a second caller that would need to honour
|
||||
it. */
|
||||
let _sd_lay = index([ 'sidebar', 'top' ], fs_defaults.layout) >= 0 ? fs_defaults.layout : '';
|
||||
let _sd_dark = index([ 'auto', 'light', 'dark' ], fs_defaults.darkmode) >= 0 ? fs_defaults.darkmode : '';
|
||||
let _sd_pal = index([ 'footstrap', 'hicontrast', 'bootstrap' ], fs_defaults.palette) >= 0 ? fs_defaults.palette : '';
|
||||
@@ -61,8 +65,9 @@
|
||||
builds the url() below; anything else becomes '' = "no background". */
|
||||
let _lb = fs_defaults.login_bg;
|
||||
let _sd_lbg = (type(_lb) == 'string' && match(_lb, /^[a-f0-9]{6,64}$/)) ? _lb : '';
|
||||
/* the router-identity Tint's DENSITY 0-200 (% of the designed chroma); '' (unset) -> -1, like
|
||||
rounding, because 0 is a real value (no tint). */
|
||||
/* the router-identity Tint's STRENGTH 0-200 (% of the designed chroma; the Appearance row is
|
||||
"Tint strength" — "Density" is the other axis entirely). '' (unset) -> -1, like rounding,
|
||||
because 0 is a real value. */
|
||||
let _ts = int(fs_defaults.tint_strength);
|
||||
let _sd_tstr = (fs_defaults.tint_strength != '' && _ts >= 0 && _ts <= 200) ? _ts : -1;
|
||||
/* the File photo's scrim opacity 0-100 (Wallpaper -> File -> Dim), a SHARED router value; unset
|
||||
@@ -150,7 +155,7 @@
|
||||
RTL set rather than only the three LuCI ships catalogues for today (ar, fa, he), because the
|
||||
cost of a language never installed is one array entry. `ltr` is written explicitly for the same
|
||||
reason data-layout is: every rule then matches positively. */
|
||||
let _lang = dispatcher.lang ?? 'en';
|
||||
let _lang = dispatcher.lang;
|
||||
let _lbase = lc(split(_lang, /[-_]/)[0]);
|
||||
let _rtl = index([ 'ar', 'arc', 'ckb', 'dv', 'fa', 'ha', 'he', 'khw', 'ks', 'ps', 'sd', 'ur', 'yi' ], _lbase) >= 0;
|
||||
%}
|
||||
@@ -226,14 +231,28 @@
|
||||
(this browser) wins; absent -> the router default in window.__fsSD; absent -> the
|
||||
built-in. */
|
||||
(() => {
|
||||
/* PER READ, not once around the block. A browser that refuses storage (Firefox with
|
||||
dom.storage.enabled=false, "block all cookies" in Chrome/Safari, several WebViews)
|
||||
throws a SecurityError on the FIRST getItem, and with one try around everything that
|
||||
aborted the whole pre-paint: dark mode still applied — it guards narrowly, three
|
||||
blocks up — while palette, density, rounding, wallpaper, the pattern axes, the five
|
||||
colour axes, the four surfaces, tint strength and photo dim were all skipped, so a
|
||||
router that had saved defaults painted none of them. Storage failing must mean "this
|
||||
browser has no override", which is exactly what the live applier's lsGet already
|
||||
means (fs-prefs.js). The outer try stays as the last resort for everything else. */
|
||||
const lsGet = (k) => { try { return localStorage.getItem(k); } catch (e) { return null; } };
|
||||
try {
|
||||
const root = document.querySelector(':root'),
|
||||
sd = window.__fsSD || {};
|
||||
let p = localStorage.getItem('fs-palette');
|
||||
let p = lsGet('fs-palette');
|
||||
/* legacy 'rvht'/'roman' = default colours + a doodle wallpaper, and legacy
|
||||
'github' = the footstrap default, now stored explicitly. Migrate to the current
|
||||
explicit value, do NOT clear (an absent key means "inherit the router default",
|
||||
which is no longer the same as "the built-in default"). The doodles they used to
|
||||
which is no longer the same as "the built-in default"). The migration is
|
||||
already redundant, and worth knowing before it is carried forward forever: a
|
||||
stray value reads as the built-in default in the live applier too
|
||||
(fs-prefs.js's currentPalette ends `return 'footstrap'`), so deleting this
|
||||
block changes nothing UNLESS that line goes with it. The doodles they used to
|
||||
turn on are gone — the Wallpaper axis now paints an SVG the admin uploads — so
|
||||
the wallpaper half of that migration has nothing to point at and is dropped;
|
||||
`fs-wallpaper` keeps whatever it holds and the whitelist below reads a doodle
|
||||
@@ -256,18 +275,18 @@
|
||||
whitelisted to hex above, so nothing user-controlled reaches the url(). */
|
||||
const pu = sd.pattern;
|
||||
if (pu) root.style.setProperty('--fs-pattern-url', 'url("/luci-static/footstrap/pattern.svg?v=' + pu + '")');
|
||||
const psRaw = localStorage.getItem('fs-pattern-size');
|
||||
const psRaw = lsGet('fs-pattern-size');
|
||||
const ps = (psRaw !== null) ? parseInt(psRaw, 10) : sd.pattern_size;
|
||||
if (ps >= 40 && ps <= 1600 && ps !== 440) root.style.setProperty('--fs-pattern-size', ps + 'px');
|
||||
const pstRaw = localStorage.getItem('fs-pattern-strength');
|
||||
const pstRaw = lsGet('fs-pattern-strength');
|
||||
const pst = (pstRaw !== null) ? parseInt(pstRaw, 10) : sd.pattern_strength;
|
||||
if (pst >= 0 && pst <= 100 && pst !== 20) root.style.setProperty('--fs-pattern-strength', String(pst / 100));
|
||||
let pi = localStorage.getItem('fs-pattern-ink');
|
||||
let pi = lsGet('fs-pattern-ink');
|
||||
if (pi === null) pi = sd.pattern_ink || null;
|
||||
if (pi === 'original') root.setAttribute('data-pattern-ink', 'original');
|
||||
else root.removeAttribute('data-pattern-ink');
|
||||
|
||||
let w = localStorage.getItem('fs-wallpaper');
|
||||
let w = lsGet('fs-wallpaper');
|
||||
if (w === null) w = sd.wallpaper || null;
|
||||
if (w === 'pattern' || w === 'file') root.setAttribute('data-wallpaper', w);
|
||||
else root.removeAttribute('data-wallpaper');
|
||||
@@ -276,7 +295,7 @@
|
||||
type and space ladders). Normal is a bare :root, so the removeAttribute
|
||||
half matters as much as the set — without it a browser that switched back
|
||||
to Normal would paint one frame still compressed. */
|
||||
let dn = localStorage.getItem('fs-density');
|
||||
let dn = lsGet('fs-density');
|
||||
if (dn === null) dn = sd.density || null;
|
||||
if (dn === 'compact' || dn === 'large') root.setAttribute('data-density', dn);
|
||||
else root.removeAttribute('data-density');
|
||||
@@ -285,7 +304,7 @@
|
||||
default is a LITERAL: this runs before the stylesheet and cannot read the
|
||||
CSS token, so axes.mjs is the only thing checking the two agree. sd.rounding
|
||||
is -1 when the router has no default. */
|
||||
const rRaw = localStorage.getItem('fs-radius');
|
||||
const rRaw = lsGet('fs-radius');
|
||||
let r = parseInt(rRaw, 10);
|
||||
if (rRaw === null) r = (sd.rounding >= 0 ? sd.rounding : 12);
|
||||
if (r >= 0 && r <= 20 && r !== 12)
|
||||
@@ -302,7 +321,7 @@
|
||||
to one shape. */
|
||||
const hex = /^#[0-9a-f]{6}$/i;
|
||||
const colour = (key, sdVal, attr, hueProp, colorProp) => {
|
||||
const raw = localStorage.getItem(key);
|
||||
const raw = lsGet(key);
|
||||
const v = String((raw === null ? sdVal : raw) ?? '').trim();
|
||||
if (hex.test(v)) {
|
||||
root.style.removeProperty(hueProp);
|
||||
@@ -327,7 +346,7 @@
|
||||
mode and therefore no attribute, just the token or nothing. Mirrors
|
||||
surfaceAxis() in fs-prefs.js. */
|
||||
const surface = (key, sdVal, prop) => {
|
||||
const raw = localStorage.getItem(key);
|
||||
const raw = lsGet(key);
|
||||
const v = String((raw === null ? sdVal : raw) ?? '').trim().toLowerCase();
|
||||
if (hex.test(v)) root.style.setProperty(prop, v);
|
||||
};
|
||||
@@ -339,19 +358,22 @@
|
||||
window.__fsSD — no localStorage). Set the property whenever a valid token
|
||||
exists; whether it PAINTS is the wallpaper axis above (data-wallpaper="file"),
|
||||
so a router whose default wallpaper is 'file' shows it here, pre-login included.
|
||||
Re-validate the token (defence in depth, same charset as the server sanitiser). */
|
||||
The token is already whitelisted to hex above, the same as sd.pattern before it, so
|
||||
nothing user-controlled reaches the url(); the test is repeated here anyway
|
||||
because this one interpolates into a url() inside a STYLE property, where a
|
||||
second reading costs one regex and the failure would be silent. */
|
||||
const lb = sd.login_bg;
|
||||
if (lb && (/^[a-f0-9]{6,64}$/).test(lb))
|
||||
root.style.setProperty('--fs-login-bg-url', "url('/luci-static/footstrap/bg?v=" + lb + "')");
|
||||
/* the router-identity Tint's density (default 100% = the designed chroma). */
|
||||
const tsRaw = localStorage.getItem('fs-tint-strength');
|
||||
const tsRaw = lsGet('fs-tint-strength');
|
||||
let ts = parseInt(tsRaw, 10);
|
||||
if (tsRaw === null) ts = (sd.tint_strength >= 0 ? sd.tint_strength : 100);
|
||||
if (ts >= 0 && ts <= 200 && ts !== 100)
|
||||
root.style.setProperty('--fs-tint-strength', String(ts / 100));
|
||||
/* the File photo's scrim opacity — an ordinary per-browser axis, same shape as
|
||||
the tint strength above; the 74% default lives in the CSS token. */
|
||||
const pdRaw = localStorage.getItem('fs-photo-dim');
|
||||
const pdRaw = lsGet('fs-photo-dim');
|
||||
let pd = parseInt(pdRaw, 10);
|
||||
if (pdRaw === null) pd = (sd.photo_dim >= 0 ? sd.photo_dim : 74);
|
||||
if (pd >= 0 && pd <= 100 && pd !== 74)
|
||||
@@ -444,9 +466,10 @@
|
||||
lang=en it spends them to deliver 13 bytes (`window.TR={};`): there is no en catalogue,
|
||||
because the msgids already ARE English. The process is the cost, not the data. So emit
|
||||
those 13 bytes inline when this language has no catalogue, and keep the tag when it has.
|
||||
The probe MIRRORS the server: action_translations calls load_catalog(lang,
|
||||
'/usr/lib/lua/luci/i18n'), which loads every `*.<lang>.lmo` there — so a router that does
|
||||
ship an en catalogue still gets the tag. Deciding by language NAME would silently drop it.
|
||||
The probe MIRRORS the server: determine_request_language() (dispatcher.uc) loads this
|
||||
request's catalogue with load_catalog(lang, '/usr/lib/lua/luci/i18n'), which takes every
|
||||
`*.<lang>.lmo` in that directory — so a router that does ship an en catalogue still gets
|
||||
the tag. Deciding by language NAME would silently drop it.
|
||||
Fails OPEN (a throwing probe keeps the tag): a missing catalogue makes every `_()` render
|
||||
English and report nothing, which is the one failure this must never risk. #}
|
||||
{%- let has_catalog = true;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
Licensed to the public under the Apache License 2.0.
|
||||
-#}
|
||||
{% if (dispatcher.lookup("admin/logout")): %}
|
||||
<a class="fs-logout" href="{{ dispatcher.build_url('admin/logout') }}" title="{{ _('Logout') }}" aria-label="{{ _('Logout') }}">
|
||||
<a class="fs-logout" href="{{ dispatcher.build_url('admin/logout') }}" title="{{ _('Log out') }}" aria-label="{{ _('Log out') }}">
|
||||
{% include('themes/footstrap/partials/icon', { body: '<path d="M14 4h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-4"/><path d="M10 12H3m0 0 3-3m-3 3 3 3"/>' }) %}
|
||||
<span class="fs-label">{{ _('Logout') }}</span>
|
||||
<span class="fs-label">{{ _('Log out') }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -1,9 +1,11 @@
|
||||
{#
|
||||
The page-search trigger (#fs-search-btn), wired by fs-search.js. It is a `.fs-themerow` — the
|
||||
SAME class the Appearance trigger uses, not a look of its own — so it is one of the right
|
||||
chrome's shared row/button class rather than a look of its own — so it is one of the right
|
||||
cluster's square icon buttons in the bar and the rail, and a full-width labelled row in the
|
||||
vertical sidebar, with no new CSS to keep in step. It sits between the poll pill (#indicators)
|
||||
and Appearance: those three share `order: 2`, so the DOM order here IS the visual order.
|
||||
and Log out, which share `order: 2` with it, so the DOM order here IS the visual order. (The
|
||||
Appearance trigger was the third member until the panel became a tab on System -> System; this is
|
||||
the only .fs-themerow the document carries now.)
|
||||
|
||||
i18n: 'Search' is CHROME and deliberately carries no msgctxt — inheriting luci-base's translation
|
||||
is a feature in the ~40 languages this theme ships no catalogue for. The palette's OWN strings
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
Copyright 2008-2012 Jo-Philipp Wich <jow@openwrt.org>
|
||||
Licensed to the public under the Apache License 2.0.
|
||||
|
||||
It exists to pass `blank_page: true`, the only thing it changes about the generic template:
|
||||
without a theme-local sysauth the dispatcher falls back to that one, which includes the header
|
||||
WITHOUT blank_page — so the whole chrome (sidebar, menu, footer) rendered around the login
|
||||
form, every control on it dead.
|
||||
It exists because the generic template includes the header WITHOUT `blank_page: true`: with no
|
||||
theme-local sysauth the dispatcher falls back to that one and the whole chrome (sidebar, menu,
|
||||
footer) rendered around the login form, every control on it dead. Everything else here is a
|
||||
DEPARTURE the notes below each justify — the `fs-login` hook the page CSS needs, the id/for/
|
||||
autocomplete attributes, the alert gating, and an HTTPS-redirect script that also fixes the
|
||||
generic's `split(https_port, …)` typo — and the rule is that none of them may drop a field the
|
||||
backend posts.
|
||||
|
||||
It is NOT a copy of bootstrap's sysauth.ut, which hides the form in a `<section hidden>` and
|
||||
needs a view module to reveal it: tried here once and it gave a BLANK PAGE WITH NO WAY TO LOG
|
||||
@@ -88,8 +91,8 @@
|
||||
sysauth that drops them is not a login form with a missing extra: the field is never
|
||||
posted, so on a router with 2FA enabled NOBODY CAN LOG IN while this theme is active,
|
||||
and the backend's "Too many failed attempts" / "System time is not calibrated" messages
|
||||
never reach the screen either. This template exists only to pass `blank_page: true`, so
|
||||
it must stay a superset of the generic one.
|
||||
never reach the screen either. Whatever else this template changes, it must stay a
|
||||
SUPERSET of the generic one's fields.
|
||||
|
||||
The leading space inside each conditional attribute is load-bearing. ucode's template
|
||||
lexer eats the whitespace immediately BEFORE a `{%` tag and the newline immediately
|
||||
|
||||
+5
-5
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=smartdns
|
||||
PKG_VERSION:=48.2
|
||||
PKG_RELEASE:=4
|
||||
PKG_VERSION:=48.4
|
||||
PKG_RELEASE:=5
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/pymumu/smartdns/tar.gz/Release$(PKG_VERSION)?
|
||||
@@ -73,9 +73,9 @@ endef
|
||||
define Download/smartdns-webui
|
||||
PROTO:=git
|
||||
URL:=https://github.com/pymumu/smartdns-webui.git
|
||||
SOURCE_DATE:=2026-03-31
|
||||
SOURCE_VERSION:=c0d2411d20bf8b172a045adbe07166df6691e58b
|
||||
MIRROR_HASH:=fa9463e912169e8907f9454c186c7bcb0b3a36c86a522e065c98a7ea38fcecd8
|
||||
SOURCE_DATE:=2026-07-29
|
||||
SOURCE_VERSION:=1c06fee693434ec7c71f06010d3f87416baba9f0
|
||||
MIRROR_HASH:=1b718d1288f6ddbdd80e3a948dda1693fd7bc8079fdb65be121f64f23ce53849
|
||||
SUBDIR:=smartdns-webui-$$$$(subst -,.,$$$$(SOURCE_DATE))~$$$$(call version_abbrev,$$$$(SOURCE_VERSION))
|
||||
FILE:=$$(SUBDIR).tar.zst
|
||||
endef
|
||||
|
||||
Reference in new issue
Block a user