Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c93bb1df9c | ||
|
|
bfb70cab63 | ||
|
|
4746d194f4 | ||
|
|
12ec415e48 | ||
|
|
89fa6ef6ca | ||
|
|
d273543070 | ||
|
|
c8da9f301c | ||
|
|
77278a11e0 | ||
|
|
81c5c8914a | ||
|
|
3b7388c473 |
No files matched your search
@@ -0,0 +1,46 @@
|
||||
# Flexera Launcher website
|
||||
|
||||
A one-page landing site for Flexera Launcher. Plain HTML, CSS and JavaScript: no build step, no
|
||||
libraries, and nothing is loaded from anyone else's server.
|
||||
|
||||
| File | What it is |
|
||||
| --- | --- |
|
||||
| `index.html` | The page and all its copy |
|
||||
| `styles.css` | Layout, colours and the phone's look |
|
||||
| `script.js` | The phone model, the scroll story, the playground, the scroll effects |
|
||||
| `assets/` | App icon (WebP and PNG favicons), QR code, link-preview image (`og.jpg`) |
|
||||
| `fonts/` | Unbounded, Space Grotesk, JetBrains Mono and Outfit, Latin subset, each with its licence |
|
||||
|
||||
## Look at it
|
||||
|
||||
Open `index.html` in a browser. To see it as a server would serve it:
|
||||
|
||||
```sh
|
||||
cd WEBSITE
|
||||
python -m http.server 8080
|
||||
```
|
||||
|
||||
and open <http://localhost:8080>.
|
||||
|
||||
## After a release
|
||||
|
||||
- Change `VERSION` at the top of `script.js`. The page fills every version number from it.
|
||||
`index.html` carries the same number as a fallback for browsers without JavaScript; replace
|
||||
`0.13.0` there too.
|
||||
- The download buttons and the QR code point at
|
||||
`.../releases/download/latest/FlexeraLauncher.apk`, the fixed-name copy that every release
|
||||
uploads, so they never need changing.
|
||||
- If the hero changes, retake `assets/og.jpg` (1200 x 630, the top of the page).
|
||||
|
||||
## The phone
|
||||
|
||||
The phone is drawn in HTML at 390 x 844 and scaled to fit, three times over: a looping teaser in
|
||||
the hero, one driven by the scroll position through the seven "How it works" chapters
|
||||
(`storyState()` in `script.js`), and one in the playground that answers to touch and mouse. All three
|
||||
render the same state object, described in `blank()`. App names and icons on it are generic
|
||||
stand-ins, not real apps.
|
||||
|
||||
## Fonts
|
||||
|
||||
All four are under the SIL Open Font License 1.1 (`fonts/*-OFL.txt`), downloaded from Google Fonts
|
||||
and served from this folder.
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 77 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="41" height="41" class="segno"><path class="qrline" stroke="currentColor" d="M0 0.5h7m2 0h2m1 0h1m2 0h2m2 0h3m1 0h1m4 0h1m2 0h2m1 0h7m-41 1h1m5 0h1m2 0h1m1 0h3m2 0h1m1 0h1m1 0h2m3 0h1m1 0h1m2 0h1m1 0h1m1 0h1m5 0h1m-41 1h1m1 0h3m1 0h1m1 0h1m1 0h1m1 0h3m6 0h3m2 0h3m5 0h1m1 0h3m1 0h1m-41 1h1m1 0h3m1 0h1m1 0h3m2 0h1m4 0h1m3 0h6m6 0h1m1 0h3m1 0h1m-41 1h1m1 0h3m1 0h1m1 0h1m3 0h1m1 0h4m1 0h5m4 0h1m1 0h3m1 0h1m1 0h3m1 0h1m-41 1h1m5 0h1m1 0h1m1 0h4m2 0h2m2 0h3m1 0h1m2 0h1m1 0h1m4 0h1m5 0h1m-41 1h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7m-33 1h1m1 0h2m3 0h1m4 0h3m1 0h1m1 0h3m-29 1h1m1 0h5m2 0h1m3 0h1m2 0h2m4 0h5m2 0h1m1 0h1m2 0h5m-38 1h1m1 0h1m4 0h1m1 0h1m1 0h1m1 0h1m1 0h4m1 0h1m1 0h1m2 0h1m1 0h1m1 0h7m1 0h3m-40 1h1m1 0h2m1 0h2m1 0h1m3 0h2m5 0h3m4 0h2m3 0h1m6 0h1m-39 1h3m4 0h1m2 0h1m3 0h2m1 0h2m1 0h4m1 0h3m2 0h2m3 0h2m1 0h1m-39 1h2m2 0h4m2 0h1m2 0h1m1 0h2m4 0h1m1 0h3m2 0h1m1 0h1m4 0h3m-38 1h1m1 0h1m1 0h1m1 0h10m1 0h1m1 0h4m1 0h2m1 0h1m2 0h2m1 0h7m-40 1h1m1 0h1m2 0h1m4 0h4m4 0h1m1 0h1m2 0h4m1 0h2m2 0h1m1 0h1m1 0h1m-37 1h1m5 0h3m2 0h1m1 0h1m3 0h6m2 0h1m1 0h1m2 0h1m2 0h2m1 0h1m2 0h1m-40 1h1m3 0h3m1 0h1m1 0h3m1 0h3m1 0h1m2 0h5m2 0h1m3 0h1m1 0h1m2 0h2m-40 1h4m1 0h1m2 0h1m4 0h3m1 0h5m1 0h1m1 0h2m1 0h5m1 0h4m1 0h2m-41 1h3m2 0h2m2 0h1m3 0h1m3 0h1m3 0h1m2 0h1m2 0h2m3 0h1m3 0h2m-37 1h1m1 0h1m4 0h1m1 0h3m2 0h4m1 0h2m6 0h1m1 0h1m1 0h3m1 0h2m-34 1h4m1 0h1m1 0h1m3 0h2m2 0h1m2 0h5m2 0h1m2 0h1m4 0h2m-39 1h1m1 0h1m5 0h1m5 0h7m2 0h1m3 0h14m-38 1h5m1 0h2m3 0h1m6 0h2m2 0h1m1 0h2m1 0h1m1 0h1m3 0h3m-37 1h2m1 0h1m7 0h1m1 0h2m2 0h1m1 0h4m2 0h2m3 0h1m2 0h1m1 0h1m-36 1h1m2 0h4m8 0h1m2 0h1m4 0h2m2 0h1m5 0h1m-35 1h1m2 0h2m2 0h1m2 0h2m2 0h1m3 0h3m1 0h1m4 0h1m2 0h1m1 0h5m2 0h1m-39 1h1m2 0h2m1 0h1m1 0h1m3 0h1m1 0h1m3 0h1m5 0h2m1 0h2m1 0h4m-36 1h2m1 0h1m1 0h1m1 0h2m2 0h1m1 0h4m3 0h2m1 0h1m2 0h1m1 0h1m1 0h4m1 0h1m1 0h1m2 0h1m-40 1h1m3 0h3m5 0h2m2 0h1m5 0h4m2 0h1m1 0h2m4 0h3m-40 1h1m3 0h2m4 0h4m4 0h1m2 0h1m1 0h2m3 0h1m1 0h5m1 0h2m2 0h1m-41 1h1m1 0h1m1 0h1m1 0h1m7 0h1m1 0h1m4 0h2m1 0h5m3 0h4m1 0h1m-38 1h1m3 0h2m4 0h2m2 0h2m5 0h1m1 0h2m1 0h3m3 0h3m1 0h2m-38 1h1m1 0h2m2 0h5m6 0h1m6 0h4m1 0h1m2 0h8m-32 1h2m3 0h2m4 0h2m2 0h1m8 0h1m3 0h1m2 0h2m-41 1h7m3 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m3 0h1m1 0h3m1 0h3m1 0h1m1 0h3m-39 1h1m5 0h1m1 0h1m1 0h2m1 0h1m1 0h2m2 0h5m2 0h1m4 0h2m3 0h1m3 0h1m-41 1h1m1 0h3m1 0h1m1 0h3m1 0h1m3 0h1m1 0h4m1 0h5m1 0h1m2 0h5m1 0h2m-40 1h1m1 0h3m1 0h1m1 0h2m1 0h2m1 0h1m2 0h1m2 0h1m2 0h1m1 0h2m6 0h1m1 0h1m1 0h1m1 0h2m-41 1h1m1 0h3m1 0h1m1 0h1m1 0h1m3 0h5m1 0h1m1 0h1m1 0h1m2 0h2m2 0h6m1 0h1m-39 1h1m5 0h1m2 0h3m2 0h3m1 0h1m1 0h2m1 0h1m2 0h2m2 0h1m1 0h1m1 0h1m1 0h2m1 0h1m-40 1h7m1 0h1m1 0h1m2 0h2m2 0h1m1 0h1m2 0h6m1 0h3m1 0h1m1 0h2m1 0h1"/></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2021 The Outfit Project Authors (https://github.com/Outfitio/Outfit-Fonts)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2022 The Unbounded Project Authors (https://github.com/googlefonts/unbounded)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,391 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Flexera Launcher · One thumb. Every app.</title>
|
||||
<meta name="description" content="Flexera is an Android launcher with a calm home screen and an app list you scrub with one thumb. No ads, no account, no tracking.">
|
||||
<meta name="theme-color" content="#07070d">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Flexera Launcher · One thumb. Every app.">
|
||||
<meta property="og:description" content="An Android launcher with a calm home screen and an app list you scrub with one thumb. No ads, no account, no tracking.">
|
||||
<meta property="og:image" content="assets/og.jpg">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<link rel="icon" type="image/png" sizes="64x64" href="assets/favicon-64.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32.png">
|
||||
<link rel="apple-touch-icon" href="assets/icon-180.png">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Line icons used by the phone and the page. 24 x 24, drawn with strokes. -->
|
||||
<svg class="sprite" aria-hidden="true" focusable="false">
|
||||
<symbol id="g-camera" viewBox="0 0 24 24"><path d="M4 8h3l2-3h6l2 3h3v11H4z"/><circle cx="12" cy="13" r="3.5"/></symbol>
|
||||
<symbol id="g-phone" viewBox="0 0 24 24"><path d="M6.6 3.5l3 3.6-1.8 2.3a12 12 0 0 0 6.8 6.8l2.3-1.8 3.6 3-1.6 2.4c-.5.7-1.4 1-2.2.8C10.3 18.8 5.2 13.7 3.4 7.3c-.2-.8.1-1.7.8-2.2z"/></symbol>
|
||||
<symbol id="g-msg" viewBox="0 0 24 24"><path d="M4 5h16v11H9.5L4 20z"/></symbol>
|
||||
<symbol id="g-mail" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3.5 6.5L12 13l8.5-6.5"/></symbol>
|
||||
<symbol id="g-map" viewBox="0 0 24 24"><path d="M12 21s-6.5-6.2-6.5-11.2a6.5 6.5 0 0 1 13 0C18.5 14.8 12 21 12 21z"/><circle cx="12" cy="9.8" r="2.3"/></symbol>
|
||||
<symbol id="g-music" viewBox="0 0 24 24"><path d="M9 18V5l11-2v13"/><circle cx="6.5" cy="18" r="2.5"/><circle cx="17.5" cy="16" r="2.5"/></symbol>
|
||||
<symbol id="g-cal" viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="15" rx="2"/><path d="M4 10h16M8 3v4M16 3v4"/></symbol>
|
||||
<symbol id="g-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></symbol>
|
||||
<symbol id="g-notes" viewBox="0 0 24 24"><path d="M6 3h9l4 4v14H6z"/><path d="M9 11h7M9 15h7M9 7h3"/></symbol>
|
||||
<symbol id="g-files" viewBox="0 0 24 24"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></symbol>
|
||||
<symbol id="g-globe" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17M12 3.5c2.5 2.6 3.5 5.5 3.5 8.5s-1 5.9-3.5 8.5c-2.5-2.6-3.5-5.5-3.5-8.5s1-5.9 3.5-8.5z"/></symbol>
|
||||
<symbol id="g-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2.5V5M12 19v2.5M2.5 12H5M19 12h2.5M5.3 5.3l1.8 1.8M16.9 16.9l1.8 1.8M5.3 18.7l1.8-1.8M16.9 7.1l1.8-1.8"/></symbol>
|
||||
<symbol id="g-wallet" viewBox="0 0 24 24"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M15.5 14.5h2"/></symbol>
|
||||
<symbol id="g-sliders" viewBox="0 0 24 24"><path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/></symbol>
|
||||
<symbol id="g-mic" viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21"/></symbol>
|
||||
<symbol id="g-pulse" viewBox="0 0 24 24"><path d="M3 12h4l2-4.5 3.5 9 2.5-4.5h6"/></symbol>
|
||||
<symbol id="g-translate" viewBox="0 0 24 24"><path d="M4 5h9M8.5 3v2M6 5c.6 3 2.5 5.6 5 7M11 5c-.6 3.2-3 6-6.5 7.5"/><path d="M13 21l4-9 4 9M14.5 17.5h5"/></symbol>
|
||||
<symbol id="g-calc" viewBox="0 0 24 24"><rect x="5" y="3" width="14" height="18" rx="2"/><path d="M8.5 7.5h7M8.5 12h.01M12 12h.01M15.5 12h.01M8.5 16.5h.01M12 16.5h.01M15.5 16.5h.01"/></symbol>
|
||||
<symbol id="g-wave" viewBox="0 0 24 24"><path d="M4 10v4M8 7v10M12 4v16M16 8v8M20 11v2"/></symbol>
|
||||
<symbol id="g-book" viewBox="0 0 24 24"><path d="M5 4h11a3 3 0 0 1 3 3v13H8a3 3 0 0 1-3-3z"/><path d="M5 17a3 3 0 0 1 3-3h11"/></symbol>
|
||||
<symbol id="g-news" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M7 9h6M7 13h10M7 16h7"/></symbol>
|
||||
<symbol id="g-play" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10 9v6l5-3z"/></symbol>
|
||||
<symbol id="g-bag" viewBox="0 0 24 24"><path d="M5 8h14l-1 12H6z"/><path d="M9 8a3 3 0 0 1 6 0"/></symbol>
|
||||
<symbol id="g-bank" viewBox="0 0 24 24"><path d="M3 9l9-5 9 5M5 10v7M9.5 10v7M14.5 10v7M19 10v7M3 20h18"/></symbol>
|
||||
<symbol id="g-shield" viewBox="0 0 24 24"><path d="M12 3l7 3v5c0 5-3 8.5-7 10-4-1.5-7-5-7-10V6z"/><path d="M9 12l2 2 4-4"/></symbol>
|
||||
<symbol id="g-person" viewBox="0 0 24 24"><circle cx="12" cy="8.5" r="3.5"/><path d="M5 20c.8-3.8 3.6-6 7-6s6.2 2.2 7 6"/></symbol>
|
||||
<symbol id="g-image" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 16l5-5 4 4 3-3 6 6"/><circle cx="15.5" cy="8.5" r="1.5"/></symbol>
|
||||
<symbol id="g-plane" viewBox="0 0 24 24"><path d="M2.5 13.5l19-8-6.5 15-3-6.5z"/></symbol>
|
||||
<symbol id="g-moon" viewBox="0 0 24 24"><path d="M19 14.5A7.5 7.5 0 1 1 9.5 5a6 6 0 0 0 9.5 9.5z"/></symbol>
|
||||
<symbol id="g-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="6.5"/><path d="M16 16l4.5 4.5"/></symbol>
|
||||
<symbol id="g-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M12 11v5.5M12 7.6v.01"/></symbol>
|
||||
<symbol id="g-widgets" viewBox="0 0 24 24"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><path d="M16.5 3.5L20 7l-3.5 3.5L13 7z"/></symbol>
|
||||
<symbol id="g-edit" viewBox="0 0 24 24"><path d="M4 20h4L19 9l-4-4L4 16z"/></symbol>
|
||||
<symbol id="g-eyeoff" viewBox="0 0 24 24"><path d="M9.9 5.2C10.6 5.07 11.3 5 12 5c4.2 0 7.7 2.6 9.5 7-.6 1.4-1.4 2.6-2.4 3.6M16.4 17.6C15.1 18.5 13.6 19 12 19c-4.2 0-7.7-2.6-9.5-7 .8-2 2-3.6 3.5-4.8"/><path d="M14.1 14.1A3 3 0 0 1 9.9 9.9"/><path d="M4 4l16 16"/></symbol>
|
||||
<symbol id="g-flip" viewBox="0 0 24 24"><path d="M4 12a8 8 0 0 1 14-5.3M20 12a8 8 0 0 1-14 5.3"/><path d="M18 3v4h-4M6 21v-4h4"/></symbol>
|
||||
<symbol id="g-arrow" viewBox="0 0 24 24"><path d="M4 12h15M13 6l6 6-6 6"/></symbol>
|
||||
<symbol id="g-down" viewBox="0 0 24 24"><path d="M12 4v15M6 13l6 6 6-6"/></symbol>
|
||||
<symbol id="g-up" viewBox="0 0 24 24"><path d="M12 20V5M6 11l6-6 6 6"/></symbol>
|
||||
<symbol id="g-star" viewBox="0 0 24 24"><path class="fill" d="M12 1.5c.7 5.6 4.9 9.8 10.5 10.5-5.6.7-9.8 4.9-10.5 10.5-.7-5.6-4.9-9.8-10.5-10.5C7.1 11.3 11.3 7.1 12 1.5z"/></symbol>
|
||||
</svg>
|
||||
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
|
||||
<header class="nav" id="top">
|
||||
<a class="nav__brand" href="#top" aria-label="Flexera Launcher, back to top">
|
||||
<img src="assets/flexera-icon-256.webp" alt="" width="40" height="40">
|
||||
<span>Flexera</span>
|
||||
</a>
|
||||
<nav class="nav__links" aria-label="Sections">
|
||||
<a href="#how">How it works</a>
|
||||
<a href="#details">Details</a>
|
||||
<a href="#privacy">Privacy</a>
|
||||
<a href="#play">Play</a>
|
||||
</nav>
|
||||
<a class="btn btn--sm btn--pink" href="#get">Get the APK</a>
|
||||
<div class="nav__progress" aria-hidden="true"><i></i></div>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero" aria-labelledby="hero-title">
|
||||
<div class="hero__ghost" aria-hidden="true" data-parallax="0.18">Flexera</div>
|
||||
<div class="hero__grid">
|
||||
<div class="hero__copy">
|
||||
<p class="kicker rv"><span class="kicker__dot"></span>Android launcher <i>/</i> v<span data-version>0.13.0</span> <i>/</i> free</p>
|
||||
<h1 class="hero__title rv" id="hero-title">
|
||||
<span class="line"><span class="line__in">One thumb.</span></span>
|
||||
<span class="line"><span class="line__in grad">Every app.</span></span>
|
||||
</h1>
|
||||
<p class="lead rv">Flexera is a home screen for Android that stays calm, with an app list you fly through using one thumb. No ads. No account. Nothing about you leaves your phone.</p>
|
||||
<div class="cta rv">
|
||||
<a class="btn btn--big btn--cyan" href="https://git.mariosem.es/msemes/Flexera-Launcher-Releases/releases/download/latest/FlexeraLauncher.apk" data-apk>
|
||||
Download the APK <svg class="gly" aria-hidden="true"><use href="#g-down"/></svg>
|
||||
</a>
|
||||
<a class="btn btn--big btn--ghost" href="#how">
|
||||
See it move <svg class="gly" aria-hidden="true"><use href="#g-arrow"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<p class="meta rv">Android 8.0 or newer <i>·</i> Updates itself <i>·</i> No Play Store needed</p>
|
||||
</div>
|
||||
|
||||
<div class="hero__stage" aria-hidden="true">
|
||||
<div class="hero__slab"></div>
|
||||
<div class="hero__tilt" data-tilt>
|
||||
<div class="phone-host" data-phone="hero"></div>
|
||||
</div>
|
||||
<span class="sticker sticker--a">Wave<br>alphabet</span>
|
||||
<span class="sticker sticker--b">0 trackers</span>
|
||||
<div class="spinner">
|
||||
<svg viewBox="0 0 120 120" class="spinner__ring">
|
||||
<defs><path id="spin-path" d="M60,60 m-45,0 a45,45 0 1,1 90,0 a45,45 0 1,1 -90,0"/></defs>
|
||||
<text><textPath href="#spin-path">Swipe up · Scrub · Launch · Repeat · </textPath></text>
|
||||
</svg>
|
||||
<svg class="gly spinner__arrow"><use href="#g-down"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="bands" aria-hidden="true">
|
||||
<div class="marquee marquee--cyan">
|
||||
<div class="marquee__track" data-marquee="70">
|
||||
<span>One-handed</span><span>No ads</span><span>No account</span><span>No tracking</span><span>Wave alphabet</span><span>Blur folders</span><span>Quick launch</span><span>Updates itself</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="marquee marquee--pink">
|
||||
<div class="marquee__track" data-marquee="-55">
|
||||
<span>Swipe up</span><span>Scrub</span><span>Launch</span><span>Repeat</span><span>Swipe up</span><span>Scrub</span><span>Launch</span><span>Repeat</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STORY -->
|
||||
<section class="story" id="how" aria-labelledby="how-title">
|
||||
<header class="section-head">
|
||||
<p class="tag rv">[ 01 ] How it works</p>
|
||||
<h2 class="h2 rv" id="how-title">
|
||||
<span class="line"><span class="line__in">Keep scrolling.</span></span>
|
||||
<span class="line"><span class="line__in grad">The phone talks.</span></span>
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div class="story__body">
|
||||
<div class="story__stage" aria-hidden="true">
|
||||
<div class="story__num" data-story-num>01</div>
|
||||
<div class="phone-host" data-phone="story"></div>
|
||||
<ol class="story__rail" data-story-rail>
|
||||
<li></li><li></li><li></li><li></li><li></li><li></li><li></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="story__steps">
|
||||
<article class="step" data-step>
|
||||
<div class="step__card">
|
||||
<p class="step__no">01 / 07</p>
|
||||
<h3 class="step__title">Home, minus the noise.</h3>
|
||||
<p class="step__text">Pages, folders and widgets, in the Android layout you already know. No feed, no ads, nothing you didn't put there yourself.</p>
|
||||
<p class="step__gesture"><span>Gesture</span> None. Just look.</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="step" data-step>
|
||||
<div class="step__card">
|
||||
<p class="step__no">02 / 07</p>
|
||||
<h3 class="step__title">Swipe up. A to Z.</h3>
|
||||
<p class="step__text">Every app in one clean column, straight over your wallpaper. New messages show up under the app's name, so you see them before you open anything.</p>
|
||||
<p class="step__gesture"><span>Gesture</span> Swipe up</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="step" data-step>
|
||||
<div class="step__card">
|
||||
<p class="step__no">03 / 07</p>
|
||||
<h3 class="step__title">Ride the wave.</h3>
|
||||
<p class="step__text">Slide your thumb along the letters on the edge. They swell around it like a wave, and the list drops that letter into the middle of the screen, right where your thumb already is. Swipe up and keep holding to scrub straight from home.</p>
|
||||
<p class="step__gesture"><span>Gesture</span> Slide on the edge</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="step" data-step>
|
||||
<div class="step__card">
|
||||
<p class="step__no">04 / 07</p>
|
||||
<h3 class="step__title">Folders in focus.</h3>
|
||||
<p class="step__text">Open a folder and the rest of the screen melts into a soft blur, wallpaper included. Just the apps you came for.</p>
|
||||
<p class="step__gesture"><span>Gesture</span> Tap</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="step" data-step>
|
||||
<div class="step__card">
|
||||
<p class="step__no">05 / 07</p>
|
||||
<h3 class="step__title">Swipe into nothing.</h3>
|
||||
<p class="step__text">There's no page left of your first one. Swipe there anyway and your camera opens. Or any app, or any shortcut you pick. Works past your last page too.</p>
|
||||
<p class="step__gesture"><span>Gesture</span> Swipe past the edge</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="step" data-step>
|
||||
<div class="step__card">
|
||||
<p class="step__no">06 / 07</p>
|
||||
<h3 class="step__title">Widgets that fit in.</h3>
|
||||
<p class="step__text">A clock that lines up with your icons: the date, a one-line forecast, left, center or right, thin to bold. Next to it, a picture frame in your icon shape that turns to the next photo every ten seconds.</p>
|
||||
<p class="step__gesture"><span>Gesture</span> Hold, then edit</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="step" data-step>
|
||||
<div class="step__card">
|
||||
<p class="step__no">07 / 07</p>
|
||||
<h3 class="step__title">Out of sight.</h3>
|
||||
<p class="step__text">Hold any app in the list and tap Hide app. Gone from the list and from search. Changed your mind? Undo, or bring it back from Settings whenever you like.</p>
|
||||
<p class="step__gesture"><span>Gesture</span> Hold, then hide</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DETAILS -->
|
||||
<section class="details" id="details" aria-labelledby="details-title">
|
||||
<header class="section-head">
|
||||
<p class="tag rv">[ 02 ] The details</p>
|
||||
<h2 class="h2 rv" id="details-title">
|
||||
<span class="line"><span class="line__in">Small things.</span></span>
|
||||
<span class="line"><span class="line__in grad">Done properly.</span></span>
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div class="cards">
|
||||
<article class="card card--ink rv">
|
||||
<div class="card__art art-flip" aria-hidden="true"><i class="art-flip__phone"><b>A B C D E F G</b></i></div>
|
||||
<span class="card__no">01</span>
|
||||
<h3>Lefty? Flip it.</h3>
|
||||
<p>Move the alphabet to the left edge. One switch, and the whole list follows your other thumb.</p>
|
||||
</article>
|
||||
<article class="card card--cyan rv">
|
||||
<div class="card__art art-rows" aria-hidden="true"><i class="art-rows__col"><b></b><b></b><b></b><b></b><b></b></i><i class="art-rows__col art-rows__col--comfy"><b></b><b></b><b></b></i></div>
|
||||
<span class="card__no">02</span>
|
||||
<h3>Compact or comfy.</h3>
|
||||
<p>Pick how tall the rows in your app list are. More apps on screen, or bigger targets.</p>
|
||||
</article>
|
||||
<article class="card card--violet rv">
|
||||
<div class="card__art art-update" aria-hidden="true"><span class="art-update__v">v<span data-version>0.13.0</span></span><i class="art-update__bar"><b></b></i></div>
|
||||
<span class="card__no">03</span>
|
||||
<h3>Updates itself.</h3>
|
||||
<p>About once a day it checks for a new version and installs it when you say so. No store needed.</p>
|
||||
</article>
|
||||
<article class="card card--pink rv">
|
||||
<div class="card__art art-move" aria-hidden="true"><i class="art-move__box">SL</i><svg class="gly art-move__arrow"><use href="#g-arrow"/></svg><i class="art-move__box art-move__box--f"><img src="assets/flexera-icon-256.webp" alt="" width="44" height="44"></i></div>
|
||||
<span class="card__no">04</span>
|
||||
<h3>Move in, one tap.</h3>
|
||||
<p>Coming from Smart Launcher? Import your home screen from its backup: layout, folders and your photo widget.</p>
|
||||
</article>
|
||||
<article class="card card--ink rv">
|
||||
<div class="card__art art-shapes" aria-hidden="true"><i></i><i></i><i></i></div>
|
||||
<span class="card__no">05</span>
|
||||
<h3>Make it yours.</h3>
|
||||
<p>Icon packs, icon shapes, themed icons and colors. Labels on or off. Your grid, your rules.</p>
|
||||
</article>
|
||||
<article class="card card--cyan rv">
|
||||
<div class="card__art art-tap" aria-hidden="true"><i></i><i></i></div>
|
||||
<span class="card__no">06</span>
|
||||
<h3>Gestures for everything.</h3>
|
||||
<p>Double-tap to lock, swipe down for notifications, and more. Every gesture is yours to set.</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat rv"><b>0</b><span>Ads</span></div>
|
||||
<div class="stat rv"><b>0</b><span>Accounts</span></div>
|
||||
<div class="stat rv"><b>2</b><span>Reasons to<br>go online</span></div>
|
||||
<div class="stat rv"><b>1</b><span>Thumb<br>needed</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PRIVACY -->
|
||||
<section class="privacy" id="privacy" aria-labelledby="privacy-title">
|
||||
<p class="tag tag--ink rv">[ 03 ] Privacy</p>
|
||||
<h2 class="privacy__title rv" id="privacy-title">It goes online for <span class="privacy__hl">exactly two</span> things.</h2>
|
||||
<div class="privacy__list">
|
||||
<div class="pitem rv">
|
||||
<span class="pitem__no">1</span>
|
||||
<h3>Updates</h3>
|
||||
<p>About once a day it asks its own release page whether there's a newer version. Installing it is your call.</p>
|
||||
</div>
|
||||
<div class="pitem rv">
|
||||
<span class="pitem__no">2</span>
|
||||
<h3>Weather, if you ask</h3>
|
||||
<p>Pick a town for the clock's weather line and it asks Open-Meteo about that place. Only that place. It never uses your location.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="privacy__end rv">Nothing else about you, your apps or your phone leaves it.</p>
|
||||
<p class="privacy__note rv">This page plays by the same rules: no cookies, no analytics, no fonts or scripts from anyone else.</p>
|
||||
</section>
|
||||
|
||||
<!-- PLAYGROUND -->
|
||||
<section class="play" id="play" aria-labelledby="play-title">
|
||||
<header class="section-head">
|
||||
<p class="tag rv">[ 04 ] Playground</p>
|
||||
<h2 class="h2 rv" id="play-title">
|
||||
<span class="line"><span class="line__in">Go on.</span></span>
|
||||
<span class="line"><span class="line__in grad">Touch it.</span></span>
|
||||
</h2>
|
||||
</header>
|
||||
<div class="play__grid">
|
||||
<div class="play__stage">
|
||||
<div class="phone-host" data-phone="play" aria-label="A working model of Flexera Launcher. Use the buttons beside it if you can't use touch or a mouse." role="img"></div>
|
||||
</div>
|
||||
<div class="play__side">
|
||||
<p class="play__caption" aria-live="polite" data-caption>Swipe up on the home screen to open your apps.</p>
|
||||
<div class="chips" role="group" aria-label="Show me">
|
||||
<button class="chip" type="button" data-demo="drawer"><svg class="gly" aria-hidden="true"><use href="#g-up"/></svg>Swipe up</button>
|
||||
<button class="chip" type="button" data-demo="scrub"><svg class="gly" aria-hidden="true"><use href="#g-wave"/></svg>Scrub the letters</button>
|
||||
<button class="chip" type="button" data-demo="folder"><svg class="gly" aria-hidden="true"><use href="#g-files"/></svg>Open a folder</button>
|
||||
<button class="chip" type="button" data-demo="hide"><svg class="gly" aria-hidden="true"><use href="#g-eyeoff"/></svg>Hide an app</button>
|
||||
<button class="chip" type="button" data-demo="quick"><svg class="gly" aria-hidden="true"><use href="#g-camera"/></svg>Quick launch</button>
|
||||
<button class="chip chip--ghost" type="button" data-demo="reset">Reset</button>
|
||||
</div>
|
||||
<ul class="howto">
|
||||
<li><b>Swipe up</b> on the home screen</li>
|
||||
<li><b>Drag</b> along the letters on the right</li>
|
||||
<li><b>Tap</b> a folder, then outside it</li>
|
||||
<li><b>Hold</b> an app in the list</li>
|
||||
<li><b>Swipe right</b> on the first page</li>
|
||||
<li><b>Hold</b> the clock to restyle it</li>
|
||||
<li><b>Swipe up</b> from the bottom edge to go home</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- GET IT -->
|
||||
<section class="get" id="get" aria-labelledby="get-title">
|
||||
<p class="tag tag--ink rv">[ 05 ] Get it</p>
|
||||
<h2 class="get__title rv" id="get-title">
|
||||
<span class="line"><span class="line__in">Get</span></span>
|
||||
<span class="line"><span class="line__in">Flexera.</span></span>
|
||||
</h2>
|
||||
<div class="get__grid">
|
||||
<figure class="qr rv">
|
||||
<svg class="qr__code" viewBox="-4 -4 49 49" role="img" aria-label="QR code that downloads the newest Flexera Launcher APK"><path class="qrline" stroke="currentColor" d="M0 0.5h7m2 0h2m1 0h1m2 0h2m2 0h3m1 0h1m4 0h1m2 0h2m1 0h7m-41 1h1m5 0h1m2 0h1m1 0h3m2 0h1m1 0h1m1 0h2m3 0h1m1 0h1m2 0h1m1 0h1m1 0h1m5 0h1m-41 1h1m1 0h3m1 0h1m1 0h1m1 0h1m1 0h3m6 0h3m2 0h3m5 0h1m1 0h3m1 0h1m-41 1h1m1 0h3m1 0h1m1 0h3m2 0h1m4 0h1m3 0h6m6 0h1m1 0h3m1 0h1m-41 1h1m1 0h3m1 0h1m1 0h1m3 0h1m1 0h4m1 0h5m4 0h1m1 0h3m1 0h1m1 0h3m1 0h1m-41 1h1m5 0h1m1 0h1m1 0h4m2 0h2m2 0h3m1 0h1m2 0h1m1 0h1m4 0h1m5 0h1m-41 1h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7m-33 1h1m1 0h2m3 0h1m4 0h3m1 0h1m1 0h3m-29 1h1m1 0h5m2 0h1m3 0h1m2 0h2m4 0h5m2 0h1m1 0h1m2 0h5m-38 1h1m1 0h1m4 0h1m1 0h1m1 0h1m1 0h1m1 0h4m1 0h1m1 0h1m2 0h1m1 0h1m1 0h7m1 0h3m-40 1h1m1 0h2m1 0h2m1 0h1m3 0h2m5 0h3m4 0h2m3 0h1m6 0h1m-39 1h3m4 0h1m2 0h1m3 0h2m1 0h2m1 0h4m1 0h3m2 0h2m3 0h2m1 0h1m-39 1h2m2 0h4m2 0h1m2 0h1m1 0h2m4 0h1m1 0h3m2 0h1m1 0h1m4 0h3m-38 1h1m1 0h1m1 0h1m1 0h10m1 0h1m1 0h4m1 0h2m1 0h1m2 0h2m1 0h7m-40 1h1m1 0h1m2 0h1m4 0h4m4 0h1m1 0h1m2 0h4m1 0h2m2 0h1m1 0h1m1 0h1m-37 1h1m5 0h3m2 0h1m1 0h1m3 0h6m2 0h1m1 0h1m2 0h1m2 0h2m1 0h1m2 0h1m-40 1h1m3 0h3m1 0h1m1 0h3m1 0h3m1 0h1m2 0h5m2 0h1m3 0h1m1 0h1m2 0h2m-40 1h4m1 0h1m2 0h1m4 0h3m1 0h5m1 0h1m1 0h2m1 0h5m1 0h4m1 0h2m-41 1h3m2 0h2m2 0h1m3 0h1m3 0h1m3 0h1m2 0h1m2 0h2m3 0h1m3 0h2m-37 1h1m1 0h1m4 0h1m1 0h3m2 0h4m1 0h2m6 0h1m1 0h1m1 0h3m1 0h2m-34 1h4m1 0h1m1 0h1m3 0h2m2 0h1m2 0h5m2 0h1m2 0h1m4 0h2m-39 1h1m1 0h1m5 0h1m5 0h7m2 0h1m3 0h14m-38 1h5m1 0h2m3 0h1m6 0h2m2 0h1m1 0h2m1 0h1m1 0h1m3 0h3m-37 1h2m1 0h1m7 0h1m1 0h2m2 0h1m1 0h4m2 0h2m3 0h1m2 0h1m1 0h1m-36 1h1m2 0h4m8 0h1m2 0h1m4 0h2m2 0h1m5 0h1m-35 1h1m2 0h2m2 0h1m2 0h2m2 0h1m3 0h3m1 0h1m4 0h1m2 0h1m1 0h5m2 0h1m-39 1h1m2 0h2m1 0h1m1 0h1m3 0h1m1 0h1m3 0h1m5 0h2m1 0h2m1 0h4m-36 1h2m1 0h1m1 0h1m1 0h2m2 0h1m1 0h4m3 0h2m1 0h1m2 0h1m1 0h1m1 0h4m1 0h1m1 0h1m2 0h1m-40 1h1m3 0h3m5 0h2m2 0h1m5 0h4m2 0h1m1 0h2m4 0h3m-40 1h1m3 0h2m4 0h4m4 0h1m2 0h1m1 0h2m3 0h1m1 0h5m1 0h2m2 0h1m-41 1h1m1 0h1m1 0h1m1 0h1m7 0h1m1 0h1m4 0h2m1 0h5m3 0h4m1 0h1m-38 1h1m3 0h2m4 0h2m2 0h2m5 0h1m1 0h2m1 0h3m3 0h3m1 0h2m-38 1h1m1 0h2m2 0h5m6 0h1m6 0h4m1 0h1m2 0h8m-32 1h2m3 0h2m4 0h2m2 0h1m8 0h1m3 0h1m2 0h2m-41 1h7m3 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m3 0h1m1 0h3m1 0h3m1 0h1m1 0h3m-39 1h1m5 0h1m1 0h1m1 0h2m1 0h1m1 0h2m2 0h5m2 0h1m4 0h2m3 0h1m3 0h1m-41 1h1m1 0h3m1 0h1m1 0h3m1 0h1m3 0h1m1 0h4m1 0h5m1 0h1m2 0h5m1 0h2m-40 1h1m1 0h3m1 0h1m1 0h2m1 0h2m1 0h1m2 0h1m2 0h1m2 0h1m1 0h2m6 0h1m1 0h1m1 0h1m1 0h2m-41 1h1m1 0h3m1 0h1m1 0h1m1 0h1m3 0h5m1 0h1m1 0h1m1 0h1m2 0h2m2 0h6m1 0h1m-39 1h1m5 0h1m2 0h3m2 0h3m1 0h1m1 0h2m1 0h1m2 0h2m2 0h1m1 0h1m1 0h1m1 0h2m1 0h1m-40 1h7m1 0h1m1 0h1m2 0h2m2 0h1m1 0h1m2 0h6m1 0h3m1 0h1m1 0h2m1 0h1"/></svg>
|
||||
<figcaption>Scan with your phone's camera</figcaption>
|
||||
</figure>
|
||||
|
||||
<div class="get__main">
|
||||
<a class="btn btn--huge btn--pink rv" href="https://git.mariosem.es/msemes/Flexera-Launcher-Releases/releases/download/latest/FlexeraLauncher.apk" data-apk>
|
||||
Download the APK <svg class="gly" aria-hidden="true"><use href="#g-down"/></svg>
|
||||
</a>
|
||||
<p class="meta meta--ink rv">Version <span data-version>0.13.0</span> <i>·</i> Android 8.0 or newer <i>·</i> Free, no ads</p>
|
||||
<ol class="steps rv">
|
||||
<li><b>Download</b> the APK on your Android phone.</li>
|
||||
<li><b>Open it.</b> When Android asks, let your browser install apps.</li>
|
||||
<li><b>Make it home.</b> Press Home and pick Flexera, or go to Settings → Apps → Default apps → Home app.</li>
|
||||
</ol>
|
||||
<a class="textlink rv" href="https://git.mariosem.es/msemes/Flexera-Launcher-Releases/releases">Every release and its notes <svg class="gly" aria-hidden="true"><use href="#g-arrow"/></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq">
|
||||
<details class="rv">
|
||||
<summary>Why isn't it on Google Play?</summary>
|
||||
<p>It's a small independent project, shipped straight from its own release page. The app checks that page for new versions, so you won't miss updates.</p>
|
||||
</details>
|
||||
<details class="rv">
|
||||
<summary>Android warns me before installing. Is that normal?</summary>
|
||||
<p>Yes. Android double-checks every app installed from outside the Play Store. Play Protect may also mention that the app can ask for notification access (for notification dots and previews) and accessibility (only for gestures such as double-tap to lock). Both stay off until you turn them on yourself.</p>
|
||||
</details>
|
||||
<details class="rv">
|
||||
<summary>Will it work on my phone?</summary>
|
||||
<p>It runs on Android 8.0 and newer. The blur behind folders needs Android 12 or newer; on older versions folders open over a plain dimmed background.</p>
|
||||
</details>
|
||||
<details class="rv">
|
||||
<summary>Can I go back to my old launcher?</summary>
|
||||
<p>Any time. Settings → Apps → Default apps → Home app, and pick the old one. Nothing is deleted.</p>
|
||||
</details>
|
||||
<details class="rv">
|
||||
<summary>What does it cost?</summary>
|
||||
<p>Nothing. No ads, no in-app purchases, no account.</p>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="foot">
|
||||
<div class="foot__word" aria-hidden="true">Flexera</div>
|
||||
<div class="foot__row">
|
||||
<p>Flexera Launcher <span data-version>0.13.0</span></p>
|
||||
<p>Built on Lawnchair and Android's Launcher3.</p>
|
||||
<p>© 2026 Flexera Launcher</p>
|
||||
<a class="textlink" href="#top">Back to top <svg class="gly" aria-hidden="true"><use href="#g-up"/></svg></a>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,754 @@
|
||||
/* Flexera Launcher landing page.
|
||||
Brutal layout (hard 2px lines, solid slabs, offset shadows) in the app icon's neon colours.
|
||||
Fonts are served from this folder: nothing is loaded from anyone else. */
|
||||
|
||||
@font-face { font-family: "Unbounded"; src: url("fonts/unbounded.woff2") format("woff2"); font-weight: 300 900; font-display: swap; }
|
||||
@font-face { font-family: "Space Grotesk"; src: url("fonts/space-grotesk.woff2") format("woff2"); font-weight: 300 700; font-display: swap; }
|
||||
@font-face { font-family: "JetBrains Mono"; src: url("fonts/jetbrains-mono.woff2") format("woff2"); font-weight: 400 800; font-display: swap; }
|
||||
@font-face { font-family: "Outfit"; src: url("fonts/outfit.woff2") format("woff2"); font-weight: 100 900; font-display: swap; }
|
||||
|
||||
:root {
|
||||
--bg: #07070d;
|
||||
--bg-2: #0e0d1d;
|
||||
--fg: #f5f4ff;
|
||||
--dim: #b4b2d2;
|
||||
--ink: #07070d;
|
||||
|
||||
/* From the icon: ice and cyan at the top of the F, electric blue, deep indigo, violet, hot pink. */
|
||||
--ice: #76fafc;
|
||||
--cyan: #1ddbfd;
|
||||
--blue: #417df4;
|
||||
--indigo: #0d0b72;
|
||||
--violet: #653df0;
|
||||
--purple: #9c0ff6;
|
||||
--pink: #fc59fb;
|
||||
--grad: linear-gradient(95deg, #76fafc 0%, #1ddbfd 16%, #417df4 40%, #653df0 64%, #c866fa 82%, #fc59fb 100%);
|
||||
|
||||
--display: "Unbounded", "Arial Black", system-ui, sans-serif;
|
||||
--body: "Space Grotesk", system-ui, sans-serif;
|
||||
--mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", monospace;
|
||||
--ui: "Outfit", system-ui, sans-serif;
|
||||
|
||||
--pad: clamp(16px, 4vw, 56px);
|
||||
--b: 2px;
|
||||
--max: 1520px;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font: 400 17px/1.55 var(--body);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-x: clip;
|
||||
}
|
||||
/* Film grain over everything, very faint. */
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 90;
|
||||
opacity: .055;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
}
|
||||
img, svg { display: block; }
|
||||
a { color: inherit; }
|
||||
::selection { background: var(--pink); color: var(--ink); }
|
||||
:focus-visible { outline: 3px solid var(--cyan); outline-offset: 3px; }
|
||||
|
||||
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
|
||||
.gly { width: 1em; height: 1em; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; flex: none; }
|
||||
.gly .fill, .fill { fill: currentColor; stroke: none; }
|
||||
|
||||
.skip {
|
||||
position: absolute; left: 12px; top: -60px; z-index: 200;
|
||||
background: var(--cyan); color: var(--ink); padding: 10px 14px; font: 700 14px var(--mono);
|
||||
border: var(--b) solid var(--ink); text-decoration: none;
|
||||
}
|
||||
.skip:focus { top: 12px; }
|
||||
|
||||
/* ---------- type helpers ---------- */
|
||||
.grad {
|
||||
background: var(--grad);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
.tag {
|
||||
margin: 0;
|
||||
font: 700 13px/1 var(--mono);
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--cyan);
|
||||
}
|
||||
.tag--ink { color: var(--ink); }
|
||||
.h2 {
|
||||
margin: .35em 0 0;
|
||||
font: 900 clamp(40px, 7.2vw, 118px)/.9 var(--display);
|
||||
letter-spacing: -.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
/* Lines that slide up out of a mask when their heading scrolls into view. */
|
||||
.line { display: block; overflow: hidden; padding-bottom: .08em; margin-bottom: -.08em; }
|
||||
.line__in { display: block; transform: translateY(108%); transition: transform 1s cubic-bezier(.2, .8, .1, 1); }
|
||||
.line + .line .line__in { transition-delay: .09s; }
|
||||
.in .line__in { transform: none; }
|
||||
|
||||
.rv:not(.hero__title):not(.h2):not(.get__title) { opacity: 0; transform: translateY(26px); transition: opacity .8s ease, transform .9s cubic-bezier(.2, .8, .1, 1); }
|
||||
.rv.in:not(.hero__title):not(.h2):not(.get__title) { opacity: 1; transform: none; }
|
||||
|
||||
.meta { margin: 18px 0 0; font: 500 13px/1.4 var(--mono); text-transform: uppercase; letter-spacing: .06em; color: var(--dim); }
|
||||
.meta i { font-style: normal; color: var(--pink); margin: 0 .35em; }
|
||||
.meta--ink { color: var(--ink); }
|
||||
.meta--ink i { color: var(--ink); }
|
||||
|
||||
/* ---------- buttons ---------- */
|
||||
.btn {
|
||||
--c: var(--cyan);
|
||||
--sh: var(--fg);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .7em;
|
||||
padding: 1.05em 1.45em;
|
||||
font: 800 15px/1 var(--display);
|
||||
letter-spacing: .01em;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
color: var(--ink);
|
||||
background: var(--c);
|
||||
border: var(--b) solid var(--fg);
|
||||
box-shadow: 6px 6px 0 var(--sh);
|
||||
transition: transform .14s ease, box-shadow .14s ease, background .2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn .gly { width: 1.25em; height: 1.25em; stroke-width: 2.6; }
|
||||
.btn:hover { transform: translate(-3px, -3px); box-shadow: 9px 9px 0 var(--sh); }
|
||||
.btn:active { transform: translate(4px, 4px); box-shadow: 2px 2px 0 var(--sh); }
|
||||
.btn--pink { --c: var(--pink); }
|
||||
.btn--cyan { --c: var(--cyan); }
|
||||
.btn--ghost { background: transparent; color: var(--fg); --sh: var(--violet); }
|
||||
.btn--sm { padding: .8em 1.05em; font-size: 12px; box-shadow: 4px 4px 0 var(--sh); }
|
||||
.btn--sm:hover { box-shadow: 6px 6px 0 var(--sh); }
|
||||
.btn--big { font-size: clamp(14px, 1.1vw, 17px); }
|
||||
.btn--huge { font-size: clamp(18px, 2.2vw, 30px); padding: 1em 1.3em; border-color: var(--ink); --sh: var(--ink); }
|
||||
|
||||
.textlink {
|
||||
display: inline-flex; align-items: center; gap: .5em;
|
||||
font: 700 14px/1 var(--mono); text-transform: uppercase; letter-spacing: .06em;
|
||||
text-decoration: none; border-bottom: var(--b) solid currentColor; padding-bottom: 4px;
|
||||
}
|
||||
.textlink:hover { color: var(--pink); }
|
||||
|
||||
/* ---------- nav ---------- */
|
||||
.nav {
|
||||
position: fixed; inset: 0 0 auto 0; z-index: 80;
|
||||
display: flex; align-items: center; gap: 28px;
|
||||
padding: 14px var(--pad);
|
||||
transition: background .3s, border-color .3s;
|
||||
border-bottom: var(--b) solid transparent;
|
||||
}
|
||||
.nav.is-scrolled { background: rgba(7, 7, 13, .86); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); }
|
||||
.nav__brand { display: flex; align-items: center; gap: 12px; text-decoration: none; font: 900 18px/1 var(--display); text-transform: uppercase; letter-spacing: -.02em; }
|
||||
.nav__brand img { width: 40px; height: 40px; }
|
||||
.nav__links { display: flex; gap: 26px; margin-left: auto; }
|
||||
.nav__links a { font: 600 13px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; text-decoration: none; padding: 6px 0; border-bottom: var(--b) solid transparent; }
|
||||
.nav__links a:hover { border-bottom-color: var(--cyan); color: var(--cyan); }
|
||||
.nav__progress { position: absolute; left: 0; right: 0; bottom: -2px; height: 3px; }
|
||||
.nav__progress i { display: block; height: 100%; background: var(--grad); transform-origin: 0 50%; transform: scaleX(0); }
|
||||
|
||||
/* ---------- hero ---------- */
|
||||
.hero {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
min-height: 100svh;
|
||||
padding: 120px var(--pad) 70px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(60% 50% at 85% 30%, rgba(101, 61, 240, .35), transparent 70%),
|
||||
radial-gradient(40% 40% at 10% 90%, rgba(29, 219, 253, .14), transparent 70%),
|
||||
var(--bg);
|
||||
}
|
||||
.hero::before { /* the layout grid, drawn */
|
||||
content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .5;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(245, 244, 255, .06) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(245, 244, 255, .06) 1px, transparent 1px);
|
||||
background-size: 96px 96px;
|
||||
mask-image: linear-gradient(to bottom, #000 40%, transparent);
|
||||
-webkit-mask-image: linear-gradient(to bottom, #000 40%, transparent);
|
||||
}
|
||||
.hero__ghost {
|
||||
position: absolute; left: -1vw; bottom: -5.5vw;
|
||||
font: 900 26vw/.8 var(--display); letter-spacing: -.06em; text-transform: uppercase;
|
||||
/* A faint solid fill, never an outline: the variable font draws its letters from overlapping
|
||||
shapes, and an outline would trace every overlap. */
|
||||
background: linear-gradient(180deg, rgba(118, 250, 252, .11), rgba(101, 61, 240, .07) 60%, rgba(252, 89, 251, .05));
|
||||
-webkit-background-clip: text; background-clip: text; color: transparent;
|
||||
white-space: nowrap; pointer-events: none; user-select: none;
|
||||
}
|
||||
.hero__grid {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.18fr) minmax(0, .82fr);
|
||||
gap: clamp(24px, 4vw, 80px);
|
||||
align-items: center;
|
||||
max-width: var(--max);
|
||||
margin: 0 auto;
|
||||
}
|
||||
.kicker {
|
||||
display: inline-flex; align-items: center; gap: 10px; margin: 0;
|
||||
padding: 9px 14px; border: var(--b) solid var(--fg);
|
||||
font: 700 12px/1 var(--mono); text-transform: uppercase; letter-spacing: .12em;
|
||||
}
|
||||
.kicker i { font-style: normal; color: var(--pink); }
|
||||
.kicker__dot { width: 9px; height: 9px; border-radius: 50%; background: var(--cyan); box-shadow: 0 0 0 4px rgba(29, 219, 253, .2); animation: blink 1.6s steps(2) infinite; }
|
||||
@keyframes blink { 50% { opacity: .2; } }
|
||||
.hero__title {
|
||||
margin: 26px 0 28px;
|
||||
font: 900 clamp(50px, 8.4vw, 158px)/.86 var(--display);
|
||||
letter-spacing: -.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.lead { margin: 0; max-width: 33em; font-size: clamp(18px, 1.45vw, 22px); line-height: 1.5; color: #dddbf3; }
|
||||
.cta { display: flex; flex-wrap: wrap; gap: 18px; margin-top: 36px; }
|
||||
|
||||
.hero__stage { position: relative; display: grid; place-items: center; perspective: 1600px; padding: 20px 0; }
|
||||
.hero__slab {
|
||||
position: absolute; inset: 9% 3% 12% 14%;
|
||||
background: var(--violet);
|
||||
border: var(--b) solid var(--fg);
|
||||
box-shadow: 14px 14px 0 var(--pink);
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
.hero__tilt {
|
||||
position: relative;
|
||||
transform: rotateY(var(--ry, -16deg)) rotateX(var(--rx, 8deg)) rotateZ(var(--rz, 3deg));
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform;
|
||||
}
|
||||
.hero .phone-host { width: min(360px, 74vw); }
|
||||
.sticker {
|
||||
position: absolute; z-index: 3;
|
||||
padding: 11px 14px;
|
||||
font: 800 14px/1.05 var(--display); text-transform: uppercase; letter-spacing: -.01em;
|
||||
color: var(--ink); background: var(--cyan);
|
||||
border: var(--b) solid var(--ink); box-shadow: 4px 4px 0 var(--ink);
|
||||
animation: bob 4s ease-in-out infinite;
|
||||
}
|
||||
.sticker--a { top: 12%; left: -2%; transform: rotate(-9deg); }
|
||||
.sticker--b { top: 54%; right: -3%; background: var(--pink); transform: rotate(7deg); animation-delay: -2s; }
|
||||
@keyframes bob { 50% { translate: 0 -10px; } }
|
||||
.spinner { position: absolute; left: 0; bottom: 4%; width: 128px; height: 128px; z-index: 3; display: grid; place-items: center; }
|
||||
.spinner__ring { position: absolute; inset: 0; width: 100%; height: 100%; animation: spin 16s linear infinite; }
|
||||
.spinner__ring text { font: 700 10.5px var(--mono); letter-spacing: .18em; text-transform: uppercase; fill: var(--fg); }
|
||||
.spinner__arrow { width: 38px; height: 38px; color: var(--ink); background: var(--ice); border-radius: 50%; padding: 8px; stroke-width: 2.8; border: var(--b) solid var(--ink); }
|
||||
@keyframes spin { to { rotate: 360deg; } }
|
||||
|
||||
/* ---------- marquees ---------- */
|
||||
.bands { position: relative; overflow: hidden; padding-bottom: 30px; } /* the tilted band stays inside */
|
||||
.marquee { position: relative; overflow: hidden; z-index: 2; }
|
||||
.marquee--cyan { background: var(--cyan); color: var(--ink); }
|
||||
.marquee--pink { background: var(--pink); color: var(--ink); transform: rotate(-1.6deg) scale(1.03); margin-top: -4px; border-top: var(--b) solid var(--ink); }
|
||||
.marquee__track { display: flex; width: max-content; white-space: nowrap; will-change: transform; }
|
||||
.marquee__track span {
|
||||
display: inline-flex; align-items: center; gap: .55em; padding: .32em .55em .32em 0;
|
||||
font: 900 clamp(26px, 3.8vw, 58px)/1 var(--display); text-transform: uppercase; letter-spacing: -.03em;
|
||||
}
|
||||
.marquee__track span::after {
|
||||
content: ""; width: .62em; height: .62em; flex: none;
|
||||
background: currentColor;
|
||||
clip-path: polygon(50% 0, 61% 39%, 100% 50%, 61% 61%, 50% 100%, 39% 61%, 0 50%, 39% 39%);
|
||||
}
|
||||
|
||||
/* ---------- sections ---------- */
|
||||
section { position: relative; }
|
||||
.section-head { max-width: var(--max); margin: 0 auto; padding: clamp(80px, 11vw, 150px) var(--pad) clamp(30px, 4vw, 60px); }
|
||||
|
||||
/* ---------- story ---------- */
|
||||
.story__body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
max-width: var(--max);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--pad);
|
||||
}
|
||||
.story__stage {
|
||||
grid-column: 2; grid-row: 1;
|
||||
position: sticky; top: 0;
|
||||
height: 100vh; height: 100svh;
|
||||
display: grid; place-items: center;
|
||||
align-self: start;
|
||||
}
|
||||
.story__num {
|
||||
position: absolute; left: 50%; top: 50%; translate: -50% -50%;
|
||||
font: 900 clamp(180px, 30vw, 480px)/1 var(--display); letter-spacing: -.07em;
|
||||
color: rgba(245, 244, 255, .045); /* filled, not outlined, for the same reason as .hero__ghost */
|
||||
pointer-events: none; user-select: none;
|
||||
}
|
||||
.story .phone-host { height: min(84vh, 880px); height: min(84svh, 880px); position: relative; z-index: 1; }
|
||||
.story__rail { position: absolute; right: 0; top: 50%; translate: 0 -50%; display: flex; flex-direction: column; gap: 10px; margin: 0; padding: 0; list-style: none; }
|
||||
.story__rail li { width: 12px; height: 34px; border: var(--b) solid var(--fg); transition: background .3s, height .3s; }
|
||||
.story__rail li.done { background: var(--fg); }
|
||||
.story__rail li.on { background: var(--pink); height: 54px; }
|
||||
.story__steps { grid-column: 1; grid-row: 1; padding-bottom: 50vh; padding-bottom: 50svh; }
|
||||
.step { min-height: 100vh; min-height: 100svh; display: flex; align-items: center; padding-right: 4vw; }
|
||||
.step__card { opacity: .16; transition: opacity .5s ease; }
|
||||
.step.is-active .step__card { opacity: 1; }
|
||||
.step__no { margin: 0; font: 700 14px/1 var(--mono); letter-spacing: .1em; color: var(--pink); }
|
||||
.step__title { margin: 18px 0 22px; font: 900 clamp(34px, 4.3vw, 74px)/.94 var(--display); letter-spacing: -.04em; text-transform: uppercase; }
|
||||
.step__text { margin: 0; max-width: 30em; font-size: clamp(18px, 1.35vw, 21px); color: #dcdaf3; }
|
||||
.step__gesture { display: inline-flex; gap: 12px; margin: 30px 0 0; padding: 10px 14px; border: var(--b) solid var(--fg); font: 700 12.5px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.step__gesture span { color: var(--cyan); }
|
||||
|
||||
/* ---------- details ---------- */
|
||||
.cards {
|
||||
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
max-width: var(--max); margin: 0 auto; padding: 0 var(--pad);
|
||||
gap: 0;
|
||||
}
|
||||
.card {
|
||||
position: relative;
|
||||
min-height: 380px;
|
||||
padding: 28px 28px 32px;
|
||||
border: var(--b) solid var(--fg);
|
||||
margin: 0 -2px -2px 0; /* collapse the borders into a single grid */
|
||||
display: flex; flex-direction: column;
|
||||
transition: transform .2s ease, box-shadow .2s ease, z-index 0s .2s;
|
||||
}
|
||||
.card:hover { transform: translate(-6px, -6px); box-shadow: 12px 12px 0 var(--fg); z-index: 2; transition: transform .2s ease, box-shadow .2s ease; }
|
||||
.card h3 { margin: auto 0 12px; font: 900 clamp(24px, 2.2vw, 34px)/1 var(--display); letter-spacing: -.03em; text-transform: uppercase; }
|
||||
.card p { margin: 0; font-size: 17px; line-height: 1.5; }
|
||||
.card__no { position: absolute; top: 22px; right: 24px; font: 800 14px var(--mono); }
|
||||
.card--ink { background: var(--bg-2); }
|
||||
.card--ink p { color: var(--dim); }
|
||||
.card--cyan { background: var(--cyan); color: var(--ink); }
|
||||
.card--violet { background: var(--violet); }
|
||||
.card--pink { background: var(--pink); color: var(--ink); }
|
||||
.card__art { height: 150px; margin-bottom: 28px; position: relative; }
|
||||
|
||||
/* card art: alphabet that hops from edge to edge */
|
||||
.art-flip__phone { position: absolute; left: 0; top: 0; width: 120px; height: 150px; border: var(--b) solid currentColor; border-radius: 18px; }
|
||||
.art-flip__phone b { position: absolute; top: 12px; right: 8px; width: 12px; font: 700 10px/1.25 var(--ui); text-align: center; word-break: break-all; color: var(--cyan); animation: flip 4s cubic-bezier(.7, 0, .3, 1) infinite; }
|
||||
@keyframes flip { 0%, 40% { right: 8px; } 50%, 90% { right: 96px; } 100% { right: 8px; } }
|
||||
/* compact and comfy rows */
|
||||
.art-rows { display: flex; gap: 22px; }
|
||||
.art-rows__col { display: flex; flex-direction: column; gap: 9px; width: 120px; padding: 12px; border: var(--b) solid var(--ink); border-radius: 14px; }
|
||||
.art-rows__col b { display: block; height: 12px; background: var(--ink); border-radius: 3px; }
|
||||
.art-rows__col--comfy { gap: 16px; }
|
||||
.art-rows__col--comfy b { height: 20px; }
|
||||
/* update bar */
|
||||
.art-update { display: flex; flex-direction: column; justify-content: center; gap: 16px; }
|
||||
.art-update__v { font: 900 46px/1 var(--display); letter-spacing: -.04em; }
|
||||
.art-update__bar { display: block; height: 22px; border: var(--b) solid var(--fg); }
|
||||
.art-update__bar b { display: block; height: 100%; background: var(--fg); animation: fillbar 3.2s cubic-bezier(.6, 0, .2, 1) infinite; }
|
||||
@keyframes fillbar { 0% { width: 0; } 70%, 100% { width: 100%; } }
|
||||
/* move in */
|
||||
.art-move { display: flex; align-items: center; gap: 18px; }
|
||||
.art-move__box { display: grid; place-items: center; width: 92px; height: 92px; border: var(--b) solid var(--ink); background: var(--fg); color: var(--ink); font: 900 26px var(--display); font-style: normal; box-shadow: 5px 5px 0 var(--ink); }
|
||||
.art-move__box--f { background: var(--ink); }
|
||||
.art-move__box--f img { width: 60px; height: 60px; }
|
||||
.art-move__arrow { width: 44px; height: 44px; stroke-width: 3; animation: nudge 1.2s ease-in-out infinite; }
|
||||
@keyframes nudge { 50% { translate: 8px 0; } }
|
||||
/* shapes that morph */
|
||||
.art-shapes { display: flex; align-items: center; gap: 18px; }
|
||||
.art-shapes i { width: 72px; height: 72px; background: var(--grad); border: var(--b) solid var(--fg); animation: morph 4.5s ease-in-out infinite; }
|
||||
.art-shapes i:nth-child(2) { animation-delay: -1.5s; }
|
||||
.art-shapes i:nth-child(3) { animation-delay: -3s; }
|
||||
@keyframes morph { 0%, 100% { border-radius: 50%; } 33% { border-radius: 30%; } 66% { border-radius: 8px; } }
|
||||
/* double tap */
|
||||
.art-tap { display: grid; place-items: center; }
|
||||
.art-tap i { position: absolute; width: 56px; height: 56px; border-radius: 50%; border: 3px solid var(--ink); animation: tap 1.8s ease-out infinite; }
|
||||
.art-tap i:nth-child(2) { animation-delay: .22s; }
|
||||
@keyframes tap { 0% { scale: .3; opacity: 1; } 60%, 100% { scale: 2.3; opacity: 0; } }
|
||||
|
||||
.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); max-width: var(--max); margin: 0 auto; padding: clamp(60px, 8vw, 110px) var(--pad); }
|
||||
.stat { padding: 0 20px; border-left: var(--b) solid var(--fg); }
|
||||
.stat b { display: block; font: 900 clamp(70px, 10vw, 170px)/.9 var(--display); letter-spacing: -.06em; background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||||
.stat span { display: block; margin-top: 14px; font: 700 13px/1.3 var(--mono); text-transform: uppercase; letter-spacing: .1em; }
|
||||
|
||||
/* ---------- privacy ---------- */
|
||||
.privacy {
|
||||
background: var(--cyan);
|
||||
color: var(--ink);
|
||||
padding: clamp(80px, 11vw, 150px) var(--pad);
|
||||
}
|
||||
.privacy > * { max-width: var(--max); margin-left: auto; margin-right: auto; }
|
||||
.privacy__title { margin-top: 22px; margin-bottom: clamp(40px, 6vw, 80px); font: 900 clamp(40px, 6.8vw, 112px)/.92 var(--display); letter-spacing: -.045em; text-transform: uppercase; }
|
||||
.privacy__hl { display: inline-block; padding: 0 .12em; background: var(--ink); color: var(--cyan); transform: rotate(-2deg); }
|
||||
.privacy__list { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
|
||||
.pitem { position: relative; padding: 34px 34px 40px 0; border-top: 3px solid var(--ink); }
|
||||
.pitem + .pitem { padding-left: 34px; border-left: 3px solid var(--ink); }
|
||||
.pitem__no { display: grid; place-items: center; width: 54px; height: 54px; border-radius: 50%; background: var(--ink); color: var(--cyan); font: 900 24px var(--display); }
|
||||
.pitem h3 { margin: 22px 0 10px; font: 900 clamp(24px, 2.4vw, 36px)/1 var(--display); text-transform: uppercase; letter-spacing: -.03em; }
|
||||
.pitem p { margin: 0; max-width: 28em; font-size: clamp(17px, 1.35vw, 20px); }
|
||||
.privacy__end { margin-top: clamp(40px, 6vw, 80px); padding-top: 34px; border-top: 3px solid var(--ink); font: 900 clamp(26px, 3.4vw, 54px)/1.02 var(--display); letter-spacing: -.035em; text-transform: uppercase; }
|
||||
.privacy__note { margin-top: 26px; font: 600 14px/1.5 var(--mono); }
|
||||
|
||||
/* ---------- playground ---------- */
|
||||
.play {
|
||||
background:
|
||||
linear-gradient(to right, rgba(245, 244, 255, .05) 1px, transparent 1px) 0 0 / 48px 48px,
|
||||
linear-gradient(to bottom, rgba(245, 244, 255, .05) 1px, transparent 1px) 0 0 / 48px 48px,
|
||||
var(--bg);
|
||||
}
|
||||
.play__grid {
|
||||
display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: clamp(30px, 5vw, 90px);
|
||||
align-items: center;
|
||||
max-width: var(--max); margin: 0 auto;
|
||||
padding: 0 var(--pad) clamp(80px, 10vw, 140px);
|
||||
}
|
||||
.play__stage { display: grid; place-items: center; position: relative; }
|
||||
.play__stage::before {
|
||||
content: ""; position: absolute; width: 70%; aspect-ratio: 1; border-radius: 50%;
|
||||
background: radial-gradient(closest-side, rgba(101, 61, 240, .55), rgba(252, 89, 251, .18) 60%, transparent);
|
||||
filter: blur(30px);
|
||||
}
|
||||
.play .phone-host { height: min(82vh, 860px); height: min(82svh, 860px); position: relative; touch-action: none; }
|
||||
.play__caption {
|
||||
margin: 0 0 28px; min-height: 3.2em;
|
||||
font: 800 clamp(22px, 2.2vw, 34px)/1.15 var(--display); letter-spacing: -.03em; text-transform: uppercase;
|
||||
}
|
||||
.chips { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: 9px;
|
||||
padding: 12px 16px;
|
||||
font: 700 13px/1 var(--mono); text-transform: uppercase; letter-spacing: .06em;
|
||||
color: var(--fg); background: transparent;
|
||||
border: var(--b) solid var(--fg); box-shadow: 4px 4px 0 var(--violet);
|
||||
cursor: pointer; transition: transform .12s, box-shadow .12s, background .2s, color .2s;
|
||||
}
|
||||
.chip .gly { width: 18px; height: 18px; }
|
||||
.chip:hover { background: var(--fg); color: var(--ink); }
|
||||
.chip:active { transform: translate(3px, 3px); box-shadow: 1px 1px 0 var(--violet); }
|
||||
.chip.is-on { background: var(--cyan); color: var(--ink); }
|
||||
.chip--ghost { box-shadow: none; border-style: dashed; }
|
||||
.howto { margin: 36px 0 0; padding: 0; list-style: none; border-top: var(--b) solid var(--fg); }
|
||||
.howto li { padding: 13px 0; border-bottom: 1px solid rgba(245, 244, 255, .2); font-size: 17px; color: var(--dim); }
|
||||
.howto b { color: var(--fg); font-weight: 700; }
|
||||
|
||||
/* ---------- get it ---------- */
|
||||
.get { background: var(--violet); padding: clamp(80px, 11vw, 150px) var(--pad); }
|
||||
.get > * { max-width: var(--max); margin-left: auto; margin-right: auto; }
|
||||
.get .tag { color: var(--fg); }
|
||||
.get__title { margin-top: 10px; margin-bottom: clamp(30px, 5vw, 70px); font: 900 clamp(44px, 14.6vw, 260px)/.82 var(--display); letter-spacing: -.07em; text-transform: uppercase; color: var(--fg); text-shadow: .045em .045em 0 var(--ink); }
|
||||
.get__grid { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: clamp(30px, 5vw, 90px); align-items: start; }
|
||||
.qr {
|
||||
margin: 0; padding: 22px; width: clamp(220px, 22vw, 300px);
|
||||
background: var(--fg); color: var(--ink);
|
||||
border: var(--b) solid var(--ink); box-shadow: 12px 12px 0 var(--ink);
|
||||
transform: rotate(-3deg);
|
||||
}
|
||||
.qr__code { width: 100%; height: auto; }
|
||||
.qr__code path { fill: none; stroke-width: 1; shape-rendering: crispEdges; }
|
||||
.qr figcaption { margin-top: 14px; font: 700 12.5px/1.3 var(--mono); text-transform: uppercase; letter-spacing: .08em; text-align: center; }
|
||||
.get__main { display: flex; flex-direction: column; align-items: flex-start; }
|
||||
.get .btn--huge { --c: var(--pink); }
|
||||
.steps { counter-reset: s; margin: 40px 0 34px; padding: 0; list-style: none; max-width: 40em; }
|
||||
.steps li { counter-increment: s; position: relative; padding: 16px 0 16px 64px; border-top: var(--b) solid var(--ink); font-size: clamp(17px, 1.3vw, 20px); }
|
||||
.steps li:last-child { border-bottom: var(--b) solid var(--ink); }
|
||||
.steps li::before { content: "0" counter(s); position: absolute; left: 0; top: 16px; font: 900 22px/1 var(--display); color: var(--ice); }
|
||||
.get .textlink { color: var(--fg); }
|
||||
.faq { margin-top: clamp(60px, 8vw, 110px); border-top: 3px solid var(--ink); }
|
||||
.faq details { border-bottom: 3px solid var(--ink); }
|
||||
.faq summary {
|
||||
list-style: none; cursor: pointer; position: relative;
|
||||
padding: 24px 60px 24px 0;
|
||||
font: 800 clamp(18px, 1.8vw, 28px)/1.15 var(--display); letter-spacing: -.02em; text-transform: uppercase;
|
||||
}
|
||||
.faq summary::-webkit-details-marker { display: none; }
|
||||
.faq summary::after {
|
||||
content: ""; position: absolute; right: 6px; top: 50%; width: 26px; height: 26px; margin-top: -13px;
|
||||
background: linear-gradient(var(--fg), var(--fg)) center / 100% 4px no-repeat, linear-gradient(var(--fg), var(--fg)) center / 4px 100% no-repeat;
|
||||
transition: rotate .25s;
|
||||
}
|
||||
.faq details[open] summary::after { rotate: 45deg; }
|
||||
.faq details p { margin: 0 0 26px; max-width: 46em; font-size: clamp(17px, 1.3vw, 19px); }
|
||||
|
||||
/* ---------- footer ---------- */
|
||||
.foot { padding: 40px var(--pad) 34px; overflow: hidden; }
|
||||
.foot__word {
|
||||
font: 900 16.6vw/.8 var(--display); letter-spacing: -.07em; text-transform: uppercase;
|
||||
background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent;
|
||||
margin: 0 0 30px -.03em; white-space: nowrap;
|
||||
}
|
||||
.foot__row { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 40px; padding-top: 22px; border-top: var(--b) solid var(--fg); font: 500 13px/1.4 var(--mono); text-transform: uppercase; letter-spacing: .06em; color: var(--dim); }
|
||||
.foot__row p { margin: 0; }
|
||||
.foot__row .textlink { margin-left: auto; color: var(--fg); }
|
||||
|
||||
/* =====================================================================
|
||||
The phone. Drawn at 390 x 844 (a phone's CSS pixels) and scaled to fit
|
||||
its host, so every measurement inside stays the same at any size.
|
||||
===================================================================== */
|
||||
.phone-host { position: relative; aspect-ratio: 414 / 868; }
|
||||
.phone {
|
||||
position: absolute; left: 0; top: 0;
|
||||
width: 414px; height: 868px;
|
||||
padding: 12px;
|
||||
border-radius: 60px;
|
||||
background: linear-gradient(150deg, #34324d, #121122 45%, #08070f);
|
||||
box-shadow:
|
||||
inset 0 0 0 2px #4a4767,
|
||||
inset 0 0 0 5px #0a0914,
|
||||
0 50px 90px -30px rgba(101, 61, 240, .7),
|
||||
0 0 0 2px #000;
|
||||
transform-origin: 0 0;
|
||||
transform: scale(var(--s, .5));
|
||||
user-select: none; -webkit-user-select: none;
|
||||
}
|
||||
.phone::before, .phone::after { /* side buttons */
|
||||
content: ""; position: absolute; right: -4px; width: 4px; border-radius: 0 3px 3px 0; background: #2b2940;
|
||||
}
|
||||
.phone::before { top: 190px; height: 70px; }
|
||||
.phone::after { top: 290px; height: 110px; }
|
||||
.screen {
|
||||
position: relative;
|
||||
width: 390px; height: 844px;
|
||||
border-radius: 48px;
|
||||
overflow: hidden;
|
||||
background: #05061a;
|
||||
font-family: var(--ui);
|
||||
color: #fff;
|
||||
isolation: isolate;
|
||||
contain: strict;
|
||||
}
|
||||
.screen * { box-sizing: border-box; }
|
||||
.screen .ic svg, .screen .gl { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
|
||||
/* wallpaper and home screen sit in one layer, so the folder blur takes both */
|
||||
.world { position: absolute; inset: 0; transform-origin: 50% 50%; }
|
||||
.wall {
|
||||
position: absolute; inset: -30px;
|
||||
background:
|
||||
radial-gradient(70% 38% at 8% 12%, rgba(29, 219, 253, .62), transparent 70%),
|
||||
radial-gradient(60% 34% at 95% 92%, rgba(252, 89, 251, .55), transparent 70%),
|
||||
radial-gradient(55% 30% at 78% 40%, rgba(101, 61, 240, .7), transparent 72%),
|
||||
linear-gradient(168deg, #2a1fb8 0%, #120d82 30%, #0a0954 58%, #050525 100%);
|
||||
}
|
||||
.wall::before, .wall::after { /* the icon's sweeping light lines */
|
||||
content: ""; position: absolute; border-radius: 50%;
|
||||
border-top: 3px solid rgba(118, 250, 252, .55);
|
||||
filter: blur(1.5px);
|
||||
}
|
||||
.wall::before { left: -40%; top: 18%; width: 150%; height: 70%; transform: rotate(-22deg); }
|
||||
.wall::after { left: -10%; top: 52%; width: 150%; height: 60%; transform: rotate(-24deg); border-top-color: rgba(252, 89, 251, .5); }
|
||||
|
||||
.sbar { position: absolute; left: 0; right: 0; top: 0; height: 40px; padding: 14px 28px 0; display: flex; justify-content: space-between; font: 600 14px/1 var(--ui); z-index: 30; pointer-events: none; }
|
||||
.sbar__icons { display: flex; align-items: center; gap: 6px; }
|
||||
.sbar__sig { display: flex; align-items: flex-end; gap: 2px; height: 11px; }
|
||||
.sbar__sig i { width: 3px; background: #fff; border-radius: 1px; }
|
||||
.sbar__sig i:nth-child(1) { height: 4px; } .sbar__sig i:nth-child(2) { height: 6px; } .sbar__sig i:nth-child(3) { height: 8px; } .sbar__sig i:nth-child(4) { height: 11px; }
|
||||
.sbar__bat { width: 22px; height: 11px; border-radius: 3px; border: 1.5px solid #fff; padding: 1.5px; }
|
||||
.sbar__bat::before { content: ""; display: block; width: 72%; height: 100%; background: #fff; border-radius: 1px; }
|
||||
.notch { position: absolute; left: 50%; top: 12px; width: 22px; height: 22px; margin-left: -11px; border-radius: 50%; background: #000; z-index: 31; box-shadow: inset 0 0 0 4px #111; }
|
||||
|
||||
.home { position: absolute; inset: 0; transform-origin: 50% 60%; }
|
||||
.pages { position: absolute; inset: 0; }
|
||||
.page { position: absolute; top: 0; width: 390px; height: 844px; }
|
||||
.cell { position: absolute; display: flex; flex-direction: column; align-items: center; padding-top: 8px; }
|
||||
.cell .lbl { margin-top: 7px; font: 500 12.5px/1.2 var(--ui); text-shadow: 0 1px 4px rgba(0, 0, 0, .55); white-space: nowrap; }
|
||||
.cell.tap .ic, .cell.tap .fic { animation: tapb .3s ease; }
|
||||
@keyframes tapb { 40% { transform: scale(.86); } }
|
||||
.ic {
|
||||
position: relative; display: grid; place-items: center;
|
||||
width: 58px; height: 58px; border-radius: 30%;
|
||||
color: #fff;
|
||||
background: linear-gradient(145deg, var(--a), var(--b));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .35), 0 6px 14px -6px rgba(0, 0, 0, .55);
|
||||
flex: none;
|
||||
}
|
||||
.ic svg { width: 46%; height: 46%; }
|
||||
.ndot { position: absolute; top: 4px; left: calc(50% + 20px); width: 13px; height: 13px; border-radius: 50%; background: var(--pink); border: 2px solid #0d0b40; }
|
||||
.fic { width: 58px; height: 58px; padding: 7px; border-radius: 30%; background: rgba(236, 233, 249, .94); display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
|
||||
.ic--mini { width: 100%; height: auto; aspect-ratio: 1; box-shadow: none; }
|
||||
.ic--mini svg { stroke-width: 2.6; }
|
||||
|
||||
.w-clock { position: absolute; padding: 26px 18px 0; }
|
||||
.clk__line { white-space: nowrap; }
|
||||
.clk__line > span { display: inline-block; will-change: transform; }
|
||||
.clk__time { font: 300 88px/1 var(--ui); letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
|
||||
.clk__date { margin-top: 8px; font: 500 18px/1.2 var(--ui); }
|
||||
.clk__wx { margin-top: 4px; font: 400 16px/1.2 var(--ui); opacity: .92; }
|
||||
|
||||
.w-pics { position: absolute; padding: 8px 12px 18px; }
|
||||
.pics { position: relative; width: 100%; height: 100%; border-radius: 30px; overflow: hidden; box-shadow: 0 10px 24px -10px rgba(0, 0, 0, .6); }
|
||||
.pic { position: absolute; inset: 0; opacity: 0; transition: opacity 1.1s ease; }
|
||||
.pic.on { opacity: 1; }
|
||||
.pic--a { /* sunset over water */
|
||||
background:
|
||||
radial-gradient(circle at 50% 58%, #fff1b8 0 9%, #ffc56b 10%, transparent 30%),
|
||||
linear-gradient(to bottom, transparent 62%, rgba(10, 6, 60, .9) 62%),
|
||||
repeating-linear-gradient(to bottom, transparent 0 7px, rgba(255, 190, 120, .28) 7px 9px) 0 62% / 100% 38% no-repeat,
|
||||
linear-gradient(to bottom, #3b1c8f, #c2399b 40%, #ff7a59 62%);
|
||||
}
|
||||
.pic--b { /* mountains */
|
||||
background:
|
||||
linear-gradient(150deg, transparent 52%, #1a1660 52.4%) 0 100% / 60% 62% no-repeat,
|
||||
linear-gradient(-140deg, transparent 50%, #2a2390 50.4%) 100% 100% / 75% 75% no-repeat,
|
||||
linear-gradient(140deg, transparent 50%, #3c32b8 50.4%) 0 100% / 75% 55% no-repeat,
|
||||
radial-gradient(circle at 72% 28%, #fff 0 6%, transparent 7%),
|
||||
linear-gradient(to bottom, #1ddbfd, #417df4 55%, #9a7dff);
|
||||
}
|
||||
.pic--c { /* neon city */
|
||||
background:
|
||||
linear-gradient(to top, #fc59fb 0 2px, transparent 2px) 0 100% / 100% 100% no-repeat,
|
||||
linear-gradient(#0a0920, #0a0920) 8% 100% / 16% 58% no-repeat,
|
||||
linear-gradient(#120f33, #120f33) 28% 100% / 18% 76% no-repeat,
|
||||
linear-gradient(#0a0920, #0a0920) 52% 100% / 14% 48% no-repeat,
|
||||
linear-gradient(#17134a, #17134a) 74% 100% / 20% 68% no-repeat,
|
||||
linear-gradient(#0a0920, #0a0920) 98% 100% / 12% 40% no-repeat,
|
||||
radial-gradient(circle at 70% 22%, #fdf6ff 0 7%, transparent 8%),
|
||||
linear-gradient(to bottom, #3a0d6e, #b8279b 70%, #fc59fb);
|
||||
}
|
||||
.pagedots { position: absolute; left: 0; right: 0; top: 748px; display: flex; justify-content: center; gap: 8px; }
|
||||
.pagedots i { width: 7px; height: 7px; border-radius: 50%; background: rgba(255, 255, 255, .45); transition: background .2s, width .2s; }
|
||||
.pagedots i.on { background: #fff; width: 18px; border-radius: 4px; }
|
||||
|
||||
.fscrim { position: absolute; inset: 0; background: #000; opacity: 0; pointer-events: none; z-index: 10; }
|
||||
.fcard {
|
||||
position: absolute; left: 45px; top: 282px; width: 300px;
|
||||
padding: 22px 12px 14px;
|
||||
border-radius: 32px;
|
||||
background: #eeebf8; color: #1c1a2d;
|
||||
z-index: 11; visibility: hidden; opacity: 0;
|
||||
box-shadow: 0 30px 60px -20px rgba(0, 0, 0, .6);
|
||||
}
|
||||
.fcard__grid { display: grid; grid-template-columns: repeat(3, 1fr); row-gap: 16px; }
|
||||
.fcard__app { display: flex; flex-direction: column; align-items: center; gap: 7px; font: 500 12.5px/1.2 var(--ui); }
|
||||
.fcard__title { margin-top: 16px; text-align: center; font: 600 17px/1 var(--ui); }
|
||||
|
||||
.drawer { position: absolute; inset: 0; background: rgba(5, 4, 20, .8); z-index: 12; opacity: 0; visibility: hidden; }
|
||||
.drawer__in { position: absolute; inset: 0; -webkit-mask-image: linear-gradient(to bottom, transparent 30px, #000 52px); mask-image: linear-gradient(to bottom, transparent 30px, #000 52px); } /* the list fades out under the status bar */
|
||||
.list { position: absolute; left: 0; right: 0; top: 46px; padding: 0 58px 0 20px; will-change: transform; }
|
||||
.search { display: flex; align-items: center; gap: 12px; height: 50px; margin: 6px 0 12px; padding: 0 18px; border-radius: 25px; background: #f2f0fb; color: #4b4867; font: 500 16px/1 var(--ui); }
|
||||
.search .gl { width: 22px; height: 22px; color: #4b4867; stroke-width: 2.4; }
|
||||
.hdr { height: 34px; padding: 12px 0 0 2px; font: 500 13px/1 var(--ui); color: #b9b6dc; }
|
||||
.row { display: flex; align-items: center; gap: 15px; height: 54px; overflow: hidden; transition: background .2s; border-radius: 14px; }
|
||||
.row--note { height: 64px; }
|
||||
.row .ic { width: 40px; height: 40px; }
|
||||
.row__t { display: flex; flex-direction: column; min-width: 0; }
|
||||
.row__n { font: 500 17.5px/1.15 var(--ui); }
|
||||
.row__p { margin-top: 3px; font: 400 13px/1.2 var(--ui); color: #cfcbef; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.row.flash { background: rgba(255, 255, 255, .12); }
|
||||
.row.held { background: rgba(255, 255, 255, .1); }
|
||||
|
||||
.alpha { position: absolute; right: 8px; top: 404px; width: 30px; display: flex; flex-direction: column; align-items: center; z-index: 2; }
|
||||
.alpha__s { width: 15px; height: 15px; margin-bottom: 8px; color: #fff; opacity: .9; }
|
||||
.alpha__l { display: block; width: 30px; height: 21px; font: 600 12.5px/21px var(--ui); text-align: center; color: rgba(255, 255, 255, .86); transform-origin: 50% 50%; }
|
||||
.alpha__l.on { color: var(--pink); font-weight: 800; }
|
||||
.bubble { position: absolute; left: 0; top: 0; width: 66px; height: 66px; display: grid; place-items: center; border-radius: 50%; background: var(--grad); color: #fff; font: 700 32px/1 var(--ui); box-shadow: 0 10px 30px -6px rgba(252, 89, 251, .6); opacity: 0; z-index: 3; pointer-events: none; text-shadow: 0 1px 6px rgba(0, 0, 0, .3); }
|
||||
|
||||
.menu { position: absolute; left: 0; top: 0; width: 236px; padding: 6px 0; border-radius: 24px; background: #f6f4fd; color: #1e1c2c; z-index: 20; opacity: 0; visibility: hidden; transform-origin: 30% 0; box-shadow: 0 24px 50px -16px rgba(0, 0, 0, .65); }
|
||||
.mi { display: flex; align-items: center; gap: 16px; height: 50px; padding: 0 20px; font: 500 16px/1 var(--ui); }
|
||||
.mi .gl { width: 22px; height: 22px; stroke-width: 1.9; color: #3a3752; }
|
||||
.mi--hide.pick { background: #e2dcff; }
|
||||
|
||||
.snack { position: absolute; left: 24px; right: 24px; bottom: 64px; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 16px 22px; border-radius: 18px; background: #f1eefb; color: #1b1a29; font: 500 15px/1.2 var(--ui); z-index: 21; opacity: 0; visibility: hidden; box-shadow: 0 18px 40px -14px rgba(0, 0, 0, .6); }
|
||||
.snack__undo { all: unset; font: 700 15px/1 var(--ui); color: #5a33e6; cursor: pointer; padding: 4px; }
|
||||
|
||||
.sheet { position: absolute; left: 0; right: 0; bottom: 0; padding: 12px 22px 36px; border-radius: 30px 30px 0 0; background: #f4f2fb; color: #1c1a2d; z-index: 22; transform: translateY(105%); box-shadow: 0 -20px 50px -20px rgba(0, 0, 0, .5); }
|
||||
.sheet__grab { display: block; width: 36px; height: 4px; margin: 0 auto 14px; border-radius: 2px; background: #a9a5c2; }
|
||||
.sheet h4 { margin: 0 0 10px; font: 600 22px/1.2 var(--ui); }
|
||||
.sheet__h { margin: 14px 0 8px; font: 600 13.5px/1 var(--ui); color: #5d5a78; }
|
||||
.seg { display: flex; border: 1.5px solid #8a86a6; border-radius: 22px; overflow: hidden; }
|
||||
.seg span { flex: 1; display: grid; place-items: center; height: 40px; font: 500 14px/1 var(--ui); border-left: 1.5px solid #8a86a6; transition: background .2s; }
|
||||
.seg span:first-child { border-left: 0; }
|
||||
.seg span.on { background: #ddd5ff; font-weight: 700; }
|
||||
|
||||
.cam { position: absolute; inset: 0; background: #000; z-index: 25; visibility: hidden; transform: translateX(-100%); }
|
||||
.cam__view {
|
||||
position: absolute; left: 0; right: 0; top: 52px; height: 560px; overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 66% 34%, #fff6d8 0 5%, #ffd48a 6%, transparent 22%),
|
||||
linear-gradient(to bottom, #4b2ac9, #c2399b 45%, #ff9b6a 70%, #ffd48a);
|
||||
}
|
||||
.cam__hill { position: absolute; left: -10%; right: -10%; bottom: 0; }
|
||||
.cam__hill--1 { height: 46%; background: #2a1563; clip-path: polygon(0 40%, 18% 22%, 36% 46%, 55% 18%, 74% 40%, 90% 28%, 100% 36%, 100% 100%, 0 100%); }
|
||||
.cam__hill--2 { height: 30%; background: #120a36; clip-path: polygon(0 30%, 25% 12%, 48% 40%, 70% 20%, 100% 44%, 100% 100%, 0 100%); }
|
||||
.cam__grid { position: absolute; inset: 0; background: linear-gradient(to right, transparent 33%, rgba(255, 255, 255, .35) 33%, rgba(255, 255, 255, .35) calc(33% + 1px), transparent calc(33% + 1px), transparent 66%, rgba(255, 255, 255, .35) 66%, rgba(255, 255, 255, .35) calc(66% + 1px), transparent calc(66% + 1px)), linear-gradient(to bottom, transparent 33%, rgba(255, 255, 255, .35) 33%, rgba(255, 255, 255, .35) calc(33% + 1px), transparent calc(33% + 1px), transparent 66%, rgba(255, 255, 255, .35) 66%, rgba(255, 255, 255, .35) calc(66% + 1px), transparent calc(66% + 1px)); }
|
||||
.cam__focus { position: absolute; left: 50%; top: 50%; width: 76px; height: 76px; margin: -38px; border: 2px solid #ffe066; border-radius: 6px; }
|
||||
.cam__modes { position: absolute; left: 0; right: 0; top: 632px; display: flex; justify-content: center; gap: 26px; font: 600 13px/1 var(--ui); letter-spacing: .08em; text-transform: uppercase; color: #9d9ab8; }
|
||||
.cam__modes .on { color: var(--ice); }
|
||||
.cam__bar { position: absolute; left: 0; right: 0; top: 684px; display: flex; align-items: center; justify-content: space-between; padding: 0 44px; }
|
||||
.cam__thumb { width: 50px; height: 50px; border-radius: 12px; background: linear-gradient(145deg, #ff9b6a, #4b2ac9); border: 2px solid #fff; }
|
||||
.cam__shutter { width: 78px; height: 78px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 5px #000, 0 0 0 9px #fff; transition: transform .12s ease; }
|
||||
.cam__flip { display: grid; place-items: center; width: 50px; height: 50px; border-radius: 50%; background: #26243a; }
|
||||
.cam__flip .gl { width: 24px; height: 24px; color: #fff; }
|
||||
.cam__close { position: absolute; top: 58px; right: 16px; width: 40px; height: 40px; display: grid; place-items: center; border: 0; border-radius: 50%; background: rgba(0, 0, 0, .45); color: #fff; font: 400 26px/1 var(--ui); cursor: pointer; }
|
||||
|
||||
.gbar { position: absolute; left: 50%; bottom: 9px; width: 124px; height: 5px; margin-left: -62px; border-radius: 3px; background: rgba(255, 255, 255, .88); z-index: 40; pointer-events: none; }
|
||||
|
||||
.finger {
|
||||
position: absolute; left: 0; top: 0; width: 50px; height: 50px; margin: -25px 0 0 -25px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, .26);
|
||||
border: 2px solid rgba(255, 255, 255, .85);
|
||||
box-shadow: 0 0 0 7px rgba(255, 255, 255, .1), 0 10px 26px rgba(0, 0, 0, .35);
|
||||
z-index: 50; opacity: 0; pointer-events: none;
|
||||
}
|
||||
.finger__ring { position: absolute; inset: -9px; border-radius: 50%; background: conic-gradient(var(--pink) calc(var(--ring, 0) * 1turn), transparent 0); -webkit-mask: radial-gradient(closest-side, transparent 78%, #000 80%); mask: radial-gradient(closest-side, transparent 78%, #000 80%); }
|
||||
|
||||
/* ---------- responsive ---------- */
|
||||
@media (max-width: 1100px) {
|
||||
.nav__links { display: none; }
|
||||
.nav .btn--sm { margin-left: auto; }
|
||||
.cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
body { font-size: 16px; }
|
||||
.hero { padding-top: 104px; }
|
||||
.hero__grid { grid-template-columns: minmax(0, 1fr); }
|
||||
.hero__stage { margin-top: 30px; }
|
||||
.hero .phone-host { width: min(300px, 70vw); }
|
||||
.sticker { font-size: 12px; }
|
||||
.sticker--a { left: 0; }
|
||||
.sticker--b { right: 0; }
|
||||
.spinner { width: 100px; height: 100px; left: 2%; }
|
||||
|
||||
.story__body { display: block; padding: 0; }
|
||||
.story__stage { position: sticky; top: 0; place-items: start center; padding-top: 70px; }
|
||||
.story .phone-host { height: 52vh; height: 52svh; }
|
||||
.story__num { top: 32%; font-size: 58vw; }
|
||||
.story__rail { right: 14px; top: 32%; gap: 6px; }
|
||||
.story__rail li { width: 8px; height: 20px; }
|
||||
.story__rail li.on { height: 32px; }
|
||||
.story__steps { position: relative; z-index: 2; margin-top: -100vh; margin-top: -100svh; padding: 0 var(--pad) 50svh; }
|
||||
.step { align-items: flex-end; padding: 0 0 4svh; }
|
||||
.step__card { opacity: 0; background: rgba(7, 7, 13, .94); border: var(--b) solid var(--fg); box-shadow: 6px 6px 0 var(--violet); padding: 18px 18px 20px; width: 100%; }
|
||||
.step__title { margin: 10px 0 10px; font-size: clamp(24px, 7vw, 34px); }
|
||||
.step__text { font-size: 15.5px; line-height: 1.45; }
|
||||
.step__gesture { display: none; }
|
||||
|
||||
.cards { grid-template-columns: minmax(0, 1fr); }
|
||||
.card { min-height: 0; }
|
||||
.stats { grid-template-columns: 1fr 1fr; row-gap: 40px; }
|
||||
.privacy__list { grid-template-columns: 1fr; }
|
||||
.pitem + .pitem { padding-left: 0; border-left: 0; }
|
||||
|
||||
.play__grid { grid-template-columns: minmax(0, 1fr); }
|
||||
.play .phone-host { height: 72svh; }
|
||||
.play__side { order: -1; }
|
||||
.play__caption { min-height: 2.4em; margin-bottom: 18px; }
|
||||
.howto { display: none; }
|
||||
|
||||
.get__grid { grid-template-columns: minmax(0, 1fr); }
|
||||
.qr { justify-self: start; }
|
||||
.foot__row .textlink { margin-left: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.cta .btn { width: 100%; justify-content: space-between; }
|
||||
.btn--huge { width: 100%; justify-content: space-between; font-size: 18px; }
|
||||
.marquee--pink { transform: rotate(-2.5deg) scale(1.06); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
*, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
|
||||
.line__in, .rv { transform: none !important; opacity: 1 !important; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- "Hide app" in an app's long-press menu: an eye with a line through it. Drawn as strokes;
|
||||
the menu tints it like its other icons. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- The eye's outline, open on the side the line crosses first. -->
|
||||
<path
|
||||
android:pathData="M9.9,5.2C10.6,5.07 11.3,5 12,5C16.2,5 19.7,7.6 21.5,12C20.9,13.4 20.1,14.6 19.1,15.6M16.4,17.6C15.1,18.5 13.6,19 12,19C7.8,19 4.3,16.4 2.5,12C3.3,10 4.5,8.4 6,7.2"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="1.8"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<!-- What the line leaves of the pupil. -->
|
||||
<path
|
||||
android:pathData="M14.1,14.1C13.56,14.66 12.82,15 12,15C10.34,15 9,13.66 9,12C9,11.18 9.34,10.44 9.9,9.9"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="1.8"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<!-- The line. -->
|
||||
<path
|
||||
android:pathData="M4,4L20,20"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="1.8"
|
||||
android:strokeLineCap="round" />
|
||||
</vector>
|
||||
@@ -93,6 +93,11 @@
|
||||
<string name="flexera_scrub_from_home_description">Slide up or down along the alphabet\'s edge of the home screen to open the app list at a letter. Swipe up and hold does the same from anywhere.</string>
|
||||
|
||||
<!-- Quick launch: a swipe on past the first or the last home screen page (app.flexera.home.QuickLaunch) -->
|
||||
<string name="flexera_hide_app">Hide app</string>
|
||||
<!-- Shown after an app was hidden from the app list. %1$s is the app's name -->
|
||||
<string name="flexera_app_hidden">%1$s is hidden</string>
|
||||
<string name="flexera_folder_blur">Blur behind open folders</string>
|
||||
<string name="flexera_folder_blur_description">Blurs the home screen while a folder is open</string>
|
||||
<string name="flexera_quick_launch">Quick launch</string>
|
||||
<string name="flexera_quick_launch_description">Swipe on from the first or the last home screen page, as if there were one more, to open an app or do something else. Has no effect while Infinite scrolling is on.</string>
|
||||
<string name="flexera_quick_launch_first_page">Swipe past the first page</string>
|
||||
@@ -153,34 +158,45 @@
|
||||
|
||||
<!-- Updates section of the About screen (app.flexera.update.FlexeraUpdater) -->
|
||||
<string name="flexera_update_heading">Updates</string>
|
||||
<!-- Button -->
|
||||
<string name="flexera_update_check">Check for updates</string>
|
||||
<string name="flexera_update_check_idle">Flexera Launcher also checks on its own, once a day</string>
|
||||
<!-- Shown on the button while the check runs -->
|
||||
<string name="flexera_update_checking">Checking for updates…</string>
|
||||
<string name="flexera_update_up_to_date">You have the latest version</string>
|
||||
<string name="flexera_update_failed_title">The check for updates didn\'t work</string>
|
||||
<!-- Button -->
|
||||
<string name="flexera_update_try_again">Try again</string>
|
||||
<!-- Button that asks Android for local network access -->
|
||||
<string name="flexera_update_allow_local_network">Allow access</string>
|
||||
<!-- Why a check for updates failed. %1$s is the update server's name, such as git.example.org -->
|
||||
<string name="flexera_update_failed_no_permission">Android does not let Flexera Launcher use the network on this phone. Allow it in the phone\'s settings for this app, under Permissions, Network, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_not_found">This phone could not find %1$s. Check the connection and any VPN or private DNS, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_local_network">%1$s is on the same network as this phone, and Android lets an app reach devices there only with your permission, which it calls Nearby devices. Tap to allow it. The check then runs again.</string>
|
||||
<string name="flexera_update_failed_no_answer">%1$s did not answer. Check the connection and any VPN, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_not_secure">A secure connection to %1$s could not be made. Check the phone\'s date and time and any VPN, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_server_error">%1$s answered with an error. Tap to try again later.</string>
|
||||
<string name="flexera_update_failed_bad_answer">The answer from %1$s could not be read. Tap to try again later.</string>
|
||||
<string name="flexera_update_failed_no_permission">Android does not let Flexera Launcher use the network on this phone. Allow it in the phone\'s settings for this app, under Permissions, Network, then try again.</string>
|
||||
<string name="flexera_update_failed_not_found">This phone could not find %1$s. Check the connection and any VPN or private DNS, then try again.</string>
|
||||
<string name="flexera_update_failed_local_network">%1$s is on the same network as this phone, and Android lets an app reach devices there only with your permission, which it calls Nearby devices. Once you allow it, the check runs again.</string>
|
||||
<string name="flexera_update_failed_no_answer">%1$s did not answer. Check the connection and any VPN, then try again.</string>
|
||||
<string name="flexera_update_failed_not_secure">A secure connection to %1$s could not be made. Check the phone\'s date and time and any VPN, then try again.</string>
|
||||
<string name="flexera_update_failed_server_error">%1$s answered with an error. Try again later.</string>
|
||||
<string name="flexera_update_failed_bad_answer">The answer from %1$s could not be read. Try again later.</string>
|
||||
<!-- %1$s is the error in the system\'s own words, for whoever looks for the cause -->
|
||||
<string name="flexera_update_failed_detail">Details: %1$s</string>
|
||||
<!-- %1$s is a version number such as 0.2.0 -->
|
||||
<string name="flexera_update_download">Download version %1$s</string>
|
||||
<string name="flexera_update_available">Version %1$s is available</string>
|
||||
<!-- Button. The download is followed by the installation without another tap -->
|
||||
<string name="flexera_update_download">Download and install</string>
|
||||
<!-- %1$s is a file size such as 24 MB -->
|
||||
<string name="flexera_update_download_size">A newer version is available (%1$s)</string>
|
||||
<string name="flexera_update_download_no_size">A newer version is available</string>
|
||||
<string name="flexera_update_download_failed">The download didn\'t finish. Tap to try again.</string>
|
||||
<string name="flexera_update_wrong_file">The downloaded file isn\'t a newer Flexera Launcher, so it was removed. Tap to try again.</string>
|
||||
<string name="flexera_update_download_size">%1$s download. The installation starts as soon as it\'s done.</string>
|
||||
<string name="flexera_update_download_no_size">The installation starts as soon as the download is done.</string>
|
||||
<string name="flexera_update_download_failed">The download didn\'t finish.</string>
|
||||
<string name="flexera_update_wrong_file">The downloaded file isn\'t a newer Flexera Launcher, so it was removed.</string>
|
||||
<!-- %1$s is a version number such as 0.2.0 -->
|
||||
<string name="flexera_update_downloading">Downloading version %1$s</string>
|
||||
<!-- %1$d is a percentage from 0 to 100 -->
|
||||
<string name="flexera_update_progress">%1$d%%</string>
|
||||
<string name="flexera_update_progress">%1$d%%. The installation starts as soon as it\'s done.</string>
|
||||
<!-- %1$s is a version number such as 0.2.0 -->
|
||||
<string name="flexera_update_install">Install version %1$s</string>
|
||||
<string name="flexera_update_install_hint">Downloaded and ready. If Android asks, allow Flexera Launcher to install apps, then tap here again.</string>
|
||||
<string name="flexera_update_ready">Version %1$s is ready to install</string>
|
||||
<!-- Button -->
|
||||
<string name="flexera_update_install">Install now</string>
|
||||
<string name="flexera_update_install_hint">If Android asks, allow Flexera Launcher to install apps. The installation then carries on by itself.</string>
|
||||
<string name="flexera_update_whats_new">What\'s new</string>
|
||||
<string name="flexera_update_notify">Notify me about new versions</string>
|
||||
<string name="flexera_update_notify_hint">Notifications are off, so a new version shows up only on this screen</string>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package app.flexera.drawer
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.lawnchair.LawnchairLauncher
|
||||
import app.lawnchair.preferences2.PreferenceManager2
|
||||
import app.lawnchair.preferences2.firstCached
|
||||
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS
|
||||
import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
|
||||
import com.android.launcher3.R
|
||||
import com.android.launcher3.model.data.ItemInfo
|
||||
import com.android.launcher3.popup.SystemShortcut
|
||||
import com.android.launcher3.util.ComponentKey
|
||||
import com.android.launcher3.views.Snackbar
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* "Hide app" in the menu a long press on an app opens in the app list.
|
||||
*
|
||||
* It puts the app on the hidden list Lawnchair already has: the one the app list and its search
|
||||
* leave out, kept under Settings > App drawer > Hidden apps, where an app is brought back. Before
|
||||
* this, hiding meant going through that settings page or through Customize. A message with Undo
|
||||
* follows, because the app is gone from the list the moment it is hidden.
|
||||
*/
|
||||
object HideApp {
|
||||
|
||||
/**
|
||||
* Offered for apps in the app list only: hiding does nothing to an icon on the home screen,
|
||||
* so offering it there would promise something else. Not offered while the home screen is
|
||||
* locked against changes, like Customize and Uninstall.
|
||||
*/
|
||||
@JvmField
|
||||
val SHORTCUT = SystemShortcut.Factory { launcher: LawnchairLauncher, itemInfo: ItemInfo, originalView: View ->
|
||||
val component = itemInfo.targetComponent
|
||||
when {
|
||||
itemInfo.container != CONTAINER_ALL_APPS -> null
|
||||
itemInfo.itemType != ITEM_TYPE_APPLICATION || component == null -> null
|
||||
PreferenceManager2.getInstance(launcher).lockHomeScreen.firstCached() -> null
|
||||
else -> Shortcut(launcher, itemInfo, originalView, ComponentKey(component, itemInfo.user))
|
||||
}
|
||||
}
|
||||
|
||||
private class Shortcut(
|
||||
launcher: LawnchairLauncher,
|
||||
itemInfo: ItemInfo,
|
||||
originalView: View,
|
||||
private val app: ComponentKey,
|
||||
) : SystemShortcut<LawnchairLauncher>(
|
||||
R.drawable.ic_flexera_hide,
|
||||
R.string.flexera_hide_app,
|
||||
launcher,
|
||||
itemInfo,
|
||||
originalView,
|
||||
) {
|
||||
override fun onClick(view: View) {
|
||||
val launcher = mTarget
|
||||
val name = mItemInfo.title?.toString().orEmpty()
|
||||
dismissTaskMenuView()
|
||||
setHidden(launcher, app, hidden = true)
|
||||
Snackbar.show(
|
||||
launcher,
|
||||
launcher.getString(R.string.flexera_app_hidden, name),
|
||||
R.string.undo,
|
||||
null,
|
||||
) { setHidden(launcher, app, hidden = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setHidden(launcher: LawnchairLauncher, app: ComponentKey, hidden: Boolean) {
|
||||
val hiddenApps = PreferenceManager2.getInstance(launcher).hiddenApps
|
||||
val key = app.toString()
|
||||
launcher.lifecycleScope.launch {
|
||||
val now = hiddenApps.get().first()
|
||||
hiddenApps.set(if (hidden) now + key else now - key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package app.flexera.home
|
||||
|
||||
import android.content.Context
|
||||
import app.lawnchair.preferences2.PreferenceManager2
|
||||
import app.lawnchair.preferences2.firstCached
|
||||
import com.android.launcher3.Utilities
|
||||
|
||||
/**
|
||||
* The blur of the home screen behind an open folder: whether the owner wants it, and how strong
|
||||
* it is. The blur itself is the depth controller's work
|
||||
* ([com.android.quickstep.util.BaseDepthControllerImpl.folderBlur]), because that already blurs
|
||||
* the same views and the wallpaper for other reasons and would undo anything done past it. The
|
||||
* folder's opening and closing animation moves it
|
||||
* ([com.android.launcher3.folder.FolderSpringAnimatorSet]).
|
||||
*/
|
||||
object FolderBlur {
|
||||
|
||||
/**
|
||||
* The blur's radius behind a fully open folder, as a share of the largest radius the
|
||||
* launcher blurs with (30dp). Strong enough that the icons behind the folder are no longer
|
||||
* read, which is the point: the folder is what is in focus.
|
||||
*/
|
||||
const val STRENGTH = 0.8f
|
||||
|
||||
/** Blurring a view needs Android 12. */
|
||||
@JvmStatic
|
||||
fun isOn(context: Context): Boolean =
|
||||
Utilities.ATLEAST_S && PreferenceManager2.getInstance(context).folderBlur.firstCached()
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -17,13 +18,24 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Download
|
||||
import androidx.compose.material.icons.rounded.InstallMobile
|
||||
import androidx.compose.material.icons.rounded.Refresh
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -39,6 +51,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
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
|
||||
@@ -114,7 +127,7 @@ fun FlexeraAbout(modifier: Modifier = Modifier) {
|
||||
|
||||
val updateRows = buildList<@Composable () -> Unit> {
|
||||
if (FlexeraUpdater.isSupported) {
|
||||
add { UpdateStatusRow(updateState) }
|
||||
add { UpdateCard(updateState) }
|
||||
val notes = updateState.release()?.notes.orEmpty()
|
||||
if (notes.isNotBlank()) {
|
||||
add {
|
||||
@@ -247,12 +260,30 @@ fun FlexeraAbout(modifier: Modifier = Modifier) {
|
||||
}
|
||||
}
|
||||
|
||||
/** What the update card shows; every part may be missing. */
|
||||
private class UpdateCardContent(
|
||||
val title: String? = null,
|
||||
val body: String? = null,
|
||||
/** From 0 to 1, while a download runs. */
|
||||
val progress: Float? = null,
|
||||
val action: UpdateAction? = null,
|
||||
)
|
||||
|
||||
/** The button at the bottom of the update card. */
|
||||
private class UpdateAction(
|
||||
val label: String,
|
||||
val icon: ImageVector?,
|
||||
val onClick: () -> Unit,
|
||||
/** Shown as a disabled button with a spinner while something is already under way. */
|
||||
val busy: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* The row that says where the updater stands and, when there is something to do, does it on tap:
|
||||
* check, download, or install.
|
||||
* The card that says where the updater stands and offers the next step as a large button: check,
|
||||
* download (which goes on to the installation by itself), or install.
|
||||
*/
|
||||
@Composable
|
||||
private fun UpdateStatusRow(state: UpdateState) {
|
||||
private fun UpdateCard(state: UpdateState) {
|
||||
val context = LocalContext.current
|
||||
// Android 17 asks the owner before an app may reach devices on the local network, and an
|
||||
// update server at home is one. The answer starts the check again; a "no" that Android no
|
||||
@@ -269,79 +300,170 @@ private fun UpdateStatusRow(state: UpdateState) {
|
||||
openAppSettings(context)
|
||||
}
|
||||
}
|
||||
when (state) {
|
||||
UpdateState.Idle -> ClickablePreference(
|
||||
label = stringResource(id = R.string.flexera_update_check),
|
||||
subtitle = stringResource(id = R.string.flexera_update_check_idle),
|
||||
onClick = { FlexeraUpdater.check(context) },
|
||||
|
||||
val check = UpdateAction(
|
||||
label = stringResource(id = R.string.flexera_update_check),
|
||||
icon = Icons.Rounded.Refresh,
|
||||
onClick = { FlexeraUpdater.check(context) },
|
||||
)
|
||||
val download = UpdateAction(
|
||||
label = stringResource(id = R.string.flexera_update_download),
|
||||
icon = Icons.Rounded.Download,
|
||||
onClick = { FlexeraUpdater.download(context) },
|
||||
)
|
||||
|
||||
val content = when (state) {
|
||||
UpdateState.Idle -> UpdateCardContent(
|
||||
body = stringResource(id = R.string.flexera_update_check_idle),
|
||||
action = check,
|
||||
)
|
||||
|
||||
UpdateState.Checking -> PreferenceTemplate(
|
||||
title = { Text(text = stringResource(id = R.string.flexera_update_checking)) },
|
||||
UpdateState.Checking -> UpdateCardContent(
|
||||
body = stringResource(id = R.string.flexera_update_check_idle),
|
||||
action = UpdateAction(
|
||||
label = stringResource(id = R.string.flexera_update_checking),
|
||||
icon = null,
|
||||
onClick = {},
|
||||
busy = true,
|
||||
),
|
||||
)
|
||||
|
||||
UpdateState.UpToDate -> ClickablePreference(
|
||||
label = stringResource(id = R.string.flexera_update_check),
|
||||
subtitle = stringResource(id = R.string.flexera_update_up_to_date),
|
||||
onClick = { FlexeraUpdater.check(context) },
|
||||
UpdateState.UpToDate -> UpdateCardContent(
|
||||
title = stringResource(id = R.string.flexera_update_up_to_date),
|
||||
body = stringResource(id = R.string.flexera_update_check_idle),
|
||||
action = check,
|
||||
)
|
||||
|
||||
is UpdateState.Available -> ClickablePreference(
|
||||
label = stringResource(id = R.string.flexera_update_download, state.release.versionName),
|
||||
subtitle = downloadSizeText(context, state.release),
|
||||
onClick = { FlexeraUpdater.download(context) },
|
||||
is UpdateState.Available -> UpdateCardContent(
|
||||
title = stringResource(id = R.string.flexera_update_available, state.release.versionName),
|
||||
body = downloadSizeText(context, state.release),
|
||||
action = download,
|
||||
)
|
||||
|
||||
is UpdateState.Downloading -> PreferenceTemplate(
|
||||
title = { Text(text = stringResource(id = R.string.flexera_update_downloading, state.release.versionName)) },
|
||||
description = {
|
||||
Column {
|
||||
Text(text = stringResource(id = R.string.flexera_update_progress, (state.progress * 100).toInt()))
|
||||
LinearProgressIndicator(
|
||||
progress = { state.progress },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
is UpdateState.Downloading -> UpdateCardContent(
|
||||
title = stringResource(id = R.string.flexera_update_downloading, state.release.versionName),
|
||||
body = stringResource(id = R.string.flexera_update_progress, (state.progress * 100).toInt()),
|
||||
progress = state.progress,
|
||||
)
|
||||
|
||||
is UpdateState.ReadyToInstall -> ClickablePreference(
|
||||
label = stringResource(id = R.string.flexera_update_install, state.release.versionName),
|
||||
subtitle = stringResource(id = R.string.flexera_update_install_hint),
|
||||
onClick = { FlexeraUpdater.install(context) },
|
||||
is UpdateState.ReadyToInstall -> UpdateCardContent(
|
||||
title = stringResource(id = R.string.flexera_update_ready, state.release.versionName),
|
||||
body = stringResource(id = R.string.flexera_update_install_hint),
|
||||
action = UpdateAction(
|
||||
label = stringResource(id = R.string.flexera_update_install),
|
||||
icon = Icons.Rounded.InstallMobile,
|
||||
onClick = { FlexeraUpdater.install(context) },
|
||||
),
|
||||
)
|
||||
|
||||
is UpdateState.Failed -> {
|
||||
val release = state.release
|
||||
if (release == null) {
|
||||
ClickablePreference(
|
||||
label = stringResource(id = R.string.flexera_update_check),
|
||||
subtitle = checkFailureText(state),
|
||||
onClick = {
|
||||
if (state.problem == CheckProblem.LOCAL_NETWORK) {
|
||||
askForLocalNetwork.launch(FlexeraUpdater.LOCAL_NETWORK_PERMISSION)
|
||||
} else {
|
||||
FlexeraUpdater.check(context)
|
||||
}
|
||||
UpdateCardContent(
|
||||
title = stringResource(id = R.string.flexera_update_failed_title),
|
||||
body = checkFailureText(state),
|
||||
action = if (state.problem == CheckProblem.LOCAL_NETWORK) {
|
||||
UpdateAction(
|
||||
label = stringResource(id = R.string.flexera_update_allow_local_network),
|
||||
icon = null,
|
||||
onClick = { askForLocalNetwork.launch(FlexeraUpdater.LOCAL_NETWORK_PERMISSION) },
|
||||
)
|
||||
} else {
|
||||
UpdateAction(
|
||||
label = stringResource(id = R.string.flexera_update_try_again),
|
||||
icon = Icons.Rounded.Refresh,
|
||||
onClick = { FlexeraUpdater.check(context) },
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
ClickablePreference(
|
||||
label = stringResource(id = R.string.flexera_update_download, release.versionName),
|
||||
subtitle = stringResource(
|
||||
UpdateCardContent(
|
||||
title = stringResource(id = R.string.flexera_update_available, release.versionName),
|
||||
body = stringResource(
|
||||
id = if (state.failure == UpdateFailure.WRONG_FILE) {
|
||||
R.string.flexera_update_wrong_file
|
||||
} else {
|
||||
R.string.flexera_update_download_failed
|
||||
},
|
||||
),
|
||||
onClick = { FlexeraUpdater.download(context) },
|
||||
action = UpdateAction(
|
||||
label = stringResource(id = R.string.flexera_update_try_again),
|
||||
icon = Icons.Rounded.Download,
|
||||
onClick = download.onClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val title = content.title
|
||||
val body = content.body
|
||||
val progress = content.progress
|
||||
val action = content.action
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
if (title != null) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
if (body != null) {
|
||||
if (title != null) Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = body,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
if (progress != null) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = { progress },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
if (action != null) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
UpdateButton(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A full-width filled button, so the next step stands out from the settings rows around it. */
|
||||
@Composable
|
||||
private fun UpdateButton(action: UpdateAction) {
|
||||
val icon = action.icon
|
||||
Button(
|
||||
onClick = action.onClick,
|
||||
enabled = !action.busy,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 56.dp),
|
||||
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||
) {
|
||||
if (action.busy) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(ButtonDefaults.IconSize),
|
||||
strokeWidth = 2.dp,
|
||||
color = LocalContentColor.current,
|
||||
)
|
||||
Spacer(Modifier.width(ButtonDefaults.IconSpacing))
|
||||
} else if (icon != null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(ButtonDefaults.IconSize),
|
||||
)
|
||||
Spacer(Modifier.width(ButtonDefaults.IconSpacing))
|
||||
}
|
||||
Text(text = action.label, style = MaterialTheme.typography.titleSmall)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,8 @@ import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.core.content.FileProvider
|
||||
@@ -24,6 +26,7 @@ import com.android.launcher3.BuildConfig
|
||||
import com.android.launcher3.R
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
@@ -167,6 +170,22 @@ object FlexeraUpdater {
|
||||
private const val KEY_LAST_ATTEMPT_OK = "last_attempt_ok"
|
||||
private const val KEY_NOTIFIED_VERSION = "notified_version"
|
||||
|
||||
/**
|
||||
* When the owner last asked for the update to be installed: by tapping Download, or Install
|
||||
* while Android still had to be told to let this app install apps. Kept on disk, because
|
||||
* Android may end the launcher's process while the owner is in that setting.
|
||||
*/
|
||||
private const val KEY_INSTALL_REQUESTED_AT = "install_requested_at"
|
||||
|
||||
/** True while the owner was sent to Android's "Install unknown apps" setting. */
|
||||
private const val KEY_AWAITING_INSTALL_PERMISSION = "awaiting_install_permission"
|
||||
|
||||
/**
|
||||
* How long after such a request the installer may still open by itself. An installer that
|
||||
* pops up hours after the owner tapped Download would come as a surprise.
|
||||
*/
|
||||
private const val AUTO_INSTALL_WINDOW_MS = 30L * 60 * 1000
|
||||
|
||||
private const val UPDATES_DIR = "updates"
|
||||
private const val APK_PREFIX = "FlexeraLauncher-"
|
||||
private const val CHANNEL_ID = "flexera_updates"
|
||||
@@ -189,17 +208,39 @@ object FlexeraUpdater {
|
||||
private var nextAutomaticCheckAt = 0L
|
||||
|
||||
/**
|
||||
* Starts the once-a-day automatic check. Called once, from the application's onCreate.
|
||||
* The activity of this app that is in front (home screen or settings), if one is. The
|
||||
* installer opens from it; Android does not let an app in the background open one.
|
||||
*/
|
||||
@Volatile
|
||||
private var frontActivity: WeakReference<Activity>? = null
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
/**
|
||||
* Starts the once-a-day automatic check, and lets a requested install go ahead whenever one
|
||||
* of the app's screens comes to the front. Called once, from the application's onCreate.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun start(app: Application) {
|
||||
if (!isSupported) return
|
||||
scope.launch { restoreDownload(app) }
|
||||
scope.launch {
|
||||
restoreDownload(app)
|
||||
// The process may have been restarted while the owner was allowing installs.
|
||||
installIfRequested()
|
||||
}
|
||||
app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
|
||||
override fun onActivityResumed(activity: Activity) = checkIfDue(activity.applicationContext)
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
frontActivity = WeakReference(activity)
|
||||
checkIfDue(activity.applicationContext)
|
||||
installIfRequested()
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
if (frontActivity?.get() === activity) frontActivity = null
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
|
||||
override fun onActivityStarted(activity: Activity) = Unit
|
||||
override fun onActivityPaused(activity: Activity) = Unit
|
||||
override fun onActivityStopped(activity: Activity) = Unit
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
||||
override fun onActivityDestroyed(activity: Activity) = Unit
|
||||
@@ -271,7 +312,10 @@ object FlexeraUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
/** Downloads the APK of the release that the last check found. */
|
||||
/**
|
||||
* Downloads the APK of the release that the last check found, then hands it to Android's
|
||||
* installer straight away (see [installIfRequested]).
|
||||
*/
|
||||
@JvmStatic
|
||||
fun download(context: Context) {
|
||||
val release = when (val current = mutableState.value) {
|
||||
@@ -280,6 +324,7 @@ object FlexeraUpdater {
|
||||
else -> null
|
||||
} ?: return
|
||||
val app = context.applicationContext
|
||||
requestInstall(app, awaitingPermission = false)
|
||||
mutableState.value = UpdateState.Downloading(release, 0f)
|
||||
|
||||
scope.launch {
|
||||
@@ -290,14 +335,17 @@ object FlexeraUpdater {
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "Update download failed", error)
|
||||
clearInstallRequest(app)
|
||||
mutableState.value = UpdateState.Failed(UpdateFailure.DOWNLOAD, release)
|
||||
return@launch
|
||||
}
|
||||
if (isValidUpdate(app, target)) {
|
||||
mutableState.value = UpdateState.ReadyToInstall(release, target)
|
||||
installIfRequested()
|
||||
} else {
|
||||
Log.w(TAG, "Downloaded file is not a newer ${app.packageName}")
|
||||
target.delete()
|
||||
clearInstallRequest(app)
|
||||
mutableState.value = UpdateState.Failed(UpdateFailure.WRONG_FILE, release)
|
||||
}
|
||||
}
|
||||
@@ -306,18 +354,20 @@ object FlexeraUpdater {
|
||||
/**
|
||||
* Hands the downloaded APK to Android's installer, which asks the owner to confirm. The first
|
||||
* time, Android wants the owner to allow this app to install apps; this opens that setting
|
||||
* instead, and the owner taps Install again afterwards.
|
||||
* instead, and the installer follows by itself when the owner comes back having allowed it.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun install(context: Context) {
|
||||
val ready = mutableState.value as? UpdateState.ReadyToInstall ?: return
|
||||
if (!ready.file.exists()) {
|
||||
// Android cleared the cache in the meantime; find the release again.
|
||||
clearInstallRequest(context)
|
||||
mutableState.value = UpdateState.Idle
|
||||
check(context)
|
||||
return
|
||||
}
|
||||
if (!context.packageManager.canRequestPackageInstalls()) {
|
||||
requestInstall(context, awaitingPermission = true)
|
||||
val settings = Intent(
|
||||
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||
Uri.parse("package:${context.packageName}"),
|
||||
@@ -325,6 +375,7 @@ object FlexeraUpdater {
|
||||
context.startActivity(settings)
|
||||
return
|
||||
}
|
||||
clearInstallRequest(context)
|
||||
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.fileprovider", ready.file)
|
||||
val install = Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
@@ -332,6 +383,48 @@ object FlexeraUpdater {
|
||||
context.startActivity(install)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the installer when the owner asked for the update a short while ago, it is downloaded,
|
||||
* and one of the app's screens is in front to open it from. Runs again each time a screen
|
||||
* comes to the front, which is how the install carries on after the owner has allowed this
|
||||
* app to install apps. Coming back without having allowed it drops the request, so the owner
|
||||
* is not sent to that setting over and over; the Install button stays.
|
||||
*/
|
||||
private fun installIfRequested() {
|
||||
mainHandler.post {
|
||||
val activity = frontActivity?.get() ?: return@post
|
||||
if (mutableState.value !is UpdateState.ReadyToInstall) return@post
|
||||
val prefs = prefs(activity)
|
||||
val requestedAt = prefs.getLong(KEY_INSTALL_REQUESTED_AT, 0L)
|
||||
if (requestedAt == 0L) return@post
|
||||
if (System.currentTimeMillis() - requestedAt !in 0 until AUTO_INSTALL_WINDOW_MS) {
|
||||
clearInstallRequest(activity)
|
||||
return@post
|
||||
}
|
||||
if (prefs.getBoolean(KEY_AWAITING_INSTALL_PERMISSION, false) &&
|
||||
!activity.packageManager.canRequestPackageInstalls()
|
||||
) {
|
||||
clearInstallRequest(activity)
|
||||
return@post
|
||||
}
|
||||
install(activity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestInstall(context: Context, awaitingPermission: Boolean) {
|
||||
prefs(context).edit()
|
||||
.putLong(KEY_INSTALL_REQUESTED_AT, System.currentTimeMillis())
|
||||
.putBoolean(KEY_AWAITING_INSTALL_PERMISSION, awaitingPermission)
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun clearInstallRequest(context: Context) {
|
||||
prefs(context).edit()
|
||||
.remove(KEY_INSTALL_REQUESTED_AT)
|
||||
.remove(KEY_AWAITING_INSTALL_PERMISSION)
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks up an update that was downloaded before the launcher process was restarted, so the
|
||||
* About screen can offer "Install" straight away. Also clears files left over from updates
|
||||
@@ -603,6 +696,7 @@ object FlexeraUpdater {
|
||||
|
||||
private fun clearDownloads(context: Context) {
|
||||
updatesDir(context).listFiles { file -> file.name.startsWith(APK_PREFIX) }?.forEach { it.delete() }
|
||||
clearInstallRequest(context)
|
||||
}
|
||||
|
||||
private fun apkFile(context: Context, release: FlexeraRelease): File {
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
# build.gradle reads this and derives the Android version code from it
|
||||
# (major * 10000 + minor * 100 + patch), so the name and the code can never disagree.
|
||||
# flexera/release.ps1 raises it for every release.
|
||||
versionName=0.11.0
|
||||
versionName=0.14.0
|
||||
@@ -283,7 +283,8 @@ class LawnchairLauncher : QuickstepLauncher() {
|
||||
super.getSupportedShortcuts(container),
|
||||
Stream.concat(
|
||||
Stream.of(LawnchairShortcut.UNINSTALL, LawnchairShortcut.CUSTOMIZE, LawnchairShortcut.OPEN_IN_STORE),
|
||||
Stream.of(LawnchairShortcut.PAUSE_APPS),
|
||||
// Flexera Launcher: "Hide app", which its factory offers in the app list only.
|
||||
Stream.of(LawnchairShortcut.PAUSE_APPS, app.flexera.drawer.HideApp.SHORTCUT),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -886,6 +886,13 @@ class PreferenceManager2 @Inject constructor(
|
||||
defaultValue = GestureHandlerConfig.NoOp,
|
||||
)
|
||||
|
||||
// Flexera Launcher: whether the home screen behind an open folder is blurred
|
||||
// (app.flexera.home.FolderBlur). Read when a folder opens, so nothing has to be reloaded.
|
||||
val folderBlur = preference(
|
||||
key = booleanPreferencesKey(name = "flexera_folder_blur"),
|
||||
defaultValue = true,
|
||||
)
|
||||
|
||||
val autoUpdaterNightly = preference(
|
||||
key = booleanPreferencesKey(name = "enable_nightly_auto_updater"),
|
||||
defaultValue = if (BuildConfig.APPLICATION_ID.contains("nightly")) {
|
||||
@@ -912,10 +919,16 @@ class PreferenceManager2 @Inject constructor(
|
||||
.onEach { cachedPreferences = it }
|
||||
.launchIn(scope)
|
||||
|
||||
// Flexera Launcher: in the five collectors below, distinctUntilChanged comes before
|
||||
// drop(1). Upstream has them the other way round, which lets the first emission after
|
||||
// the dropped one through whatever its value: the first write of any preference after
|
||||
// the launcher started then reloaded the model, rebuilt the grid three times and
|
||||
// recreated the launcher. Hiding an app from the app list is such a write, made while
|
||||
// the owner looks at the launcher.
|
||||
initializeIconShape(iconShape.firstCached(this))
|
||||
iconShape.get()
|
||||
.drop(1)
|
||||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.onEach { shape ->
|
||||
initializeIconShape(shape)
|
||||
L3ThemeManager.INSTANCE.get(context)
|
||||
@@ -924,14 +937,14 @@ class PreferenceManager2 @Inject constructor(
|
||||
.launchIn(scope)
|
||||
|
||||
hotseatMode.get()
|
||||
.drop(1)
|
||||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.onEach { reloadHelper.restart() }
|
||||
.launchIn(scope)
|
||||
|
||||
isHotseatEnabled.get()
|
||||
.drop(1)
|
||||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.onEach {
|
||||
reloadHelper.recreate()
|
||||
reloadHelper.reloadGrid()
|
||||
@@ -939,14 +952,14 @@ class PreferenceManager2 @Inject constructor(
|
||||
.launchIn(scope)
|
||||
|
||||
hotseatQsbProvider.get()
|
||||
.drop(1)
|
||||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.onEach { reloadHelper.recreate() }
|
||||
.launchIn(scope)
|
||||
|
||||
enableLabelInDock.get()
|
||||
.drop(1)
|
||||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.onEach { reloadHelper.reloadGrid() }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import app.lawnchair.ui.preferences.components.layout.PreferenceGroup
|
||||
import app.lawnchair.ui.preferences.components.layout.PreferenceLayout
|
||||
import app.lawnchair.ui.preferences.navigation.GeneralIconShape
|
||||
import com.android.launcher3.R
|
||||
import com.android.launcher3.Utilities
|
||||
|
||||
@Composable
|
||||
fun FolderPreferences(
|
||||
@@ -75,6 +76,14 @@ fun FolderPreferences(
|
||||
valueRange = 0F..1F,
|
||||
showAsPercentage = true,
|
||||
)
|
||||
// Flexera Launcher: blurring a view needs Android 12.
|
||||
if (Utilities.ATLEAST_S) {
|
||||
SwitchPreference(
|
||||
adapter = prefs2.folderBlur.getAdapter(),
|
||||
label = stringResource(id = R.string.flexera_folder_blur),
|
||||
description = stringResource(id = R.string.flexera_folder_blur_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
PreferenceGroup(heading = stringResource(id = R.string.grid)) {
|
||||
SliderPreference(
|
||||
|
||||
@@ -32,6 +32,7 @@ import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.launcher3.AbstractFloatingView;
|
||||
import com.android.launcher3.BaseActivity;
|
||||
import com.android.launcher3.LauncherState;
|
||||
import com.android.launcher3.Utilities;
|
||||
@@ -220,6 +221,12 @@ public class DepthController extends BaseDepthControllerImpl implements StateHan
|
||||
* Clears a stuck workspace icon blur after resume when settled on home.
|
||||
*/
|
||||
public void clearStuckBlurOnResumeIfHome() {
|
||||
// Flexera Launcher: a folder blur with no folder on the screen, open or still closing,
|
||||
// is a stuck one too. It should not come about; this makes sure it could not stay.
|
||||
if (folderBlur.getValue() > 0f && AbstractFloatingView.getAnyView(
|
||||
mLauncher, AbstractFloatingView.TYPE_FOLDER) == null) {
|
||||
folderBlur.setValue(0f);
|
||||
}
|
||||
if (mLauncher.getStateManager().getState() == LauncherState.NORMAL
|
||||
&& !mLauncher.getStateManager().isInTransition()) {
|
||||
clearWorkspaceRenderEffects();
|
||||
@@ -227,10 +234,18 @@ public class DepthController extends BaseDepthControllerImpl implements StateHan
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isDepthEffectEnabled() {
|
||||
return mEnableDepth;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyDepthAndBlur() {
|
||||
try {
|
||||
if (LawnchairQuickstepCompat.ATLEAST_R && mEnableDepth) {
|
||||
// Flexera Launcher: the blur behind an open folder is a setting of its own, and
|
||||
// also comes where the wallpaper's zoom is switched off.
|
||||
if (LawnchairQuickstepCompat.ATLEAST_R
|
||||
&& (mEnableDepth || folderBlurNeedsApplying())) {
|
||||
ensureDependencies();
|
||||
super.applyDepthAndBlur();
|
||||
} else if (mLauncher.getStateManager().getState() == LauncherState.NORMAL
|
||||
|
||||
@@ -90,6 +90,11 @@ public class BaseDepthControllerImpl implements BaseDepthController {
|
||||
public final MultiProperty widgetDepth;
|
||||
/** Property to set just the wallpaper zoom for folder expansion. */
|
||||
public final MultiProperty folderZoom;
|
||||
/**
|
||||
* Flexera Launcher: property to set the blur behind an open folder, from 0 to 1. Unlike the
|
||||
* depth it comes with no zoom, and it reaches the icons as well as the wallpaper.
|
||||
*/
|
||||
public final MultiProperty folderBlur;
|
||||
|
||||
/**
|
||||
* Blur radius when completely zoomed out, in pixels.
|
||||
@@ -106,6 +111,10 @@ public class BaseDepthControllerImpl implements BaseDepthController {
|
||||
private float mDepth;
|
||||
// Used just for animating wallpaper zoom without affecting blur.
|
||||
private final AnimatedFloat mWallpaperZoomOnly = new AnimatedFloat(this::applyDepthAndBlur);
|
||||
// Flexera Launcher: the blur behind an open folder, see folderBlur.
|
||||
private final AnimatedFloat mFolderBlur = new AnimatedFloat(this::applyDepthAndBlur);
|
||||
// Whether a folder blur is on the launcher's surface, and so has to come off again.
|
||||
private boolean mFolderBlurApplied;
|
||||
|
||||
protected SurfaceControl mBaseSurface;
|
||||
|
||||
@@ -164,6 +173,9 @@ public class BaseDepthControllerImpl implements BaseDepthController {
|
||||
} else {
|
||||
folderZoom = null;
|
||||
}
|
||||
// Flexera Launcher
|
||||
folderBlur = new MultiPropertyFactory<>(mFolderBlur, AnimatedFloat.VALUE, 1, Float::max)
|
||||
.get(0);
|
||||
|
||||
// mEarlyWakeupInfo.token = new Binder();
|
||||
// mEarlyWakeupInfo.trace = BaseDepthControllerImpl.class.getName();
|
||||
@@ -184,6 +196,53 @@ public class BaseDepthControllerImpl implements BaseDepthController {
|
||||
return folderZoom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiProperty getFolderBlur() {
|
||||
return folderBlur;
|
||||
}
|
||||
|
||||
/** Flexera Launcher: the blur behind an open folder as it is now, from 0 to 1. */
|
||||
private float folderBlurAmount() {
|
||||
return Utilities.boundToRange(mFolderBlur.value, 0f, 1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flexera Launcher: true while a folder blur is wanted, and after that until it has been
|
||||
* taken off the launcher's surface again.
|
||||
*/
|
||||
protected boolean folderBlurNeedsApplying() {
|
||||
return folderBlurAmount() > 0f || mFolderBlurApplied;
|
||||
}
|
||||
|
||||
/** Flexera Launcher: false where the owner switched the wallpaper's zoom off. */
|
||||
protected boolean isDepthEffectEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flexera Launcher: blurs the icons and widgets behind an open folder. That works without
|
||||
* blur across windows as well, so also where the wallpaper has to stay sharp.
|
||||
*
|
||||
* @return {@code false} when no folder blur is wanted, and nothing was done
|
||||
*/
|
||||
private boolean blurFolderBackdrop() {
|
||||
float amount = folderBlurAmount();
|
||||
if (!Utilities.ATLEAST_S || amount <= 0f) {
|
||||
return false;
|
||||
}
|
||||
float radius = Math.max(mCurrentBlur,
|
||||
amount * app.flexera.home.FolderBlur.STRENGTH * mMaxBlurRadius);
|
||||
if (radius < 1f) {
|
||||
// Too little to see, and a blur of no radius is not one the system accepts.
|
||||
mLauncher.getDepthBlurTargets().forEach(target -> target.setRenderEffect(null));
|
||||
return true;
|
||||
}
|
||||
RenderEffect blurEffect = RenderEffect.createBlurEffect(
|
||||
radius, radius, Shader.TileMode.DECAL);
|
||||
mLauncher.getDepthBlurTargets().forEach(target -> target.setRenderEffect(blurEffect));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if cross window blurs are enabled. In other words, whether launcher should use blurs
|
||||
* style UI or fallback style UI.
|
||||
@@ -236,7 +295,9 @@ public class BaseDepthControllerImpl implements BaseDepthController {
|
||||
boolean applyImmediately, boolean skipSimilarBlur) {
|
||||
float depth = mDepth;
|
||||
float wallpaperZoom = depth;
|
||||
if (Flags.enableExpressiveFolderExpansion()) {
|
||||
// Flexera Launcher: with the zoom switched off this used to be reached by accident
|
||||
// only; the folder blur now brings it here on purpose, and must not bring the zoom along.
|
||||
if (Flags.enableExpressiveFolderExpansion() && isDepthEffectEnabled()) {
|
||||
wallpaperZoom = Math.max(wallpaperZoom, mWallpaperZoomOnly.value);
|
||||
}
|
||||
IBinder windowToken = mLauncher.getRootView().getWindowToken();
|
||||
@@ -261,6 +322,10 @@ public class BaseDepthControllerImpl implements BaseDepthController {
|
||||
} else {
|
||||
blurAmount = depth;
|
||||
}
|
||||
// Flexera Launcher: an open folder blurs what is behind it, whatever the depth is.
|
||||
float behindFolder = folderBlurAmount() * app.flexera.home.FolderBlur.STRENGTH;
|
||||
blurAmount = Math.max(blurAmount, behindFolder);
|
||||
mFolderBlurApplied = behindFolder > 0f;
|
||||
|
||||
int previousBlur = mCurrentBlur;
|
||||
int newBlur = BlurUtils.supportsBlursOnWindows() && mCrossWindowBlursEnabled
|
||||
@@ -378,6 +443,11 @@ public class BaseDepthControllerImpl implements BaseDepthController {
|
||||
if (!Utilities.ATLEAST_S) {
|
||||
return false;
|
||||
}
|
||||
// Flexera Launcher: an open folder blurs these views in every state, the home screen
|
||||
// at rest included, which the rules below would leave sharp.
|
||||
if (blurFolderBackdrop()) {
|
||||
return true;
|
||||
}
|
||||
StateManager<LauncherState, Launcher> stateManager = mLauncher.getStateManager();
|
||||
LauncherState settledState = stateManager.getState();
|
||||
|
||||
@@ -414,6 +484,10 @@ public class BaseDepthControllerImpl implements BaseDepthController {
|
||||
if (!Utilities.ATLEAST_S) {
|
||||
return;
|
||||
}
|
||||
// Flexera Launcher: the blur behind an open folder is not a stuck one, and stays.
|
||||
if (blurFolderBackdrop()) {
|
||||
return;
|
||||
}
|
||||
mLauncher.getDepthBlurTargets().forEach(target -> {
|
||||
target.setRenderEffect(null);
|
||||
target.invalidate();
|
||||
|
||||
@@ -185,8 +185,16 @@ public class Hotseat extends FrameLayout implements Insettable {
|
||||
// Create an initial page so alpha/translation channels have a target during construction.
|
||||
mPagedView.resetPages(false, null, mActivity.getDeviceProfile());
|
||||
|
||||
mIconsAlphaChannels = new MultiValueAlpha(mIconsContainer, ALPHA_CHANNEL_CHANNELS_COUNT);
|
||||
// Flexera Launcher: one more channel than upstream, which hides the icons of a dock
|
||||
// that is switched off. Such a dock has no height, but its icons were still drawn,
|
||||
// just below the screen edge, and the zoom-out behind an open folder brought their
|
||||
// tops into view.
|
||||
mIconsAlphaChannels = new MultiValueAlpha(mIconsContainer,
|
||||
ALPHA_CHANNEL_CHANNELS_COUNT + 1);
|
||||
mIconsAlphaChannels.setUpdateVisibility(true);
|
||||
if (!hotseatEnabled) {
|
||||
mIconsAlphaChannels.get(ALPHA_CHANNEL_CHANNELS_COUNT).setValue(0f);
|
||||
}
|
||||
if (mQsb instanceof Reorderable qsbReorderable) {
|
||||
mQsbTranslationX = qsbReorderable.getTranslateDelegate()
|
||||
.getTranslationX(MultiTranslateDelegate.INDEX_NAV_BAR_ANIM);
|
||||
|
||||
@@ -81,6 +81,8 @@ class FolderAnimationSpringBuilderManager(
|
||||
val launcher = launcherDelegate.launcher ?: return
|
||||
val scrim = launcher.scrimView
|
||||
launcherDelegate.launcher?.depthController?.folderZoom?.value = 0f
|
||||
// Flexera Launcher: the blur behind the folder goes with it.
|
||||
launcherDelegate.launcher?.depthController?.folderBlur?.value = 0f
|
||||
if (!launcher.isInState(LauncherState.ALL_APPS)) {
|
||||
scrim.alpha = 1f
|
||||
scrim.setBackgroundColor(Color.TRANSPARENT)
|
||||
|
||||
@@ -57,6 +57,8 @@ class FolderScrimAnimationListener(
|
||||
// Folder.closeComplete's restore path, leaving workspace/hotseat visually dimmed/scaled.
|
||||
SCALE_PROPERTY.set(launcher.workspace, 1f)
|
||||
SCALE_PROPERTY.set(launcher.hotseat, 1f)
|
||||
// Flexera Launcher: and the blur behind the folder, for the same reason.
|
||||
launcher.depthController?.folderBlur?.value = 0f
|
||||
if (Utilities.ATLEAST_S && launcher.stateManager.state.getDepth(launcher) == 0f) {
|
||||
launcher.depthBlurTargets.forEach { it.setRenderEffect(null) }
|
||||
}
|
||||
|
||||
@@ -91,6 +91,12 @@ class FolderSpringAnimatorSet(val animatorSet: AnimatorSet) {
|
||||
launcherDelegate.launcher?.depthController?.folderZoom?.let {
|
||||
addWallpaperZoomAnimator(folder.context, animatorSet, folderAnimData.isOpening, it)
|
||||
}
|
||||
addBackdropBlurAnimator(
|
||||
folder.context,
|
||||
animatorSet,
|
||||
folderAnimData.isOpening,
|
||||
launcherDelegate,
|
||||
)
|
||||
iconAnimData.forEach { addContentIconAnimators(folder.context, animatorSet, it) }
|
||||
return FolderSpringAnimatorSet(animatorSet)
|
||||
}
|
||||
@@ -258,6 +264,39 @@ class FolderSpringAnimatorSet(val animatorSet: AnimatorSet) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Flexera Launcher: blurs the home screen behind the folder as it opens, and clears it
|
||||
* again as it closes, in step with the dimming. Closing always plays, also when the
|
||||
* setting has been switched off since the folder was opened.
|
||||
*/
|
||||
private fun addBackdropBlurAnimator(
|
||||
context: Context,
|
||||
animatorSet: AnimatorSet,
|
||||
isOpening: Boolean,
|
||||
launcherDelegate: LauncherDelegate,
|
||||
) {
|
||||
val launcher = launcherDelegate.launcher ?: return
|
||||
// As with the dimming: a folder in the app list leaves the list as it is.
|
||||
if (launcher.isInState(LauncherState.ALL_APPS)) return
|
||||
val blur = launcher.depthController?.folderBlur ?: return
|
||||
val wanted =
|
||||
if (isOpening) app.flexera.home.FolderBlur.isOn(context) else blur.value > 0f
|
||||
if (!wanted) return
|
||||
playSpringAnimation(
|
||||
context = context,
|
||||
animatorSet = animatorSet,
|
||||
isOpening = isOpening,
|
||||
startDelay = 0,
|
||||
stiffness = STIFFNESS_LAUNCHER_SCRIM,
|
||||
damping = DAMPING_LAUNCHER_SCRIM,
|
||||
startValue = 0f,
|
||||
endValue = 1f,
|
||||
minVisibleChange = MIN_VISIBLE_CHANGE_ALPHA,
|
||||
property = MULTI_PROPERTY_VALUE,
|
||||
view = blur,
|
||||
)
|
||||
}
|
||||
|
||||
private fun addAlphaAndColorAnimators(
|
||||
folder: Folder,
|
||||
animatorSet: AnimatorSet,
|
||||
|
||||
@@ -40,4 +40,10 @@ interface BaseDepthController {
|
||||
* Property for folder expansion wallpaper zoom.
|
||||
*/
|
||||
val folderZoom: MultiPropertyFactory<*>.MultiProperty?
|
||||
|
||||
/**
|
||||
* Flexera Launcher: property for the blur of the home screen behind an open folder, from 0
|
||||
* (none) to 1 (as strong as it gets).
|
||||
*/
|
||||
val folderBlur: MultiPropertyFactory<*>.MultiProperty?
|
||||
}
|
||||