Compare commits
13 Commits
f6ad944988
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 73f6373b84 | |||
| 469baa99bb | |||
| 338bca798c | |||
| e9d8c9d88e | |||
| 5bfc769121 | |||
| 7bfffd6b82 | |||
| cef84c818f | |||
| ad3cbd2ee4 | |||
| e38a4044a7 | |||
| 5d5284d76e | |||
| d822e54f91 | |||
| 1eb389ea5e | |||
| 120d1672a3 |
@@ -9,6 +9,9 @@ build/
|
|||||||
|
|
||||||
# Local machine config
|
# Local machine config
|
||||||
local.properties
|
local.properties
|
||||||
|
keystore.properties
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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>.
|
||||||
@@ -25,6 +25,11 @@ Offline-first, ad-free QR/barcode scanner built with Kotlin, Jetpack Compose, Ca
|
|||||||
- Keine `INTERNET`-Permission im Manifest
|
- Keine `INTERNET`-Permission im Manifest
|
||||||
- Datenschutzerklärung in der App und als [PRIVACY_POLICY.md](PRIVACY_POLICY.md)
|
- Datenschutzerklärung in der App und als [PRIVACY_POLICY.md](PRIVACY_POLICY.md)
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
Copyright (C) 2026 SoftwareApp-HB.
|
||||||
|
|
||||||
|
Private QR Scanner is licensed under the GNU General Public License v3.0. See [LICENSE](LICENSE).
|
||||||
|
|
||||||
## MVP Features
|
## MVP Features
|
||||||
- Home: Scan-Button, lokaler Historie-Toggle (Default: OFF), Datenschutz-Dialog
|
- Home: Scan-Button, lokaler Historie-Toggle (Default: OFF), Datenschutz-Dialog
|
||||||
- Scanner: CameraX Live-Preview, Fadenkreuz-Overlay, Taschenlampe, Debounce gegen Doppelscans, Live-Hinweise zu erkannten/lesbaren Codes
|
- Scanner: CameraX Live-Preview, Fadenkreuz-Overlay, Taschenlampe, Debounce gegen Doppelscans, Live-Hinweise zu erkannten/lesbaren Codes
|
||||||
|
|||||||
@@ -1,9 +1,51 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("com.google.devtools.ksp")
|
id("com.google.devtools.ksp")
|
||||||
id("org.jetbrains.kotlin.plugin.compose")
|
id("org.jetbrains.kotlin.plugin.compose")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
|
val keystoreProperties = Properties().apply {
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystorePropertiesFile.inputStream().use(::load)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun signingValue(propertyName: String, envName: String): String? {
|
||||||
|
return (keystoreProperties[propertyName] as? String)
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?: System.getenv(envName)?.takeIf { it.isNotBlank() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val releaseStoreFile = signingValue("storeFile", "RELEASE_STORE_FILE")
|
||||||
|
val releaseStorePassword = signingValue("storePassword", "RELEASE_STORE_PASSWORD")
|
||||||
|
val releaseKeyAlias = signingValue("keyAlias", "RELEASE_KEY_ALIAS")
|
||||||
|
val releaseKeyPassword = signingValue("keyPassword", "RELEASE_KEY_PASSWORD")
|
||||||
|
val hasReleaseSigning = listOf(
|
||||||
|
releaseStoreFile,
|
||||||
|
releaseStorePassword,
|
||||||
|
releaseKeyAlias,
|
||||||
|
releaseKeyPassword
|
||||||
|
).all { !it.isNullOrBlank() }
|
||||||
|
|
||||||
|
gradle.taskGraph.whenReady {
|
||||||
|
val releaseBundleRequested = allTasks.any {
|
||||||
|
it.path == ":app:bundleRelease" || it.name == "bundleRelease"
|
||||||
|
}
|
||||||
|
val releaseApkRequested = allTasks.any {
|
||||||
|
it.path == ":app:assembleRelease" || it.name == "assembleRelease"
|
||||||
|
}
|
||||||
|
if ((releaseBundleRequested || releaseApkRequested) && !hasReleaseSigning) {
|
||||||
|
throw GradleException(
|
||||||
|
"Release signing is not configured. Fill storePassword, keyAlias, and keyPassword " +
|
||||||
|
"in keystore.properties, or set RELEASE_STORE_FILE, RELEASE_STORE_PASSWORD, " +
|
||||||
|
"RELEASE_KEY_ALIAS, and RELEASE_KEY_PASSWORD."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "de.softwareapp_hb.privateqrscanner"
|
namespace = "de.softwareapp_hb.privateqrscanner"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
@@ -12,7 +54,7 @@ android {
|
|||||||
applicationId = "de.softwareapp_hb.privateqrscanner"
|
applicationId = "de.softwareapp_hb.privateqrscanner"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 2
|
||||||
versionName = "1.0.0"
|
versionName = "1.0.0"
|
||||||
buildConfigField("boolean", "FEATURE_PAYWALL_ENABLED", "false")
|
buildConfigField("boolean", "FEATURE_PAYWALL_ENABLED", "false")
|
||||||
|
|
||||||
@@ -22,9 +64,26 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
if (hasReleaseSigning) {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file(releaseStoreFile!!)
|
||||||
|
storePassword = releaseStorePassword
|
||||||
|
keyAlias = releaseKeyAlias
|
||||||
|
keyPassword = releaseKeyPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
|
if (hasReleaseSigning) {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
|
ndk {
|
||||||
|
debugSymbolLevel = "SYMBOL_TABLE"
|
||||||
|
}
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
@@ -89,7 +148,7 @@ dependencies {
|
|||||||
implementation("com.google.mlkit:barcode-scanning:17.3.0")
|
implementation("com.google.mlkit:barcode-scanning:17.3.0")
|
||||||
implementation("com.google.android.play:review:2.0.2")
|
implementation("com.google.android.play:review:2.0.2")
|
||||||
implementation("com.google.android.play:review-ktx:2.0.2")
|
implementation("com.google.android.play:review-ktx:2.0.2")
|
||||||
implementation("com.github.bitfireAT:vcard4android:main-SNAPSHOT")
|
implementation("com.github.bitfireAT:vcard4android:7dbab269865e99eb4f46a25313d6397b51cd6ba8")
|
||||||
|
|
||||||
implementation("androidx.room:room-runtime:2.8.4")
|
implementation("androidx.room:room-runtime:2.8.4")
|
||||||
implementation("androidx.room:room-ktx:2.8.4")
|
implementation("androidx.room:room-ktx:2.8.4")
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
package de.softwareapp_hb.privateqrscanner.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.SwipeToDismissBox
|
|
||||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import de.softwareapp_hb.privateqrscanner.R
|
|
||||||
import de.softwareapp_hb.privateqrscanner.domain.ScanRecord
|
|
||||||
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
|
|
||||||
import de.softwareapp_hb.privateqrscanner.ui.capabilities
|
|
||||||
import de.softwareapp_hb.privateqrscanner.util.HistoryExportFormatter
|
|
||||||
import de.softwareapp_hb.privateqrscanner.util.Intents
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HistoryScreen(
|
|
||||||
query: String,
|
|
||||||
history: List<ScanRecord>,
|
|
||||||
useCaseView: UseCaseView,
|
|
||||||
onQueryChange: (String) -> Unit,
|
|
||||||
onDelete: (Long) -> Unit,
|
|
||||||
onClearAll: () -> Unit
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val capabilities = useCaseView.capabilities()
|
|
||||||
val showDeleteAll = remember { mutableStateOf(false) }
|
|
||||||
val selectedItem = remember { mutableStateOf<ScanRecord?>(null) }
|
|
||||||
|
|
||||||
if (showDeleteAll.value) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showDeleteAll.value = false },
|
|
||||||
title = { Text(stringResource(R.string.delete_all)) },
|
|
||||||
text = { Text(stringResource(R.string.confirm_delete_all)) },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
onClearAll()
|
|
||||||
showDeleteAll.value = false
|
|
||||||
}) { Text(stringResource(R.string.confirm)) }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showDeleteAll.value = false }) {
|
|
||||||
Text(stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val detail = selectedItem.value
|
|
||||||
if (detail != null) {
|
|
||||||
val detailIsBase64 = detail.isBase64Encoded()
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { selectedItem.value = null },
|
|
||||||
title = { Text(text = detail.type) },
|
|
||||||
text = {
|
|
||||||
if (detailIsBase64) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text(text = stringResource(R.string.base64_encoded_notice))
|
|
||||||
Text(text = detail.content)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text(text = detail.content)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = { selectedItem.value = null }) {
|
|
||||||
Text(text = stringResource(R.string.confirm))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.Top
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = query,
|
|
||||||
onValueChange = onQueryChange,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
label = { Text(stringResource(R.string.search)) }
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
if (capabilities.allowHistoryExport) {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
val exportText = HistoryExportFormatter.formatText(history)
|
|
||||||
Intents.shareContent(context, exportText, "text/plain")
|
|
||||||
},
|
|
||||||
enabled = history.isNotEmpty()
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.share_txt))
|
|
||||||
}
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
val exportCsv = HistoryExportFormatter.formatCsv(history)
|
|
||||||
Intents.shareContent(context, exportCsv, "text/csv")
|
|
||||||
},
|
|
||||||
enabled = history.isNotEmpty()
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.share_csv))
|
|
||||||
}
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
val exportJson = HistoryExportFormatter.formatJson(history)
|
|
||||||
Intents.shareContent(context, exportJson, "application/json")
|
|
||||||
},
|
|
||||||
enabled = history.isNotEmpty()
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.share_json))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TextButton(onClick = { showDeleteAll.value = true }) {
|
|
||||||
Text(stringResource(R.string.delete_all))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn {
|
|
||||||
items(history, key = { it.id }) { item ->
|
|
||||||
HistoryRow(
|
|
||||||
item = item,
|
|
||||||
onDelete = onDelete,
|
|
||||||
onOpenDetails = { selectedItem.value = item }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun HistoryRow(
|
|
||||||
item: ScanRecord,
|
|
||||||
onDelete: (Long) -> Unit,
|
|
||||||
onOpenDetails: () -> Unit
|
|
||||||
) {
|
|
||||||
val dismissState = rememberSwipeToDismissBoxState(
|
|
||||||
confirmValueChange = {
|
|
||||||
if (it == SwipeToDismissBoxValue.EndToStart || it == SwipeToDismissBoxValue.StartToEnd) {
|
|
||||||
onDelete(item.id)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
SwipeToDismissBox(
|
|
||||||
state = dismissState,
|
|
||||||
backgroundContent = {},
|
|
||||||
content = {
|
|
||||||
Column(modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { onOpenDetails() }
|
|
||||||
.padding(vertical = 12.dp)) {
|
|
||||||
Text(text = item.type)
|
|
||||||
Text(
|
|
||||||
text = if (item.isBase64Encoded()) {
|
|
||||||
stringResource(R.string.base64_encoded_inline, item.content)
|
|
||||||
} else {
|
|
||||||
item.content
|
|
||||||
},
|
|
||||||
maxLines = 2
|
|
||||||
)
|
|
||||||
Text(text = DateFormat.getDateTimeInstance().format(Date(item.timestamp)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ScanRecord.isBase64Encoded(): Boolean {
|
|
||||||
return type.contains("base64", ignoreCase = true)
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
package de.softwareapp_hb.privateqrscanner.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import de.softwareapp_hb.privateqrscanner.R
|
|
||||||
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
|
|
||||||
import de.softwareapp_hb.privateqrscanner.util.InAppReviewRequester
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SettingsScreen(
|
|
||||||
historyEnabled: Boolean,
|
|
||||||
warningsEnabled: Boolean,
|
|
||||||
scanFeedbackEnabled: Boolean,
|
|
||||||
selectedUseCaseView: UseCaseView,
|
|
||||||
onHistoryToggle: (Boolean, Boolean) -> Unit,
|
|
||||||
onWarningsToggle: (Boolean) -> Unit,
|
|
||||||
onScanFeedbackToggle: (Boolean) -> Unit,
|
|
||||||
onUseCaseViewSelected: (UseCaseView) -> Unit
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val showDeleteConfirm = remember { mutableStateOf(false) }
|
|
||||||
val showUseCasePicker = remember { mutableStateOf(false) }
|
|
||||||
val showPrivacyPolicy = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (showDeleteConfirm.value) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showDeleteConfirm.value = false },
|
|
||||||
title = { Text(stringResource(R.string.settings)) },
|
|
||||||
text = { Text(stringResource(R.string.delete_history_on_disable)) },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
onHistoryToggle(false, true)
|
|
||||||
showDeleteConfirm.value = false
|
|
||||||
}) { Text(stringResource(R.string.confirm)) }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
onHistoryToggle(false, false)
|
|
||||||
showDeleteConfirm.value = false
|
|
||||||
}) { Text(stringResource(R.string.cancel)) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showUseCasePicker.value) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showUseCasePicker.value = false },
|
|
||||||
title = { Text(stringResource(R.string.select_use_case_view)) },
|
|
||||||
text = {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
||||||
UseCaseView.entries.forEach { candidate ->
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onUseCaseViewSelected(candidate)
|
|
||||||
showUseCasePicker.value = false
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(candidate.titleRes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showUseCasePicker.value = false }) {
|
|
||||||
Text(stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showPrivacyPolicy.value) {
|
|
||||||
PrivacyPolicyDialog(onDismiss = { showPrivacyPolicy.value = false })
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(24.dp),
|
|
||||||
verticalArrangement = Arrangement.Top
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(R.string.save_history))
|
|
||||||
Switch(
|
|
||||||
checked = historyEnabled,
|
|
||||||
onCheckedChange = { enabled ->
|
|
||||||
if (!enabled && historyEnabled) {
|
|
||||||
showDeleteConfirm.value = true
|
|
||||||
} else {
|
|
||||||
onHistoryToggle(enabled, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Text(text = stringResource(R.string.security_warnings))
|
|
||||||
Switch(checked = warningsEnabled, onCheckedChange = onWarningsToggle)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Text(text = stringResource(R.string.scan_feedback))
|
|
||||||
Switch(checked = scanFeedbackEnabled, onCheckedChange = onScanFeedbackToggle)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Text(text = stringResource(R.string.active_use_case_view))
|
|
||||||
Text(text = stringResource(selectedUseCaseView.titleRes))
|
|
||||||
TextButton(onClick = { showUseCasePicker.value = true }) {
|
|
||||||
Text(stringResource(R.string.select_use_case_view))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
Text(text = stringResource(R.string.about))
|
|
||||||
Text(text = stringResource(R.string.version))
|
|
||||||
Text(text = stringResource(R.string.licenses))
|
|
||||||
Text(text = stringResource(R.string.contact))
|
|
||||||
TextButton(onClick = { showPrivacyPolicy.value = true }) {
|
|
||||||
Text(text = stringResource(R.string.privacy_policy))
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
TextButton(onClick = { InAppReviewRequester.requestReview(context) }) {
|
|
||||||
Text(text = stringResource(R.string.review_app))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package de.softwareapp_hb.privateqrscanner.ui.theme
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
|
|
||||||
private val LightColors = lightColorScheme()
|
|
||||||
private val DarkColors = darkColorScheme()
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CleanScannerTheme(content: @Composable () -> Unit) {
|
|
||||||
val darkTheme = isSystemInDarkTheme()
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
|
||||||
} else {
|
|
||||||
if (darkTheme) DarkColors else LightColors
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialTheme(
|
|
||||||
colorScheme = colorScheme,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,14 @@ package de.softwareapp_hb.privateqrscanner.ui
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.History
|
||||||
|
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -20,6 +26,7 @@ import de.softwareapp_hb.privateqrscanner.R
|
|||||||
import de.softwareapp_hb.privateqrscanner.ui.screens.HistoryScreen
|
import de.softwareapp_hb.privateqrscanner.ui.screens.HistoryScreen
|
||||||
import de.softwareapp_hb.privateqrscanner.ui.screens.ScannerScreen
|
import de.softwareapp_hb.privateqrscanner.ui.screens.ScannerScreen
|
||||||
import de.softwareapp_hb.privateqrscanner.ui.screens.SettingsScreen
|
import de.softwareapp_hb.privateqrscanner.ui.screens.SettingsScreen
|
||||||
|
import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
|
||||||
|
|
||||||
private enum class RootTab { Scanner, History, Settings }
|
private enum class RootTab { Scanner, History, Settings }
|
||||||
|
|
||||||
@@ -33,8 +40,16 @@ fun CleanScannerAppRoot(container: AppContainer) {
|
|||||||
var activeTab by remember { mutableStateOf(RootTab.Scanner) }
|
var activeTab by remember { mutableStateOf(RootTab.Scanner) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
containerColor = PrivateQrColors.AppBackground,
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
val navColors = NavigationBarItemDefaults.colors(
|
||||||
|
selectedIconColor = PrivateQrColors.Teal700,
|
||||||
|
selectedTextColor = PrivateQrColors.Teal700,
|
||||||
|
indicatorColor = PrivateQrColors.Mint,
|
||||||
|
unselectedIconColor = PrivateQrColors.TextSecondary,
|
||||||
|
unselectedTextColor = PrivateQrColors.TextSecondary
|
||||||
|
)
|
||||||
|
NavigationBar(containerColor = PrivateQrColors.Surface) {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = activeTab == RootTab.Scanner,
|
selected = activeTab == RootTab.Scanner,
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -42,7 +57,8 @@ fun CleanScannerAppRoot(container: AppContainer) {
|
|||||||
scannerViewModel.resumeScanning()
|
scannerViewModel.resumeScanning()
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.scan)) },
|
label = { Text(stringResource(R.string.scan)) },
|
||||||
icon = {}
|
icon = { Icon(Icons.Default.QrCodeScanner, contentDescription = null) },
|
||||||
|
colors = navColors
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = activeTab == RootTab.History,
|
selected = activeTab == RootTab.History,
|
||||||
@@ -50,7 +66,8 @@ fun CleanScannerAppRoot(container: AppContainer) {
|
|||||||
activeTab = RootTab.History
|
activeTab = RootTab.History
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.history)) },
|
label = { Text(stringResource(R.string.history)) },
|
||||||
icon = {}
|
icon = { Icon(Icons.Default.History, contentDescription = null) },
|
||||||
|
colors = navColors
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = activeTab == RootTab.Settings,
|
selected = activeTab == RootTab.Settings,
|
||||||
@@ -58,7 +75,8 @@ fun CleanScannerAppRoot(container: AppContainer) {
|
|||||||
activeTab = RootTab.Settings
|
activeTab = RootTab.Settings
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.settings)) },
|
label = { Text(stringResource(R.string.settings)) },
|
||||||
icon = {}
|
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
|
||||||
|
colors = navColors
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,9 +109,15 @@ fun CleanScannerAppRoot(container: AppContainer) {
|
|||||||
query = appState.searchQuery,
|
query = appState.searchQuery,
|
||||||
history = appState.history,
|
history = appState.history,
|
||||||
useCaseView = appState.useCaseView,
|
useCaseView = appState.useCaseView,
|
||||||
|
historyEnabled = appState.historyEnabled,
|
||||||
|
warningsEnabled = appState.warningsEnabled,
|
||||||
|
scanFeedbackEnabled = appState.scanFeedbackEnabled,
|
||||||
onQueryChange = appViewModel::setQuery,
|
onQueryChange = appViewModel::setQuery,
|
||||||
onDelete = appViewModel::deleteHistoryItem,
|
onDelete = appViewModel::deleteHistoryItem,
|
||||||
onClearAll = appViewModel::clearHistory
|
onClearAll = appViewModel::clearHistory,
|
||||||
|
onHistoryToggle = appViewModel::setHistoryEnabled,
|
||||||
|
onWarningsToggle = appViewModel::setWarningsEnabled,
|
||||||
|
onScanFeedbackToggle = appViewModel::setScanFeedbackEnabled
|
||||||
)
|
)
|
||||||
|
|
||||||
RootTab.Settings -> SettingsScreen(
|
RootTab.Settings -> SettingsScreen(
|
||||||
@@ -47,7 +47,14 @@ fun UseCaseView.capabilities(): UseCaseCapabilities {
|
|||||||
allowBatchMode = false,
|
allowBatchMode = false,
|
||||||
allowCopy = true,
|
allowCopy = true,
|
||||||
allowShare = true,
|
allowShare = true,
|
||||||
allowOpenUrl = true
|
allowOpenUrl = true,
|
||||||
|
allowAddContact = true,
|
||||||
|
allowDialPhone = true,
|
||||||
|
allowSendSms = true,
|
||||||
|
allowSendEmail = true,
|
||||||
|
allowOpenWifiSettings = true,
|
||||||
|
allowAddCalendarEvent = true,
|
||||||
|
allowHistoryExport = true
|
||||||
)
|
)
|
||||||
|
|
||||||
UseCaseView.EventTicketing -> UseCaseCapabilities(
|
UseCaseView.EventTicketing -> UseCaseCapabilities(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.clean.scanner.ui.components
|
package de.softwareapp_hb.privateqrscanner.ui.components
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
@@ -0,0 +1,683 @@
|
|||||||
|
package de.softwareapp_hb.privateqrscanner.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Email
|
||||||
|
import androidx.compose.material.icons.filled.History
|
||||||
|
import androidx.compose.material.icons.filled.Link
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material.icons.filled.Wifi
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.SwipeToDismissBox
|
||||||
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.softwareapp_hb.privateqrscanner.R
|
||||||
|
import de.softwareapp_hb.privateqrscanner.domain.ScanRecord
|
||||||
|
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
|
||||||
|
import de.softwareapp_hb.privateqrscanner.ui.capabilities
|
||||||
|
import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
|
||||||
|
import de.softwareapp_hb.privateqrscanner.util.HistoryExportFormatter
|
||||||
|
import de.softwareapp_hb.privateqrscanner.util.Intents
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HistoryScreen(
|
||||||
|
query: String,
|
||||||
|
history: List<ScanRecord>,
|
||||||
|
useCaseView: UseCaseView,
|
||||||
|
historyEnabled: Boolean,
|
||||||
|
warningsEnabled: Boolean,
|
||||||
|
scanFeedbackEnabled: Boolean,
|
||||||
|
onQueryChange: (String) -> Unit,
|
||||||
|
onDelete: (Long) -> Unit,
|
||||||
|
onClearAll: () -> Unit,
|
||||||
|
onHistoryToggle: (Boolean, Boolean) -> Unit,
|
||||||
|
onWarningsToggle: (Boolean) -> Unit,
|
||||||
|
onScanFeedbackToggle: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val capabilities = useCaseView.capabilities()
|
||||||
|
val showDeleteAll = remember { mutableStateOf(false) }
|
||||||
|
val selectedItem = remember { mutableStateOf<ScanRecord?>(null) }
|
||||||
|
|
||||||
|
if (showDeleteAll.value) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showDeleteAll.value = false },
|
||||||
|
title = { Text(stringResource(R.string.delete_all)) },
|
||||||
|
text = { Text(stringResource(R.string.confirm_delete_all)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onClearAll()
|
||||||
|
showDeleteAll.value = false
|
||||||
|
}) { Text(stringResource(R.string.confirm)) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDeleteAll.value = false }) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val detail = selectedItem.value
|
||||||
|
if (detail != null) {
|
||||||
|
val detailIsBase64 = detail.isBase64Encoded()
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { selectedItem.value = null },
|
||||||
|
title = { Text(text = detail.type) },
|
||||||
|
text = {
|
||||||
|
if (detailIsBase64) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(text = stringResource(R.string.base64_encoded_notice))
|
||||||
|
Text(text = detail.content)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(text = detail.content)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { selectedItem.value = null }) {
|
||||||
|
Text(text = stringResource(R.string.confirm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(PrivateQrColors.AppBackground)
|
||||||
|
) {
|
||||||
|
val wideLayout = maxWidth >= 600.dp
|
||||||
|
|
||||||
|
if (wideLayout) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
HistoryHeader()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(24.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1.1f)
|
||||||
|
.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.history),
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
TextButton(
|
||||||
|
onClick = { showDeleteAll.value = true },
|
||||||
|
enabled = history.isNotEmpty(),
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = Color(0xFFBE123C),
|
||||||
|
disabledContentColor = PrivateQrColors.TextSecondary.copy(alpha = 0.45f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.delete_all), fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HistorySearchField(query = query, onQueryChange = onQueryChange)
|
||||||
|
HistoryList(
|
||||||
|
history = history,
|
||||||
|
onDelete = onDelete,
|
||||||
|
onOpenDetails = { selectedItem.value = it },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(0.95f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.local_controls),
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
LocalControlsCard(
|
||||||
|
history = history,
|
||||||
|
historyEnabled = historyEnabled,
|
||||||
|
warningsEnabled = warningsEnabled,
|
||||||
|
scanFeedbackEnabled = scanFeedbackEnabled,
|
||||||
|
allowHistoryExport = capabilities.allowHistoryExport,
|
||||||
|
onHistoryToggle = onHistoryToggle,
|
||||||
|
onWarningsToggle = onWarningsToggle,
|
||||||
|
onScanFeedbackToggle = onScanFeedbackToggle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
HistoryHeader()
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.history),
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
HistorySearchField(query = query, onQueryChange = onQueryChange)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (capabilities.allowHistoryExport) {
|
||||||
|
ExportButton(
|
||||||
|
text = stringResource(R.string.share_txt),
|
||||||
|
enabled = history.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
val exportText = HistoryExportFormatter.formatText(history)
|
||||||
|
Intents.shareContent(context, exportText, "text/plain")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ExportButton(
|
||||||
|
text = stringResource(R.string.share_csv),
|
||||||
|
enabled = history.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
val exportCsv = HistoryExportFormatter.formatCsv(history)
|
||||||
|
Intents.shareContent(context, exportCsv, "text/csv")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ExportButton(
|
||||||
|
text = stringResource(R.string.share_json),
|
||||||
|
enabled = history.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
val exportJson = HistoryExportFormatter.formatJson(history)
|
||||||
|
Intents.shareContent(context, exportJson, "application/json")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
TextButton(
|
||||||
|
onClick = { showDeleteAll.value = true },
|
||||||
|
enabled = history.isNotEmpty(),
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = Color(0xFFBE123C),
|
||||||
|
disabledContentColor = PrivateQrColors.TextSecondary.copy(alpha = 0.45f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.delete_all), fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (history.isEmpty()) {
|
||||||
|
item {
|
||||||
|
EmptyHistoryCard()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(history, key = { it.id }) { item ->
|
||||||
|
HistoryRow(
|
||||||
|
item = item,
|
||||||
|
onDelete = onDelete,
|
||||||
|
onOpenDetails = { selectedItem.value = item }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HistorySearchField(
|
||||||
|
query: String,
|
||||||
|
onQueryChange: (String) -> Unit
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = query,
|
||||||
|
onValueChange = onQueryChange,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = PrivateQrColors.Teal700
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = { Text(stringResource(R.string.search)) },
|
||||||
|
singleLine = true,
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = PrivateQrColors.Teal300,
|
||||||
|
unfocusedBorderColor = PrivateQrColors.Divider,
|
||||||
|
focusedContainerColor = PrivateQrColors.Surface,
|
||||||
|
unfocusedContainerColor = PrivateQrColors.Surface
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HistoryList(
|
||||||
|
history: List<ScanRecord>,
|
||||||
|
onDelete: (Long) -> Unit,
|
||||||
|
onOpenDetails: (ScanRecord) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
if (history.isEmpty()) {
|
||||||
|
item {
|
||||||
|
EmptyHistoryCard()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(history, key = { it.id }) { item ->
|
||||||
|
HistoryRow(
|
||||||
|
item = item,
|
||||||
|
onDelete = onDelete,
|
||||||
|
onOpenDetails = { onOpenDetails(item) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LocalControlsCard(
|
||||||
|
history: List<ScanRecord>,
|
||||||
|
historyEnabled: Boolean,
|
||||||
|
warningsEnabled: Boolean,
|
||||||
|
scanFeedbackEnabled: Boolean,
|
||||||
|
allowHistoryExport: Boolean,
|
||||||
|
onHistoryToggle: (Boolean, Boolean) -> Unit,
|
||||||
|
onWarningsToggle: (Boolean) -> Unit,
|
||||||
|
onScanFeedbackToggle: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
|
||||||
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
LocalControlRow(
|
||||||
|
title = stringResource(R.string.save_history),
|
||||||
|
checked = historyEnabled,
|
||||||
|
onCheckedChange = { onHistoryToggle(it, false) }
|
||||||
|
)
|
||||||
|
LocalControlRow(
|
||||||
|
title = stringResource(R.string.security_warnings),
|
||||||
|
checked = warningsEnabled,
|
||||||
|
onCheckedChange = onWarningsToggle
|
||||||
|
)
|
||||||
|
LocalControlRow(
|
||||||
|
title = stringResource(R.string.scan_feedback),
|
||||||
|
checked = scanFeedbackEnabled,
|
||||||
|
onCheckedChange = onScanFeedbackToggle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowHistoryExport) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
ExportButton(
|
||||||
|
text = stringResource(R.string.share_txt),
|
||||||
|
enabled = history.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
Intents.shareContent(context, HistoryExportFormatter.formatText(history), "text/plain")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ExportButton(
|
||||||
|
text = stringResource(R.string.share_csv),
|
||||||
|
enabled = history.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
Intents.shareContent(context, HistoryExportFormatter.formatCsv(history), "text/csv")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ExportButton(
|
||||||
|
text = stringResource(R.string.share_json),
|
||||||
|
enabled = history.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
Intents.shareContent(context, HistoryExportFormatter.formatJson(history), "application/json")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LocalControlRow(
|
||||||
|
title: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(58.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = PrivateQrColors.Surface,
|
||||||
|
checkedTrackColor = PrivateQrColors.Teal700,
|
||||||
|
uncheckedThumbColor = PrivateQrColors.Surface,
|
||||||
|
uncheckedTrackColor = PrivateQrColors.TextSecondary.copy(alpha = 0.35f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HistoryHeader() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.ic_launcher_legacy),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.app_name),
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.history_header_subtitle),
|
||||||
|
color = PrivateQrColors.TextSecondary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
|
||||||
|
shape = RoundedCornerShape(30.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(
|
||||||
|
colors = listOf(PrivateQrColors.Navy, PrivateQrColors.Teal900)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.history_hero_title),
|
||||||
|
color = PrivateQrColors.Surface,
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.history_hero_subtitle),
|
||||||
|
color = PrivateQrColors.Mint,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
PrivacyPill(stringResource(R.string.privacy_pill_no_ads))
|
||||||
|
PrivacyPill(stringResource(R.string.privacy_pill_no_tracking))
|
||||||
|
PrivacyPill(stringResource(R.string.privacy_pill_no_account))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PrivacyPill(text: String) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = PrivateQrColors.Mint.copy(alpha = 0.12f),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 9.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = PrivateQrColors.Mint,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ExportButton(
|
||||||
|
text: String,
|
||||||
|
enabled: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
containerColor = PrivateQrColors.Mint,
|
||||||
|
contentColor = PrivateQrColors.Teal700,
|
||||||
|
disabledContainerColor = PrivateQrColors.Mint.copy(alpha = 0.45f),
|
||||||
|
disabledContentColor = PrivateQrColors.Teal700.copy(alpha = 0.45f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(15.dp)
|
||||||
|
) {
|
||||||
|
Text(text = text, fontWeight = FontWeight.ExtraBold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyHistoryCard() {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.no_saved_scans_yet),
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.empty_history_description),
|
||||||
|
color = PrivateQrColors.TextSecondary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun HistoryRow(
|
||||||
|
item: ScanRecord,
|
||||||
|
onDelete: (Long) -> Unit,
|
||||||
|
onOpenDetails: () -> Unit
|
||||||
|
) {
|
||||||
|
val dismissState = rememberSwipeToDismissBoxState(
|
||||||
|
confirmValueChange = {
|
||||||
|
if (it == SwipeToDismissBoxValue.EndToStart || it == SwipeToDismissBoxValue.StartToEnd) {
|
||||||
|
onDelete(item.id)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwipeToDismissBox(
|
||||||
|
state = dismissState,
|
||||||
|
backgroundContent = {},
|
||||||
|
content = {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onOpenDetails() },
|
||||||
|
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(18.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(52.dp)
|
||||||
|
.background(
|
||||||
|
color = PrivateQrColors.Mint.copy(alpha = 0.55f),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = iconForType(item.type),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = PrivateQrColors.Teal700
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = item.type,
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
|
||||||
|
.format(Date(item.timestamp)),
|
||||||
|
color = PrivateQrColors.TextSecondary,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = if (item.isBase64Encoded()) {
|
||||||
|
stringResource(R.string.base64_encoded_inline, item.content)
|
||||||
|
} else {
|
||||||
|
item.content
|
||||||
|
},
|
||||||
|
color = PrivateQrColors.TextSecondary,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun iconForType(type: String): ImageVector {
|
||||||
|
return when {
|
||||||
|
type.equals("URL", ignoreCase = true) -> Icons.Default.Link
|
||||||
|
type.equals("Email", ignoreCase = true) -> Icons.Default.Email
|
||||||
|
type.equals("WiFi", ignoreCase = true) -> Icons.Default.Wifi
|
||||||
|
else -> Icons.Default.History
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ScanRecord.isBase64Encoded(): Boolean {
|
||||||
|
return type.contains("base64", ignoreCase = true)
|
||||||
|
}
|
||||||
@@ -9,16 +9,23 @@ import android.os.Build
|
|||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -46,6 +53,7 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
@@ -54,6 +62,7 @@ import de.softwareapp_hb.privateqrscanner.R
|
|||||||
import de.softwareapp_hb.privateqrscanner.data.scanner.DetectionBox
|
import de.softwareapp_hb.privateqrscanner.data.scanner.DetectionBox
|
||||||
import de.softwareapp_hb.privateqrscanner.data.scanner.DetectionPoint
|
import de.softwareapp_hb.privateqrscanner.data.scanner.DetectionPoint
|
||||||
import de.softwareapp_hb.privateqrscanner.domain.ScanResult
|
import de.softwareapp_hb.privateqrscanner.domain.ScanResult
|
||||||
|
import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
|
||||||
import de.softwareapp_hb.privateqrscanner.util.readableBarcodePayload
|
import de.softwareapp_hb.privateqrscanner.util.readableBarcodePayload
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScanner
|
import com.google.mlkit.vision.barcode.BarcodeScanner
|
||||||
@@ -104,6 +113,7 @@ internal fun GalleryScanPreviewDialog(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val bitmap = remember(imageUri) { imageUri?.let { loadBitmapFromUri(context, it) } }
|
val bitmap = remember(imageUri) { imageUri?.let { loadBitmapFromUri(context, it) } }
|
||||||
var liveCandidates by remember(imageUri, candidates) { mutableStateOf(candidates) }
|
var liveCandidates by remember(imageUri, candidates) { mutableStateOf(candidates) }
|
||||||
|
var selectedIndex by remember(imageUri) { mutableIntStateOf(0) }
|
||||||
var zoom by remember(imageUri) { mutableFloatStateOf(1f) }
|
var zoom by remember(imageUri) { mutableFloatStateOf(1f) }
|
||||||
var pan by remember(imageUri) { mutableStateOf(Offset.Zero) }
|
var pan by remember(imageUri) { mutableStateOf(Offset.Zero) }
|
||||||
var viewportSize by remember { mutableStateOf(IntSize.Zero) }
|
var viewportSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
@@ -130,6 +140,14 @@ internal fun GalleryScanPreviewDialog(
|
|||||||
onDispose { scanner.close() }
|
onDispose { scanner.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(liveCandidates.size) {
|
||||||
|
selectedIndex = if (liveCandidates.isEmpty()) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
selectedIndex.coerceIn(0, liveCandidates.lastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(bitmap, viewportSize, zoom, pan, scanTick) {
|
LaunchedEffect(bitmap, viewportSize, zoom, pan, scanTick) {
|
||||||
val bmp = bitmap ?: return@LaunchedEffect
|
val bmp = bitmap ?: return@LaunchedEffect
|
||||||
val vw = viewportSize.width.toFloat()
|
val vw = viewportSize.width.toFloat()
|
||||||
@@ -204,14 +222,33 @@ internal fun GalleryScanPreviewDialog(
|
|||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(stringResource(R.string.image_scan_pick_title, liveCandidates.size)) },
|
containerColor = PrivateQrColors.Surface,
|
||||||
|
shape = RoundedCornerShape(30.dp),
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.image_scan_pick_title, liveCandidates.size),
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
},
|
||||||
text = {
|
text = {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (liveCandidates.isEmpty()) {
|
||||||
|
stringResource(R.string.no_code_found_in_image)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.image_scan_pick_subtitle)
|
||||||
|
},
|
||||||
|
color = PrivateQrColors.TextSecondary,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -228,7 +265,7 @@ internal fun GalleryScanPreviewDialog(
|
|||||||
scanTick++
|
scanTick++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color.Black.copy(alpha = 0.32f), RoundedCornerShape(12.dp)),
|
.background(PrivateQrColors.Navy, RoundedCornerShape(24.dp)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
@@ -267,7 +304,7 @@ internal fun GalleryScanPreviewDialog(
|
|||||||
|
|
||||||
liveCandidates.forEachIndexed { index, candidate ->
|
liveCandidates.forEachIndexed { index, candidate ->
|
||||||
val box = candidate.box ?: return@forEachIndexed
|
val box = candidate.box ?: return@forEachIndexed
|
||||||
val color = Color(0xFF4AE3A3).copy(alpha = 0.96f)
|
val color = PrivateQrColors.Teal300.copy(alpha = 0.96f)
|
||||||
val points = box.corners.map { p ->
|
val points = box.corners.map { p ->
|
||||||
imageToScreen(p.x * imageW, p.y * imageH)
|
imageToScreen(p.x * imageW, p.y * imageH)
|
||||||
}
|
}
|
||||||
@@ -322,20 +359,45 @@ internal fun GalleryScanPreviewDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (liveCandidates.isEmpty()) {
|
if (liveCandidates.isNotEmpty()) {
|
||||||
Text(text = stringResource(R.string.no_code_found_in_image))
|
|
||||||
} else {
|
|
||||||
Text(text = stringResource(R.string.image_scan_pick_subtitle))
|
|
||||||
liveCandidates.forEachIndexed { index, candidate ->
|
liveCandidates.forEachIndexed { index, candidate ->
|
||||||
TextButton(
|
val selected = index == selectedIndex
|
||||||
onClick = { onPick(candidate) },
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = if (selected) Color(0xFFECFDF5) else PrivateQrColors.AppBackground,
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 2.dp,
|
||||||
|
color = if (selected) PrivateQrColors.Teal300 else Color.Transparent,
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
)
|
||||||
|
.clickable { selectedIndex = index }
|
||||||
|
.padding(18.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(54.dp)
|
||||||
|
.background(PrivateQrColors.Mint, RoundedCornerShape(16.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Text(
|
Text(
|
||||||
text = "${index + 1}. ${candidate.result.displayType}",
|
text = "${index + 1}",
|
||||||
textAlign = TextAlign.Start,
|
color = PrivateQrColors.Teal700,
|
||||||
modifier = Modifier.fillMaxWidth()
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = candidate.result.displayType,
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = if (candidate.result.isBase64Encoded) {
|
text = if (candidate.result.isBase64Encoded) {
|
||||||
@@ -343,10 +405,11 @@ internal fun GalleryScanPreviewDialog(
|
|||||||
} else {
|
} else {
|
||||||
candidate.result.content
|
candidate.result.content
|
||||||
},
|
},
|
||||||
|
color = PrivateQrColors.TextSecondary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis
|
||||||
textAlign = TextAlign.Start,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,9 +417,29 @@ internal fun GalleryScanPreviewDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {},
|
confirmButton = {
|
||||||
|
val selected = liveCandidates.getOrNull(selectedIndex)
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
if (selected != null) onPick(selected)
|
||||||
|
},
|
||||||
|
enabled = selected != null,
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
containerColor = PrivateQrColors.Teal700,
|
||||||
|
contentColor = PrivateQrColors.Surface,
|
||||||
|
disabledContainerColor = PrivateQrColors.TextSecondary.copy(alpha = 0.18f),
|
||||||
|
disabledContentColor = PrivateQrColors.TextSecondary
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(18.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.image_scan_use_selected))
|
||||||
|
}
|
||||||
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Teal700)
|
||||||
|
) {
|
||||||
Text(stringResource(R.string.cancel))
|
Text(stringResource(R.string.cancel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,9 +13,12 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.ContentCopy
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.IconToggleButton
|
import androidx.compose.material3.IconToggleButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -25,10 +28,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.softwareapp_hb.privateqrscanner.R
|
import de.softwareapp_hb.privateqrscanner.R
|
||||||
import de.softwareapp_hb.privateqrscanner.ui.BatchScanRecord
|
import de.softwareapp_hb.privateqrscanner.ui.BatchScanRecord
|
||||||
|
import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
|
||||||
import de.softwareapp_hb.privateqrscanner.util.ClipboardUtil
|
import de.softwareapp_hb.privateqrscanner.util.ClipboardUtil
|
||||||
import de.softwareapp_hb.privateqrscanner.util.Intents
|
import de.softwareapp_hb.privateqrscanner.util.Intents
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@@ -91,22 +96,46 @@ internal fun BatchResultsPanel(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(
|
.background(
|
||||||
color = Color.Black.copy(alpha = 0.42f),
|
color = PrivateQrColors.Deep.copy(alpha = 0.88f),
|
||||||
shape = RoundedCornerShape(14.dp)
|
shape = RoundedCornerShape(28.dp)
|
||||||
)
|
)
|
||||||
.padding(12.dp),
|
.padding(20.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.batch_captures_count, results.size),
|
text = stringResource(R.string.batch_captures_count, results.size),
|
||||||
color = Color.White
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
)
|
)
|
||||||
|
if (allowShare) {
|
||||||
|
TextButton(
|
||||||
|
onClick = { Intents.shareText(context, buildBatchExport(results)) },
|
||||||
|
enabled = results.isNotEmpty(),
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
containerColor = PrivateQrColors.Mint,
|
||||||
|
contentColor = PrivateQrColors.Teal700,
|
||||||
|
disabledContainerColor = PrivateQrColors.Mint.copy(alpha = 0.32f),
|
||||||
|
disabledContentColor = PrivateQrColors.Teal700.copy(alpha = 0.45f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(18.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.share_batch), fontWeight = FontWeight.ExtraBold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
results.take(3).forEach { item ->
|
results.take(3).forEach { item ->
|
||||||
val contentText = if (item.result.isBase64Encoded) {
|
val contentText = if (item.result.isBase64Encoded) {
|
||||||
stringResource(R.string.base64_encoded_inline, item.result.content)
|
stringResource(R.string.base64_encoded_inline, item.result.content)
|
||||||
} else {
|
} else {
|
||||||
item.result.content
|
item.result.content
|
||||||
}
|
}
|
||||||
|
HorizontalDivider(color = Color.White.copy(alpha = 0.12f))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -116,11 +145,15 @@ internal fun BatchResultsPanel(
|
|||||||
Text(
|
Text(
|
||||||
text = "${item.result.displayType}: $contentText",
|
text = "${item.result.displayType}: $contentText",
|
||||||
color = Color.White.copy(alpha = 0.92f),
|
color = Color.White.copy(alpha = 0.92f),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = timeFormat.format(Date(item.timestamp)),
|
text = timeFormat.format(Date(item.timestamp)),
|
||||||
color = Color.White.copy(alpha = 0.7f)
|
color = Color.White.copy(alpha = 0.7f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
@@ -143,18 +176,43 @@ internal fun BatchResultsPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(
|
||||||
TextButton(onClick = onClear, enabled = results.isNotEmpty()) {
|
modifier = Modifier
|
||||||
Text(stringResource(R.string.clear_batch))
|
.fillMaxWidth()
|
||||||
}
|
.background(Color(0xFF831843).copy(alpha = 0.42f), RoundedCornerShape(18.dp))
|
||||||
if (allowShare) {
|
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||||
TextButton(
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
onClick = { Intents.shareText(context, buildBatchExport(results)) },
|
verticalAlignment = Alignment.CenterVertically
|
||||||
enabled = results.isNotEmpty()
|
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.share_batch))
|
Text(text = "▲", color = Color(0xFFFF7A9A), fontWeight = FontWeight.ExtraBold)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.batch_ticket_privacy_note),
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = onClear,
|
||||||
|
enabled = results.isNotEmpty(),
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Mint)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.clear_batch), fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.aim_center_hint),
|
||||||
|
color = Color.White.copy(alpha = 0.68f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!allowShare && results.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.batch_ticket_privacy_note),
|
||||||
|
color = Color.White.copy(alpha = 0.68f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.softwareapp_hb.privateqrscanner.R
|
import de.softwareapp_hb.privateqrscanner.R
|
||||||
import de.softwareapp_hb.privateqrscanner.domain.ScanResult
|
import de.softwareapp_hb.privateqrscanner.domain.ScanResult
|
||||||
|
import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
|
||||||
import de.softwareapp_hb.privateqrscanner.util.ParsedContact
|
import de.softwareapp_hb.privateqrscanner.util.ParsedContact
|
||||||
import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers
|
import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers
|
||||||
import de.softwareapp_hb.privateqrscanner.util.UrlRiskScorer
|
import de.softwareapp_hb.privateqrscanner.util.UrlRiskScorer
|
||||||
@@ -60,39 +61,59 @@ internal fun ResultVisualCard(
|
|||||||
val fields = remember(result) { buildResultFields(result) }
|
val fields = remember(result) { buildResultFields(result) }
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
colors = CardDefaults.cardColors(containerColor = Color(0xFFF2F7FF)),
|
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.SoftSurface),
|
||||||
shape = RoundedCornerShape(14.dp)
|
shape = RoundedCornerShape(26.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(14.dp),
|
modifier = Modifier.padding(22.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||||
) {
|
) {
|
||||||
if (!result.isBase64Encoded && result.type == "WiFi") {
|
if (!result.isBase64Encoded && result.type == "WiFi") {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Wifi,
|
imageVector = Icons.Default.Wifi,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color(0xFF1D4ED8)
|
tint = PrivateQrColors.Teal700
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Wi-Fi",
|
text = "Wi-Fi",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.headlineSmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = result.displayType,
|
text = result.displayType,
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = PrivateQrColors.TextPrimary
|
||||||
)
|
)
|
||||||
|
if (!result.isBase64Encoded && result.type == "URL") {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.result_local_check),
|
||||||
|
color = PrivateQrColors.Teal700,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = PrivateQrColors.Mint,
|
||||||
|
shape = RoundedCornerShape(50)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (result.isBase64Encoded) {
|
if (result.isBase64Encoded) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.base64_encoded_notice),
|
text = stringResource(R.string.base64_encoded_notice),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = Color(0xFF4F6277)
|
color = PrivateQrColors.TextSecondary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (fields.isEmpty()) {
|
if (fields.isEmpty()) {
|
||||||
@@ -106,7 +127,7 @@ internal fun ResultVisualCard(
|
|||||||
Text(
|
Text(
|
||||||
text = field.label,
|
text = field.label,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = Color(0xFF4F6277)
|
color = PrivateQrColors.TextSecondary
|
||||||
)
|
)
|
||||||
val isClickableUrl = result.type == "URL" &&
|
val isClickableUrl = result.type == "URL" &&
|
||||||
field.label == "Link" &&
|
field.label == "Link" &&
|
||||||
@@ -114,7 +135,7 @@ internal fun ResultVisualCard(
|
|||||||
Text(
|
Text(
|
||||||
text = field.value,
|
text = field.value,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = if (isClickableUrl) Color(0xFF1D4ED8) else Color.Unspecified,
|
color = if (isClickableUrl) Color(0xFF1D4ED8) else PrivateQrColors.TextPrimary,
|
||||||
textDecoration = if (isClickableUrl) TextDecoration.Underline else null,
|
textDecoration = if (isClickableUrl) TextDecoration.Underline else null,
|
||||||
modifier = if (isClickableUrl) {
|
modifier = if (isClickableUrl) {
|
||||||
Modifier.clickable { onOpenUrl(field.value) }
|
Modifier.clickable { onOpenUrl(field.value) }
|
||||||
@@ -127,6 +148,30 @@ internal fun ResultVisualCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!result.isBase64Encoded && result.type == "URL") {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = Color(0xFFECFDF5),
|
||||||
|
shape = RoundedCornerShape(18.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "✓",
|
||||||
|
color = PrivateQrColors.Teal700,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.result_checked_on_device),
|
||||||
|
color = PrivateQrColors.Teal700,
|
||||||
|
style = MaterialTheme.typography.labelLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,9 +21,11 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||||
import androidx.compose.material.icons.automirrored.filled.ViewList
|
import androidx.compose.material.icons.automirrored.filled.ViewList
|
||||||
import androidx.compose.material.icons.filled.ContentCopy
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.FlashOff
|
import androidx.compose.material.icons.filled.FlashOff
|
||||||
@@ -34,9 +36,11 @@ import androidx.compose.material.icons.filled.UploadFile
|
|||||||
import androidx.compose.material.icons.filled.ViewModule
|
import androidx.compose.material.icons.filled.ViewModule
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
@@ -57,6 +61,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
@@ -67,6 +72,8 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -79,8 +86,9 @@ import de.softwareapp_hb.privateqrscanner.domain.ScanResult
|
|||||||
import de.softwareapp_hb.privateqrscanner.ui.BatchScanRecord
|
import de.softwareapp_hb.privateqrscanner.ui.BatchScanRecord
|
||||||
import de.softwareapp_hb.privateqrscanner.ui.EventTicketScanDecision
|
import de.softwareapp_hb.privateqrscanner.ui.EventTicketScanDecision
|
||||||
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
|
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
|
||||||
import com.clean.scanner.ui.components.CameraPreview
|
import de.softwareapp_hb.privateqrscanner.ui.components.CameraPreview
|
||||||
import de.softwareapp_hb.privateqrscanner.ui.capabilities
|
import de.softwareapp_hb.privateqrscanner.ui.capabilities
|
||||||
|
import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
|
||||||
import de.softwareapp_hb.privateqrscanner.util.ClipboardUtil
|
import de.softwareapp_hb.privateqrscanner.util.ClipboardUtil
|
||||||
import de.softwareapp_hb.privateqrscanner.util.Intents
|
import de.softwareapp_hb.privateqrscanner.util.Intents
|
||||||
import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers
|
import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers
|
||||||
@@ -183,6 +191,17 @@ fun ScannerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestOpenUrl(url: String) {
|
||||||
|
val risk = UrlRiskScorer.score(url)
|
||||||
|
val risky = warningsEnabled && risk.score >= 3
|
||||||
|
if (risky) {
|
||||||
|
pendingOpenUrl = url
|
||||||
|
showRiskWarning = true
|
||||||
|
} else {
|
||||||
|
Intents.openUrl(context, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val permissionLauncher = rememberLauncherForActivityResult(
|
val permissionLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.RequestPermission()
|
contract = ActivityResultContracts.RequestPermission()
|
||||||
) { granted ->
|
) { granted ->
|
||||||
@@ -317,14 +336,15 @@ fun ScannerScreen(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.background(PrivateQrColors.Deep)
|
||||||
.onSizeChanged { containerSize = it }
|
.onSizeChanged { containerSize = it }
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val viewW = containerSize.width.toFloat()
|
val viewW = containerSize.width.toFloat()
|
||||||
val viewH = containerSize.height.toFloat()
|
val viewH = containerSize.height.toFloat()
|
||||||
val galleryOpen = imageScanPreviewUri != null
|
val galleryOpen = imageScanPreviewUri != null
|
||||||
val aimW = viewW * 0.62f
|
val aimW = viewW * 0.70f
|
||||||
val aimH = with(density) { 200.dp.toPx() }
|
val aimH = with(density) { 230.dp.toPx() }
|
||||||
val aimLeft = (viewW - aimW) / 2f
|
val aimLeft = (viewW - aimW) / 2f
|
||||||
val aimTop = (viewH - aimH) / 2f
|
val aimTop = (viewH - aimH) / 2f
|
||||||
val aimRight = aimLeft + aimW
|
val aimRight = aimLeft + aimW
|
||||||
@@ -385,6 +405,20 @@ fun ScannerScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
PrivateQrColors.Deep.copy(alpha = 0.72f),
|
||||||
|
PrivateQrColors.Teal900.copy(alpha = 0.34f),
|
||||||
|
PrivateQrColors.Deep.copy(alpha = 0.78f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if (detectionBoxes.isNotEmpty()) {
|
if (detectionBoxes.isNotEmpty()) {
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -421,15 +455,15 @@ fun ScannerScreen(
|
|||||||
drawPath(
|
drawPath(
|
||||||
path = outline,
|
path = outline,
|
||||||
color = boxColor.copy(alpha = 0.96f),
|
color = boxColor.copy(alpha = 0.96f),
|
||||||
style = Stroke(width = 4f)
|
style = Stroke(width = 5.5f)
|
||||||
)
|
)
|
||||||
} else if (right > left && bottom > top) {
|
} else if (right > left && bottom > top) {
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
color = boxColor.copy(alpha = 0.95f),
|
color = boxColor.copy(alpha = 0.95f),
|
||||||
topLeft = Offset(left, top),
|
topLeft = Offset(left, top),
|
||||||
size = Size(right - left, bottom - top),
|
size = Size(right - left, bottom - top),
|
||||||
cornerRadius = CornerRadius(14f, 14f),
|
cornerRadius = CornerRadius(22f, 22f),
|
||||||
style = Stroke(width = 4f)
|
style = Stroke(width = 5.5f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,22 +473,22 @@ fun ScannerScreen(
|
|||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.fillMaxWidth(0.62f)
|
.fillMaxWidth(0.70f)
|
||||||
.height(200.dp)
|
.height(230.dp)
|
||||||
) {
|
) {
|
||||||
val guideColor = when {
|
val guideColor = when {
|
||||||
hasReadableInView -> Color(0xFF4AE3A3)
|
hasReadableInView -> Color(0xFF4AE3A3)
|
||||||
hasPotentialInView -> Color(0xFFFFC857)
|
hasPotentialInView -> PrivateQrColors.Warning
|
||||||
else -> Color(0xFF7CE6C6)
|
else -> PrivateQrColors.Teal300
|
||||||
}
|
}
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
color = guideColor.copy(alpha = 0.08f),
|
color = guideColor.copy(alpha = 0.12f),
|
||||||
cornerRadius = CornerRadius(22f, 22f)
|
cornerRadius = CornerRadius(32f, 32f)
|
||||||
)
|
)
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
color = guideColor.copy(alpha = 0.90f),
|
color = guideColor.copy(alpha = 0.90f),
|
||||||
cornerRadius = CornerRadius(22f, 22f),
|
cornerRadius = CornerRadius(32f, 32f),
|
||||||
style = Stroke(width = 3.5f)
|
style = Stroke(width = 5f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,15 +499,16 @@ fun ScannerScreen(
|
|||||||
else -> stringResource(R.string.aim_center_hint)
|
else -> stringResource(R.string.aim_center_hint)
|
||||||
},
|
},
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
.padding(bottom = if (isBatchModeActive) 190.dp else 56.dp)
|
.padding(bottom = if (isBatchModeActive) 190.dp else 56.dp)
|
||||||
.background(
|
.background(
|
||||||
color = Color.Black.copy(alpha = 0.35f),
|
color = Color.Black.copy(alpha = 0.45f),
|
||||||
shape = RoundedCornerShape(18.dp)
|
shape = RoundedCornerShape(24.dp)
|
||||||
)
|
)
|
||||||
.padding(horizontal = 14.dp, vertical = 8.dp)
|
.padding(horizontal = 18.dp, vertical = 10.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (capabilities.allowScanFromImage) {
|
if (capabilities.allowScanFromImage) {
|
||||||
@@ -483,8 +518,8 @@ fun ScannerScreen(
|
|||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(top = 12.dp, end = 12.dp)
|
.padding(top = 12.dp, end = 12.dp)
|
||||||
.background(
|
.background(
|
||||||
color = Color.Black.copy(alpha = 0.35f),
|
color = Color.Black.copy(alpha = 0.38f),
|
||||||
shape = RoundedCornerShape(14.dp)
|
shape = RoundedCornerShape(18.dp)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -501,8 +536,8 @@ fun ScannerScreen(
|
|||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(top = 12.dp, end = if (capabilities.allowScanFromImage) 64.dp else 12.dp)
|
.padding(top = 12.dp, end = if (capabilities.allowScanFromImage) 64.dp else 12.dp)
|
||||||
.background(
|
.background(
|
||||||
color = Color.Black.copy(alpha = 0.35f),
|
color = Color.Black.copy(alpha = 0.38f),
|
||||||
shape = RoundedCornerShape(14.dp)
|
shape = RoundedCornerShape(18.dp)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -516,15 +551,16 @@ fun ScannerScreen(
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(useCaseView.titleRes),
|
text = stringResource(useCaseView.titleRes),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
.padding(top = 16.dp, start = 64.dp, end = 64.dp)
|
.padding(top = 16.dp, start = 64.dp, end = 64.dp)
|
||||||
.background(
|
.background(
|
||||||
color = Color.Black.copy(alpha = 0.4f),
|
color = Color.Black.copy(alpha = 0.52f),
|
||||||
shape = RoundedCornerShape(14.dp)
|
shape = RoundedCornerShape(22.dp)
|
||||||
)
|
)
|
||||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
.padding(horizontal = 18.dp, vertical = 10.dp)
|
||||||
)
|
)
|
||||||
if (useCaseView == UseCaseView.EventTicketing) {
|
if (useCaseView == UseCaseView.EventTicketing) {
|
||||||
Text(
|
Text(
|
||||||
@@ -613,62 +649,62 @@ fun ScannerScreen(
|
|||||||
if (lastResult.isBase64Encoded) null else ScanContentParsers.parseCalendarEvent(lastResult.content)
|
if (lastResult.isBase64Encoded) null else ScanContentParsers.parseCalendarEvent(lastResult.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
ModalBottomSheet(onDismissRequest = onScanAgain) {
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onScanAgain,
|
||||||
|
containerColor = PrivateQrColors.Surface,
|
||||||
|
shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp)
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(start = 20.dp, top = 4.dp, end = 20.dp, bottom = 28.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||||
) {
|
) {
|
||||||
ResultVisualCard(
|
ResultVisualCard(
|
||||||
result = lastResult,
|
result = lastResult,
|
||||||
onOpenUrl = { url ->
|
onOpenUrl = ::requestOpenUrl
|
||||||
val risk = UrlRiskScorer.score(url)
|
|
||||||
val risky = warningsEnabled && risk.score >= 3
|
|
||||||
if (risky) {
|
|
||||||
pendingOpenUrl = url
|
|
||||||
showRiskWarning = true
|
|
||||||
} else {
|
|
||||||
Intents.openUrl(context, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
val hasQuickActions = capabilities.allowCopy ||
|
val hasQuickActions = capabilities.allowCopy ||
|
||||||
capabilities.allowShare ||
|
capabilities.allowShare ||
|
||||||
(capabilities.allowAddContact && parsedContact != null)
|
(capabilities.allowAddContact && parsedContact != null) ||
|
||||||
|
(capabilities.allowOpenUrl && !lastResult.isBase64Encoded && lastResult.type == "URL")
|
||||||
|
|
||||||
if (hasQuickActions) {
|
if (hasQuickActions) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.horizontalScroll(rememberScrollState()),
|
.horizontalScroll(rememberScrollState()),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
if (capabilities.allowAddContact && parsedContact != null) {
|
if (capabilities.allowAddContact && parsedContact != null) {
|
||||||
IconButton(onClick = {
|
ResultActionPill(
|
||||||
Intents.addContact(context, parsedContact, lastResult.content)
|
text = stringResource(R.string.add_contact),
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.PersonAdd,
|
imageVector = Icons.Default.PersonAdd,
|
||||||
contentDescription = stringResource(R.string.add_contact)
|
onClick = { Intents.addContact(context, parsedContact, lastResult.content) },
|
||||||
|
primary = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (capabilities.allowCopy) {
|
if (capabilities.allowCopy) {
|
||||||
IconButton(onClick = { ClipboardUtil.copy(context, lastResult.content) }) {
|
ResultActionPill(
|
||||||
Icon(
|
text = stringResource(R.string.copy),
|
||||||
imageVector = Icons.Default.ContentCopy,
|
imageVector = Icons.Default.ContentCopy,
|
||||||
contentDescription = stringResource(R.string.copy)
|
onClick = { ClipboardUtil.copy(context, lastResult.content) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (capabilities.allowShare) {
|
if (capabilities.allowShare) {
|
||||||
IconButton(onClick = { Intents.shareText(context, lastResult.content) }) {
|
ResultActionPill(
|
||||||
Icon(
|
text = stringResource(R.string.share),
|
||||||
imageVector = Icons.Default.Share,
|
imageVector = Icons.Default.Share,
|
||||||
contentDescription = stringResource(R.string.share)
|
onClick = { Intents.shareText(context, lastResult.content) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (capabilities.allowOpenUrl && !lastResult.isBase64Encoded && lastResult.type == "URL") {
|
||||||
|
ResultActionPill(
|
||||||
|
text = stringResource(R.string.open),
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
|
||||||
|
onClick = { requestOpenUrl(lastResult.content) },
|
||||||
|
primary = true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -677,9 +713,14 @@ fun ScannerScreen(
|
|||||||
when (lastResult.type) {
|
when (lastResult.type) {
|
||||||
"Phone" -> {
|
"Phone" -> {
|
||||||
if (capabilities.allowDialPhone) {
|
if (capabilities.allowDialPhone) {
|
||||||
Button(onClick = {
|
Button(
|
||||||
|
onClick = {
|
||||||
Intents.dialPhone(context, ScanContentParsers.extractPhoneNumber(lastResult.content))
|
Intents.dialPhone(context, ScanContentParsers.extractPhoneNumber(lastResult.content))
|
||||||
}) {
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = PrivateQrColors.Teal700
|
||||||
|
)
|
||||||
|
) {
|
||||||
Text(stringResource(R.string.call_number))
|
Text(stringResource(R.string.call_number))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -687,10 +728,15 @@ fun ScannerScreen(
|
|||||||
|
|
||||||
"SMS" -> {
|
"SMS" -> {
|
||||||
if (capabilities.allowSendSms) {
|
if (capabilities.allowSendSms) {
|
||||||
Button(onClick = {
|
Button(
|
||||||
|
onClick = {
|
||||||
val smsData = ScanContentParsers.parseSms(lastResult.content)
|
val smsData = ScanContentParsers.parseSms(lastResult.content)
|
||||||
Intents.sendSms(context, smsData.first, smsData.second)
|
Intents.sendSms(context, smsData.first, smsData.second)
|
||||||
}) {
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = PrivateQrColors.Teal700
|
||||||
|
)
|
||||||
|
) {
|
||||||
Text(stringResource(R.string.send_sms))
|
Text(stringResource(R.string.send_sms))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,9 +744,14 @@ fun ScannerScreen(
|
|||||||
|
|
||||||
"Email" -> {
|
"Email" -> {
|
||||||
if (capabilities.allowSendEmail) {
|
if (capabilities.allowSendEmail) {
|
||||||
Button(onClick = {
|
Button(
|
||||||
|
onClick = {
|
||||||
Intents.sendEmail(context, ScanContentParsers.extractEmail(lastResult.content), null)
|
Intents.sendEmail(context, ScanContentParsers.extractEmail(lastResult.content), null)
|
||||||
}) {
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = PrivateQrColors.Teal700
|
||||||
|
)
|
||||||
|
) {
|
||||||
Text(stringResource(R.string.send_email))
|
Text(stringResource(R.string.send_email))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -708,7 +759,12 @@ fun ScannerScreen(
|
|||||||
|
|
||||||
"WiFi" -> {
|
"WiFi" -> {
|
||||||
if (capabilities.allowOpenWifiSettings) {
|
if (capabilities.allowOpenWifiSettings) {
|
||||||
Button(onClick = { Intents.openWifiSettings(context) }) {
|
Button(
|
||||||
|
onClick = { Intents.openWifiSettings(context) },
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = PrivateQrColors.Teal700
|
||||||
|
)
|
||||||
|
) {
|
||||||
Text(stringResource(R.string.open_wifi_settings))
|
Text(stringResource(R.string.open_wifi_settings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -716,9 +772,14 @@ fun ScannerScreen(
|
|||||||
|
|
||||||
"Calendar" -> {
|
"Calendar" -> {
|
||||||
if (capabilities.allowAddCalendarEvent) {
|
if (capabilities.allowAddCalendarEvent) {
|
||||||
Button(onClick = {
|
Button(
|
||||||
|
onClick = {
|
||||||
Intents.addCalendarEvent(context, parsedEvent, lastResult.content)
|
Intents.addCalendarEvent(context, parsedEvent, lastResult.content)
|
||||||
}) {
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = PrivateQrColors.Teal700
|
||||||
|
)
|
||||||
|
) {
|
||||||
Text(stringResource(R.string.add_calendar_event))
|
Text(stringResource(R.string.add_calendar_event))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -732,15 +793,44 @@ fun ScannerScreen(
|
|||||||
if (showRiskWarning && pendingOpenUrl != null) {
|
if (showRiskWarning && pendingOpenUrl != null) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showRiskWarning = false },
|
onDismissRequest = { showRiskWarning = false },
|
||||||
text = { Text(stringResource(R.string.risk_warning)) },
|
containerColor = PrivateQrColors.Surface,
|
||||||
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.risk_warning_title),
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.risk_warning),
|
||||||
|
color = PrivateQrColors.TextSecondary,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(
|
||||||
|
onClick = {
|
||||||
Intents.openUrl(context, pendingOpenUrl!!)
|
Intents.openUrl(context, pendingOpenUrl!!)
|
||||||
showRiskWarning = false
|
showRiskWarning = false
|
||||||
}) { Text(stringResource(R.string.open_anyway)) }
|
},
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
containerColor = PrivateQrColors.Teal700,
|
||||||
|
contentColor = PrivateQrColors.Surface
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(18.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.open_anyway))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showRiskWarning = false }) {
|
TextButton(
|
||||||
|
onClick = { showRiskWarning = false },
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = PrivateQrColors.Teal700
|
||||||
|
)
|
||||||
|
) {
|
||||||
Text(stringResource(R.string.cancel))
|
Text(stringResource(R.string.cancel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -831,3 +921,31 @@ private fun parseWhitelistIds(raw: String): Set<String> {
|
|||||||
.filter { it.isNotBlank() }
|
.filter { it.isNotBlank() }
|
||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ResultActionPill(
|
||||||
|
text: String,
|
||||||
|
imageVector: ImageVector,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
primary: Boolean = false
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onClick,
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
containerColor = if (primary) PrivateQrColors.Teal700 else PrivateQrColors.AppBackground,
|
||||||
|
contentColor = if (primary) PrivateQrColors.Surface else PrivateQrColors.TextPrimary
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(18.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = imageVector,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
package de.softwareapp_hb.privateqrscanner.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.softwareapp_hb.privateqrscanner.R
|
||||||
|
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
|
||||||
|
import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
|
||||||
|
import de.softwareapp_hb.privateqrscanner.util.InAppReviewRequester
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
historyEnabled: Boolean,
|
||||||
|
warningsEnabled: Boolean,
|
||||||
|
scanFeedbackEnabled: Boolean,
|
||||||
|
selectedUseCaseView: UseCaseView,
|
||||||
|
onHistoryToggle: (Boolean, Boolean) -> Unit,
|
||||||
|
onWarningsToggle: (Boolean) -> Unit,
|
||||||
|
onScanFeedbackToggle: (Boolean) -> Unit,
|
||||||
|
onUseCaseViewSelected: (UseCaseView) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val showDeleteConfirm = remember { mutableStateOf(false) }
|
||||||
|
val showPrivacyPolicy = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showDeleteConfirm.value) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showDeleteConfirm.value = false },
|
||||||
|
title = { Text(stringResource(R.string.settings)) },
|
||||||
|
text = { Text(stringResource(R.string.delete_history_on_disable)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onHistoryToggle(false, true)
|
||||||
|
showDeleteConfirm.value = false
|
||||||
|
}) { Text(stringResource(R.string.confirm)) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onHistoryToggle(false, false)
|
||||||
|
showDeleteConfirm.value = false
|
||||||
|
}) { Text(stringResource(R.string.cancel)) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPrivacyPolicy.value) {
|
||||||
|
PrivacyPolicyDialog(onDismiss = { showPrivacyPolicy.value = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(PrivateQrColors.AppBackground)
|
||||||
|
) {
|
||||||
|
val wideLayout = maxWidth >= 600.dp
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||||
|
) {
|
||||||
|
SettingsHeader()
|
||||||
|
|
||||||
|
if (wideLayout) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(18.dp)
|
||||||
|
) {
|
||||||
|
SettingsToggleCard(
|
||||||
|
historyEnabled = historyEnabled,
|
||||||
|
warningsEnabled = warningsEnabled,
|
||||||
|
scanFeedbackEnabled = scanFeedbackEnabled,
|
||||||
|
onHistoryToggle = onHistoryToggle,
|
||||||
|
onWarningsToggle = onWarningsToggle,
|
||||||
|
onScanFeedbackToggle = onScanFeedbackToggle,
|
||||||
|
onRequestDisableHistory = { showDeleteConfirm.value = true }
|
||||||
|
)
|
||||||
|
AboutCard(
|
||||||
|
onPrivacyClick = { showPrivacyPolicy.value = true },
|
||||||
|
onReviewClick = { InAppReviewRequester.requestReview(context) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
UseCaseSelectionCard(
|
||||||
|
selectedUseCaseView = selectedUseCaseView,
|
||||||
|
onUseCaseViewSelected = onUseCaseViewSelected,
|
||||||
|
modifier = Modifier.weight(0.9f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SettingsToggleCard(
|
||||||
|
historyEnabled = historyEnabled,
|
||||||
|
warningsEnabled = warningsEnabled,
|
||||||
|
scanFeedbackEnabled = scanFeedbackEnabled,
|
||||||
|
onHistoryToggle = onHistoryToggle,
|
||||||
|
onWarningsToggle = onWarningsToggle,
|
||||||
|
onScanFeedbackToggle = onScanFeedbackToggle,
|
||||||
|
onRequestDisableHistory = { showDeleteConfirm.value = true }
|
||||||
|
)
|
||||||
|
UseCaseSelectionCard(
|
||||||
|
selectedUseCaseView = selectedUseCaseView,
|
||||||
|
onUseCaseViewSelected = onUseCaseViewSelected
|
||||||
|
)
|
||||||
|
AboutCard(
|
||||||
|
onPrivacyClick = { showPrivacyPolicy.value = true },
|
||||||
|
onReviewClick = { InAppReviewRequester.requestReview(context) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsToggleCard(
|
||||||
|
historyEnabled: Boolean,
|
||||||
|
warningsEnabled: Boolean,
|
||||||
|
scanFeedbackEnabled: Boolean,
|
||||||
|
onHistoryToggle: (Boolean, Boolean) -> Unit,
|
||||||
|
onWarningsToggle: (Boolean) -> Unit,
|
||||||
|
onScanFeedbackToggle: (Boolean) -> Unit,
|
||||||
|
onRequestDisableHistory: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
|
||||||
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp)) {
|
||||||
|
SettingsToggleRow(
|
||||||
|
title = stringResource(R.string.save_history),
|
||||||
|
checked = historyEnabled,
|
||||||
|
onCheckedChange = { enabled ->
|
||||||
|
if (!enabled && historyEnabled) {
|
||||||
|
onRequestDisableHistory()
|
||||||
|
} else {
|
||||||
|
onHistoryToggle(enabled, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SettingsDivider()
|
||||||
|
SettingsToggleRow(
|
||||||
|
title = stringResource(R.string.security_warnings),
|
||||||
|
checked = warningsEnabled,
|
||||||
|
onCheckedChange = onWarningsToggle
|
||||||
|
)
|
||||||
|
SettingsDivider()
|
||||||
|
SettingsToggleRow(
|
||||||
|
title = stringResource(R.string.scan_feedback),
|
||||||
|
checked = scanFeedbackEnabled,
|
||||||
|
onCheckedChange = onScanFeedbackToggle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UseCaseSelectionCard(
|
||||||
|
selectedUseCaseView: UseCaseView,
|
||||||
|
onUseCaseViewSelected: (UseCaseView) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.select_use_case_view),
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
UseCaseView.entries.forEach { candidate ->
|
||||||
|
UseCasePickerOption(
|
||||||
|
candidate = candidate,
|
||||||
|
selected = candidate == selectedUseCaseView,
|
||||||
|
onClick = { onUseCaseViewSelected(candidate) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AboutCard(
|
||||||
|
onPrivacyClick: () -> Unit,
|
||||||
|
onReviewClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
|
||||||
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.about),
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
InfoLine(stringResource(R.string.version))
|
||||||
|
InfoLine(stringResource(R.string.licenses))
|
||||||
|
InfoLine(stringResource(R.string.contact))
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onPrivacyClick,
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Teal700)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.privacy_policy), fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = onReviewClick,
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Teal700)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.review_app), fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UseCasePickerOption(
|
||||||
|
candidate: UseCaseView,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val borderColor = if (selected) PrivateQrColors.Teal300 else Color.Transparent
|
||||||
|
val backgroundColor = if (selected) Color(0xFFECFDF5) else PrivateQrColors.AppBackground
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor, RoundedCornerShape(24.dp))
|
||||||
|
.border(2.dp, borderColor, RoundedCornerShape(24.dp))
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(18.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(58.dp)
|
||||||
|
.background(PrivateQrColors.Mint, RoundedCornerShape(16.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (selected) "✓" else "◎",
|
||||||
|
color = PrivateQrColors.Teal700,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(candidate.titleRes),
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(candidate.descriptionRes()),
|
||||||
|
color = PrivateQrColors.TextSecondary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun UseCaseView.descriptionRes(): Int {
|
||||||
|
return when (this) {
|
||||||
|
UseCaseView.EverydayPersonal -> R.string.use_case_everyday_description
|
||||||
|
UseCaseView.EventTicketing -> R.string.use_case_event_ticketing_description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsHeader() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.ic_launcher_legacy),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.app_name),
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_header_subtitle),
|
||||||
|
color = PrivateQrColors.TextSecondary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
|
||||||
|
shape = RoundedCornerShape(30.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(
|
||||||
|
colors = listOf(PrivateQrColors.Navy, PrivateQrColors.Teal900)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_hero_title),
|
||||||
|
color = PrivateQrColors.Surface,
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_hero_subtitle),
|
||||||
|
color = PrivateQrColors.Mint,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsToggleRow(
|
||||||
|
title: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(74.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = PrivateQrColors.TextPrimary,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = PrivateQrColors.Surface,
|
||||||
|
checkedTrackColor = PrivateQrColors.Teal700,
|
||||||
|
uncheckedThumbColor = PrivateQrColors.Surface,
|
||||||
|
uncheckedTrackColor = PrivateQrColors.TextSecondary.copy(alpha = 0.35f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsDivider() {
|
||||||
|
HorizontalDivider(color = PrivateQrColors.Divider)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoLine(text: String) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = PrivateQrColors.TextSecondary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package de.softwareapp_hb.privateqrscanner.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
object PrivateQrColors {
|
||||||
|
val Deep = Color(0xFF07111F)
|
||||||
|
val Navy = Color(0xFF0B1220)
|
||||||
|
val Teal900 = Color(0xFF123B3F)
|
||||||
|
val Teal800 = Color(0xFF155E63)
|
||||||
|
val Teal700 = Color(0xFF0F766E)
|
||||||
|
val Teal300 = Color(0xFF2DD4BF)
|
||||||
|
val Mint = Color(0xFFDFF7F2)
|
||||||
|
val AppBackground = Color(0xFFF6FBFA)
|
||||||
|
val Surface = Color(0xFFFFFFFF)
|
||||||
|
val SoftSurface = Color(0xFFF2F7FF)
|
||||||
|
val TextPrimary = Color(0xFF0B1220)
|
||||||
|
val TextSecondary = Color(0xFF607080)
|
||||||
|
val Divider = Color(0xFFDCE8E6)
|
||||||
|
val Success = Color(0xFF10B981)
|
||||||
|
val Warning = Color(0xFFFFC857)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package de.softwareapp_hb.privateqrscanner.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
private val LightColors = lightColorScheme(
|
||||||
|
primary = PrivateQrColors.Teal700,
|
||||||
|
onPrimary = PrivateQrColors.Surface,
|
||||||
|
secondary = PrivateQrColors.Teal300,
|
||||||
|
onSecondary = PrivateQrColors.Navy,
|
||||||
|
background = PrivateQrColors.AppBackground,
|
||||||
|
onBackground = PrivateQrColors.TextPrimary,
|
||||||
|
surface = PrivateQrColors.Surface,
|
||||||
|
onSurface = PrivateQrColors.TextPrimary,
|
||||||
|
surfaceVariant = PrivateQrColors.Mint,
|
||||||
|
onSurfaceVariant = PrivateQrColors.TextSecondary
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DarkColors = darkColorScheme(
|
||||||
|
primary = PrivateQrColors.Teal300,
|
||||||
|
onPrimary = PrivateQrColors.Navy,
|
||||||
|
secondary = PrivateQrColors.Mint,
|
||||||
|
onSecondary = PrivateQrColors.Navy,
|
||||||
|
background = PrivateQrColors.Deep,
|
||||||
|
onBackground = PrivateQrColors.Surface,
|
||||||
|
surface = PrivateQrColors.Navy,
|
||||||
|
onSurface = PrivateQrColors.Surface,
|
||||||
|
surfaceVariant = PrivateQrColors.Teal900,
|
||||||
|
onSurfaceVariant = PrivateQrColors.Mint
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CleanScannerTheme(content: @Composable () -> Unit) {
|
||||||
|
val darkTheme = isSystemInDarkTheme()
|
||||||
|
val colorScheme = if (darkTheme) DarkColors else LightColors
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@
|
|||||||
<string name="open">Öffnen</string>
|
<string name="open">Öffnen</string>
|
||||||
<string name="cancel">Abbrechen</string>
|
<string name="cancel">Abbrechen</string>
|
||||||
<string name="open_anyway">Trotzdem öffnen</string>
|
<string name="open_anyway">Trotzdem öffnen</string>
|
||||||
<string name="risk_warning">Diese URL wirkt ungewöhnlich. Prüfe sie, bevor du öffnest.</string>
|
<string name="risk_warning_title">Diese URL wirkt ungewöhnlich</string>
|
||||||
|
<string name="risk_warning">Prüfe den Link vor dem Öffnen. Die Warnung wird auf deinem Gerät berechnet, ohne den Scan irgendwohin zu senden.</string>
|
||||||
<string name="delete_all">Alles löschen</string>
|
<string name="delete_all">Alles löschen</string>
|
||||||
<string name="confirm_delete_all">Alle Historie-Einträge löschen?</string>
|
<string name="confirm_delete_all">Alle Historie-Einträge löschen?</string>
|
||||||
<string name="confirm">Bestätigen</string>
|
<string name="confirm">Bestätigen</string>
|
||||||
@@ -44,6 +45,15 @@
|
|||||||
<string name="share_txt">TXT</string>
|
<string name="share_txt">TXT</string>
|
||||||
<string name="share_csv">CSV</string>
|
<string name="share_csv">CSV</string>
|
||||||
<string name="share_json">JSON</string>
|
<string name="share_json">JSON</string>
|
||||||
|
<string name="history_header_subtitle">Optionale Historie bleibt auf deinem Gerät</string>
|
||||||
|
<string name="history_hero_title">Vergangene Scans lokal prüfen</string>
|
||||||
|
<string name="history_hero_subtitle">Gespeicherte Scans jederzeit suchen, exportieren oder löschen.</string>
|
||||||
|
<string name="privacy_pill_no_ads">Keine Werbung</string>
|
||||||
|
<string name="privacy_pill_no_tracking">Kein Tracking</string>
|
||||||
|
<string name="privacy_pill_no_account">Kein Konto</string>
|
||||||
|
<string name="local_controls">Lokale Kontrolle</string>
|
||||||
|
<string name="no_saved_scans_yet">Noch keine gespeicherten Scans</string>
|
||||||
|
<string name="empty_history_description">Aktiviere lokale Historie in den Einstellungen, um private Einträge auf diesem Gerät zu speichern.</string>
|
||||||
<string name="scan_from_image">Aus Bild scannen</string>
|
<string name="scan_from_image">Aus Bild scannen</string>
|
||||||
<string name="import_whitelist">Whitelist importieren</string>
|
<string name="import_whitelist">Whitelist importieren</string>
|
||||||
<string name="whitelist_loaded_count">Geladene registrierte IDs: %1$d</string>
|
<string name="whitelist_loaded_count">Geladene registrierte IDs: %1$d</string>
|
||||||
@@ -55,7 +65,8 @@
|
|||||||
<string name="share_batch">Stapel teilen</string>
|
<string name="share_batch">Stapel teilen</string>
|
||||||
<string name="no_code_found_in_image">Im gewählten Bild wurde kein QR- oder Barcode gefunden.</string>
|
<string name="no_code_found_in_image">Im gewählten Bild wurde kein QR- oder Barcode gefunden.</string>
|
||||||
<string name="image_scan_pick_title">%1$d Codes im Bild gefunden</string>
|
<string name="image_scan_pick_title">%1$d Codes im Bild gefunden</string>
|
||||||
<string name="image_scan_pick_subtitle">Wähle ein Ergebnis aus:</string>
|
<string name="image_scan_pick_subtitle">Wähle das Ergebnis, das du verwenden möchtest. Erkennung und Auswertung erfolgen lokal.</string>
|
||||||
|
<string name="image_scan_use_selected">Ausgewähltes verwenden</string>
|
||||||
<string name="image_scan_failed">Dieses Bild konnte nicht gelesen werden. Bitte anderes Bild versuchen.</string>
|
<string name="image_scan_failed">Dieses Bild konnte nicht gelesen werden. Bitte anderes Bild versuchen.</string>
|
||||||
<string name="already_scanned">Bereits gescannt</string>
|
<string name="already_scanned">Bereits gescannt</string>
|
||||||
<string name="duplicate_ticket_alert_title">Doppeltes Ticket erkannt</string>
|
<string name="duplicate_ticket_alert_title">Doppeltes Ticket erkannt</string>
|
||||||
@@ -76,4 +87,13 @@
|
|||||||
<string name="select_use_case_view">Use-Case-Ansicht wählen</string>
|
<string name="select_use_case_view">Use-Case-Ansicht wählen</string>
|
||||||
<string name="use_case_everyday_personal">Alltägliche private Nutzung</string>
|
<string name="use_case_everyday_personal">Alltägliche private Nutzung</string>
|
||||||
<string name="use_case_event_ticketing">Events & Ticketing</string>
|
<string name="use_case_event_ticketing">Events & Ticketing</string>
|
||||||
|
<string name="use_case_everyday_description">Vollständiger privater Scanner mit lokalem Verlauf und üblichen Ergebnisaktionen.</string>
|
||||||
|
<string name="use_case_event_ticketing_description">Batch-Scanning, Duplikaterkennung, Whitelist-Import und Batch-Teilen.</string>
|
||||||
|
<string name="settings_header_subtitle">Einstellungen, die dir Kontrolle geben</string>
|
||||||
|
<string name="settings_hero_title">Deine Privatsphäre-Einstellungen</string>
|
||||||
|
<string name="settings_hero_subtitle">Wähle lokale Historie, Sicherheitswarnungen, Feedback und die Scanneransicht, die zu deinem Workflow passt.</string>
|
||||||
|
<string name="result_local_check">Lokale Prüfung</string>
|
||||||
|
<string name="result_checked_on_device">Vor dem Öffnen auf dem Gerät geprüft</string>
|
||||||
|
<string name="result_encoded_data">Codierte Daten</string>
|
||||||
|
<string name="batch_ticket_privacy_note">Warnungen zu doppelten und nicht registrierten Tickets bleiben auf dem Gerät.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
<string name="open">Open</string>
|
<string name="open">Open</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="open_anyway">Open anyway</string>
|
<string name="open_anyway">Open anyway</string>
|
||||||
<string name="risk_warning">This URL looks unusual. Check it before opening.</string>
|
<string name="risk_warning_title">This URL looks unusual</string>
|
||||||
|
<string name="risk_warning">Check the link before opening. The warning is calculated on your device, without sending the scan anywhere.</string>
|
||||||
<string name="delete_all">Delete all</string>
|
<string name="delete_all">Delete all</string>
|
||||||
<string name="confirm_delete_all">Delete all history entries?</string>
|
<string name="confirm_delete_all">Delete all history entries?</string>
|
||||||
<string name="confirm">Confirm</string>
|
<string name="confirm">Confirm</string>
|
||||||
@@ -44,6 +45,15 @@
|
|||||||
<string name="share_txt">TXT</string>
|
<string name="share_txt">TXT</string>
|
||||||
<string name="share_csv">CSV</string>
|
<string name="share_csv">CSV</string>
|
||||||
<string name="share_json">JSON</string>
|
<string name="share_json">JSON</string>
|
||||||
|
<string name="history_header_subtitle">Optional history stays on your device</string>
|
||||||
|
<string name="history_hero_title">Review past scans locally</string>
|
||||||
|
<string name="history_hero_subtitle">Search, export, or delete saved scans whenever you choose.</string>
|
||||||
|
<string name="privacy_pill_no_ads">No ads</string>
|
||||||
|
<string name="privacy_pill_no_tracking">No tracking</string>
|
||||||
|
<string name="privacy_pill_no_account">No account</string>
|
||||||
|
<string name="local_controls">Local controls</string>
|
||||||
|
<string name="no_saved_scans_yet">No saved scans yet</string>
|
||||||
|
<string name="empty_history_description">Enable local history in settings to keep a private record on this device.</string>
|
||||||
<string name="scan_from_image">Scan from image</string>
|
<string name="scan_from_image">Scan from image</string>
|
||||||
<string name="import_whitelist">Import whitelist</string>
|
<string name="import_whitelist">Import whitelist</string>
|
||||||
<string name="whitelist_loaded_count">Registered IDs loaded: %1$d</string>
|
<string name="whitelist_loaded_count">Registered IDs loaded: %1$d</string>
|
||||||
@@ -55,7 +65,8 @@
|
|||||||
<string name="share_batch">Share batch</string>
|
<string name="share_batch">Share batch</string>
|
||||||
<string name="no_code_found_in_image">No QR or barcode found in the selected image.</string>
|
<string name="no_code_found_in_image">No QR or barcode found in the selected image.</string>
|
||||||
<string name="image_scan_pick_title">Found %1$d codes in image</string>
|
<string name="image_scan_pick_title">Found %1$d codes in image</string>
|
||||||
<string name="image_scan_pick_subtitle">Choose a result to use:</string>
|
<string name="image_scan_pick_subtitle">Choose the result you want to use. Detection and parsing happen locally.</string>
|
||||||
|
<string name="image_scan_use_selected">Use selected</string>
|
||||||
<string name="image_scan_failed">Could not read this image. Try another one.</string>
|
<string name="image_scan_failed">Could not read this image. Try another one.</string>
|
||||||
<string name="already_scanned">Already scanned</string>
|
<string name="already_scanned">Already scanned</string>
|
||||||
<string name="duplicate_ticket_alert_title">Duplicate ticket detected</string>
|
<string name="duplicate_ticket_alert_title">Duplicate ticket detected</string>
|
||||||
@@ -76,4 +87,13 @@
|
|||||||
<string name="select_use_case_view">Select use-case view</string>
|
<string name="select_use_case_view">Select use-case view</string>
|
||||||
<string name="use_case_everyday_personal">Everyday personal use</string>
|
<string name="use_case_everyday_personal">Everyday personal use</string>
|
||||||
<string name="use_case_event_ticketing">Event & ticketing</string>
|
<string name="use_case_event_ticketing">Event & ticketing</string>
|
||||||
|
<string name="use_case_everyday_description">Full personal scanner with local history and common result actions.</string>
|
||||||
|
<string name="use_case_event_ticketing_description">Batch scanning, duplicate detection, whitelist import, and batch sharing.</string>
|
||||||
|
<string name="settings_header_subtitle">Settings that keep you in control</string>
|
||||||
|
<string name="settings_hero_title">Privacy settings are yours</string>
|
||||||
|
<string name="settings_hero_subtitle">Choose local history, security warnings, feedback, and the scanner view that fits your workflow.</string>
|
||||||
|
<string name="result_local_check">Local check</string>
|
||||||
|
<string name="result_checked_on_device">Checked on device before opening</string>
|
||||||
|
<string name="result_encoded_data">Encoded data</string>
|
||||||
|
<string name="batch_ticket_privacy_note">Duplicate and unregistered ticket alerts stay on device.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<en-US>
|
||||||
|
Initial release of Private QR Scanner.
|
||||||
|
|
||||||
|
Scan QR codes and barcodes with local processing, optional on-device history, scan-from-image support, URL safety warnings, and practical actions for links, contacts, Wi-Fi codes, email, SMS, phone numbers, and calendar events.
|
||||||
|
</en-US>
|
||||||
|
|
||||||
|
<de-DE>
|
||||||
|
Erste Version von Private QR Scanner.
|
||||||
|
|
||||||
|
Scanne QR-Codes und Barcodes mit lokaler Verarbeitung, optionalem Verlauf auf dem Gerät, Bild-Scan, URL-Sicherheitswarnungen und praktischen Aktionen für Links, Kontakte, WLAN-Codes, E-Mail, SMS, Telefonnummern und Kalenderereignisse.
|
||||||
|
</de-DE>
|
||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 793 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 892 KiB |
|
After Width: | Height: | Size: 865 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 809 KiB |
@@ -0,0 +1,42 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=1800, height=3200, initial-scale=1">
|
||||||
|
<title>Private QR Scanner 10-inch Tablet Screenshots</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 1800px;
|
||||||
|
height: 3200px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f6fbfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto auto 0;
|
||||||
|
width: 1440px;
|
||||||
|
height: 2560px;
|
||||||
|
border: 0;
|
||||||
|
transform: scale(1.25);
|
||||||
|
transform-origin: top left;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<iframe id="source" title="Private QR Scanner screenshot source"></iframe>
|
||||||
|
<script>
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const screen = params.get("screen") || "1";
|
||||||
|
document.getElementById("source").src =
|
||||||
|
`private-qr-scanner-7in-tablet-screenshots.html?screen=${encodeURIComponent(screen)}`;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 902 KiB |
|
After Width: | Height: | Size: 578 KiB |
|
After Width: | Height: | Size: 988 KiB |
|
After Width: | Height: | Size: 731 KiB |
|
After Width: | Height: | Size: 641 KiB |
|
After Width: | Height: | Size: 617 KiB |
|
After Width: | Height: | Size: 832 KiB |
|
After Width: | Height: | Size: 581 KiB |
@@ -0,0 +1,389 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=1024, height=500, initial-scale=1">
|
||||||
|
<title>Private QR Scanner Feature Graphic</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--ink: #f8fafc;
|
||||||
|
--muted: #cbe7e3;
|
||||||
|
--deep: #07111f;
|
||||||
|
--navy: #0b1220;
|
||||||
|
--teal-900: #0f3f45;
|
||||||
|
--teal-700: #155e63;
|
||||||
|
--teal-300: #2dd4bf;
|
||||||
|
--mint: #dff7f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 1024px;
|
||||||
|
height: 500px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--deep);
|
||||||
|
font-family: Inter, "DejaVu Sans", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: grid;
|
||||||
|
place-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
position: relative;
|
||||||
|
width: 1024px;
|
||||||
|
height: 500px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--ink);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 78% 34%, rgba(45, 212, 191, 0.24), transparent 29%),
|
||||||
|
radial-gradient(circle at 12% 86%, rgba(223, 247, 242, 0.12), transparent 24%),
|
||||||
|
linear-gradient(132deg, #0b1220 0%, #103a42 50%, #07111f 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature::before,
|
||||||
|
.feature::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -80px;
|
||||||
|
right: -80px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature::before {
|
||||||
|
top: 210px;
|
||||||
|
height: 210px;
|
||||||
|
background: #061525;
|
||||||
|
clip-path: polygon(0 58%, 16% 29%, 33% 20%, 51% 33%, 70% 36%, 88% 21%, 100% 8%, 100% 100%, 0 100%);
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature::after {
|
||||||
|
top: -32px;
|
||||||
|
height: 214px;
|
||||||
|
background: #1f7778;
|
||||||
|
clip-path: polygon(0 0, 100% 0, 100% 46%, 83% 65%, 67% 58%, 51% 40%, 31% 35%, 15% 54%, 0 75%);
|
||||||
|
opacity: 0.36;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 506px 1fr;
|
||||||
|
gap: 38px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 56px 64px 48px 74px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kicker {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 16px 0 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--mint);
|
||||||
|
background: rgba(223, 247, 242, 0.1);
|
||||||
|
border: 1px solid rgba(223, 247, 242, 0.22);
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kicker svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 18px 0 0;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 68px;
|
||||||
|
line-height: 0.98;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
width: 445px;
|
||||||
|
margin: 22px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1.23;
|
||||||
|
font-weight: 520;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checks {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 17px 0 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: #061525;
|
||||||
|
background: var(--mint);
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 10px 24px rgba(2, 6, 23, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.check:nth-child(2) {
|
||||||
|
background: var(--teal-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.check:nth-child(3) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check svg {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual {
|
||||||
|
position: relative;
|
||||||
|
height: 390px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.halo {
|
||||||
|
position: absolute;
|
||||||
|
inset: 16px 5px 12px 34px;
|
||||||
|
border-radius: 44px;
|
||||||
|
background: rgba(223, 247, 242, 0.06);
|
||||||
|
border: 1px solid rgba(223, 247, 242, 0.08);
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone {
|
||||||
|
position: absolute;
|
||||||
|
right: 40px;
|
||||||
|
top: 9px;
|
||||||
|
width: 238px;
|
||||||
|
height: 374px;
|
||||||
|
border-radius: 36px;
|
||||||
|
padding: 15px;
|
||||||
|
background: linear-gradient(145deg, #e5fbf7, #f8fafc);
|
||||||
|
box-shadow: 0 28px 48px rgba(2, 6, 23, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 9px;
|
||||||
|
left: 91px;
|
||||||
|
width: 56px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #0b1220;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 25px;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(45, 212, 191, 0.12) 1px, transparent 1px) 0 0 / 28px 28px,
|
||||||
|
linear-gradient(0deg, rgba(45, 212, 191, 0.12) 1px, transparent 1px) 0 0 / 28px 28px,
|
||||||
|
radial-gradient(circle at 47% 42%, rgba(45, 212, 191, 0.18), transparent 27%),
|
||||||
|
linear-gradient(160deg, #07111f 0%, #123b3f 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-window {
|
||||||
|
position: absolute;
|
||||||
|
left: 39px;
|
||||||
|
top: 77px;
|
||||||
|
width: 132px;
|
||||||
|
height: 132px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner {
|
||||||
|
position: absolute;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-color: var(--teal-300);
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner.tl {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-width: 7px 0 0 7px;
|
||||||
|
border-radius: 13px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner.tr {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border-width: 7px 7px 0 0;
|
||||||
|
border-radius: 0 13px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner.bl {
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-width: 0 0 7px 7px;
|
||||||
|
border-radius: 0 0 0 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner.br {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-width: 0 7px 7px 0;
|
||||||
|
border-radius: 0 0 13px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr {
|
||||||
|
position: absolute;
|
||||||
|
inset: 21px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(248, 250, 252, 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr span {
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #08111f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr span:nth-child(5n+3),
|
||||||
|
.qr span:nth-child(7),
|
||||||
|
.qr span:nth-child(18) {
|
||||||
|
background: var(--teal-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 25px;
|
||||||
|
right: 25px;
|
||||||
|
top: 139px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--teal-300), transparent);
|
||||||
|
box-shadow: 0 0 22px rgba(45, 212, 191, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
position: absolute;
|
||||||
|
left: 23px;
|
||||||
|
right: 23px;
|
||||||
|
bottom: 25px;
|
||||||
|
height: 76px;
|
||||||
|
padding: 14px 15px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(248, 250, 252, 0.94);
|
||||||
|
color: var(--navy);
|
||||||
|
box-shadow: 0 14px 28px rgba(2, 6, 23, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result span {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #155e63;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.1;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="feature">
|
||||||
|
<section class="grid">
|
||||||
|
<div class="copy">
|
||||||
|
<div class="kicker">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M12 3L19 6V11C19 15.7 16.1 19.9 12 21.8C7.9 19.9 5 15.7 5 11V6L12 3Z" fill="#2DD4BF"/>
|
||||||
|
<path d="M9 12L11.1 14.1L15.5 9.7" stroke="#061525" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
Local-first privacy
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Private QR<br>Scanner</h1>
|
||||||
|
<p class="subtitle">Scan QR codes and barcodes without ads, tracking, or accounts.</p>
|
||||||
|
|
||||||
|
<div class="checks" aria-label="Feature highlights">
|
||||||
|
<div class="check">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M20 6L9 17L4 12" stroke="#061525" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
Local
|
||||||
|
</div>
|
||||||
|
<div class="check">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M20 6L9 17L4 12" stroke="#061525" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
No ads
|
||||||
|
</div>
|
||||||
|
<div class="check">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M20 6L9 17L4 12" stroke="#061525" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
No tracking
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="visual" aria-hidden="true">
|
||||||
|
<div class="halo"></div>
|
||||||
|
<div class="phone">
|
||||||
|
<div class="screen">
|
||||||
|
<div class="scan-window">
|
||||||
|
<div class="qr">
|
||||||
|
<span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<div class="corner tl"></div>
|
||||||
|
<div class="corner tr"></div>
|
||||||
|
<div class="corner bl"></div>
|
||||||
|
<div class="corner br"></div>
|
||||||
|
</div>
|
||||||
|
<div class="scan-line"></div>
|
||||||
|
<div class="result">
|
||||||
|
<strong>Ready to scan</strong>
|
||||||
|
<span>Inspect first</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 276 KiB |
@@ -0,0 +1,515 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
|
||||||
|
<title>Private QR Scanner Screenshot 1</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #07111f;
|
||||||
|
--surface: #f8fafc;
|
||||||
|
--surface-2: #eef7f5;
|
||||||
|
--ink: #0b1220;
|
||||||
|
--muted: #5e7282;
|
||||||
|
--teal: #2dd4bf;
|
||||||
|
--mint: #dff7f2;
|
||||||
|
--blue: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 1080px;
|
||||||
|
height: 1920px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: Inter, "DejaVu Sans", Arial, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shot {
|
||||||
|
position: relative;
|
||||||
|
width: 1080px;
|
||||||
|
height: 1920px;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 72% 17%, rgba(45, 212, 191, 0.22), transparent 27%),
|
||||||
|
radial-gradient(circle at 22% 70%, rgba(223, 247, 242, 0.10), transparent 24%),
|
||||||
|
linear-gradient(180deg, #0b1220 0%, #103840 52%, #07111f 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(45, 212, 191, 0.09) 1px, transparent 1px) 0 0 / 68px 68px,
|
||||||
|
linear-gradient(0deg, rgba(45, 212, 191, 0.08) 1px, transparent 1px) 0 0 / 68px 68px,
|
||||||
|
radial-gradient(circle at 58% 38%, rgba(45, 212, 191, 0.17), transparent 22%),
|
||||||
|
linear-gradient(150deg, rgba(8, 17, 31, 0.15), rgba(8, 17, 31, 0.78));
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera::before,
|
||||||
|
.camera::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -120px;
|
||||||
|
right: -120px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera::before {
|
||||||
|
top: 314px;
|
||||||
|
height: 424px;
|
||||||
|
background: rgba(223, 247, 242, 0.10);
|
||||||
|
clip-path: polygon(0 63%, 19% 42%, 38% 48%, 58% 33%, 78% 42%, 100% 21%, 100% 100%, 0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera::after {
|
||||||
|
top: 642px;
|
||||||
|
height: 470px;
|
||||||
|
background: rgba(2, 6, 23, 0.58);
|
||||||
|
clip-path: polygon(0 23%, 18% 10%, 36% 28%, 54% 18%, 73% 31%, 100% 13%, 100% 100%, 0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 92px;
|
||||||
|
padding: 22px 44px 0;
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 760;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 4px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal span {
|
||||||
|
width: 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal span:nth-child(1) { height: 8px; }
|
||||||
|
.signal span:nth-child(2) { height: 12px; }
|
||||||
|
.signal span:nth-child(3) { height: 16px; }
|
||||||
|
.signal span:nth-child(4) { height: 21px; }
|
||||||
|
|
||||||
|
.battery {
|
||||||
|
width: 42px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #f8fafc;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.battery::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 27px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-chip {
|
||||||
|
position: absolute;
|
||||||
|
top: 118px;
|
||||||
|
left: 212px;
|
||||||
|
right: 212px;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 11px 18px;
|
||||||
|
border-radius: 18px;
|
||||||
|
color: white;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
font-size: 23px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 18px 42px rgba(2, 6, 23, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
position: absolute;
|
||||||
|
top: 112px;
|
||||||
|
right: 42px;
|
||||||
|
z-index: 3;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border: 1px solid rgba(248, 250, 252, 0.22);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(0, 0, 0, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aim {
|
||||||
|
position: absolute;
|
||||||
|
top: 392px;
|
||||||
|
left: 166px;
|
||||||
|
z-index: 2;
|
||||||
|
width: 748px;
|
||||||
|
height: 480px;
|
||||||
|
border-radius: 54px;
|
||||||
|
background: rgba(45, 212, 191, 0.06);
|
||||||
|
border: 7px solid rgba(45, 212, 191, 0.9);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 2px rgba(223, 247, 242, 0.20),
|
||||||
|
0 0 72px rgba(45, 212, 191, 0.27);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-target {
|
||||||
|
position: absolute;
|
||||||
|
top: 478px;
|
||||||
|
left: 332px;
|
||||||
|
z-index: 3;
|
||||||
|
width: 416px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 26px;
|
||||||
|
padding: 26px;
|
||||||
|
background: rgba(248, 250, 252, 0.97);
|
||||||
|
box-shadow: 0 28px 70px rgba(2, 6, 23, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
grid-template-rows: repeat(5, 1fr);
|
||||||
|
gap: 13px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-grid span {
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #07111f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-grid span:nth-child(4),
|
||||||
|
.qr-grid span:nth-child(10),
|
||||||
|
.qr-grid span:nth-child(18),
|
||||||
|
.qr-grid span:nth-child(25),
|
||||||
|
.qr-grid span:nth-child(32) {
|
||||||
|
background: var(--teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detect-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 466px;
|
||||||
|
left: 314px;
|
||||||
|
z-index: 4;
|
||||||
|
width: 452px;
|
||||||
|
height: 336px;
|
||||||
|
border: 5px solid #4ae3a3;
|
||||||
|
border-radius: 32px;
|
||||||
|
box-shadow: 0 0 0 999px rgba(2, 6, 23, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-line {
|
||||||
|
position: absolute;
|
||||||
|
top: 642px;
|
||||||
|
left: 214px;
|
||||||
|
right: 214px;
|
||||||
|
z-index: 5;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, transparent, #2dd4bf, transparent);
|
||||||
|
box-shadow: 0 0 34px rgba(45, 212, 191, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
position: absolute;
|
||||||
|
left: 238px;
|
||||||
|
right: 238px;
|
||||||
|
bottom: 616px;
|
||||||
|
z-index: 6;
|
||||||
|
min-height: 58px;
|
||||||
|
padding: 13px 22px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: 660;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 118px;
|
||||||
|
z-index: 8;
|
||||||
|
min-height: 612px;
|
||||||
|
padding: 22px 38px 34px;
|
||||||
|
border-radius: 42px 42px 0 0;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: var(--ink);
|
||||||
|
box-shadow: 0 -34px 72px rgba(2, 6, 23, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
width: 94px;
|
||||||
|
height: 8px;
|
||||||
|
margin: 0 auto 26px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
padding: 28px;
|
||||||
|
border-radius: 26px;
|
||||||
|
background: #f2f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
margin: 0 0 24px;
|
||||||
|
color: #172033;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--mint);
|
||||||
|
color: #0f766e;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 820;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
color: #526879;
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 760;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-value {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--blue);
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.28;
|
||||||
|
font-weight: 640;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 22px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #065f46;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 740;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk svg {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 18px;
|
||||||
|
margin-top: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #eef2f7;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
margin-left: auto;
|
||||||
|
width: 250px;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: #0f766e;
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 820;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
height: 118px;
|
||||||
|
padding: 12px 56px 18px;
|
||||||
|
background: #fcfffe;
|
||||||
|
color: #607080;
|
||||||
|
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 740;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item svg {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="shot">
|
||||||
|
<div class="camera"></div>
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
<div>9:41</div>
|
||||||
|
<div class="system-icons">
|
||||||
|
<div class="signal"><span></span><span></span><span></span><span></span></div>
|
||||||
|
<div class="battery"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="top-chip">Everyday personal use</div>
|
||||||
|
<div class="gallery" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M4 5H20V19H4V5Z" stroke="white" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<path d="M7 16L10.6 12.4L13 14.8L15 12.8L19 16.8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M15.5 9.2H15.52" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aim"></div>
|
||||||
|
<div class="qr-target">
|
||||||
|
<div class="qr-grid">
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detect-box"></div>
|
||||||
|
<div class="scan-line"></div>
|
||||||
|
<div class="hint">Readable code detected.</div>
|
||||||
|
|
||||||
|
<section class="sheet">
|
||||||
|
<div class="handle"></div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h1 class="result-title">URL <span class="badge">Local check</span></h1>
|
||||||
|
<div class="field">
|
||||||
|
<div class="field-label">Link</div>
|
||||||
|
<div class="field-value">https://example.org/menu</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="field-label">Risk score</div>
|
||||||
|
<div class="field-value" style="color:#0f172a;text-decoration:none">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="risk">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 3L19 6V11C19 15.7 16.1 19.9 12 21.8C7.9 19.9 5 15.7 5 11V6L12 3Z" fill="#10B981"/>
|
||||||
|
<path d="M9 12L11.1 14.1L15.5 9.7" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
Checked on device before opening
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<div class="action" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="action" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M18 8C19.7 8 21 6.7 21 5C21 3.3 19.7 2 18 2C16.3 2 15 3.3 15 5C15 5.2 15 5.4 15.1 5.6L8.6 9.2C8.1 8.5 7.1 8 6 8C4.3 8 3 9.3 3 11C3 12.7 4.3 14 6 14C7.1 14 8.1 13.5 8.6 12.8L15.1 16.4C15 16.6 15 16.8 15 17C15 18.7 16.3 20 18 20C19.7 20 21 18.7 21 17C21 15.3 19.7 14 18 14C16.9 14 15.9 14.5 15.4 15.2L8.9 11.6C9 11.4 9 11.2 9 11C9 10.8 9 10.6 8.9 10.4L15.4 6.8C15.9 7.5 16.9 8 18 8Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="action primary">Open</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<div class="nav-item active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||||
|
<path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Scan
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
History
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Settings
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 544 KiB |
@@ -0,0 +1,525 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
|
||||||
|
<title>Private QR Scanner Screenshot 2</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f6fbfa;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-2: #edf8f5;
|
||||||
|
--ink: #0b1220;
|
||||||
|
--muted: #607080;
|
||||||
|
--line: #dce8e6;
|
||||||
|
--teal: #0f766e;
|
||||||
|
--teal-bright: #2dd4bf;
|
||||||
|
--mint: #dff7f2;
|
||||||
|
--blue: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 1080px;
|
||||||
|
height: 1920px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: Inter, "DejaVu Sans", Arial, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shot {
|
||||||
|
position: relative;
|
||||||
|
width: 1080px;
|
||||||
|
height: 1920px;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 92% -4%, rgba(45, 212, 191, 0.20), transparent 31%),
|
||||||
|
linear-gradient(180deg, #f6fbfa 0%, #eff8f6 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 92px;
|
||||||
|
padding: 22px 44px 0;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 760;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 4px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal span {
|
||||||
|
width: 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal span:nth-child(1) { height: 8px; }
|
||||||
|
.signal span:nth-child(2) { height: 12px; }
|
||||||
|
.signal span:nth-child(3) { height: 16px; }
|
||||||
|
.signal span:nth-child(4) { height: 21px; }
|
||||||
|
|
||||||
|
.battery {
|
||||||
|
width: 42px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--ink);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.battery::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 27px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--teal-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 30px 48px 144px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 14px 28px rgba(15, 118, 110, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 34px;
|
||||||
|
line-height: 1.08;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-subtitle {
|
||||||
|
margin-top: 5px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 660;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
padding: 28px;
|
||||||
|
border-radius: 34px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 88% 20%, rgba(45, 212, 191, 0.18), transparent 34%),
|
||||||
|
linear-gradient(145deg, #0b1220, #123b3f);
|
||||||
|
color: #f8fafc;
|
||||||
|
box-shadow: 0 22px 44px rgba(7, 17, 31, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 152px;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 47px;
|
||||||
|
line-height: 1.04;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero p {
|
||||||
|
margin: 16px 0 0;
|
||||||
|
color: #cbe7e3;
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 1.25;
|
||||||
|
font-weight: 560;
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-card {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 152px;
|
||||||
|
height: 152px;
|
||||||
|
border-radius: 30px;
|
||||||
|
background: rgba(223, 247, 242, 0.12);
|
||||||
|
border: 1px solid rgba(223, 247, 242, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-card svg {
|
||||||
|
width: 86px;
|
||||||
|
height: 86px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-strip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 13px;
|
||||||
|
margin-top: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 58px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(223, 247, 242, 0.12);
|
||||||
|
color: #dff7f2;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 820;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
color: #132032;
|
||||||
|
font-size: 31px;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
height: 72px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 0 22px;
|
||||||
|
border: 2px solid #cfe0de;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 640;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search svg {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
color: var(--teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export {
|
||||||
|
height: 50px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: var(--mint);
|
||||||
|
color: var(--teal);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 840;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
margin-left: auto;
|
||||||
|
background: #fff1f2;
|
||||||
|
color: #be123c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 58px 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 19px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-icon svg {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
color: #132032;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
color: #7b8b98;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 680;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
margin-top: 7px;
|
||||||
|
color: #41566a;
|
||||||
|
font-size: 21px;
|
||||||
|
line-height: 1.25;
|
||||||
|
font-weight: 620;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 30px;
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: 0 18px 38px rgba(15, 23, 42, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 74px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
color: #172033;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 780;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
width: 70px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--teal);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 4px 8px rgba(15, 23, 42, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
height: 118px;
|
||||||
|
padding: 12px 56px 18px;
|
||||||
|
background: #fcfffe;
|
||||||
|
color: #607080;
|
||||||
|
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 740;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item svg {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
color: var(--teal);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="shot">
|
||||||
|
<div class="status">
|
||||||
|
<div>9:41</div>
|
||||||
|
<div class="system-icons">
|
||||||
|
<div class="signal"><span></span><span></span><span></span><span></span></div>
|
||||||
|
<div class="battery"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="content">
|
||||||
|
<header class="app-head">
|
||||||
|
<img class="app-icon" src="./private-qr-scanner-icon.svg" alt="">
|
||||||
|
<div>
|
||||||
|
<h1 class="app-title">Private QR Scanner</h1>
|
||||||
|
<div class="app-subtitle">Optional history stays on your device</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero-row">
|
||||||
|
<div>
|
||||||
|
<h1>Review past scans locally</h1>
|
||||||
|
<p>Search, export, or delete saved scans whenever you choose.</p>
|
||||||
|
</div>
|
||||||
|
<div class="safe-card" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 3L19 6V11C19 15.7 16.1 19.9 12 21.8C7.9 19.9 5 15.7 5 11V6L12 3Z" fill="#2DD4BF"/>
|
||||||
|
<path d="M8.6 12.2L10.9 14.5L15.8 9.5" stroke="#07111F" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="privacy-strip">
|
||||||
|
<div class="privacy-pill">No ads</div>
|
||||||
|
<div class="privacy-pill">No tracking</div>
|
||||||
|
<div class="privacy-pill">No account</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<h2 class="section-title">History</h2>
|
||||||
|
<div class="search">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M10.8 18.2C14.9 18.2 18.2 14.9 18.2 10.8C18.2 6.7 14.9 3.4 10.8 3.4C6.7 3.4 3.4 6.7 3.4 10.8C3.4 14.9 6.7 18.2 10.8 18.2Z" stroke="currentColor" stroke-width="2.2"/>
|
||||||
|
<path d="M16.4 16.4L21 21" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Search saved scans
|
||||||
|
</div>
|
||||||
|
<div class="export-row">
|
||||||
|
<button class="export">TXT</button>
|
||||||
|
<button class="export">CSV</button>
|
||||||
|
<button class="export">JSON</button>
|
||||||
|
<button class="export delete">Delete all</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-list">
|
||||||
|
<article class="history-item">
|
||||||
|
<div class="type-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M10 13A5 5 0 0 0 17.1 13L20 10.1A5 5 0 0 0 12.9 3L11.8 4.1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M14 11A5 5 0 0 0 6.9 11L4 13.9A5 5 0 0 0 11.1 21L12.2 19.9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="row-top"><div class="type">URL</div><div class="time">Today, 9:38 AM</div></div>
|
||||||
|
<div class="value">https://example.org/menu</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="history-item">
|
||||||
|
<div class="type-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M4 8L12 13L20 8" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5 6H19C19.6 6 20 6.4 20 7V17C20 17.6 19.6 18 19 18H5C4.4 18 4 17.6 4 17V7C4 6.4 4.4 6 5 6Z" stroke="currentColor" stroke-width="2.2"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="row-top"><div class="type">Email</div><div class="time">Today, 9:21 AM</div></div>
|
||||||
|
<div class="value">support@example.org</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="history-item">
|
||||||
|
<div class="type-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 12.5C8.9 8.7 15.1 8.7 19 12.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
<path d="M8.2 15.4C10.3 13.4 13.7 13.4 15.8 15.4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
<path d="M12 19H12.02" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="row-top"><div class="type">Wi-Fi</div><div class="time">Yesterday</div></div>
|
||||||
|
<div class="value">SSID: Guest Network</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="settings-panel">
|
||||||
|
<div class="setting">
|
||||||
|
<span>Save history (local)</span>
|
||||||
|
<span class="switch"></span>
|
||||||
|
</div>
|
||||||
|
<div class="setting">
|
||||||
|
<span>Security warnings</span>
|
||||||
|
<span class="switch"></span>
|
||||||
|
</div>
|
||||||
|
<div class="setting">
|
||||||
|
<span>Scan feedback</span>
|
||||||
|
<span class="switch"></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<div class="nav-item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||||
|
<path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Scan
|
||||||
|
</div>
|
||||||
|
<div class="nav-item active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
History
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Settings
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 404 KiB |
@@ -0,0 +1,576 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
|
||||||
|
<title>Private QR Scanner Screenshot 3</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--deep: #07111f;
|
||||||
|
--navy: #0b1220;
|
||||||
|
--teal-900: #123b3f;
|
||||||
|
--teal-700: #0f766e;
|
||||||
|
--teal-300: #2dd4bf;
|
||||||
|
--mint: #dff7f2;
|
||||||
|
--white: #f8fafc;
|
||||||
|
--muted: #cbe7e3;
|
||||||
|
--warning: #ffc857;
|
||||||
|
--danger: #f43f5e;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 1080px;
|
||||||
|
height: 1920px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: Inter, "DejaVu Sans", Arial, sans-serif;
|
||||||
|
background: var(--deep);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shot {
|
||||||
|
position: relative;
|
||||||
|
width: 1080px;
|
||||||
|
height: 1920px;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 78% 18%, rgba(45, 212, 191, 0.24), transparent 26%),
|
||||||
|
linear-gradient(180deg, #07111f 0%, #103840 48%, #07111f 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(45, 212, 191, 0.10) 1px, transparent 1px) 0 0 / 68px 68px,
|
||||||
|
linear-gradient(0deg, rgba(45, 212, 191, 0.08) 1px, transparent 1px) 0 0 / 68px 68px,
|
||||||
|
radial-gradient(circle at 50% 42%, rgba(45, 212, 191, 0.19), transparent 25%),
|
||||||
|
linear-gradient(150deg, rgba(8, 17, 31, 0.16), rgba(8, 17, 31, 0.82));
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera::before,
|
||||||
|
.camera::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -130px;
|
||||||
|
right: -130px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera::before {
|
||||||
|
top: 326px;
|
||||||
|
height: 420px;
|
||||||
|
background: rgba(223, 247, 242, 0.10);
|
||||||
|
clip-path: polygon(0 56%, 19% 33%, 41% 45%, 61% 28%, 80% 39%, 100% 18%, 100% 100%, 0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera::after {
|
||||||
|
top: 674px;
|
||||||
|
height: 488px;
|
||||||
|
background: rgba(2, 6, 23, 0.62);
|
||||||
|
clip-path: polygon(0 18%, 18% 8%, 36% 27%, 54% 18%, 74% 34%, 100% 16%, 100% 100%, 0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 92px;
|
||||||
|
padding: 22px 44px 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 760;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 4px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal span {
|
||||||
|
width: 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal span:nth-child(1) { height: 8px; }
|
||||||
|
.signal span:nth-child(2) { height: 12px; }
|
||||||
|
.signal span:nth-child(3) { height: 16px; }
|
||||||
|
.signal span:nth-child(4) { height: 21px; }
|
||||||
|
|
||||||
|
.battery {
|
||||||
|
width: 42px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--white);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.battery::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 27px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--teal-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-chip {
|
||||||
|
position: absolute;
|
||||||
|
top: 118px;
|
||||||
|
left: 212px;
|
||||||
|
right: 212px;
|
||||||
|
z-index: 4;
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 11px 18px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(0, 0, 0, 0.52);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 23px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whitelist {
|
||||||
|
position: absolute;
|
||||||
|
top: 188px;
|
||||||
|
left: 276px;
|
||||||
|
right: 276px;
|
||||||
|
z-index: 4;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.40);
|
||||||
|
color: var(--mint);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 760;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import,
|
||||||
|
.gallery {
|
||||||
|
position: absolute;
|
||||||
|
top: 112px;
|
||||||
|
z-index: 4;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border: 1px solid rgba(248, 250, 252, 0.22);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(0, 0, 0, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import { right: 118px; }
|
||||||
|
.gallery { right: 42px; opacity: 0.48; }
|
||||||
|
|
||||||
|
.import svg,
|
||||||
|
.gallery svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode {
|
||||||
|
position: absolute;
|
||||||
|
top: 116px;
|
||||||
|
left: 42px;
|
||||||
|
z-index: 4;
|
||||||
|
width: 104px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(0, 0, 0, 0.38);
|
||||||
|
text-align: center;
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 760;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode svg {
|
||||||
|
display: block;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
margin: 0 auto 6px;
|
||||||
|
color: var(--teal-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aim {
|
||||||
|
position: absolute;
|
||||||
|
top: 388px;
|
||||||
|
left: 166px;
|
||||||
|
z-index: 2;
|
||||||
|
width: 748px;
|
||||||
|
height: 480px;
|
||||||
|
border-radius: 54px;
|
||||||
|
background: rgba(45, 212, 191, 0.07);
|
||||||
|
border: 7px solid rgba(45, 212, 191, 0.95);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 2px rgba(223, 247, 242, 0.20),
|
||||||
|
0 0 72px rgba(45, 212, 191, 0.30);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket {
|
||||||
|
position: absolute;
|
||||||
|
top: 500px;
|
||||||
|
left: 268px;
|
||||||
|
z-index: 3;
|
||||||
|
width: 544px;
|
||||||
|
height: 248px;
|
||||||
|
border-radius: 32px;
|
||||||
|
padding: 28px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 0 50%, transparent 26px, var(--white) 27px),
|
||||||
|
radial-gradient(circle at 100% 50%, transparent 26px, var(--white) 27px),
|
||||||
|
linear-gradient(90deg, var(--white), #e9fffb);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
box-shadow: 0 28px 70px rgba(2, 6, 23, 0.36);
|
||||||
|
color: var(--navy);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--mint);
|
||||||
|
color: var(--teal-700);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barcode {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(18, 1fr);
|
||||||
|
gap: 7px;
|
||||||
|
height: 82px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barcode span {
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--navy);
|
||||||
|
}
|
||||||
|
|
||||||
|
.barcode span:nth-child(3n),
|
||||||
|
.barcode span:nth-child(7),
|
||||||
|
.barcode span:nth-child(16) {
|
||||||
|
background: var(--teal-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-id {
|
||||||
|
color: #526879;
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detect-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 484px;
|
||||||
|
left: 246px;
|
||||||
|
z-index: 4;
|
||||||
|
width: 588px;
|
||||||
|
height: 282px;
|
||||||
|
border: 5px solid #4ae3a3;
|
||||||
|
border-radius: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-line {
|
||||||
|
position: absolute;
|
||||||
|
top: 638px;
|
||||||
|
left: 214px;
|
||||||
|
right: 214px;
|
||||||
|
z-index: 5;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--teal-300), transparent);
|
||||||
|
box-shadow: 0 0 34px rgba(45, 212, 191, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
position: absolute;
|
||||||
|
left: 238px;
|
||||||
|
right: 238px;
|
||||||
|
bottom: 616px;
|
||||||
|
z-index: 6;
|
||||||
|
min-height: 58px;
|
||||||
|
padding: 13px 22px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: 760;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch {
|
||||||
|
position: absolute;
|
||||||
|
left: 28px;
|
||||||
|
right: 28px;
|
||||||
|
bottom: 132px;
|
||||||
|
z-index: 8;
|
||||||
|
padding: 22px;
|
||||||
|
border-radius: 28px;
|
||||||
|
background: rgba(2, 6, 23, 0.58);
|
||||||
|
color: var(--white);
|
||||||
|
box-shadow: 0 -20px 60px rgba(2, 6, 23, 0.36);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share {
|
||||||
|
padding: 9px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--mint);
|
||||||
|
color: var(--teal-700);
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capture {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 0;
|
||||||
|
border-top: 1px solid rgba(223, 247, 242, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.capture:first-of-type {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capture strong {
|
||||||
|
display: block;
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 21px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capture span {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: rgba(223, 247, 242, 0.72);
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 680;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: var(--white);
|
||||||
|
background: rgba(223, 247, 242, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 13px 14px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(244, 63, 94, 0.16);
|
||||||
|
color: #ffe4e6;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
height: 118px;
|
||||||
|
padding: 12px 56px 18px;
|
||||||
|
background: #fcfffe;
|
||||||
|
color: #607080;
|
||||||
|
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 740;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item svg {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
color: var(--teal-700);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="shot">
|
||||||
|
<div class="camera"></div>
|
||||||
|
<div class="status">
|
||||||
|
<div>9:41</div>
|
||||||
|
<div class="system-icons">
|
||||||
|
<div class="signal"><span></span><span></span><span></span><span></span></div>
|
||||||
|
<div class="battery"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mode" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M4 5H20V19H4V5Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Batch
|
||||||
|
</div>
|
||||||
|
<div class="top-chip">Event & ticketing</div>
|
||||||
|
<div class="whitelist">Registered IDs loaded: 412</div>
|
||||||
|
<div class="import" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 3V15M12 3L8 7M12 3L16 7" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5 15V19H19V15" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="gallery" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M4 5H20V19H4V5Z" stroke="white" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<path d="M7 16L10.6 12.4L13 14.8L15 12.8L19 16.8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="aim"></div>
|
||||||
|
<section class="ticket">
|
||||||
|
<div class="ticket-top">
|
||||||
|
<div class="event">Entry Pass</div>
|
||||||
|
<div class="valid">VALID</div>
|
||||||
|
</div>
|
||||||
|
<div class="barcode">
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<div class="ticket-id">ID: EVT-24-0187</div>
|
||||||
|
</section>
|
||||||
|
<div class="detect-box"></div>
|
||||||
|
<div class="scan-line"></div>
|
||||||
|
<div class="hint">Readable code detected.</div>
|
||||||
|
|
||||||
|
<section class="batch">
|
||||||
|
<div class="batch-title">
|
||||||
|
<div>Batch captures: 3</div>
|
||||||
|
<div class="share">Share batch</div>
|
||||||
|
</div>
|
||||||
|
<div class="capture">
|
||||||
|
<div><strong>Barcode: EVT-24-0187</strong><span>9:41 AM</span></div>
|
||||||
|
<div class="copy">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="capture">
|
||||||
|
<div><strong>QR Code: EVT-24-0186</strong><span>9:40 AM</span></div>
|
||||||
|
<div class="copy">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="capture">
|
||||||
|
<div><strong>QR Code: EVT-24-0185</strong><span>9:39 AM</span></div>
|
||||||
|
<div class="copy">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="warning">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 3L22 20H2L12 3Z" fill="#F43F5E"/>
|
||||||
|
<path d="M12 9V13M12 17H12.02" stroke="white" stroke-width="2.3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Duplicate and unregistered ticket alerts stay on device.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<div class="nav-item active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||||
|
<path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Scan
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
History
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Settings
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 632 KiB |
@@ -0,0 +1,592 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
|
||||||
|
<title>Private QR Scanner Screenshot 4</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f6fbfa;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--soft: #f2f7ff;
|
||||||
|
--ink: #0b1220;
|
||||||
|
--muted: #607080;
|
||||||
|
--line: #dce8e6;
|
||||||
|
--teal: #0f766e;
|
||||||
|
--teal-300: #2dd4bf;
|
||||||
|
--mint: #dff7f2;
|
||||||
|
--navy: #07111f;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 1080px;
|
||||||
|
height: 1920px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: Inter, "DejaVu Sans", Arial, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shot {
|
||||||
|
position: relative;
|
||||||
|
width: 1080px;
|
||||||
|
height: 1920px;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 88% -4%, rgba(45, 212, 191, 0.18), transparent 31%),
|
||||||
|
linear-gradient(180deg, #f7fcfb 0%, #eef8f6 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 92px;
|
||||||
|
padding: 22px 44px 0;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 760;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 4px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal span {
|
||||||
|
width: 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal span:nth-child(1) { height: 8px; }
|
||||||
|
.signal span:nth-child(2) { height: 12px; }
|
||||||
|
.signal span:nth-child(3) { height: 16px; }
|
||||||
|
.signal span:nth-child(4) { height: 21px; }
|
||||||
|
|
||||||
|
.battery {
|
||||||
|
width: 42px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--ink);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.battery::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 27px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--teal-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 30px 48px 144px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 14px 28px rgba(15, 118, 110, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 34px;
|
||||||
|
line-height: 1.08;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-subtitle {
|
||||||
|
margin-top: 5px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 660;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
padding: 28px;
|
||||||
|
border-radius: 34px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 84% 26%, rgba(45, 212, 191, 0.20), transparent 34%),
|
||||||
|
linear-gradient(145deg, #0b1220, #123b3f);
|
||||||
|
color: #f8fafc;
|
||||||
|
box-shadow: 0 22px 44px rgba(7, 17, 31, 0.18);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: -80px;
|
||||||
|
bottom: -70px;
|
||||||
|
width: 330px;
|
||||||
|
height: 230px;
|
||||||
|
border-radius: 48px;
|
||||||
|
background: rgba(223, 247, 242, 0.07);
|
||||||
|
transform: rotate(-10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
margin: 0;
|
||||||
|
width: 720px;
|
||||||
|
font-size: 49px;
|
||||||
|
line-height: 1.04;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero p {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 650px;
|
||||||
|
margin: 16px 0 0;
|
||||||
|
color: #cbe7e3;
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 1.25;
|
||||||
|
font-weight: 560;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chips {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(223, 247, 242, 0.12);
|
||||||
|
color: var(--mint);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
color: #132032;
|
||||||
|
font-size: 31px;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-scan {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 230px;
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
padding: 22px;
|
||||||
|
border-radius: 28px;
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: 0 16px 34px rgba(15, 23, 42, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-copy h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #132032;
|
||||||
|
font-size: 29px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-copy p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1.28;
|
||||||
|
font-weight: 620;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
position: relative;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(45, 212, 191, 0.11) 1px, transparent 1px) 0 0 / 25px 25px,
|
||||||
|
linear-gradient(0deg, rgba(45, 212, 191, 0.10) 1px, transparent 1px) 0 0 / 25px 25px,
|
||||||
|
linear-gradient(145deg, #07111f, #123b3f);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-qr {
|
||||||
|
position: absolute;
|
||||||
|
left: 50px;
|
||||||
|
top: 40px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
width: 130px;
|
||||||
|
height: 100px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 13px;
|
||||||
|
background: #f8fafc;
|
||||||
|
transform: rotate(-4deg);
|
||||||
|
box-shadow: 0 16px 34px rgba(2, 6, 23, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-qr span {
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--navy);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-qr span:nth-child(5n+3),
|
||||||
|
.mini-qr span:nth-child(7),
|
||||||
|
.mini-qr span:nth-child(18) {
|
||||||
|
background: var(--teal-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.found {
|
||||||
|
position: absolute;
|
||||||
|
inset: 28px 34px;
|
||||||
|
border: 4px solid var(--teal-300);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card {
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 30px;
|
||||||
|
background: linear-gradient(135deg, #081c3b, #0f2e58, #134b73);
|
||||||
|
color: #f8fafc;
|
||||||
|
box-shadow: 0 18px 38px rgba(15, 23, 42, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initials {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 74px;
|
||||||
|
height: 74px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(122, 247, 207, 0.18);
|
||||||
|
color: #7af7cf;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 34px;
|
||||||
|
line-height: 1.05;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person p {
|
||||||
|
margin: 7px 0 0;
|
||||||
|
color: #c7d6e8;
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 680;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-lines {
|
||||||
|
display: grid;
|
||||||
|
gap: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 102px 1fr;
|
||||||
|
gap: 18px;
|
||||||
|
align-items: baseline;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1.26;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line span:first-child {
|
||||||
|
color: #c7d6e8;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line span:last-child {
|
||||||
|
color: #f8fafc;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
margin: 18px 0 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 9px;
|
||||||
|
min-height: 62px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 850;
|
||||||
|
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action.primary {
|
||||||
|
background: var(--teal);
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action svg {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 58px 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 19px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(223, 247, 242, 0.65);
|
||||||
|
color: var(--teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-icon svg {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-row strong {
|
||||||
|
display: block;
|
||||||
|
color: #132032;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.12;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-row span {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 21px;
|
||||||
|
line-height: 1.24;
|
||||||
|
font-weight: 620;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
height: 118px;
|
||||||
|
padding: 12px 56px 18px;
|
||||||
|
background: #fcfffe;
|
||||||
|
color: #607080;
|
||||||
|
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 740;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item svg {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
color: var(--teal);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="shot">
|
||||||
|
<div class="status">
|
||||||
|
<div>9:41</div>
|
||||||
|
<div class="system-icons">
|
||||||
|
<div class="signal"><span></span><span></span><span></span><span></span></div>
|
||||||
|
<div class="battery"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="content">
|
||||||
|
<header class="app-head">
|
||||||
|
<img class="app-icon" src="./private-qr-scanner-icon.svg" alt="">
|
||||||
|
<div>
|
||||||
|
<h1 class="app-title">Private QR Scanner</h1>
|
||||||
|
<div class="app-subtitle">Structured results, practical actions</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="hero">
|
||||||
|
<h1>Scan from camera or image</h1>
|
||||||
|
<p>Recognize contacts, email, Wi-Fi, calendar events, links, and more.</p>
|
||||||
|
<div class="chips">
|
||||||
|
<div class="chip">Copy</div>
|
||||||
|
<div class="chip">Share</div>
|
||||||
|
<div class="chip">Add contact</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="image-scan">
|
||||||
|
<div class="image-copy">
|
||||||
|
<h2>Scan from image</h2>
|
||||||
|
<p>Choose a saved photo and pick the detected code you want to use.</p>
|
||||||
|
</div>
|
||||||
|
<div class="photo" aria-hidden="true">
|
||||||
|
<div class="mini-qr">
|
||||||
|
<span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span>
|
||||||
|
<span></span><span></span><span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<div class="found"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<h2 class="section-title">Contact result</h2>
|
||||||
|
<section class="contact-card">
|
||||||
|
<div class="contact-top">
|
||||||
|
<div class="initials">AR</div>
|
||||||
|
<div class="person">
|
||||||
|
<h2>Avery Reed</h2>
|
||||||
|
<p>Operations Lead • North Hall Events</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="contact-lines">
|
||||||
|
<div class="line"><span>Phone</span><span>+1 555 0134</span></div>
|
||||||
|
<div class="line"><span>Email</span><span>avery@example.org</span></div>
|
||||||
|
<div class="line"><span>Address</span><span>240 Market Street, Suite 8</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="actions">
|
||||||
|
<div class="action primary">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 12C14.2 12 16 10.2 16 8C16 5.8 14.2 4 12 4C9.8 4 8 5.8 8 8C8 10.2 9.8 12 12 12Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M4 21C4.7 17.6 7.8 15 12 15C16.2 15 19.3 17.6 20 21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M19 5V11M16 8H22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Add contact
|
||||||
|
</div>
|
||||||
|
<div class="action">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
Copy
|
||||||
|
</div>
|
||||||
|
<div class="action">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M18 8C19.7 8 21 6.7 21 5C21 3.3 19.7 2 18 2C16.3 2 15 3.3 15 5C15 5.2 15 5.4 15.1 5.6L8.6 9.2C8.1 8.5 7.1 8 6 8C4.3 8 3 9.3 3 11C3 12.7 4.3 14 6 14C7.1 14 8.1 13.5 8.6 12.8L15.1 16.4C15 16.6 15 16.8 15 17C15 18.7 16.3 20 18 20C19.7 20 21 18.7 21 17C21 15.3 19.7 14 18 14C16.9 14 15.9 14.5 15.4 15.2L8.9 11.6C9 11.4 9 11.2 9 11C9 10.8 9 10.6 8.9 10.4L15.4 6.8C15.9 7.5 16.9 8 18 8Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
Share
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="result-grid">
|
||||||
|
<article class="result-row">
|
||||||
|
<div class="type-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 12.5C8.9 8.7 15.1 8.7 19 12.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
<path d="M8.2 15.4C10.3 13.4 13.7 13.4 15.8 15.4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
<path d="M12 19H12.02" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div><strong>Wi-Fi QR codes</strong><span>Open Wi-Fi settings after inspecting network details.</span></div>
|
||||||
|
</article>
|
||||||
|
<article class="result-row">
|
||||||
|
<div class="type-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M7 4V7M17 4V7M5 9H19M6 6H18C18.6 6 19 6.4 19 7V19C19 19.6 18.6 20 18 20H6C5.4 20 5 19.6 5 19V7C5 6.4 5.4 6 6 6Z" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div><strong>Calendar events</strong><span>Review title, location, and time before adding.</span></div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<div class="nav-item active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||||
|
<path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Scan
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
History
|
||||||
|
</div>
|
||||||
|
<div class="nav-item">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Settings
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 562 KiB |
@@ -0,0 +1,58 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
|
||||||
|
<title>Private QR Scanner Screenshot 5</title>
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box} html,body{width:1080px;height:1920px;margin:0;overflow:hidden;font-family:Inter,"DejaVu Sans",Arial,sans-serif;background:#07111f;color:#f8fafc}
|
||||||
|
.shot{position:relative;width:1080px;height:1920px;overflow:hidden;background:radial-gradient(circle at 70% 22%,rgba(45,212,191,.22),transparent 26%),linear-gradient(180deg,#07111f,#103840 50%,#07111f)}
|
||||||
|
.camera{position:absolute;inset:0;background:linear-gradient(90deg,rgba(45,212,191,.09) 1px,transparent 1px) 0 0/68px 68px,linear-gradient(0deg,rgba(45,212,191,.08) 1px,transparent 1px) 0 0/68px 68px,radial-gradient(circle at 50% 39%,rgba(45,212,191,.16),transparent 24%),linear-gradient(150deg,rgba(8,17,31,.18),rgba(8,17,31,.82))}
|
||||||
|
.camera:before,.camera:after{content:"";position:absolute;left:-120px;right:-120px}.camera:before{top:330px;height:420px;background:rgba(223,247,242,.1);clip-path:polygon(0 60%,19% 34%,40% 45%,61% 29%,80% 41%,100% 18%,100% 100%,0 100%)}.camera:after{top:680px;height:480px;background:rgba(2,6,23,.62);clip-path:polygon(0 18%,20% 8%,38% 27%,56% 17%,75% 33%,100% 14%,100% 100%,0 100%)}
|
||||||
|
.status{position:absolute;z-index:8;left:0;right:0;top:0;height:92px;padding:22px 44px 0;display:flex;justify-content:space-between;align-items:center;font-size:24px;font-weight:760}.icons{display:flex;gap:10px;align-items:center}.signal{display:flex;gap:4px;align-items:end;height:22px}.signal span{width:5px;border-radius:99px;background:#f8fafc}.signal span:nth-child(1){height:8px}.signal span:nth-child(2){height:12px}.signal span:nth-child(3){height:16px}.signal span:nth-child(4){height:21px}.battery{width:42px;height:20px;border:2px solid #f8fafc;border-radius:6px;padding:3px}.battery:after{content:"";display:block;width:27px;height:10px;border-radius:3px;background:#2dd4bf}
|
||||||
|
.chip{position:absolute;z-index:5;top:118px;left:212px;right:212px;min-height:56px;padding:11px 18px;border-radius:18px;background:rgba(0,0,0,.52);text-align:center;font-size:23px;font-weight:800}.gallery{position:absolute;z-index:5;right:42px;top:112px;display:grid;place-items:center;width:64px;height:64px;border-radius:18px;border:1px solid rgba(248,250,252,.22);background:rgba(0,0,0,.38)}.gallery svg{width:32px;height:32px}
|
||||||
|
.aim{position:absolute;z-index:2;top:390px;left:166px;width:748px;height:480px;border-radius:54px;background:rgba(45,212,191,.06);border:7px solid rgba(45,212,191,.95);box-shadow:0 0 72px rgba(45,212,191,.3),inset 0 0 0 2px rgba(223,247,242,.2)}
|
||||||
|
.qr{position:absolute;z-index:4;top:478px;left:332px;width:416px;height:300px;border-radius:26px;padding:26px;background:rgba(248,250,252,.97);box-shadow:0 28px 70px rgba(2,6,23,.36);display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(5,1fr);gap:13px}.qr span{border-radius:5px;background:#07111f}.qr span:nth-child(4),.qr span:nth-child(10),.qr span:nth-child(18),.qr span:nth-child(25),.qr span:nth-child(32){background:#2dd4bf}
|
||||||
|
.detect{position:absolute;z-index:5;top:466px;left:314px;width:452px;height:336px;border:5px solid #4ae3a3;border-radius:32px}.line{position:absolute;z-index:6;top:642px;left:214px;right:214px;height:7px;border-radius:99px;background:linear-gradient(90deg,transparent,#2dd4bf,transparent);box-shadow:0 0 34px rgba(45,212,191,.92)}
|
||||||
|
.hint{position:absolute;z-index:6;left:238px;right:238px;bottom:632px;padding:13px 22px;border-radius:24px;background:rgba(0,0,0,.45);font-size:25px;font-weight:760;text-align:center}
|
||||||
|
.sheet{position:absolute;left:0;right:0;bottom:118px;z-index:7;min-height:568px;padding:22px 38px 34px;border-radius:42px 42px 0 0;background:#f8fafc;color:#0b1220;box-shadow:0 -34px 72px rgba(2,6,23,.35)}.handle{width:94px;height:8px;margin:0 auto 26px;border-radius:99px;background:#cbd5e1}
|
||||||
|
.card{padding:28px;border-radius:26px;background:#f2f7ff}.title{display:flex;gap:14px;align-items:center;margin:0 0 22px;font-size:32px;font-weight:900}.badge{padding:8px 14px;border-radius:99px;background:#fff1f2;color:#be123c;font-size:18px;font-weight:900}.label{margin-top:18px;color:#526879;font-size:19px;font-weight:760}.value{margin-top:6px;color:#1d4ed8;font-size:24px;line-height:1.28;font-weight:700;text-decoration:underline}.score{color:#be123c;text-decoration:none}
|
||||||
|
.warn-pill{display:flex;gap:12px;align-items:center;margin-top:22px;padding:16px 18px;border-radius:20px;background:#fff1f2;color:#be123c;font-size:22px;font-weight:800}.warn-pill svg{width:28px;height:28px}
|
||||||
|
.modal-shade{position:absolute;inset:0;z-index:10;background:rgba(2,6,23,.48)}.dialog{position:absolute;z-index:11;left:70px;right:70px;top:730px;padding:30px;border-radius:28px;background:#fff;color:#0b1220;box-shadow:0 30px 80px rgba(2,6,23,.42)}.dialog h2{margin:0 0 14px;font-size:32px;font-weight:900}.dialog p{margin:0;color:#526879;font-size:24px;line-height:1.3;font-weight:620}.dialog-actions{display:flex;justify-content:flex-end;gap:14px;margin-top:28px}.btn{padding:15px 20px;border-radius:18px;font-size:22px;font-weight:900;color:#0f766e}.btn.danger{background:#0f766e;color:#fff}
|
||||||
|
.nav{position:absolute;z-index:12;left:0;right:0;bottom:0;height:118px;padding:12px 56px 18px;display:grid;grid-template-columns:repeat(3,1fr);background:#fcfffe;color:#607080;border-top:1px solid rgba(15,23,42,.08)}.nav-item{display:grid;place-items:center;align-content:center;gap:7px;font-size:20px;font-weight:740}.nav-item svg{width:28px;height:28px}.active{color:#0f766e}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="shot">
|
||||||
|
<div class="camera"></div>
|
||||||
|
<div class="status"><div>9:41</div><div class="icons"><div class="signal"><span></span><span></span><span></span><span></span></div><div class="battery"></div></div></div>
|
||||||
|
<div class="chip">Everyday personal use</div>
|
||||||
|
<div class="gallery"><svg viewBox="0 0 24 24" fill="none"><path d="M4 5H20V19H4V5Z" stroke="white" stroke-width="2"/><path d="M7 16L10.6 12.4L13 14.8L15 12.8L19 16.8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||||
|
<div class="aim"></div>
|
||||||
|
<div class="qr">
|
||||||
|
<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
<div class="detect"></div><div class="line"></div><div class="hint">Readable code detected.</div>
|
||||||
|
<section class="sheet">
|
||||||
|
<div class="handle"></div>
|
||||||
|
<div class="card">
|
||||||
|
<h1 class="title">URL <span class="badge">Unusual link</span></h1>
|
||||||
|
<div class="label">Link</div><div class="value">https://login.example-secure.co/session</div>
|
||||||
|
<div class="label">Risk score</div><div class="value score">4</div>
|
||||||
|
<div class="warn-pill"><svg viewBox="0 0 24 24" fill="none"><path d="M12 3L22 20H2L12 3Z" fill="#F43F5E"/><path d="M12 9V13M12 17H12.02" stroke="white" stroke-width="2.3" stroke-linecap="round"/></svg>Checked locally before opening</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="modal-shade"></div>
|
||||||
|
<section class="dialog">
|
||||||
|
<h2>This URL looks unusual</h2>
|
||||||
|
<p>Check the link before opening. The warning is calculated on your device, without sending the scan anywhere.</p>
|
||||||
|
<div class="dialog-actions"><div class="btn">Cancel</div><div class="btn danger">Open anyway</div></div>
|
||||||
|
</section>
|
||||||
|
<nav class="nav">
|
||||||
|
<div class="nav-item active"><svg viewBox="0 0 24 24" fill="none"><path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>Scan</div>
|
||||||
|
<div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/><path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>History</div>
|
||||||
|
<div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/><path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>Settings</div>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 348 KiB |
@@ -0,0 +1,41 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
|
||||||
|
<title>Private QR Scanner Screenshot 6</title>
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box}html,body{width:1080px;height:1920px;margin:0;overflow:hidden;font-family:Inter,"DejaVu Sans",Arial,sans-serif;background:#f6fbfa;color:#0b1220}.shot{position:relative;width:1080px;height:1920px;overflow:hidden;background:radial-gradient(circle at 90% -4%,rgba(45,212,191,.18),transparent 31%),linear-gradient(180deg,#f7fcfb,#eef8f6)}
|
||||||
|
.status{height:92px;padding:22px 44px 0;display:flex;justify-content:space-between;align-items:center;font-size:24px;font-weight:760}.icons{display:flex;gap:10px;align-items:center}.signal{display:flex;gap:4px;align-items:end;height:22px}.signal span{width:5px;border-radius:99px;background:#0b1220}.signal span:nth-child(1){height:8px}.signal span:nth-child(2){height:12px}.signal span:nth-child(3){height:16px}.signal span:nth-child(4){height:21px}.battery{width:42px;height:20px;border:2px solid #0b1220;border-radius:6px;padding:3px}.battery:after{content:"";display:block;width:27px;height:10px;border-radius:3px;background:#2dd4bf}
|
||||||
|
.content{padding:30px 48px 144px}.head{display:flex;gap:18px;align-items:center;margin-bottom:28px}.icon{width:80px;height:80px;border-radius:20px;box-shadow:0 14px 28px rgba(15,118,110,.18)}.title{margin:0;font-size:34px;line-height:1.08;font-weight:900}.sub{margin-top:5px;color:#607080;font-size:20px;font-weight:700}
|
||||||
|
.hero{margin-bottom:28px;padding:30px;border-radius:34px;background:radial-gradient(circle at 88% 24%,rgba(45,212,191,.18),transparent 34%),linear-gradient(145deg,#0b1220,#123b3f);color:#f8fafc;box-shadow:0 22px 44px rgba(7,17,31,.18)}.hero h1{margin:0;font-size:49px;line-height:1.04;font-weight:900}.hero p{margin:16px 0 0;color:#cbe7e3;font-size:25px;line-height:1.25;font-weight:600}.chips{display:flex;gap:12px;margin-top:24px}.chip{padding:14px 18px;border-radius:18px;background:rgba(223,247,242,.12);color:#dff7f2;font-size:20px;font-weight:850}
|
||||||
|
.wifi-card{padding:28px;border-radius:30px;background:#fff;box-shadow:0 18px 38px rgba(15,23,42,.08);margin-bottom:20px}.wifi-head{display:flex;gap:18px;align-items:center;margin-bottom:24px}.wifi-icon{display:grid;place-items:center;width:76px;height:76px;border-radius:22px;background:#dff7f2;color:#0f766e}.wifi-icon svg{width:42px;height:42px}.wifi-head h2{margin:0;font-size:38px;font-weight:900}.wifi-head p{margin:5px 0 0;color:#607080;font-size:21px;font-weight:700}.field{padding:18px 0;border-top:1px solid #dce8e6}.field:first-of-type{border-top:0}.field label{display:block;color:#607080;font-size:18px;font-weight:850}.field div{margin-top:7px;color:#0b1220;font-size:26px;font-weight:800}.password{display:flex;justify-content:space-between;align-items:center}.dots{letter-spacing:4px}
|
||||||
|
.button{display:flex;align-items:center;justify-content:center;gap:12px;min-height:70px;margin:20px 0 28px;border-radius:22px;background:#0f766e;color:#f8fafc;font-size:24px;font-weight:900}.button svg{width:28px;height:28px}
|
||||||
|
.note{display:flex;gap:14px;align-items:flex-start;padding:22px;border-radius:26px;background:#ecfdf5;color:#065f46;font-size:22px;line-height:1.32;font-weight:750;box-shadow:0 14px 30px rgba(15,23,42,.05)}.note svg{width:30px;height:30px;flex:0 0 auto}
|
||||||
|
.list{display:grid;gap:14px;margin-top:28px}.row{display:grid;grid-template-columns:58px 1fr;gap:16px;padding:19px;border-radius:24px;background:#fff;box-shadow:0 14px 30px rgba(15,23,42,.06)}.row-icon{display:grid;place-items:center;width:58px;height:58px;border-radius:16px;background:rgba(223,247,242,.65);color:#0f766e}.row-icon svg{width:30px;height:30px}.row strong{display:block;color:#132032;font-size:24px;font-weight:900}.row span{display:block;margin-top:6px;color:#607080;font-size:21px;line-height:1.24;font-weight:620}
|
||||||
|
.nav{position:absolute;left:0;right:0;bottom:0;height:118px;padding:12px 56px 18px;display:grid;grid-template-columns:repeat(3,1fr);background:#fcfffe;color:#607080;border-top:1px solid rgba(15,23,42,.08)}.nav-item{display:grid;place-items:center;align-content:center;gap:7px;font-size:20px;font-weight:740}.nav-item svg{width:28px;height:28px}.active{color:#0f766e}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="shot">
|
||||||
|
<div class="status"><div>9:41</div><div class="icons"><div class="signal"><span></span><span></span><span></span><span></span></div><div class="battery"></div></div></div>
|
||||||
|
<section class="content">
|
||||||
|
<header class="head"><img class="icon" src="./private-qr-scanner-icon.svg" alt=""><div><h1 class="title">Private QR Scanner</h1><div class="sub">Structured QR details before acting</div></div></header>
|
||||||
|
<section class="hero"><h1>Wi-Fi codes stay readable</h1><p>Inspect network name and security type before opening Android Wi-Fi settings.</p><div class="chips"><div class="chip">Local parsing</div><div class="chip">No tracking</div><div class="chip">No account</div></div></section>
|
||||||
|
<section class="wifi-card">
|
||||||
|
<div class="wifi-head"><div class="wifi-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M5 12.5C8.9 8.7 15.1 8.7 19 12.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M8.2 15.4C10.3 13.4 13.7 13.4 15.8 15.4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M12 19H12.02" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/></svg></div><div><h2>Wi-Fi</h2><p>Scanned result</p></div></div>
|
||||||
|
<div class="field"><label>SSID</label><div>Guest Network</div></div>
|
||||||
|
<div class="field"><label>Security</label><div>WPA/WPA2</div></div>
|
||||||
|
<div class="field password"><div><label>Password</label><div class="dots">••••••••••</div></div><div style="color:#0f766e;font-size:20px;font-weight:900">Hidden</div></div>
|
||||||
|
</section>
|
||||||
|
<div class="button"><svg viewBox="0 0 24 24" fill="none"><path d="M5 12.5C8.9 8.7 15.1 8.7 19 12.5M8.2 15.4C10.3 13.4 13.7 13.4 15.8 15.4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M12 19H12.02" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/></svg>Open Wi-Fi settings</div>
|
||||||
|
<div class="note"><svg viewBox="0 0 24 24" fill="none"><path d="M12 3L19 6V11C19 15.7 16.1 19.9 12 21.8C7.9 19.9 5 15.7 5 11V6L12 3Z" fill="#10B981"/><path d="M9 12L11.1 14.1L15.5 9.7" stroke="white" stroke-width="2.2" stroke-linecap="round"/></svg><div>Parsing happens on the device. Data leaves only when you choose an external action.</div></div>
|
||||||
|
<div class="list">
|
||||||
|
<div class="row"><div class="row-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/></svg></div><div><strong>Copy result</strong><span>Save the raw QR payload when needed.</span></div></div>
|
||||||
|
<div class="row"><div class="row-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M18 8C19.7 8 21 6.7 21 5C21 3.3 19.7 2 18 2C16.3 2 15 3.3 15 5C15 5.2 15 5.4 15.1 5.6L8.6 9.2C8.1 8.5 7.1 8 6 8C4.3 8 3 9.3 3 11C3 12.7 4.3 14 6 14C7.1 14 8.1 13.5 8.6 12.8L15.1 16.4C15 16.6 15 16.8 15 17C15 18.7 16.3 20 18 20C19.7 20 21 18.7 21 17C21 15.3 19.7 14 18 14C16.9 14 15.9 14.5 15.4 15.2L8.9 11.6C9 11.4 9 11.2 9 11C9 10.8 9 10.6 8.9 10.4L15.4 6.8C15.9 7.5 16.9 8 18 8Z" stroke="currentColor" stroke-width="2"/></svg></div><div><strong>Share deliberately</strong><span>Send scanned content only when you choose.</span></div></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<nav class="nav"><div class="nav-item active"><svg viewBox="0 0 24 24" fill="none"><path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>Scan</div><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/><path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>History</div><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/><path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>Settings</div></nav>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 387 KiB |
@@ -0,0 +1,31 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
|
||||||
|
<title>Private QR Scanner Screenshot 7</title>
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box}html,body{width:1080px;height:1920px;margin:0;overflow:hidden;font-family:Inter,"DejaVu Sans",Arial,sans-serif;background:#07111f;color:#f8fafc}.shot{position:relative;width:1080px;height:1920px;overflow:hidden;background:radial-gradient(circle at 72% 18%,rgba(45,212,191,.22),transparent 28%),linear-gradient(180deg,#07111f,#103840 52%,#07111f)}
|
||||||
|
.grid{position:absolute;inset:0;background:linear-gradient(90deg,rgba(45,212,191,.09) 1px,transparent 1px) 0 0/68px 68px,linear-gradient(0deg,rgba(45,212,191,.08) 1px,transparent 1px) 0 0/68px 68px}.status{position:absolute;z-index:8;left:0;right:0;top:0;height:92px;padding:22px 44px 0;display:flex;justify-content:space-between;align-items:center;font-size:24px;font-weight:760}.icons{display:flex;gap:10px;align-items:center}.signal{display:flex;gap:4px;align-items:end;height:22px}.signal span{width:5px;border-radius:99px;background:#f8fafc}.signal span:nth-child(1){height:8px}.signal span:nth-child(2){height:12px}.signal span:nth-child(3){height:16px}.signal span:nth-child(4){height:21px}.battery{width:42px;height:20px;border:2px solid #f8fafc;border-radius:6px;padding:3px}.battery:after{content:"";display:block;width:27px;height:10px;border-radius:3px;background:#2dd4bf}
|
||||||
|
.top{position:absolute;z-index:4;top:118px;left:212px;right:212px;min-height:56px;padding:11px 18px;border-radius:18px;background:rgba(0,0,0,.52);text-align:center;font-size:23px;font-weight:800}.gallery{position:absolute;z-index:5;right:42px;top:112px;display:grid;place-items:center;width:64px;height:64px;border-radius:18px;border:1px solid rgba(248,250,252,.22);background:rgba(0,0,0,.38)}.gallery svg{width:32px;height:32px}
|
||||||
|
.phone-photo{position:absolute;z-index:2;left:98px;right:98px;top:292px;height:710px;border-radius:38px;background:linear-gradient(150deg,#0b1220,#155e63);box-shadow:0 28px 70px rgba(2,6,23,.36);overflow:hidden}.phone-photo:before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 30% 30%,rgba(223,247,242,.12),transparent 26%),radial-gradient(circle at 82% 68%,rgba(45,212,191,.18),transparent 30%)}.paper{position:absolute;left:134px;top:160px;width:520px;height:350px;border-radius:30px;background:#f8fafc;transform:rotate(-4deg);box-shadow:0 26px 60px rgba(2,6,23,.4)}.paper h2{margin:34px 34px 18px;color:#0b1220;font-size:34px;font-weight:900}.mini{position:absolute;left:320px;top:118px;width:170px;height:145px;border-radius:18px;background:#edf8f6;padding:15px;display:grid;grid-template-columns:repeat(5,1fr);gap:8px}.mini span{border-radius:4px;background:#07111f}.mini span:nth-child(5n+3),.mini span:nth-child(7),.mini span:nth-child(18){background:#2dd4bf}.bars{position:absolute;left:40px;right:40px;bottom:50px;display:grid;gap:14px}.bars span{height:18px;border-radius:99px;background:#cbd5e1}.bars span:nth-child(2){width:70%}.found{position:absolute;left:286px;top:84px;width:230px;height:210px;border:6px solid #2dd4bf;border-radius:28px}.found.two{left:80px;top:196px;width:260px;height:126px;border-color:#ffc857}
|
||||||
|
.dialog{position:absolute;z-index:7;left:42px;right:42px;bottom:146px;padding:28px;border-radius:34px;background:#fff;color:#0b1220;box-shadow:0 -24px 80px rgba(2,6,23,.4)}.dialog h1{margin:0;font-size:34px;font-weight:900}.dialog p{margin:10px 0 22px;color:#607080;font-size:23px;line-height:1.3;font-weight:650}.preview{height:240px;border-radius:28px;background:linear-gradient(145deg,#07111f,#123b3f);position:relative;overflow:hidden;margin-bottom:20px}.preview .mini{left:595px;top:55px;transform:rotate(-4deg);width:170px;height:140px}.preview .found{left:570px;top:36px;width:220px;height:180px}.candidate{display:grid;grid-template-columns:54px 1fr;gap:15px;align-items:center;padding:18px;border-radius:24px;background:#f6fbfa;margin-top:12px;border:2px solid transparent}.candidate.active{border-color:#2dd4bf;background:#ecfdf5}.candidate-icon{display:grid;place-items:center;width:54px;height:54px;border-radius:16px;background:#dff7f2;color:#0f766e}.candidate-icon svg{width:30px;height:30px}.candidate strong{display:block;font-size:23px;font-weight:900}.candidate span{display:block;margin-top:5px;color:#607080;font-size:20px;line-height:1.2;font-weight:650}.actions{display:flex;justify-content:flex-end;gap:14px;margin-top:24px}.btn{padding:15px 20px;border-radius:18px;font-size:22px;font-weight:900;color:#0f766e}.btn.primary{background:#0f766e;color:#fff}
|
||||||
|
.nav{position:absolute;z-index:9;left:0;right:0;bottom:0;height:118px;padding:12px 56px 18px;display:grid;grid-template-columns:repeat(3,1fr);background:#fcfffe;color:#607080;border-top:1px solid rgba(15,23,42,.08)}.nav-item{display:grid;place-items:center;align-content:center;gap:7px;font-size:20px;font-weight:740}.nav-item svg{width:28px;height:28px}.active{color:#0f766e}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="shot">
|
||||||
|
<div class="grid"></div><div class="status"><div>9:41</div><div class="icons"><div class="signal"><span></span><span></span><span></span><span></span></div><div class="battery"></div></div></div>
|
||||||
|
<div class="top">Scan from image</div><div class="gallery"><svg viewBox="0 0 24 24" fill="none"><path d="M4 5H20V19H4V5Z" stroke="white" stroke-width="2"/><path d="M7 16L10.6 12.4L13 14.8L15 12.8L19 16.8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||||
|
<section class="phone-photo"><div class="paper"><h2>Meeting flyer</h2><div class="mini"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div><div class="bars"><span></span><span></span><span></span></div><div class="found"></div><div class="found two"></div></div></section>
|
||||||
|
<section class="dialog">
|
||||||
|
<h1>Found 2 codes in image</h1><p>Choose the result you want to use. Detection and parsing happen locally.</p>
|
||||||
|
<div class="preview"><div class="mini"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div><div class="found"></div></div>
|
||||||
|
<div class="candidate active"><div class="candidate-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M10 13A5 5 0 0 0 17.1 13L20 10.1A5 5 0 0 0 12.9 3L11.8 4.1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M14 11A5 5 0 0 0 6.9 11L4 13.9A5 5 0 0 0 11.1 21L12.2 19.9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></div><div><strong>URL</strong><span>https://example.org/register</span></div></div>
|
||||||
|
<div class="candidate"><div class="candidate-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M4 8L12 13L20 8" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/><path d="M5 6H19C19.6 6 20 6.4 20 7V17C20 17.6 19.6 18 19 18H5C4.4 18 4 17.6 4 17V7C4 6.4 4.4 6 5 6Z" stroke="currentColor" stroke-width="2.2"/></svg></div><div><strong>Email</strong><span>hello@example.org</span></div></div>
|
||||||
|
<div class="actions"><div class="btn">Cancel</div><div class="btn primary">Use selected</div></div>
|
||||||
|
</section>
|
||||||
|
<nav class="nav"><div class="nav-item active"><svg viewBox="0 0 24 24" fill="none"><path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>Scan</div><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/><path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>History</div><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/><path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>Settings</div></nav>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 634 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
|
||||||
|
<title>Private QR Scanner Screenshot 8</title>
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box}html,body{width:1080px;height:1920px;margin:0;overflow:hidden;font-family:Inter,"DejaVu Sans",Arial,sans-serif;background:#f6fbfa;color:#0b1220}.shot{position:relative;width:1080px;height:1920px;overflow:hidden;background:radial-gradient(circle at 92% -4%,rgba(45,212,191,.2),transparent 31%),linear-gradient(180deg,#f6fbfa,#eff8f6)}
|
||||||
|
.status{height:92px;padding:22px 44px 0;display:flex;justify-content:space-between;align-items:center;font-size:24px;font-weight:760}.icons{display:flex;gap:10px;align-items:center}.signal{display:flex;gap:4px;align-items:end;height:22px}.signal span{width:5px;border-radius:99px;background:#0b1220}.signal span:nth-child(1){height:8px}.signal span:nth-child(2){height:12px}.signal span:nth-child(3){height:16px}.signal span:nth-child(4){height:21px}.battery{width:42px;height:20px;border:2px solid #0b1220;border-radius:6px;padding:3px}.battery:after{content:"";display:block;width:27px;height:10px;border-radius:3px;background:#2dd4bf}
|
||||||
|
.content{padding:30px 48px 144px}.head{display:flex;gap:18px;align-items:center;margin-bottom:28px}.icon{width:80px;height:80px;border-radius:20px;box-shadow:0 14px 28px rgba(15,118,110,.18)}.title{margin:0;font-size:34px;line-height:1.08;font-weight:900}.sub{margin-top:5px;color:#607080;font-size:20px;font-weight:700}
|
||||||
|
.hero{margin-bottom:28px;padding:30px;border-radius:34px;background:radial-gradient(circle at 88% 24%,rgba(45,212,191,.18),transparent 34%),linear-gradient(145deg,#0b1220,#123b3f);color:#f8fafc;box-shadow:0 22px 44px rgba(7,17,31,.18)}.hero h1{margin:0;font-size:49px;line-height:1.04;font-weight:900}.hero p{margin:16px 0 0;color:#cbe7e3;font-size:25px;line-height:1.25;font-weight:600}
|
||||||
|
.panel{padding:8px 24px;border-radius:30px;background:#fff;box-shadow:0 18px 38px rgba(15,23,42,.07);margin-bottom:22px}.setting{display:flex;justify-content:space-between;align-items:center;min-height:86px;border-top:1px solid #dce8e6;font-size:25px;font-weight:850}.setting:first-child{border-top:0}.switch{position:relative;width:70px;height:40px;border-radius:99px;background:#0f766e}.switch:after{content:"";position:absolute;right:5px;top:5px;width:30px;height:30px;border-radius:50%;background:#fff;box-shadow:0 4px 8px rgba(15,23,42,.24)}
|
||||||
|
.section-title{margin:0 0 14px;color:#132032;font-size:31px;font-weight:900}.usecase{padding:24px;border-radius:28px;background:#fff;box-shadow:0 18px 38px rgba(15,23,42,.07);margin-bottom:22px}.usecase strong{display:block;font-size:25px;font-weight:900}.usecase span{display:block;margin-top:8px;color:#607080;font-size:22px;font-weight:650}.choose{display:inline-flex;margin-top:16px;padding:13px 18px;border-radius:18px;background:#dff7f2;color:#0f766e;font-size:20px;font-weight:900}
|
||||||
|
.about{display:grid;gap:12px;padding:24px;border-radius:28px;background:#fff;box-shadow:0 18px 38px rgba(15,23,42,.07);color:#607080;font-size:21px;font-weight:650}.about strong{color:#0b1220;font-size:25px;font-weight:900}.links{display:flex;gap:12px;margin-top:8px}.link{padding:13px 16px;border-radius:18px;background:#dff7f2;color:#0f766e;font-size:19px;font-weight:900}
|
||||||
|
.shade{position:absolute;inset:0;background:rgba(2,6,23,.38);z-index:6}.dialog{position:absolute;left:70px;right:70px;top:665px;z-index:7;padding:30px;border-radius:30px;background:#fff;color:#0b1220;box-shadow:0 30px 80px rgba(2,6,23,.42)}.dialog h2{margin:0 0 18px;font-size:32px;font-weight:900}.option{display:grid;grid-template-columns:58px 1fr;gap:16px;align-items:center;padding:18px;border-radius:24px;background:#f6fbfa;margin-top:12px;border:2px solid transparent}.option.active{border-color:#2dd4bf;background:#ecfdf5}.check{display:grid;place-items:center;width:58px;height:58px;border-radius:16px;background:#dff7f2;color:#0f766e;font-size:30px;font-weight:900}.option strong{display:block;font-size:23px;font-weight:900}.option span{display:block;margin-top:5px;color:#607080;font-size:19px;line-height:1.2;font-weight:650}.actions{display:flex;justify-content:flex-end;margin-top:24px}.btn{padding:15px 20px;border-radius:18px;color:#0f766e;font-size:22px;font-weight:900}
|
||||||
|
.nav{position:absolute;z-index:9;left:0;right:0;bottom:0;height:118px;padding:12px 56px 18px;display:grid;grid-template-columns:repeat(3,1fr);background:#fcfffe;color:#607080;border-top:1px solid rgba(15,23,42,.08)}.nav-item{display:grid;place-items:center;align-content:center;gap:7px;font-size:20px;font-weight:740}.nav-item svg{width:28px;height:28px}.active-nav{color:#0f766e}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="shot">
|
||||||
|
<div class="status"><div>9:41</div><div class="icons"><div class="signal"><span></span><span></span><span></span><span></span></div><div class="battery"></div></div></div>
|
||||||
|
<section class="content">
|
||||||
|
<header class="head"><img class="icon" src="./private-qr-scanner-icon.svg" alt=""><div><h1 class="title">Private QR Scanner</h1><div class="sub">Settings that keep you in control</div></div></header>
|
||||||
|
<section class="hero"><h1>Privacy settings are yours</h1><p>Choose local history, security warnings, feedback, and the scanner view that fits your workflow.</p></section>
|
||||||
|
<section class="panel"><div class="setting"><span>Save history (local)</span><span class="switch"></span></div><div class="setting"><span>Security warnings</span><span class="switch"></span></div><div class="setting"><span>Scan feedback</span><span class="switch"></span></div></section>
|
||||||
|
<h2 class="section-title">Active use-case view</h2>
|
||||||
|
<section class="usecase"><strong>Everyday personal use</strong><span>For menus, links, Wi-Fi, contacts, calendar codes, and common daily scans.</span><div class="choose">Select use-case view</div></section>
|
||||||
|
<section class="about"><strong>About</strong><span>Version 1.0.0</span><span>Contact: softwareapp.hb@gmail.com</span><div class="links"><div class="link">Privacy Policy</div><div class="link">Review on Google Play</div></div></section>
|
||||||
|
</section>
|
||||||
|
<div class="shade"></div>
|
||||||
|
<section class="dialog">
|
||||||
|
<h2>Select use-case view</h2>
|
||||||
|
<div class="option active"><div class="check">✓</div><div><strong>Everyday personal use</strong><span>Full personal scanner with local history and common result actions.</span></div></div>
|
||||||
|
<div class="option"><div class="check">◎</div><div><strong>Event & ticketing</strong><span>Batch scanning, duplicate detection, whitelist import, and batch sharing.</span></div></div>
|
||||||
|
<div class="actions"><div class="btn">Cancel</div></div>
|
||||||
|
</section>
|
||||||
|
<nav class="nav"><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>Scan</div><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/><path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>History</div><div class="nav-item active-nav"><svg viewBox="0 0 24 24" fill="none"><path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/><path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>Settings</div></nav>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 358 KiB |