Compare commits
267 Commits
v0.3.14-be
...
v0.3.14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c6a719aa5 | ||
|
|
693051b699 | ||
|
|
91865a0e14 | ||
|
|
ebd45375f6 | ||
|
|
1c9f24c533 | ||
|
|
d6daa87ce6 | ||
|
|
99eaee1477 | ||
|
|
47824a8e5f | ||
|
|
83f45f9c5d | ||
|
|
5e36f57e82 | ||
|
|
ec65d9e6f8 | ||
|
|
e3ede1160e | ||
|
|
a72d570065 | ||
|
|
db2227cc04 | ||
|
|
1a77f94af9 | ||
|
|
5bba116e01 | ||
|
|
5c177cc225 | ||
|
|
1d0826f854 | ||
|
|
85fdd3876a | ||
|
|
59def39114 | ||
|
|
463644c1d2 | ||
|
|
117654c95c | ||
|
|
21e97d6933 | ||
|
|
36d9f91009 | ||
|
|
07c15f0782 | ||
|
|
97d3223122 | ||
|
|
2e12d756df | ||
|
|
1ca1763138 | ||
|
|
93f6ba3f69 | ||
|
|
83fd9a27a8 | ||
|
|
4c8587fa1e | ||
|
|
92f4ef12d9 | ||
|
|
db401e0b69 | ||
|
|
ca5706b638 | ||
|
|
63d132ad3c | ||
|
|
97456645e4 | ||
|
|
81f949a1ee | ||
|
|
438ca8c7ad | ||
|
|
51bd2cc5cd | ||
|
|
864ee749ca | ||
|
|
ba40307f0f | ||
|
|
ae091bdae8 | ||
|
|
e98b12f3c5 | ||
|
|
ae49342ae7 | ||
|
|
85ad815ae7 | ||
|
|
948e9bb75f | ||
|
|
898dc25cb6 | ||
|
|
33f2336676 | ||
|
|
e64ab1e75d | ||
|
|
d46e95ceac | ||
|
|
dd25420e03 | ||
|
|
b29519a784 | ||
|
|
2d62398186 | ||
|
|
573e55f1c4 | ||
|
|
523eabee17 | ||
|
|
913872e4cd | ||
|
|
c12a802e5e | ||
|
|
3129617402 | ||
|
|
dc2d130b13 | ||
|
|
e9b140a9fe | ||
|
|
4189955554 | ||
|
|
51f0843a2f | ||
|
|
2496ada14b | ||
|
|
544e3857fc | ||
|
|
f3d076b51e | ||
|
|
40efbd0f65 | ||
|
|
8d667297c2 | ||
|
|
dbb3f97abd | ||
|
|
627919e34f | ||
|
|
9f67789337 | ||
|
|
96d830e5c4 | ||
|
|
80ca97388c | ||
|
|
afa021f67d | ||
|
|
e14f54cac1 | ||
|
|
21c1915233 | ||
|
|
5af80a2270 | ||
|
|
8a1a3d3bb4 | ||
|
|
a2f15606c7 | ||
|
|
01e2ee7835 | ||
|
|
7adc045752 | ||
|
|
266af61e3a | ||
|
|
a814190012 | ||
|
|
109c323369 | ||
|
|
654e160503 | ||
|
|
3aa6d7ccbe | ||
|
|
5d447d7a8f | ||
|
|
25bfd61814 | ||
|
|
b4caa66377 | ||
|
|
e5dde63efc | ||
|
|
78f64adbbf | ||
|
|
3f39bc8768 | ||
|
|
d2274c4d9e | ||
|
|
8674a04a5c | ||
|
|
2f14529902 | ||
|
|
1d74a17b98 | ||
|
|
52435d9837 | ||
|
|
b6fbbe5a91 | ||
|
|
3f85e1167c | ||
|
|
9c05096184 | ||
|
|
ef3bc015b0 | ||
|
|
75fd600448 | ||
|
|
2f01e7770f | ||
|
|
12b6edf872 | ||
|
|
6053f2d16b | ||
|
|
636c5f4df4 | ||
|
|
bb0bd478cf | ||
|
|
79eb080811 | ||
|
|
b5b82836bc | ||
|
|
cef0f2b53d | ||
|
|
dbf031469f | ||
|
|
5b87c933da | ||
|
|
adc4b9a372 | ||
|
|
0ff8f7776e | ||
|
|
c04fdeb491 | ||
|
|
295d8e5326 | ||
|
|
b032ac64f7 | ||
|
|
8ebe99d2c9 | ||
|
|
f0b027557b | ||
|
|
462030bcd7 | ||
|
|
888af9d28d | ||
|
|
ea159527f3 | ||
|
|
0dc0f53a91 | ||
|
|
d5aac7ac14 | ||
|
|
9f58088545 | ||
|
|
b684f1759d | ||
|
|
aa7a264d6c | ||
|
|
6ac537c517 | ||
|
|
2386ae7749 | ||
|
|
7d559acfae | ||
|
|
7783b9b218 | ||
|
|
548f7d7b1e | ||
|
|
4629c07812 | ||
|
|
3b2b7da841 | ||
|
|
25ef53510a | ||
|
|
0064f248d3 | ||
|
|
0c721696f2 | ||
|
|
131ab6214d | ||
|
|
70bc7a1236 | ||
|
|
6c88716a2a | ||
|
|
ff3c37e360 | ||
|
|
58bab443c4 | ||
|
|
a8b0a6d555 | ||
|
|
0a430b4b0a | ||
|
|
8b76c5ce3b | ||
|
|
c81f5f7015 | ||
|
|
cf6b186269 | ||
|
|
bd25ddb92e | ||
|
|
62bdd31af3 | ||
|
|
d4af89bf99 | ||
|
|
1c38a42c0b | ||
|
|
6d1ebb74fb | ||
|
|
9673e6de5c | ||
|
|
ab709e2c69 | ||
|
|
9144708cf0 | ||
|
|
38136de39d | ||
|
|
beb800a76e | ||
|
|
aab738526a | ||
|
|
86bdad61a4 | ||
|
|
e4c56cab03 | ||
|
|
cef1c4e3f6 | ||
|
|
b2721c9faa | ||
|
|
ab4ae62ffe | ||
|
|
74244bab74 | ||
|
|
57112ae692 | ||
|
|
fd1314ccba | ||
|
|
45d99df104 | ||
|
|
17b87f6543 | ||
|
|
d62e82569d | ||
|
|
dc5e00cc07 | ||
|
|
6402511d38 | ||
|
|
83c1f70077 | ||
|
|
f3375f48ef | ||
|
|
e1b911086b | ||
|
|
b60c0cef51 | ||
|
|
0c42185700 | ||
|
|
ee3c779b17 | ||
|
|
b5e6655c84 | ||
|
|
e1b45b9193 | ||
|
|
588713bd55 | ||
|
|
43ad452174 | ||
|
|
2cf9146536 | ||
|
|
f81331baed | ||
|
|
9c9c3b9428 | ||
|
|
4b64d81c21 | ||
|
|
e826f600f0 | ||
|
|
0f845a9784 | ||
|
|
a2805bedca | ||
|
|
d860bbfb90 | ||
|
|
70e2d34410 | ||
|
|
1c49a11824 | ||
|
|
cd2a0000c0 | ||
|
|
1304e49eb4 | ||
|
|
9658cecb88 | ||
|
|
4c23d5bafc | ||
|
|
82238c8c1a | ||
|
|
ead74e1c26 | ||
|
|
d58371be81 | ||
|
|
844d194533 | ||
|
|
84abc929d0 | ||
|
|
7497470875 | ||
|
|
cc5df41daa | ||
|
|
d87b290a32 | ||
|
|
a0f859ad03 | ||
|
|
c86892ec0b | ||
|
|
c85fea0799 | ||
|
|
765e34a01d | ||
|
|
96e7f2eeac | ||
|
|
23dddfd16e | ||
|
|
e2318d0af1 | ||
|
|
ef3b840dce | ||
|
|
9b9c5fa70e | ||
|
|
6f0216cf9f | ||
|
|
e0ae0abda2 | ||
|
|
d8e7f686ff | ||
|
|
052a8eb993 | ||
|
|
60a81b720d | ||
|
|
db641bade1 | ||
|
|
d295e5e7f3 | ||
|
|
197e6c3149 | ||
|
|
8c14db2109 | ||
|
|
838634cd2c | ||
|
|
3987e94c27 | ||
|
|
2e2ca970a2 | ||
|
|
2c183dc428 | ||
|
|
62f6f58e5e | ||
|
|
9bf97c9f46 | ||
|
|
21d2ddf2d4 | ||
|
|
7eb0a00ce9 | ||
|
|
db865debcf | ||
|
|
cae1803446 | ||
|
|
49cfa038f1 | ||
|
|
2702d0b228 | ||
|
|
864ad378a2 | ||
|
|
0769e612f6 | ||
|
|
45dd433bb0 | ||
|
|
c782266cb2 | ||
|
|
c5f79e16c1 | ||
|
|
858064f7a7 | ||
|
|
9b51acb104 | ||
|
|
8f7f4a7f7d | ||
|
|
90e4263fd1 | ||
|
|
d381934376 | ||
|
|
50ae0a5683 | ||
|
|
4df8f63596 | ||
|
|
2126bc0851 | ||
|
|
738c6373ca | ||
|
|
14dd85f607 | ||
|
|
eddc9f297e | ||
|
|
4711897c03 | ||
|
|
c4cfa36d07 | ||
|
|
47387bac9b | ||
|
|
06e5d64700 | ||
|
|
013f6d5f95 | ||
|
|
406070da62 | ||
|
|
f9c3da6f09 | ||
|
|
6f19422c6b | ||
|
|
06ff12c29a | ||
|
|
b04d797022 | ||
|
|
29d224f6b1 | ||
|
|
c8203f1104 | ||
|
|
0f1357f6fb | ||
|
|
495884147e | ||
|
|
123806ce4a | ||
|
|
ce298e3311 | ||
|
|
647ed26e87 | ||
|
|
0958808c3e | ||
|
|
f14e52bc43 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
github: [patrickgold]
|
||||
custom: ["https://paypal.me/devpatrickgold", "https://explorer.bitcoin.com/btc/address/1GKPJuRTZbVM7L8Kd3wtrqzc259Sjmoh9x"]
|
||||
custom: ["https://paypal.me/devpatrickgold"]
|
||||
|
||||
147
CONTRIBUTING.md
147
CONTRIBUTING.md
@@ -2,127 +2,104 @@
|
||||
|
||||
First off, thanks for considering contributing to FlorisBoard!
|
||||
|
||||
There are several ways to contribute to FlorisBoard. This document
|
||||
provides some general guidelines for each type of contribution.
|
||||
There are several ways to contribute to FlorisBoard. This document provides some general guidelines for each type of
|
||||
contribution.
|
||||
|
||||
## Giving general feedback
|
||||
|
||||
NEW! You can now [give general feedback](https://github.com/florisboard/florisboard/discussions/new?category=feedback)
|
||||
directly here on GitHub. This is the preferred way to give feedback, as
|
||||
it allows not only for me to read and respond to feedback, but for everyone
|
||||
in this community.
|
||||
directly here on GitHub. This is the preferred way to give feedback, as it allows not only for me to read and respond to
|
||||
feedback, but for everyone in this community.
|
||||
|
||||
Optionally you can also use the review function within Google Play or email me
|
||||
at [florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev). I
|
||||
love to hear from you! Note, that the amount of feedback emails I get
|
||||
is overwhelmingly high - so if I don't answer or answer really late, I
|
||||
apologize - I guarantee though that I read through every email and that
|
||||
I will use every feedback to improve FlorisBoard :)
|
||||
at [florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev). I love to hear from you! Note, that the amount of
|
||||
feedback emails I get is overwhelmingly high - so if I don't answer or answer really late, I apologize - I guarantee
|
||||
though that I read through every email and that I will use every feedback to improve FlorisBoard :)
|
||||
|
||||
## Translations
|
||||
|
||||
To make FlorisBoard accessible in as many languages as possible, the
|
||||
platform [Crowdin](https://crowdin.florisboard.patrickgold.dev) is used
|
||||
to crowdsource and manage translations. This is the only source of
|
||||
translations from now on - **PRs that add/update translations are no
|
||||
longer accepted.** The list of languages in Crowdin covers the top 20
|
||||
languages, but feel free to email me at
|
||||
[florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev) to
|
||||
request a language and I'll add it.
|
||||
platform [Crowdin](https://crowdin.florisboard.patrickgold.dev) is used to crowdsource and manage translations. This is
|
||||
the only source of translations from now on - **PRs that add/update translations are no longer accepted.** The list of
|
||||
languages in Crowdin covers the top 20 languages, but feel free to email me at
|
||||
[florisboard@patrickgold.dev](mailto:florisboard@patrickgold.dev) to request a language and I'll add it.
|
||||
|
||||
## Adding a new feature or making large changes
|
||||
|
||||
If you intend to add a new feature or to make large changes, please
|
||||
discuss this first through a proposal on GitHub. Discussing your idea
|
||||
enables both you and the dev team that we are on the same page before
|
||||
you start on working on your change. If you have any questions, feel
|
||||
free to ask for help at any time!
|
||||
If you intend to add a new feature or to make large changes, please discuss this first through a proposal on GitHub.
|
||||
Discussing your idea enables both you and the dev team that we are on the same page before you start on working on your
|
||||
change. If you have any questions, feel free to ask for help at any time!
|
||||
|
||||
## Adding a new keyboard layout
|
||||
|
||||
Adding a layout to FlorisBoard is very simple and does not require any
|
||||
coding skills, although you should understand the basics of the JSON
|
||||
syntax (it is very easy though by just looking at some other layout files).
|
||||
There are two main steps in adding new layouts, though the config step can
|
||||
be skipped if you only add a layout without a new default language support.
|
||||
|
||||
### The config file ([`app/src/main/assets/ime/config.json`](app/src/main/assets/ime/config.json))
|
||||
|
||||
This file is very important, as it defines all default currency sets as
|
||||
well as all default subtypes available in the Settings Subtype UI. Note
|
||||
that you don't have to modify this file if you add a layout for an already
|
||||
pre-configured language.
|
||||
|
||||
- `currencySets`: This is a list of all currency sets, which can be chosen
|
||||
for each subtype. If you consider adding a new one, make sure that the
|
||||
first currency symbol matches the name of the currency set and also
|
||||
ensure that you have exactly 6 currency symbols. This is important as the
|
||||
symbol layouts have exactly 6 slots available to fill these defined
|
||||
currency symbols in.
|
||||
- `defaultSubtyes`: This is a list of all pre-made subtypes. Each time the
|
||||
user selects a language in the `Subtype Add`-dialog, all options configured
|
||||
here will get pre-selected. The language tag must adhere to the IETF BCP
|
||||
47 code ([ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
|
||||
and [ISO 3166-1 region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements)).
|
||||
For example, Dutch as spoken in Belgium is `nl-be`. Use a unique value
|
||||
for `id` to avoid possible crashes caused by duplicate ids.
|
||||
Adding a layout to FlorisBoard is very simple and does not require any coding skills, although you should understand the
|
||||
basics of the JSON syntax (it is very easy though by just looking at some other layout files). Most of the time is
|
||||
enough to look at the existing layout files, but the following attempts to help you in creating layouts from scratch.
|
||||
|
||||
### Adding the layout
|
||||
|
||||
Since v0.3.10-beta05 it is possible to add custom layouts for all types.
|
||||
Since v0.3.14-beta06 it is possible to add custom layouts for all types using the new extension format, Flex.
|
||||
|
||||
To add a new layout, head to [`app/src/main/assets/ime/text`](app/src/main/assets/ime/text) and then select
|
||||
the correct sub-directory for the type of layout you want to add. In most cases
|
||||
this will be `characters` to add a layout like QWERTY etc.
|
||||
Keyboard layout assets are grouped in [`app/src/main/assets/ime/keyboard`](app/src/main/assets/ime/keyboard) and are
|
||||
further sub-grouped into the following:
|
||||
|
||||
For the `code` field of each key, make sure to use the UTF-8 code. An
|
||||
useful tool for finding the correct code is [unicode-table.com](https://unicode-table.com/en/).
|
||||
From there, you search for your letter and then use the HTML code, but without the `&#;`
|
||||
- `org.florisboard.composers`: Defines standard composers for interpreting input, currently supports basic typing and
|
||||
Korean input. Most of the time you won't need to add new composers, so if you don't know what they are always
|
||||
assume `appender` (the default composer which does not alter input in any way) is in use.
|
||||
- `org.florisboard.currencysets`: Lists all currency sets, which can be chosen for each subtype. If you consider adding
|
||||
a new one, make sure that the first currency symbol matches the name of the currency set and also ensure that you have
|
||||
exactly 6 currency symbols. This is important as the symbol layouts have exactly 6 slots available to fill these
|
||||
defined currency symbols in.
|
||||
- `org.florisboard.layouts`: Contains the actual layout files for all layout types.
|
||||
- `org.florisboard.localization`: Contains all popup mappings and subtype presets (formally the `config.json` file). The
|
||||
subtype presets are a list of all pre-made subtypes. Each time the user selects a language in the `Subtype Add`
|
||||
-dialog, all options configured here will get selected if found in the presets. The language tag must adhere to the
|
||||
IETF BCP 47 code ([ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
|
||||
and [ISO 3166-1 region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements)). For
|
||||
example, Dutch as spoken in Belgium is `nl-be`.
|
||||
|
||||
To add a new layout, head to above directory and add the necessary files to each extension group.
|
||||
|
||||
For the `code` field of each key, make sure to use the UTF-8 code. An useful tool for finding the correct code
|
||||
is [unicode-table.com](https://unicode-table.com/en/). From there, you search for your letter and then use the HTML
|
||||
code, but without the `&#;`
|
||||
For internal codes of functional or UI keys, see
|
||||
[`app/src/main/java/dev/patrickgold/florisboard/ime/text/key/KeyCode.kt`](app/src/main/java/dev/patrickgold/florisboard/ime/text/key/KeyCode.kt).
|
||||
[`app/src/main/java/dev/patrickgold/florisboard/ime/text/key/KeyCode.kt`](app/src/main/java/dev/patrickgold/florisboard/ime/text/key/KeyCode.kt)
|
||||
.
|
||||
|
||||
The label is equally important and should always match up with the defined
|
||||
code. If `code` and `label` don't match up, FlorisBoard won't crash but
|
||||
it will most likely lead to confusion in the key processing logic.
|
||||
The label is equally important and should always match up with the defined code. If `code` and `label` don't match up,
|
||||
FlorisBoard won't crash but it will most likely lead to confusion in the key processing logic.
|
||||
|
||||
Any accents or diacritics that should be exposed via long press can be
|
||||
added at [`app/src/main/assets/ime/text/characters/extended_popups/<languageTag_name_here>.json`](app/src/main/assets/ime/text/characters/extended_popups).
|
||||
For each key, you can add 1 main and several relevant accents. The main
|
||||
accent should be used for accents which are important for the language
|
||||
you add. The main field is used for determining if a hint or an accent
|
||||
should take priority, so please make sure to leave main empty and just
|
||||
use relevant for accents which are not-so important.
|
||||
Any accents or diacritics that should be exposed via long press can be added
|
||||
at [`app/src/main/assets/ime/keyboard/org.florisboard.localization/popupMappings/<languageTag>.json`](app/src/main/assets/ime/keyboard/org.florisboard.localization/popupMappings)
|
||||
. For each key, you can add 1 main and several relevant accents. The main accent should be used for accents which are
|
||||
important for the language you add. The main field is used for determining if a hint or an accent should take priority,
|
||||
so please make sure to leave main empty and just use relevant for accents which are not-so important.
|
||||
|
||||
For popups of non-`characters` layout, simply add the popup directly to
|
||||
each key via the `popup` field.
|
||||
For popups of non-`characters` layout, simply add the popup directly to each key via the `popup` field.
|
||||
|
||||
## Adding a new dictionary for a language
|
||||
|
||||
Currently the suggestions implementation is highly experimental and will
|
||||
get a major if not complete rework, so dictionaries are currently not
|
||||
accepted.
|
||||
Currently the suggestions implementation is highly experimental and not available until 0.4.0, so dictionaries are
|
||||
currently not accepted.
|
||||
|
||||
## Bug reporting
|
||||
|
||||
This kind of contribution is the most important, as it tells where
|
||||
FlorisBoard has flaws and thus should be improved to maximize stability
|
||||
and user experience. To make this process as smooth as possible, please
|
||||
use the pre-made [issue template](.github/ISSUE_TEMPLATE/bug_report.md)
|
||||
for bug reporting. This makes it easy for us to understand what the bug
|
||||
is and how to solve it.
|
||||
This kind of contribution is the most important, as it tells where FlorisBoard has flaws and thus should be improved to
|
||||
maximize stability and user experience. To make this process as smooth as possible, please use the
|
||||
pre-made [issue template](.github/ISSUE_TEMPLATE/bug_report.md)
|
||||
for bug reporting. This makes it easy for us to understand what the bug is and how to solve it.
|
||||
|
||||
### Capturing error logs
|
||||
|
||||
Logs are captured by FlorisBoard's crash handler, which gives you the
|
||||
ability to copy it to the clipboard and paste it in GitHub. This is the
|
||||
preferred way to capture logs.
|
||||
Logs are captured by FlorisBoard's crash handler, which gives you the ability to copy it to the clipboard and paste it
|
||||
in GitHub. This is the preferred way to capture logs.
|
||||
|
||||
Alternatively, you can also use ADB (Android Debug Bridge) to capture
|
||||
the error log. This is recommended for experienced users only.
|
||||
Alternatively, you can also use ADB (Android Debug Bridge) to capture the error log. This is recommended for experienced
|
||||
users only.
|
||||
|
||||
## Donating
|
||||
|
||||
If none of the above options are feasible for you but you still want to
|
||||
show your support, you can also buy me a coffee, so I can stay up all night
|
||||
and chase away bugs or add new cool stuff :)
|
||||
If none of the above options are feasible for you but you still want to show your support, you can also buy me a coffee,
|
||||
so I can stay up all night and chase away bugs or add new cool stuff :)
|
||||
See the `Sponsors` button for available options!
|
||||
|
||||
36
README.md
36
README.md
@@ -18,10 +18,10 @@ fully respecting your privacy. Currently in early-beta state.
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<p><i>Major versions only, 1 release per 1-3 months</i><br><br>Updates are more polished, new features are matured and tested through to ensure a stable experience.</p>
|
||||
<p><i>Major versions only, 1 release per 1-5 months</i><br><br>Updates are more polished, new features are matured and tested through to ensure a stable experience.</p>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<p><i>Beta versions, 1-2 releases per week</i><br><br>Updates contain new features that may not be fully matured yet and bugs are more likely to occur. Allows you to give early feedback.</p>
|
||||
<p><i>Beta versions, up to 1-2 releases per week</i><br><br>Updates contain new features that may not be fully matured yet and bugs are more likely to occur. Allows you to give early feedback.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -57,12 +57,14 @@ fully respecting your privacy. Currently in early-beta state.
|
||||
Beginning with v0.4.0 FlorisBoard will follow [SemVer](https://semver.org/#summary) versioning scheme and enter the public beta on Google Play.
|
||||
|
||||
## Highlighted features
|
||||
- Spell checking service
|
||||
- Word suggestions (currently English only and may not work on all devices)
|
||||
- Glide typing (currently English only)
|
||||
- Advanced theming support and customization
|
||||
- Integrated clipboard manager / history
|
||||
- Emoji keyboard (although lacking some features)
|
||||
- Advanced theming support and customization
|
||||
- Integrated extension support (still evolving)
|
||||
- Emoji keyboard
|
||||
- Spell checking service
|
||||
- Glide typing (currently English only)
|
||||
|
||||
Word suggestions are not included in the current releases and are a major goal for the v0.4.0 milestone.
|
||||
|
||||
Feature roadmap: See [ROADMAP.md](ROADMAP.md)
|
||||
|
||||
@@ -78,18 +80,18 @@ Please refer to this [page](https://github.com/florisboard/florisboard/wiki/List
|
||||
to get more information on this topic.
|
||||
|
||||
## Used libraries, components and icons
|
||||
* [Google Flexbox Layout for Android](https://github.com/google/flexbox-layout)
|
||||
by [google](https://github.com/google)
|
||||
* [AndroidX libraries](https://github.com/androidx/androidx) by
|
||||
[Android Jetpack](https://github.com/androidx)
|
||||
* [Accompanist Compose UI libraries](https://github.com/google/accompanist/) by
|
||||
[Google](https://github.com/google)
|
||||
* [Google Material icons](https://github.com/google/material-design-icons) by
|
||||
[google](https://github.com/google)
|
||||
[Google](https://github.com/google)
|
||||
* [JetPref preference library](https://github.com/patrickgold/jetpref) by
|
||||
[patrickgold](https://github.com/patrickgold)
|
||||
* [KotlinX coroutines library](https://github.com/Kotlin/kotlinx.coroutines) by
|
||||
[Kotlin](https://github.com/Kotlin)
|
||||
* [KotlinX serialization library](https://github.com/Kotlin/kotlinx.serialization) by
|
||||
[Kotlin](https://github.com/Kotlin)
|
||||
* [ColorPicker preference](https://github.com/jaredrummler/ColorPicker) by
|
||||
[Jared Rummler](https://github.com/jaredrummler)
|
||||
* [Timber](https://github.com/JakeWharton/timber) by
|
||||
[JakeWharton](https://github.com/JakeWharton)
|
||||
* [expandable-fab](https://github.com/nambicompany/expandable-fab) by
|
||||
[Nambi](https://github.com/nambicompany)
|
||||
* [ICU4C](https://github.com/unicode-org/icu) by
|
||||
[The Unicode Consortium](https://github.com/unicode-org)
|
||||
* [Nuspell](https://github.com/nuspell/nuspell) by
|
||||
@@ -97,7 +99,7 @@ to get more information on this topic.
|
||||
|
||||
## License
|
||||
```
|
||||
Copyright 2020 Patrick Goldinger
|
||||
Copyright 2020-2022 Patrick Goldinger
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
133
ROADMAP.md
133
ROADMAP.md
@@ -1,98 +1,91 @@
|
||||
# FlorisBoard's feature roadmap & milestones
|
||||
|
||||
This feature roadmap intents to provide transparency to what I want to add
|
||||
to FlorisBoard in the foreseeable future. Note that there are no ETAs for any
|
||||
version milestones down below, experience says these won't hold anyways.
|
||||
This feature roadmap intents to provide transparency to what I want to add to FlorisBoard in the foreseeable future.
|
||||
Note that there are no ETAs for any version milestones down below, experience says these won't hold anyways.
|
||||
|
||||
I try my best to release regularly, though some features take a lot longer
|
||||
than others and thus releases can be spaced out a bit on the stable track.
|
||||
If you are interested in following the development more closely, make sure to
|
||||
follow along the beta track releases! These are generally more unstable but
|
||||
you get new stuff faster and can provide early feedback, which helps a lot!
|
||||
I try my best to release regularly, though some features take a lot longer than others and thus releases can be spaced
|
||||
out a bit on the stable track. If you are interested in following the development more closely, make sure to follow
|
||||
along the beta track releases! These are generally more unstable but you get new stuff faster and can provide early
|
||||
feedback, which helps a lot!
|
||||
|
||||
## 0.3.x and 0.4.0
|
||||
Releases in this section still follow the old versioning scheme, meaning the
|
||||
patch number is a feature upgrade. As this naming convention is more confusing
|
||||
than useful, after the v0.4.0 release a new release/development cycle will be
|
||||
introduced.
|
||||
## 0.3.x
|
||||
|
||||
### 0.3.13 (done)
|
||||
- Spell checking (mainly completed and relatively well working, Smartbar integration still missing)
|
||||
- Performance improvements in keyboard rendering
|
||||
- Audio/haptic feedback rework
|
||||
- Lots and lots of bug fixing in all areas, really fix some annoying bugs
|
||||
- New layouts added by contributors
|
||||
Releases in this section still follow the old versioning scheme, meaning the patch number is a feature upgrade. As this
|
||||
naming convention is more confusing than useful, beginning with v0.4.0 development a new release/development cycle will
|
||||
be introduced.
|
||||
|
||||
### 0.3.14 (almost completed, release candidate phase)
|
||||
|
||||
### 0.3.14 (currently in progress)
|
||||
- Re-write of the Preference core
|
||||
- Reduce redundancy in key/default value definitions
|
||||
- Avoid having to manually add redundant code for adding a new pref
|
||||
- Goes hand-in-hand with the Settings UI re-write
|
||||
- Reduce redundancy in key/default value definitions
|
||||
- Avoid having to manually add redundant code for adding a new pref
|
||||
- Goes hand-in-hand with the Settings UI re-write
|
||||
- Re-write of the Settings UI with Jetpack Compose
|
||||
- Also re-structure UI into a more list-like panel
|
||||
- Adjust theme colors of Settings a bit to make it more modern
|
||||
- Preview the keyboard at any time from within the Settings
|
||||
- Settings language different than device language
|
||||
- Also re-structure UI into a more list-like panel
|
||||
- Adjust theme colors of Settings a bit to make it more modern
|
||||
- Preview the keyboard at any time from within the Settings
|
||||
- Settings language different than device language
|
||||
- Re-write the Setup UI in Jetpack Compose
|
||||
- Simplify screen based on previously discussed ideas and mock-ups
|
||||
- Improve backend setup logic
|
||||
- Implement base-UI for extensions and further continue development
|
||||
of existing Flex (FlorisBoard extension) format
|
||||
- Allows for a continuous experience of customizing FlorisBoard in different areas
|
||||
- Planned what will use Flex:
|
||||
- Themes
|
||||
- Layouts (Characters, symbols, numeric, ...)
|
||||
- Composers for non-Latin script languages
|
||||
- Word suggestion dictionaries (not in 0.3.14)
|
||||
- Spell check dictionaries
|
||||
- User dictionaries (not in 0.3.14)
|
||||
- Other features that require only data and no logic (not in 0.3.14)
|
||||
- Simplify screen based on previously discussed ideas and mock-ups
|
||||
- Improve backend setup logic
|
||||
- Implement base-UI for extensions and further continue development of existing Flex (FlorisBoard extension) format
|
||||
- Allows for a continuous experience of customizing FlorisBoard in different areas
|
||||
- Planned what will use Flex:
|
||||
- Themes
|
||||
- Layouts (Characters, symbols, numeric, ...)
|
||||
- Composers for non-Latin script languages
|
||||
- Word suggestion dictionaries (in 0.4.0)
|
||||
- Spell check dictionaries
|
||||
- User dictionaries (not in 0.3.14)
|
||||
- Other features that require only data and no logic (not in 0.3.14)
|
||||
- Maybe full backup of preferences? Not 100% confirmed though and may be pushed back
|
||||
- Theme rework part I:
|
||||
- Custom key corner radius
|
||||
- Custom key border color (not shadow!!)
|
||||
- Re-work theme internals so they use Flex extension format and FlexCSS
|
||||
- Community repository on GitHub for theme sharing across users (when Theme Flex format is ready)
|
||||
- Improvement of the Smartbar
|
||||
- Allow to have multiple Smartbars
|
||||
- Better candidate view (in prep for 0.3.15/0.3.16)
|
||||
- Allow to have multiple Smartbars
|
||||
- Better candidate view (in prep for 0.4.0)
|
||||
|
||||
### 0.3.15 & 0.3.16
|
||||
|
||||
- Hotfix releases for possible bugs in the preference rework, may be skipped.
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### 0.3.15/16/17
|
||||
- Note that 0.3.15 may be a hotfix release for the preference rework and will not contain the
|
||||
planned new word predictions
|
||||
- Re-adding word suggestions (at least for Latin-based languages at first)
|
||||
- Importing the dictionaries as well as management relies on the Flex extension core and UI in Kotlin
|
||||
- Actually parsing and generating suggestions happens in C++ to avoid another OOM catastrophe like in 0.3.9/10
|
||||
- The actual format of the dictionary and word list source is not decided yet
|
||||
- Importing the dictionaries as well as management relies on the Flex extension core and UI in Kotlin
|
||||
- Actually parsing and generating suggestions happens in C++ to avoid another OOM catastrophe like in 0.3.9/10
|
||||
- The actual format of the dictionary and word list source is not decided yet
|
||||
- Community repository on GitHub for theme sharing across users (may be 0.5.0)
|
||||
|
||||
### 0.4.0
|
||||
- Prepare FlorisBoard repository and app store presence for public beta release
|
||||
on Google Play
|
||||
- Rework branding images and texts of FlorisBoard for the app stores
|
||||
- Focus on polishing the app and fixing bugs/crashes
|
||||
|
||||
With this release the versioning scheme changes: the second number now indicates new features,
|
||||
changes in the third "patch" number now indicates bug fixes for the stable track. The development
|
||||
cycle for each 0.x release will have -betaXX and -rcXX (release candidate) releases on the beta
|
||||
track for interested people to follow along the development.
|
||||
With this release the versioning scheme changes: the second number now indicates new features, changes in the third "
|
||||
patch" number now indicates bug fixes and minor feature additions for the stable track. The development cycle for each
|
||||
0.x release will have `-alphaXX`, `-betaXX` and `-rcXX` (release candidate) releases on the beta track for interested
|
||||
people to follow along the development. The first release to follow the new scheme will be `0.4.0-alpha01` on the beta
|
||||
track.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
- Complete rework of the Emoji panel
|
||||
- Recently used / Emoji history
|
||||
- Emoji search
|
||||
- Emoji suggestions when using :emoji_name: syntax
|
||||
- Kaomoji panel implementation (the third tab which currently has "not yet implemented")
|
||||
- Full Smartbar customization
|
||||
- Includes internal rework how Smartbar is build and assembled
|
||||
- Allow for more than one Smartbar / Stackable and Collapsible Smartbars
|
||||
- Customizable quick actions, clipboard row
|
||||
- Recently used / Emoji history (already implemented with 0.3.14)
|
||||
- Emoji search
|
||||
- Emoji suggestions when using :emoji_name: syntax
|
||||
- Kaomoji panel implementation (the third tab which currently has "not yet implemented")
|
||||
- Smartbar customization improvements
|
||||
- Quick actions customization (order and which buttons to show)
|
||||
- Prepare FlorisBoard repository and app store presence for public beta release on Google Play (will go live with stable
|
||||
0.5.0!!)
|
||||
- Rework branding images and texts of FlorisBoard for the app stores
|
||||
- Focus on stability and experience improvements of the app and keyboard
|
||||
|
||||
## 0.6.0
|
||||
- Full on-board layout editor which allows users to create their own layouts
|
||||
without writing a JSON file
|
||||
|
||||
- Full on-board layout editor which allows users to create their own layouts without writing a JSON file
|
||||
- Import/Export of custom layout files packed in Flex extensions
|
||||
|
||||
## Backlog / Features that MAY be added
|
||||
## Backlog / Features that MAY be added, even in versions not mentioned above if the feature implementation fits perfectly with another feature
|
||||
|
||||
- Theme rework part II
|
||||
- Adaptive themes v2
|
||||
- Voice-to-text with Mozilla's open-source voice service
|
||||
|
||||
@@ -2,7 +2,7 @@ plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
kotlin("plugin.serialization") version "1.6.0"
|
||||
kotlin("plugin.serialization")
|
||||
id("com.google.android.gms.oss-licenses-plugin")
|
||||
id("de.mannodermaus.android-junit5")
|
||||
}
|
||||
@@ -13,7 +13,6 @@ android {
|
||||
ndkVersion = "22.1.7171670"
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
@@ -31,10 +30,9 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "dev.patrickgold.florisboard"
|
||||
minSdk = 23
|
||||
targetSdk = 30
|
||||
versionCode = 63
|
||||
targetSdk = 31
|
||||
versionCode = 75
|
||||
versionName = "0.3.14"
|
||||
multiDexEnabled = true
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -72,13 +70,21 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
bundle {
|
||||
language {
|
||||
// We disable language split because FlorisBoard does not use
|
||||
// runtime Google Play Service APIs and thus cannot dynamically
|
||||
// request to download the language resources for a specific locale.
|
||||
enableSplit = false
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.1.0-beta04"
|
||||
kotlinCompilerExtensionVersion = "1.1.1"
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
@@ -93,7 +99,7 @@ android {
|
||||
versionNameSuffix = "-debug"
|
||||
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
isJniDebuggable = false
|
||||
|
||||
ndk {
|
||||
// For running FlorisBoard on the emulator
|
||||
@@ -109,7 +115,7 @@ android {
|
||||
create("beta") // Needed because by default the "beta" BuildType does not exist
|
||||
named("beta").configure {
|
||||
applicationIdSuffix = ".beta"
|
||||
versionNameSuffix = "-beta07"
|
||||
versionNameSuffix = ""
|
||||
proguardFiles.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
|
||||
resValue("mipmap", "floris_app_icon", "@mipmap/ic_app_icon_beta")
|
||||
@@ -136,10 +142,6 @@ android {
|
||||
it.useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
isAbortOnError = false
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
@@ -148,37 +150,34 @@ tasks.withType<Test> {
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.activity:activity-compose:1.4.0")
|
||||
implementation("androidx.activity:activity-ktx:1.4.0") // possibly remove after settings rework
|
||||
implementation("androidx.appcompat:appcompat:1.3.1") // possibly remove after settings rework
|
||||
implementation("androidx.activity:activity-ktx:1.4.0")
|
||||
implementation("androidx.autofill:autofill:1.1.0")
|
||||
implementation("androidx.compose.material:material:1.1.0-beta04")
|
||||
implementation("androidx.compose.runtime:runtime-livedata:1.1.0-beta04")
|
||||
implementation("androidx.compose.ui:ui:1.1.0-beta04")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview:1.1.0-beta04")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0") // possibly remove after settings rework
|
||||
implementation("androidx.collection:collection-ktx:1.2.0")
|
||||
implementation("androidx.compose.material:material:1.1.1")
|
||||
implementation("androidx.compose.runtime:runtime-livedata:1.1.1")
|
||||
implementation("androidx.compose.ui:ui:1.1.1")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview:1.1.1")
|
||||
implementation("androidx.core:core-ktx:1.7.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||
implementation("androidx.fragment:fragment-ktx:1.3.6") // possibly remove after settings rework
|
||||
implementation("androidx.navigation:navigation-compose:2.4.0-beta02")
|
||||
implementation("androidx.preference:preference-ktx:1.1.1") // possibly remove after settings rework
|
||||
implementation("com.google.accompanist:accompanist-systemuicontroller:0.20.2")
|
||||
implementation("com.google.android.flexbox:flexbox:3.0.0") // possibly remove after settings rework
|
||||
implementation("com.google.android.material:material:1.4.0") // possibly remove after settings rework
|
||||
implementation("com.jaredrummler:colorpicker:1.1.0") // possibly remove after settings rework
|
||||
implementation("com.nambimobile.widgets:expandable-fab:1.0.2") // possibly remove after settings rework
|
||||
implementation("dev.patrickgold.jetpref:jetpref-datastore-model:0.1.0-beta01")
|
||||
implementation("dev.patrickgold.jetpref:jetpref-datastore-ui:0.1.0-beta01")
|
||||
implementation("dev.patrickgold.jetpref:jetpref-material-ui:0.1.0-beta01")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
|
||||
implementation("androidx.room:room-runtime:2.3.0")
|
||||
kapt("androidx.room:room-compiler:2.3.0")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-beta01")
|
||||
implementation("androidx.emoji2:emoji2:1.1.0")
|
||||
implementation("androidx.emoji2:emoji2-views:1.1.0")
|
||||
implementation("androidx.navigation:navigation-compose:2.4.1")
|
||||
implementation("com.google.accompanist:accompanist-flowlayout:0.23.0")
|
||||
implementation("com.google.accompanist:accompanist-insets:0.23.0")
|
||||
implementation("com.google.accompanist:accompanist-systemuicontroller:0.23.0")
|
||||
implementation("dev.patrickgold.jetpref:jetpref-datastore-model:0.1.0-beta08")
|
||||
implementation("dev.patrickgold.jetpref:jetpref-datastore-ui:0.1.0-beta08")
|
||||
implementation("dev.patrickgold.jetpref:jetpref-material-ui:0.1.0-beta08")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
|
||||
implementation("androidx.room:room-runtime:2.4.2")
|
||||
kapt("androidx.room:room-compiler:2.4.2")
|
||||
|
||||
testImplementation("io.kotest:kotest-runner-junit5:4.6.3")
|
||||
testImplementation("io.kotest:kotest-assertions-core:4.6.3")
|
||||
testImplementation("io.kotest:kotest-property:4.6.3")
|
||||
testImplementation("io.kotest.extensions:kotest-extensions-robolectric:0.4.0")
|
||||
testImplementation("io.kotest:kotest-runner-junit5:5.1.0")
|
||||
testImplementation("io.kotest:kotest-assertions-core:5.1.0")
|
||||
testImplementation("io.kotest:kotest-property:5.1.0")
|
||||
testImplementation("io.kotest.extensions:kotest-extensions-robolectric:0.5.0")
|
||||
testImplementation("nl.jqno.equalsverifier:equalsverifier:3.8.3")
|
||||
|
||||
androidTestImplementation("androidx.test.ext", "junit", "1.1.2")
|
||||
androidTestImplementation("androidx.test.espresso", "espresso-core", "3.3.0")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2020 Patrick Goldinger
|
||||
<!-- Copyright (C) 2020-2022 Patrick Goldinger
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -15,6 +15,7 @@
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="dev.patrickgold.florisboard">
|
||||
|
||||
<!-- Permission needed to vibrate if the user has key press vibration enabled -->
|
||||
@@ -35,12 +36,15 @@
|
||||
|
||||
<application
|
||||
android:name="dev.patrickgold.florisboard.FlorisApplication"
|
||||
android:allowBackup="false"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/floris_app_name"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/SettingsTheme">
|
||||
android:theme="@style/FlorisAppTheme"
|
||||
tools:targetApi="s">
|
||||
|
||||
<!-- IME service -->
|
||||
<service
|
||||
@@ -48,7 +52,8 @@
|
||||
android:label="@string/floris_app_name"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD"
|
||||
android:directBootAware="true"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
tools:targetApi="n">
|
||||
<intent-filter>
|
||||
<action android:name="android.view.InputMethod"/>
|
||||
</intent-filter>
|
||||
@@ -75,7 +80,12 @@
|
||||
android:launchMode="singleTask"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/FlorisAppTheme"/>
|
||||
android:theme="@style/FlorisAppTheme"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<data android:scheme="florisboard" android:host="app-ui"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Using an activity alias to disable/enable the app icon in the launcher -->
|
||||
<activity-alias
|
||||
@@ -93,26 +103,23 @@
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- User Dictionary Manager Activity -->
|
||||
<!-- Import File Bridging Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.oldsettings.UdmActivity"
|
||||
android:name="dev.patrickgold.florisboard.app.ui.ext.ImportFileActivity"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__title"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Theme Selector Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.oldsettings.ThemeManagerActivity"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__title"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<!-- Theme Editor Activity -->
|
||||
<activity
|
||||
android:name="dev.patrickgold.florisboard.oldsettings.ThemeEditorActivity"
|
||||
android:icon="@mipmap/floris_app_icon"
|
||||
android:label="@string/settings__theme_editor__title"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
android:launchMode="singleTask"
|
||||
android:roundIcon="@mipmap/floris_app_icon_round"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/FlorisAppTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="*" android:host="*" android:pathPattern=".*\\.flex"/>
|
||||
<data android:scheme="*" android:host="*" android:pathPattern=".*\\.xpi"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Crash Dialog Activity -->
|
||||
<activity
|
||||
@@ -121,13 +128,25 @@
|
||||
android:label="@string/crash_dialog__title"
|
||||
android:theme="@style/CrashDialogTheme"/>
|
||||
|
||||
<!-- Clipboard Image File Provider -->
|
||||
<provider
|
||||
android:name="dev.patrickgold.florisboard.ime.clipboard.provider.FlorisContentProvider"
|
||||
android:authorities="${applicationId}.provider.clip"
|
||||
android:name="dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardImagesProvider"
|
||||
android:authorities="${applicationId}.provider.clipboard"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
</provider>
|
||||
|
||||
<!-- Default file provider to share files from the "files" or "cache" dir -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider.file"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
{
|
||||
"package": "dev.patrickgold.florisboard",
|
||||
"$": "ime.extension.keyboard",
|
||||
"meta": {
|
||||
"id": "org.florisboard.composers",
|
||||
"version": "0.1.0",
|
||||
"title": "Default composers",
|
||||
"description": "Default composers which are always available.",
|
||||
"maintainers": [ "patrickgold <patrick@patrickgold.dev>" ],
|
||||
"license": "apache-2.0"
|
||||
},
|
||||
"composers": [
|
||||
{ "$": "appender" },
|
||||
{ "$": "hangul-unicode" },
|
||||
{ "$": "kana-unicode" },
|
||||
{ "$": "with-rules",
|
||||
"name": "basic-telex",
|
||||
"id": "basic-telex",
|
||||
"label": "Basic Telex",
|
||||
"rules": {
|
||||
"aw": "ă", "aa": "â", "dd": "đ", "ee": "ê", "oo": "ô", "ow": "ơ", "uw": "ư", "w": "ư",
|
||||
@@ -50,8 +58,5 @@
|
||||
"ỳz": "y", "ỷz": "y", "ỹz": "y", "ýz": "y", "ỵz": "y"
|
||||
}
|
||||
}
|
||||
],
|
||||
"currencySets": [],
|
||||
"defaultSubtypes": [
|
||||
]
|
||||
}
|
||||
@@ -235,6 +235,18 @@
|
||||
"authors": [ "patrickgold" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
{
|
||||
"id": "rusyn",
|
||||
"label": "Rusyn",
|
||||
"authors": [ "svvvst" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
{
|
||||
"id": "rusyn_us",
|
||||
"label": "Rusyn (Phonetic)",
|
||||
"authors": [ "svvvst" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
{
|
||||
"id": "sangaline",
|
||||
"label": "Sangaline",
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"$": "auto_text_key", "code": 105, "label": "i",
|
||||
"popup": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 7574, "label": "ᵻ" },
|
||||
{ "$": "auto_text_key", "code": 7547, "label": "ᵻ" },
|
||||
{ "$": "auto_text_key", "code": 616, "label": "ɨ" },
|
||||
{ "$": "auto_text_key", "code": 618, "label": "ɪ" }
|
||||
]
|
||||
@@ -167,10 +167,10 @@
|
||||
"$": "auto_text_key", "code": 106, "label": "j",
|
||||
"popup": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 668, "label": "ʝ" },
|
||||
{ "$": "auto_text_key", "code": 669, "label": "ʝ" },
|
||||
{ "$": "auto_text_key", "code": 607, "label": "ɟ" },
|
||||
{ "$": "auto_text_key", "code": 690, "label": "◌ʲ" },
|
||||
{ "$": "auto_text_key", "code": 664, "label": "ʄ" }
|
||||
{ "$": "auto_text_key", "code": 644, "label": "ʄ" }
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -258,7 +258,7 @@
|
||||
{ "$": "auto_text_key", "code": 628, "label": "ɴ" },
|
||||
{ "$": "auto_text_key", "code": 8319, "label": "◌ⁿ" },
|
||||
{ "$": "auto_text_key", "code": 771, "label": "◌̃" },
|
||||
{ "$": "auto_text_key", "code": 631, "label": "ŋ" }
|
||||
{ "$": "auto_text_key", "code": 331, "label": "ŋ" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[
|
||||
[
|
||||
[
|
||||
{ "code": 1602, "label": "ق", "popup": {
|
||||
"main": { "code": 1647, "label": "ٯ" }
|
||||
} },
|
||||
{ "code": 1608, "label": "و", "popup": {
|
||||
"main": { "code": -255, "label": "وو" }
|
||||
} },
|
||||
{ "code": 1749, "label": "ﻪ", "popup": {
|
||||
"main": { "code": 1577, "label": "ة" }
|
||||
{ "code": 1749, "label": "ە", "popup": {
|
||||
"main": { "code": 1577, "label": "ة" }
|
||||
} },
|
||||
{ "code": 1585, "label": "ر", "popup": {
|
||||
"main": { "code": 1685, "label": "ڕ" }
|
||||
@@ -15,50 +15,49 @@
|
||||
{ "code": 1578, "label": "ت", "popup": {
|
||||
"main": { "code": 1591, "label": "ط" }
|
||||
} },
|
||||
{ "code": 1740, "label": "ی", "popup": {
|
||||
"main": { "code": 1742, "label": "ێ" }
|
||||
} },
|
||||
{ "code": 1740, "label": "ی" },
|
||||
{ "code": 1742, "label": "ێ" },
|
||||
{ "code": 1574, "label": "ﺋ", "popup": {
|
||||
"main": { "code": 1569, "label": "ء" }
|
||||
} },
|
||||
{ "code": 1593, "label": "ع", "popup": {
|
||||
"main": { "code": 1594, "label": "غ" }
|
||||
} },
|
||||
{ "code": 1734, "label": "ۆ" },
|
||||
{ "code": 1662, "label": "پ", "popup": {
|
||||
"main": { "code": 1579, "label": "ث" }
|
||||
} }
|
||||
],
|
||||
[
|
||||
{"code": 1575, "label": "ا"},
|
||||
{ "code": 1587, "label": "س" },
|
||||
{ "code": 1588, "label": "ش" },
|
||||
{ "code": 1583, "label": "د", "popup": {
|
||||
"main": {"code": 1584, "label": "ذ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1575, "label": "ا"},
|
||||
{ "code": 1587, "label": "س" },
|
||||
{ "code": 1588, "label": "ش" },
|
||||
{ "code": 1583, "label": "د", "popup": {
|
||||
"main": {"code": 1584, "label": "ذ" }
|
||||
} },
|
||||
{ "code": 1601, "label": "ف" , "popup": {
|
||||
"main": {"code": 1700, "label": "ڤ" }
|
||||
} },
|
||||
{ "code": 1601, "label": "ف" , "popup": {
|
||||
"main": {"code": 1700, "label": "ڤ" }
|
||||
} },
|
||||
{ "code": 1607, "label": "ھ" },
|
||||
{ "code": 1688, "label": "ژ", "popup": {
|
||||
"main": { "code": 1600, "label": "▬" }
|
||||
} },
|
||||
{ "code": 1604, "label": "ل", "popup": {
|
||||
"main": { "code": 1717, "label": "ڵ" }
|
||||
} },
|
||||
{ "code": 1705, "label": "ک" },
|
||||
{ "code": 1711, "label": "گ" }
|
||||
],
|
||||
[
|
||||
{ "code": 1586, "label": "ز", "popup": {
|
||||
"main": {"code": 1592, "label": "ظ" }
|
||||
} },
|
||||
{ "code": 1582, "label": "خ" },
|
||||
{ "code": 1580, "label": "ج" },
|
||||
{ "code": 1670, "label": "چ" },
|
||||
{ "code": 1581, "label": "ح" },
|
||||
{ "code": 1576, "label": "ب" },
|
||||
{ "code": 1606, "label": "ن" },
|
||||
{ "code": 1605, "label": "م" }
|
||||
{ "code": 1726, "label": "ھ" },
|
||||
{ "code": 1688, "label": "ژ", "popup": {
|
||||
"main": { "code": 1600, "label": "━" }
|
||||
} },
|
||||
{ "code": 1604, "label": "ل", "popup": {
|
||||
"main": { "code": 1717, "label": "ڵ" }
|
||||
} },
|
||||
{ "code": 1705, "label": "ک" },
|
||||
{ "code": 1711, "label": "گ" , "popup": {
|
||||
"main": { "code": 1594, "label": "غ" }
|
||||
} }
|
||||
],
|
||||
[
|
||||
{ "code": 1586, "label": "ز", "popup": {
|
||||
"main": {"code": 1592, "label": "ظ" }
|
||||
} },
|
||||
{ "code": 1582, "label": "خ" },
|
||||
{ "code": 1580, "label": "ج" },
|
||||
{ "code": 1670, "label": "چ" },
|
||||
{ "code": 1581, "label": "ح" },
|
||||
{ "code": 1593, "label": "ع" },
|
||||
{ "code": 1576, "label": "ب" },
|
||||
{ "code": 1606, "label": "ن" },
|
||||
{ "code": 1605, "label": "م" }
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
{ "code": 1605, "label": "م" },
|
||||
{ "code": 1567, "label": "؟" },
|
||||
{ "code": 1548, "label": "،" },
|
||||
{ "code": 46, "label": "." }
|
||||
{ "code": 58, "label": ":" }
|
||||
|
||||
]
|
||||
]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
[
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 1081, "label": "й" },
|
||||
{ "$": "auto_text_key", "code": 1094, "label": "ц" },
|
||||
{ "$": "auto_text_key", "code": 1091, "label": "у" },
|
||||
{ "$": "auto_text_key", "code": 1082, "label": "к" },
|
||||
{ "$": "auto_text_key", "code": 1077, "label": "е" },
|
||||
{ "$": "auto_text_key", "code": 1085, "label": "н" },
|
||||
{ "$": "auto_text_key", "code": 1075, "label": "г" },
|
||||
{ "$": "auto_text_key", "code": 1096, "label": "ш" },
|
||||
{ "$": "auto_text_key", "code": 1097, "label": "щ" },
|
||||
{ "$": "auto_text_key", "code": 1079, "label": "з" },
|
||||
{ "$": "auto_text_key", "code": 1093, "label": "х" },
|
||||
{ "$": "auto_text_key", "code": 1031, "label": "ї" }
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 1092 , "label": "ф" },
|
||||
|
||||
{ "$": "auto_text_key", "code": 1110 , "label": "і" },
|
||||
{ "$": "auto_text_key", "code": 1074 , "label": "в" },
|
||||
{ "$": "auto_text_key", "code": 1072 , "label": "а" },
|
||||
{ "$": "auto_text_key", "code": 1087 , "label": "п" },
|
||||
|
||||
|
||||
{ "$": "auto_text_key", "code": 1088 , "label": "р" },
|
||||
{ "$": "auto_text_key", "code": 1086 , "label": "о" },
|
||||
|
||||
{ "$": "auto_text_key", "code": 1083 , "label": "л" },
|
||||
{ "$": "auto_text_key", "code": 1076 , "label": "д" },
|
||||
{ "$": "auto_text_key", "code": 1078 , "label": "ж" },
|
||||
{ "$": "auto_text_key", "code": 1108 , "label": "є" },
|
||||
{ "$": "auto_text_key", "code": 1067 , "label": "ы" }
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 1169 , "label": "ґ" },
|
||||
{ "$": "auto_text_key", "code": 1103 , "label": "я" },
|
||||
{ "$": "auto_text_key", "code": 1095 , "label": "ч" },
|
||||
{ "$": "auto_text_key", "code": 1089 , "label": "с" },
|
||||
{ "$": "auto_text_key", "code": 1084 , "label": "м" },
|
||||
{ "$": "auto_text_key", "code": 1080 , "label": "и" },
|
||||
{ "$": "auto_text_key", "code": 1090 , "label": "т" },
|
||||
{ "$": "auto_text_key", "code": 1100 , "label": "ь" },
|
||||
{ "$": "auto_text_key", "code": 1073 , "label": "б" },
|
||||
{ "$": "auto_text_key", "code": 1102 , "label": "ю" },
|
||||
{ "$": "auto_text_key", "code": 1025 , "label": "ё" }
|
||||
]
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
[
|
||||
[
|
||||
|
||||
{ "$": "auto_text_key", "code": 1094, "label": "ц" },
|
||||
{ "$": "auto_text_key", "code": 1108 , "label": "є" },
|
||||
|
||||
{ "$": "auto_text_key", "code": 1077, "label": "е" },
|
||||
{ "$": "auto_text_key", "code": 1088 , "label": "р" },
|
||||
{ "$": "auto_text_key", "code": 1090 , "label": "т" },
|
||||
{ "$": "auto_text_key", "code": 1081, "label": "й" },
|
||||
{ "$": "auto_text_key", "code": 1091, "label": "у" },
|
||||
{ "$": "auto_text_key", "code": 1102 , "label": "ю" },
|
||||
{ "$": "auto_text_key", "code": 1110 , "label": "і" },
|
||||
{ "$": "auto_text_key", "code": 1031, "label": "ї" },
|
||||
{ "$": "auto_text_key", "code": 1086 , "label": "о" },
|
||||
{ "$": "auto_text_key", "code": 1025 , "label": "ё" },
|
||||
{ "$": "auto_text_key", "code": 1087 , "label": "п" }
|
||||
],
|
||||
[
|
||||
{ "$": "auto_text_key", "code": 1103 , "label": "я" },
|
||||
{ "$": "auto_text_key", "code": 1072 , "label": "а" },
|
||||
{ "$": "auto_text_key", "code": 1089 , "label": "с" },
|
||||
{ "$": "auto_text_key", "code": 1076 , "label": "д" },
|
||||
{ "$": "auto_text_key", "code": 1092 , "label": "ф" },
|
||||
{ "$": "auto_text_key", "code": 1169 , "label": "ґ" },
|
||||
{ "$": "auto_text_key", "code": 1067 , "label": "ы" },
|
||||
{ "$": "auto_text_key", "code": 1075, "label": "г" },
|
||||
{ "$": "auto_text_key", "code": 1078 , "label": "ж" },
|
||||
{ "$": "auto_text_key", "code": 1082, "label": "к" },
|
||||
{ "$": "auto_text_key", "code": 1083 , "label": "л" },
|
||||
{ "$": "auto_text_key", "code": 1096, "label": "ш" },
|
||||
{ "$": "auto_text_key", "code": 1097, "label": "щ" }
|
||||
],
|
||||
[
|
||||
|
||||
{ "$": "auto_text_key", "code": 1079, "label": "з" },
|
||||
{ "$": "auto_text_key", "code": 1093, "label": "х" },
|
||||
{ "$": "auto_text_key", "code": 1095 , "label": "ч" },
|
||||
{ "$": "auto_text_key", "code": 1074 , "label": "в" },
|
||||
{ "$": "auto_text_key", "code": 1073 , "label": "б" },
|
||||
{ "$": "auto_text_key", "code": 1085, "label": "н" },
|
||||
{ "$": "auto_text_key", "code": 1084 , "label": "м" },
|
||||
{ "$": "auto_text_key", "code": 1080 , "label": "и" },
|
||||
{ "$": "auto_text_key", "code": 1100 , "label": "ь" }
|
||||
]
|
||||
]
|
||||
@@ -4,7 +4,7 @@
|
||||
{ "code": 1608, "label": "و" },
|
||||
{ "code": 1593, "label": "ع" },
|
||||
{ "code": 1585, "label": "ر" },
|
||||
{ "code": 1587, "label": "ت" },
|
||||
{ "code": 1578, "label": "ت" },
|
||||
{ "code": 1746, "label": "ے" },
|
||||
{ "code": 1569, "label": "ء" },
|
||||
{ "code": 1740, "label": "ی" },
|
||||
@@ -27,7 +27,7 @@
|
||||
{ "code": 1588, "label": "ش" },
|
||||
{ "code": 1670, "label": "چ" },
|
||||
{ "code": 1591, "label": "ط" },
|
||||
{ "code": 1576, "label": "پ" },
|
||||
{ "code": 1576, "label": "ب" },
|
||||
{ "code": 1606, "label": "ن" },
|
||||
{ "code": 1605, "label": "م" }
|
||||
]
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
[
|
||||
[
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 45, "label": "-" }
|
||||
],
|
||||
[
|
||||
|
||||
@@ -32,22 +32,42 @@
|
||||
{ "code": 43, "label": "+", "popup": {
|
||||
"main": { "code": 177, "label": "±" }
|
||||
} },
|
||||
{ "code": 40, "label": "(", "popup": {
|
||||
"main": { "code":64830, "label": "﴾" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 60, "label": "<" },
|
||||
{ "code": 123, "label": "{" }
|
||||
]
|
||||
} },
|
||||
{ "code": 41, "label": ")", "popup": {
|
||||
"main": { "code":64831, "label": "﴿" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "]" },
|
||||
{ "code": 62, "label": ">" },
|
||||
{ "code": 125, "label": "}" }
|
||||
]
|
||||
} },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(", "popup": {
|
||||
"main": { "code":64830, "label": "﴾" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 60, "label": "<" },
|
||||
{ "code": 123, "label": "{" }
|
||||
]
|
||||
} },
|
||||
"rtl": { "code": 41, "label": "(", "popup": {
|
||||
"main": { "code":64830, "label": "﴾" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "[" },
|
||||
{ "code": 62, "label": "<" },
|
||||
{ "code": 125, "label": "{" }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")", "popup": {
|
||||
"main": { "code":64831, "label": "﴿" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "]" },
|
||||
{ "code": 62, "label": ">" },
|
||||
{ "code": 125, "label": "}" }
|
||||
]
|
||||
} },
|
||||
"rtl": { "code": 40, "label": ")", "popup": {
|
||||
"main": { "code":64831, "label": "﴿" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "]" },
|
||||
{ "code": 60, "label": ">" },
|
||||
{ "code": 123, "label": "}" }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "code": 47, "label": "/" }
|
||||
],
|
||||
[
|
||||
|
||||
@@ -2,12 +2,24 @@
|
||||
[
|
||||
{ "code": 8230, "label": "…" },
|
||||
{ "code": 95, "label": "_" },
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 93, "label": "]" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 91, "label": "[" },
|
||||
"rtl": { "code": 93, "label": "[" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 93, "label": "]" },
|
||||
"rtl": { "code": 91, "label": "]" }
|
||||
},
|
||||
{ "code": 94, "label": "^" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 60, "label": "<" },
|
||||
{ "code": 62, "label": ">" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 60, "label": "<" },
|
||||
"rtl": { "code": 62, "label": "<" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 62, "label": ">" },
|
||||
"rtl": { "code": 60, "label": ">" }
|
||||
},
|
||||
{ "code": 61, "label": "=" },
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 383, "label": "ſ" }
|
||||
@@ -15,12 +27,24 @@
|
||||
[
|
||||
{ "code": 92, "label": "\\" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 123, "label": "{" },
|
||||
{ "code": 125, "label": "}" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 123, "label": "{" },
|
||||
"rtl": { "code": 125, "label": "{" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 125, "label": "}" },
|
||||
"rtl": { "code": 123, "label": "}" }
|
||||
},
|
||||
{ "code": 42, "label": "*" },
|
||||
{ "code": 63, "label": "?" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 64, "label": "@" }
|
||||
|
||||
@@ -32,22 +32,42 @@
|
||||
{ "code": 43, "label": "+", "popup": {
|
||||
"main": { "code": 177, "label": "±" }
|
||||
} },
|
||||
{ "code": 40, "label": "(", "popup": {
|
||||
"main": { "code":64830, "label": "﴾" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 60, "label": "<" },
|
||||
{ "code": 123, "label": "{" }
|
||||
]
|
||||
} },
|
||||
{ "code": 41, "label": ")", "popup": {
|
||||
"main": { "code":64831, "label": "﴿" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "]" },
|
||||
{ "code": 62, "label": ">" },
|
||||
{ "code": 125, "label": "}" }
|
||||
]
|
||||
} },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(", "popup": {
|
||||
"main": { "code":64830, "label": "﴾" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 60, "label": "<" },
|
||||
{ "code": 123, "label": "{" }
|
||||
]
|
||||
} },
|
||||
"rtl": { "code": 41, "label": "(", "popup": {
|
||||
"main": { "code":64830, "label": "﴾" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "[" },
|
||||
{ "code": 62, "label": "<" },
|
||||
{ "code": 125, "label": "{" }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")", "popup": {
|
||||
"main": { "code":64831, "label": "﴿" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "]" },
|
||||
{ "code": 62, "label": ">" },
|
||||
{ "code": 125, "label": "}" }
|
||||
]
|
||||
} },
|
||||
"rtl": { "code": 40, "label": ")", "popup": {
|
||||
"main": { "code":64831, "label": "﴿" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "]" },
|
||||
{ "code": 60, "label": ">" },
|
||||
{ "code": 123, "label": "}" }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "code": 1643, "label": "٫", "popup": {
|
||||
"main": { "code": 1644, "label": "٬" },
|
||||
"relevant": [
|
||||
|
||||
@@ -31,20 +31,38 @@
|
||||
{ "code": 43, "label": "+", "popup": {
|
||||
"main": { "code": 177, "label": "±" }
|
||||
} },
|
||||
{ "code": 40, "label": "(", "popup": {
|
||||
"main": { "code": 60, "label": "<" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 123, "label": "{" }
|
||||
]
|
||||
} },
|
||||
{ "code": 41, "label": ")", "popup": {
|
||||
"main": { "code": 62, "label": ">" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "]" },
|
||||
{ "code": 125, "label": "}" }
|
||||
]
|
||||
} },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(", "popup": {
|
||||
"main": { "code": 60, "label": "<" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 123, "label": "{" }
|
||||
]
|
||||
} },
|
||||
"rtl": { "code": 41, "label": "(", "popup": {
|
||||
"main": { "code": 62, "label": "<" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "[" },
|
||||
{ "code": 125, "label": "{" }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")", "popup": {
|
||||
"main": { "code": 62, "label": ">" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "]" },
|
||||
{ "code": 125, "label": "}" }
|
||||
]
|
||||
} },
|
||||
"rtl": { "code": 40, "label": ")", "popup": {
|
||||
"main": { "code": 60, "label": ">" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "]" },
|
||||
{ "code": 123, "label": "}" }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "code": 47, "label": "/" }
|
||||
],
|
||||
[
|
||||
|
||||
@@ -43,20 +43,38 @@
|
||||
{ "code": 43, "label": "+", "popup": {
|
||||
"main": { "code": 177, "label": "±" }
|
||||
} },
|
||||
{ "code": 40, "label": "(", "popup": {
|
||||
"main": { "code": 60, "label": "<" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 123, "label": "{" }
|
||||
]
|
||||
} },
|
||||
{ "code": 41, "label": ")", "popup": {
|
||||
"main": { "code": 62, "label": ">" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "]" },
|
||||
{ "code": 125, "label": "}" }
|
||||
]
|
||||
} },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(", "popup": {
|
||||
"main": { "code": 60, "label": "<" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 123, "label": "{" }
|
||||
]
|
||||
} },
|
||||
"rtl": { "code": 41, "label": "(", "popup": {
|
||||
"main": { "code": 62, "label": "<" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "[" },
|
||||
{ "code": 125, "label": "{" }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")", "popup": {
|
||||
"main": { "code": 62, "label": ">" },
|
||||
"relevant": [
|
||||
{ "code": 93, "label": "]" },
|
||||
{ "code": 125, "label": "}" }
|
||||
]
|
||||
} },
|
||||
"rtl": { "code": 40, "label": ")", "popup": {
|
||||
"main": { "code": 60, "label": ">" },
|
||||
"relevant": [
|
||||
{ "code": 91, "label": "]" },
|
||||
{ "code": 123, "label": "}" }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "code": 47, "label": "/" }
|
||||
],
|
||||
[
|
||||
|
||||
@@ -56,12 +56,22 @@
|
||||
{ "code": 8776, "label": "≈" }
|
||||
]
|
||||
} },
|
||||
{ "code": 123, "label": "{", "popup": {
|
||||
"main": { "code": 40, "label": "(" }
|
||||
} },
|
||||
{ "code": 125, "label": "}", "popup": {
|
||||
"main": { "code": 41, "label": ")" }
|
||||
} },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 123, "label": "{", "popup": {
|
||||
"main": { "code": 40, "label": "(" }
|
||||
} },
|
||||
"rtl": { "code": 125, "label": "{", "popup": {
|
||||
"main": { "code": 41, "label": "(" }
|
||||
} }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 125, "label": "}", "popup": {
|
||||
"main": { "code": 41, "label": ")" }
|
||||
} },
|
||||
"rtl": { "code": 123, "label": "}", "popup": {
|
||||
"main": { "code": 40, "label": ")" }
|
||||
} }
|
||||
},
|
||||
{ "code": 92, "label": "\\" }
|
||||
],
|
||||
[
|
||||
@@ -70,7 +80,13 @@
|
||||
{ "code": 174, "label": "®" },
|
||||
{ "code": 8482, "label": "™" },
|
||||
{ "code": 10003, "label": "✓" },
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 93, "label": "]" }
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 91, "label": "[" },
|
||||
"rtl": { "code": 93, "label": "[" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 93, "label": "]" },
|
||||
"rtl": { "code": 91, "label": "]" }
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -56,12 +56,22 @@
|
||||
{ "code": 8776, "label": "≈" }
|
||||
]
|
||||
} },
|
||||
{ "code": 123, "label": "{", "popup": {
|
||||
"main": { "code": 40, "label": "(" }
|
||||
} },
|
||||
{ "code": 125, "label": "}", "popup": {
|
||||
"main": { "code": 41, "label": ")" }
|
||||
} },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 123, "label": "{", "popup": {
|
||||
"main": { "code": 40, "label": "(" }
|
||||
} },
|
||||
"rtl": { "code": 125, "label": "{", "popup": {
|
||||
"main": { "code": 41, "label": "(" }
|
||||
} }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 125, "label": "}", "popup": {
|
||||
"main": { "code": 41, "label": ")" }
|
||||
} },
|
||||
"rtl": { "code": 123, "label": "}", "popup": {
|
||||
"main": { "code": 40, "label": ")" }
|
||||
} }
|
||||
},
|
||||
{ "code": 92, "label": "\\" }
|
||||
],
|
||||
[
|
||||
@@ -70,7 +80,13 @@
|
||||
{ "code": 174, "label": "®" },
|
||||
{ "code": 8482, "label": "™" },
|
||||
{ "code": 10003, "label": "✓" },
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 93, "label": "]" }
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 91, "label": "[" },
|
||||
"rtl": { "code": 93, "label": "[" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 93, "label": "]" },
|
||||
"rtl": { "code": 91, "label": "]" }
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -56,12 +56,22 @@
|
||||
{ "code": 8776, "label": "≈" }
|
||||
]
|
||||
} },
|
||||
{ "code": 123, "label": "{", "popup": {
|
||||
"main": { "code": 40, "label": "(" }
|
||||
} },
|
||||
{ "code": 125, "label": "}", "popup": {
|
||||
"main": { "code": 41, "label": ")" }
|
||||
} },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 123, "label": "{", "popup": {
|
||||
"main": { "code": 40, "label": "(" }
|
||||
} },
|
||||
"rtl": { "code": 125, "label": "{", "popup": {
|
||||
"main": { "code": 41, "label": "(" }
|
||||
} }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 125, "label": "}", "popup": {
|
||||
"main": { "code": 41, "label": ")" }
|
||||
} },
|
||||
"rtl": { "code": 123, "label": "}", "popup": {
|
||||
"main": { "code": 40, "label": ")" }
|
||||
} }
|
||||
},
|
||||
{ "code": 92, "label": "\\" }
|
||||
],
|
||||
[
|
||||
@@ -70,7 +80,13 @@
|
||||
{ "code": 174, "label": "®" },
|
||||
{ "code": 8482, "label": "™" },
|
||||
{ "code": 10003, "label": "✓" },
|
||||
{ "code": 91, "label": "[" },
|
||||
{ "code": 93, "label": "]" }
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 91, "label": "[" },
|
||||
"rtl": { "code": 93, "label": "[" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 93, "label": "]" },
|
||||
"rtl": { "code": 91, "label": "]" }
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -6,24 +6,44 @@
|
||||
],
|
||||
[
|
||||
{ "code": -201, "label": "view_characters", "type": "system_gui" },
|
||||
{ "code": 60, "label": "<", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 171, "label": "«" },
|
||||
{ "code": 8804, "label": "≤" },
|
||||
{ "code": 8249, "label": "‹" },
|
||||
{ "code":10216, "label": "⟨" }
|
||||
]
|
||||
} },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 60, "label": "<", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 171, "label": "«" },
|
||||
{ "code": 8804, "label": "≤" },
|
||||
{ "code": 8249, "label": "‹" },
|
||||
{ "code":10216, "label": "⟨" }
|
||||
]
|
||||
} },
|
||||
"rtl": { "code": 62, "label": "<", "popup": {
|
||||
"relevant": [
|
||||
{ "code": 187, "label": "«" },
|
||||
{ "code": 8805, "label": "≤" },
|
||||
{ "code": 8250, "label": "‹" },
|
||||
{ "code":10217, "label": "⟨" }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "code": -205, "label": "view_numeric_advanced", "type": "system_gui" },
|
||||
{ "code": 32, "label": "space" },
|
||||
{ "code": 62, "label": ">", "popup": {
|
||||
"relevant": [
|
||||
{ "code":10217, "label": "⟩" },
|
||||
{ "code": 8250, "label": "›" },
|
||||
{ "code": 8805, "label": "≥" },
|
||||
{ "code": 187, "label": "»" }
|
||||
]
|
||||
} },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 62, "label": ">", "popup": {
|
||||
"relevant": [
|
||||
{ "code":10217, "label": "⟩" },
|
||||
{ "code": 8250, "label": "›" },
|
||||
{ "code": 8805, "label": "≥" },
|
||||
{ "code": 187, "label": "»" }
|
||||
]
|
||||
} },
|
||||
"rtl": { "code": 60, "label": ">", "popup": {
|
||||
"relevant": [
|
||||
{ "code":10216, "label": "⟩" },
|
||||
{ "code": 8249, "label": "›" },
|
||||
{ "code": 8804, "label": "≥" },
|
||||
{ "code": 171, "label": "»" }
|
||||
]
|
||||
} }
|
||||
},
|
||||
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
|
||||
]
|
||||
]
|
||||
|
||||
@@ -150,6 +150,10 @@
|
||||
"id": "ru",
|
||||
"authors": [ "williamtheaker", "33kk" ]
|
||||
},
|
||||
{
|
||||
"id": "rue",
|
||||
"authors": [ "svvvst" ]
|
||||
},
|
||||
{
|
||||
"id": "sk",
|
||||
"authors": [ "stefan-misik", "majso" ]
|
||||
@@ -467,6 +471,15 @@
|
||||
"characters": "org.florisboard.layouts:jcuken_russian"
|
||||
}
|
||||
},
|
||||
{
|
||||
"languageTag": "rue",
|
||||
"composer": "org.florisboard.composers:appender",
|
||||
"currencySet": "org.florisboard.currencysets:euro",
|
||||
"popupMapping": "org.florisboard.localization:rue",
|
||||
"preferred": {
|
||||
"characters": "org.florisboard.layouts:rusyn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"languageTag": "uk",
|
||||
"composer": "org.florisboard.composers:appender",
|
||||
|
||||
@@ -18,8 +18,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -91,8 +91,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -88,8 +88,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -106,8 +106,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -85,8 +85,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -52,8 +52,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -80,8 +80,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -63,8 +63,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -88,8 +88,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -91,8 +91,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -111,8 +111,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -94,8 +94,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -34,8 +34,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -42,8 +42,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -83,8 +83,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -76,8 +76,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -13,8 +13,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -48,8 +48,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
]
|
||||
},
|
||||
"s": {
|
||||
"main": { "$": "auto_text_key", "code": 219, "label": "ș" },
|
||||
"main": { "$": "auto_text_key", "code": 351, "label": "ş" },
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 347, "label": "ś" },
|
||||
{ "$": "auto_text_key", "code": 349, "label": "ŝ" },
|
||||
@@ -102,8 +102,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -140,8 +140,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -80,8 +80,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -72,8 +72,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -82,8 +82,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -60,8 +60,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -81,8 +81,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -81,8 +81,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -1,19 +1,43 @@
|
||||
{
|
||||
"all": {
|
||||
"a": {
|
||||
"main": { "$": "auto_text_key", "code": 259, "label": "ă" },
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 259, "label": "ă" },
|
||||
{ "$": "auto_text_key", "code": 226, "label": "â" }
|
||||
]
|
||||
},
|
||||
"c": {
|
||||
"relevant": [
|
||||
{ "code": 8217, "label": "’" }
|
||||
]
|
||||
},
|
||||
"h": {
|
||||
"relevant": [
|
||||
{ "code": 8209, "label": "‑" },
|
||||
{ "code": 8211, "label": "–" },
|
||||
{ "code": 8212, "label": "—" }
|
||||
]
|
||||
},
|
||||
"i": {
|
||||
"main": { "$": "auto_text_key", "code": 238, "label": "î" }
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 238, "label": "î" }
|
||||
]
|
||||
},
|
||||
"s": {
|
||||
"main": { "$": "auto_text_key", "code": 537, "label": "ș" }
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 537, "label": "ș" }
|
||||
]
|
||||
},
|
||||
"t": {
|
||||
"main": { "$": "auto_text_key", "code": 539, "label": "ț" }
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 539, "label": "ț" }
|
||||
]
|
||||
},
|
||||
"x": {
|
||||
"relevant": [
|
||||
{ "code": 8222, "label": "„" },
|
||||
{ "code": 8221, "label": "”" }
|
||||
]
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
@@ -28,8 +52,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
@@ -40,10 +70,10 @@
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".ro" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".eu" },
|
||||
{ "code": -255, "label": ".net" },
|
||||
{ "code": -255, "label": ".edu" }
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".ro" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"all": {
|
||||
"е": {
|
||||
"main": { "$": "auto_text_key", "code": 234, "label": "ê" },
|
||||
"relevant": [{ "$": "auto_text_key", "code": 1105, "label": "ё" }]
|
||||
},
|
||||
"у": {
|
||||
"main": { "$": "auto_text_key", "code": 1263, "label": "ӯ" },
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 1118, "label": "ў" },
|
||||
{ "$": "auto_text_key", "code": 1265, "label": "ӱ" },
|
||||
{ "$": "auto_text_key", "code": 375, "label": "ŷ" }
|
||||
]
|
||||
},
|
||||
"г": {
|
||||
"main": { "$": "auto_text_key", "code": 1169, "label": "ґ" }
|
||||
},
|
||||
"і": {
|
||||
"main": { "$": "auto_text_key", "code": 1123, "label": "î" },
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 1123, "label": "ѣ" },
|
||||
{ "$": "auto_text_key", "code": 1111, "label": "ї" }
|
||||
]
|
||||
},
|
||||
"о": {
|
||||
"main": { "$": "auto_text_key", "code": 333, "label": "ō" },
|
||||
"relevant": [{ "$": "auto_text_key", "code": 244, "label": "ô" }]
|
||||
},
|
||||
"ь": {
|
||||
"main": { "$": "auto_text_key", "code": 1098, "label": "ъ" }
|
||||
},
|
||||
"~right": {
|
||||
"main": { "code": 44, "label": "," },
|
||||
"relevant": [
|
||||
{ "code": 38, "label": "&" },
|
||||
{ "code": 37, "label": "%" },
|
||||
{ "code": 43, "label": "+" },
|
||||
{ "code": 34, "label": "\"" },
|
||||
{ "code": 45, "label": "-" },
|
||||
{ "code": 58, "label": ":" },
|
||||
{ "code": 39, "label": "'" },
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": {
|
||||
"~right": {
|
||||
"main": { "code": -255, "label": ".com" },
|
||||
"relevant": [
|
||||
{ "code": -255, "label": ".edu" },
|
||||
{ "code": -255, "label": ".org" },
|
||||
{ "code": -255, "label": ".net" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,8 +89,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -32,8 +32,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -133,8 +133,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -40,8 +40,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -18,8 +18,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
@@ -40,8 +40,14 @@
|
||||
{ "code": 64, "label": "@" },
|
||||
{ "code": 59, "label": ";" },
|
||||
{ "code": 47, "label": "/" },
|
||||
{ "code": 40, "label": "(" },
|
||||
{ "code": 41, "label": ")" },
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 40, "label": "(" },
|
||||
"rtl": { "code": 41, "label": "(" }
|
||||
},
|
||||
{ "$": "layout_direction_selector",
|
||||
"ltr": { "code": 41, "label": ")" },
|
||||
"rtl": { "code": 40, "label": ")" }
|
||||
},
|
||||
{ "code": 35, "label": "#" },
|
||||
{ "code": 33, "label": "!" },
|
||||
{ "code": 63, "label": "?" }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
3642
app/src/main/assets/ime/media/emoji/root.txt
Normal file
3642
app/src/main/assets/ime/media/emoji/root.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"$": "ime.extension.theme",
|
||||
"meta": {
|
||||
"id": "org.florisboard.theme.floris_night",
|
||||
"version": "0.1.0",
|
||||
"title": "Floris Night",
|
||||
"description": "Default dark/night theme for the keyboard UI",
|
||||
"authors": [ "patrickgold <patrick@patrickgold.dev>" ],
|
||||
"license": "apache-2.0"
|
||||
},
|
||||
"theme": {
|
||||
"isNight": true,
|
||||
"isMaterialYouAware": false,
|
||||
"isBorderless": false,
|
||||
"stylesheet": "floris_night.json"
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"navigationBar": {
|
||||
"background": "transparent"
|
||||
},
|
||||
"window": {
|
||||
"background": "#212121",
|
||||
"foreground": "#FFFFFF"
|
||||
},
|
||||
"key": {
|
||||
"background": "#424242",
|
||||
"foreground": "#616161"
|
||||
},
|
||||
"key:pressed": {
|
||||
"background": "#616161",
|
||||
"foreground": "#616161"
|
||||
},
|
||||
"key[code={c:enter}]": {
|
||||
"background": "#4CAF50",
|
||||
"foreground": "#FFFFFF"
|
||||
},
|
||||
"key[code={c:enter}]:pressed": {
|
||||
"background": "#388E3C",
|
||||
"foreground": "#FFFFFF"
|
||||
},
|
||||
"key[code={c:shift}][inputmode={m:capslock}]": {
|
||||
"foreground": "#FF9800"
|
||||
},
|
||||
"key[code={c:shift}][inputmode={m:capslock}]:pressed": {
|
||||
"foreground": "#FF9800"
|
||||
},
|
||||
"media": {
|
||||
"foreground": "#FFFFFF",
|
||||
"foregroundAlt": "#BDBDBD"
|
||||
},
|
||||
"oneHanded": {
|
||||
"background": "#1B5E20",
|
||||
"foreground": "#EEEEEE"
|
||||
},
|
||||
"popupWindow": {
|
||||
"background": "#757575",
|
||||
"foreground": "#FFFFFF"
|
||||
},
|
||||
"popupKey:focus": {
|
||||
"background": "#BDBDBD"
|
||||
},
|
||||
"privateMode": {
|
||||
"background": "#A000FF",
|
||||
"foreground": "#FFFFFF"
|
||||
},
|
||||
"smartbar": {
|
||||
"background": "transparent",
|
||||
"foreground": "#FFFFFF",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"smartbarButton": {
|
||||
"background": "@key/background",
|
||||
"foreground": "@key/foreground"
|
||||
},
|
||||
"extractEditLayout": {
|
||||
"background": "#282828",
|
||||
"foreground": "#FFFFFF",
|
||||
"foregroundAlt": "#73FFFFFF"
|
||||
},
|
||||
"extractActionButton": {
|
||||
"background": "@smartbarButton/background",
|
||||
"foreground": "@smartbarButton/foreground"
|
||||
},
|
||||
"glideTrail": {
|
||||
"foreground": "#204CAF50"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$": "ime.extension.theme",
|
||||
"meta": {
|
||||
"id": "org.florisboard.themes",
|
||||
"version": "0.1.0",
|
||||
"title": "FlorisBoard default themes",
|
||||
"description": "Default themes (both day and night) for the keyboard UI",
|
||||
"maintainers": [ "patrickgold <patrick@patrickgold.dev>" ],
|
||||
"license": "apache-2.0"
|
||||
},
|
||||
"themes": [
|
||||
{
|
||||
"id": "floris_day",
|
||||
"label": "Floris Day",
|
||||
"authors": [ "patrickgold" ],
|
||||
"isNight": false,
|
||||
"isBorderless": false,
|
||||
"isMaterialYouAware": false
|
||||
},
|
||||
{
|
||||
"id": "floris_night",
|
||||
"label": "Floris Night",
|
||||
"authors": [ "patrickgold" ],
|
||||
"isNight": true,
|
||||
"isBorderless": false,
|
||||
"isMaterialYouAware": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"@defines": {
|
||||
"--primary": "#4caf50",
|
||||
"--primary-variant": "#388e3c",
|
||||
"--secondary": "#ff9800",
|
||||
"--secondary-variant": "#e65100",
|
||||
"--background": "#e0e0e0",
|
||||
"--surface": "#ffffff",
|
||||
"--surface-variant": "#f5f5f5",
|
||||
|
||||
"--on-background": "#121212",
|
||||
"--on-surface": "#000000",
|
||||
"--on-surface-variant": "#5f5f5f",
|
||||
|
||||
"--shape": "rounded-corner(8dp, 8dp, 8dp, 8dp)",
|
||||
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
|
||||
},
|
||||
|
||||
"keyboard": {
|
||||
"background": "var(--background)"
|
||||
},
|
||||
|
||||
"key": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "22sp",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"key:pressed": {
|
||||
"background": "var(--surface-variant)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"key[code={c:enter}]": {
|
||||
"background": "var(--primary)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"key[code={c:enter}]:pressed": {
|
||||
"background": "var(--primary-variant)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"key[code={c:shift}][mode={m:capslock}]": {
|
||||
"foreground": "var(--secondary)"
|
||||
},
|
||||
"key[code={c:space}]": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"font-size": "12sp"
|
||||
},
|
||||
"key-hint": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"font-size": "12sp"
|
||||
},
|
||||
"key-popup": {
|
||||
"background": "#eeeeee",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "22sp",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"key-popup:focus": {
|
||||
"background": "#bdbdbd",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
|
||||
"smartbar-primary-actions-toggle": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-secondary-actions-toggle": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-quick-action": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-key:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#12121248"
|
||||
},
|
||||
"smartbar-candidate-word": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rectangle()"
|
||||
},
|
||||
"smartbar-candidate-word:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-background)"
|
||||
},
|
||||
"smartbar-candidate-clip": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
|
||||
},
|
||||
"smartbar-candidate-clip:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-background)"
|
||||
},
|
||||
"smartbar-candidate-spacer": {
|
||||
"foreground": "var(--surface)"
|
||||
},
|
||||
|
||||
"clipboard-header": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"clipboard-item": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape-variant)"
|
||||
},
|
||||
"clipboard-item-popup": {
|
||||
"background": "var(--surface-variant)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape-variant)"
|
||||
},
|
||||
|
||||
"emoji-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "22sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"emoji-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"emoji-key-popup": {
|
||||
"background": "#eeeeee",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "22sp",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"emoji-tab": {
|
||||
"foreground": "var(--on-background)"
|
||||
},
|
||||
"emoji-tab:focus": {
|
||||
"foreground": "var(--primary)"
|
||||
},
|
||||
|
||||
"glide-trail": {
|
||||
"foreground": "var(--primary)"
|
||||
},
|
||||
|
||||
"one-handed-panel": {
|
||||
"background": "#e8f5e9",
|
||||
"foreground": "#424242"
|
||||
},
|
||||
|
||||
"system-nav-bar": {
|
||||
"background": "var(--background)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"@defines": {
|
||||
"--primary": "#4caf50",
|
||||
"--primary-variant": "#388e3c",
|
||||
"--secondary": "#f57c00",
|
||||
"--secondary-variant": "#e65100",
|
||||
"--background": "#212121",
|
||||
"--surface": "#424242",
|
||||
"--surface-variant": "#616161",
|
||||
|
||||
"--on-background": "#dcdcdc",
|
||||
"--on-surface": "#ffffff",
|
||||
"--on-surface-variant": "#a0a0a0",
|
||||
|
||||
"--shape": "rounded-corner(8dp, 8dp, 8dp, 8dp)",
|
||||
"--shape-variant": "rounded-corner(12dp, 12dp, 12dp, 12dp)"
|
||||
},
|
||||
|
||||
"keyboard": {
|
||||
"background": "var(--background)"
|
||||
},
|
||||
|
||||
"key": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "22sp",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"key:pressed": {
|
||||
"background": "var(--surface-variant)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"key[code={c:enter}]": {
|
||||
"background": "var(--primary)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"key[code={c:enter}]:pressed": {
|
||||
"background": "var(--primary-variant)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"key[code={c:shift}][mode={m:capslock}]": {
|
||||
"foreground": "var(--secondary)"
|
||||
},
|
||||
"key[code={c:space}]": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"font-size": "12sp"
|
||||
},
|
||||
"key-hint": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"font-size": "12sp"
|
||||
},
|
||||
"key-popup": {
|
||||
"background": "#757575",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "22sp",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"key-popup:focus": {
|
||||
"background": "#bdbdbd",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
|
||||
"smartbar-primary-actions-toggle": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-secondary-actions-toggle": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface-variant)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-quick-action": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"shape": "circle()"
|
||||
},
|
||||
"smartbar-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "18sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"smartbar-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"smartbar-key:disabled": {
|
||||
"background": "transparent",
|
||||
"foreground": "#dcdcdc48"
|
||||
},
|
||||
"smartbar-candidate-word": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rectangle()"
|
||||
},
|
||||
"smartbar-candidate-word:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-background)"
|
||||
},
|
||||
"smartbar-candidate-clip": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "14sp",
|
||||
"shape": "rounded-corner(8%, 8%, 8%, 8%)"
|
||||
},
|
||||
"smartbar-candidate-clip:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-background)"
|
||||
},
|
||||
"smartbar-candidate-spacer": {
|
||||
"foreground": "var(--surface)"
|
||||
},
|
||||
|
||||
"clipboard-header": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "16sp"
|
||||
},
|
||||
"clipboard-item": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape-variant)"
|
||||
},
|
||||
"clipboard-item-popup": {
|
||||
"background": "var(--surface-variant)",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "14sp",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape-variant)"
|
||||
},
|
||||
|
||||
"emoji-key": {
|
||||
"background": "transparent",
|
||||
"foreground": "var(--on-background)",
|
||||
"font-size": "22sp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"emoji-key:pressed": {
|
||||
"background": "var(--surface)",
|
||||
"foreground": "var(--on-surface)"
|
||||
},
|
||||
"emoji-key-popup": {
|
||||
"background": "#757575",
|
||||
"foreground": "var(--on-surface)",
|
||||
"font-size": "22sp",
|
||||
"shadow-elevation": "2dp",
|
||||
"shape": "var(--shape)"
|
||||
},
|
||||
"emoji-tab": {
|
||||
"foreground": "var(--on-background)"
|
||||
},
|
||||
"emoji-tab:focus": {
|
||||
"foreground": "var(--primary)"
|
||||
},
|
||||
|
||||
"glide-trail": {
|
||||
"foreground": "var(--primary)"
|
||||
},
|
||||
|
||||
"one-handed-panel": {
|
||||
"background": "#1b5e20",
|
||||
"foreground": "#eeeeee"
|
||||
},
|
||||
|
||||
"system-nav-bar": {
|
||||
"background": "var(--background)"
|
||||
}
|
||||
}
|
||||
@@ -24,26 +24,28 @@ import android.content.IntentFilter
|
||||
import androidx.core.os.UserManagerCompat
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.common.NativeStr
|
||||
import dev.patrickgold.florisboard.common.android.AndroidVersion
|
||||
import dev.patrickgold.florisboard.common.toNativeStr
|
||||
import dev.patrickgold.florisboard.crashutility.CrashUtility
|
||||
import dev.patrickgold.florisboard.debug.Flog
|
||||
import dev.patrickgold.florisboard.debug.LogTopic
|
||||
import dev.patrickgold.florisboard.debug.flogError
|
||||
import dev.patrickgold.florisboard.debug.flogInfo
|
||||
import dev.patrickgold.florisboard.ime.clipboard.ClipboardManager
|
||||
import dev.patrickgold.florisboard.ime.core.SubtypeManager
|
||||
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardManager
|
||||
import dev.patrickgold.florisboard.ime.nlp.NlpManager
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingManager
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingService
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.res.AssetManager
|
||||
import dev.patrickgold.florisboard.res.cache.CacheManager
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionManager
|
||||
import dev.patrickgold.florisboard.common.android.AndroidVersion
|
||||
import dev.patrickgold.florisboard.ime.clipboard.ClipboardManager
|
||||
import dev.patrickgold.florisboard.ime.nlp.NlpManager
|
||||
import dev.patrickgold.jetpref.datastore.JetPrefManager
|
||||
import dev.patrickgold.florisboard.res.io.deleteContentsRecursively
|
||||
import dev.patrickgold.jetpref.datastore.JetPref
|
||||
import java.io.File
|
||||
import kotlin.Exception
|
||||
|
||||
@Suppress("unused")
|
||||
class FlorisApplication : Application() {
|
||||
@@ -63,18 +65,21 @@ class FlorisApplication : Application() {
|
||||
private val prefs by florisPreferenceModel()
|
||||
|
||||
val assetManager = lazy { AssetManager(this) }
|
||||
val cacheManager = lazy { CacheManager(this) }
|
||||
val clipboardManager = lazy { ClipboardManager(this) }
|
||||
val extensionManager = lazy { ExtensionManager(this) }
|
||||
val glideTypingManager = lazy { GlideTypingManager(this) }
|
||||
val keyboardManager = lazy { KeyboardManager(this) }
|
||||
val nlpManager = lazy { NlpManager(this) }
|
||||
val spellingManager = lazy { SpellingManager(this) }
|
||||
val spellingService = lazy { SpellingService(this) }
|
||||
val subtypeManager = lazy { SubtypeManager(this) }
|
||||
val themeManager = lazy { ThemeManager(this) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
try {
|
||||
JetPrefManager.init(saveIntervalMs = 1_000)
|
||||
JetPref.configure(saveIntervalMs = 500)
|
||||
Flog.install(
|
||||
context = this,
|
||||
isFloggingEnabled = BuildConfig.DEBUG,
|
||||
@@ -87,16 +92,16 @@ class FlorisApplication : Application() {
|
||||
if (AndroidVersion.ATLEAST_API24_N && !UserManagerCompat.isUserUnlocked(this)) {
|
||||
val context = createDeviceProtectedStorageContext()
|
||||
initICU(context)
|
||||
prefs.initializeForContext(context)
|
||||
prefs.initializeBlocking(context)
|
||||
registerReceiver(BootComplete(), IntentFilter(Intent.ACTION_USER_UNLOCKED))
|
||||
} else {
|
||||
initICU(this)
|
||||
prefs.initializeForContext(this)
|
||||
cacheDir?.deleteContentsRecursively()
|
||||
prefs.initializeBlocking(this)
|
||||
clipboardManager.value.initializeForContext(this)
|
||||
}
|
||||
|
||||
DictionaryManager.init(this)
|
||||
ThemeManager.init(this, assetManager.value)
|
||||
} catch (e: Exception) {
|
||||
CrashUtility.stageException(e)
|
||||
return
|
||||
@@ -134,7 +139,8 @@ class FlorisApplication : Application() {
|
||||
} catch (e: Exception) {
|
||||
flogError { e.toString() }
|
||||
}
|
||||
prefs.initializeForContext(this@FlorisApplication)
|
||||
cacheDir?.deleteContentsRecursively()
|
||||
prefs.initializeBlocking(this@FlorisApplication)
|
||||
clipboardManager.value.initializeForContext(this@FlorisApplication)
|
||||
}
|
||||
}
|
||||
@@ -152,10 +158,14 @@ fun Context.appContext() = lazy { this.florisApplication() }
|
||||
|
||||
fun Context.assetManager() = lazy { this.florisApplication().assetManager.value }
|
||||
|
||||
fun Context.cacheManager() = lazy { this.florisApplication().cacheManager.value }
|
||||
|
||||
fun Context.clipboardManager() = lazy { this.florisApplication().clipboardManager.value }
|
||||
|
||||
fun Context.extensionManager() = lazy { this.florisApplication().extensionManager.value }
|
||||
|
||||
fun Context.glideTypingManager() = lazy { this.florisApplication().glideTypingManager.value }
|
||||
|
||||
fun Context.keyboardManager() = lazy { this.florisApplication().keyboardManager.value }
|
||||
|
||||
fun Context.nlpManager() = lazy { this.florisApplication().nlpManager.value }
|
||||
@@ -165,3 +175,5 @@ fun Context.spellingManager() = lazy { this.florisApplication().spellingManager.
|
||||
fun Context.spellingService() = lazy { this.florisApplication().spellingService.value }
|
||||
|
||||
fun Context.subtypeManager() = lazy { this.florisApplication().subtypeManager.value }
|
||||
|
||||
fun Context.themeManager() = lazy { this.florisApplication().themeManager.value }
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
package dev.patrickgold.florisboard
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Size
|
||||
@@ -35,21 +37,23 @@ import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
@@ -59,11 +63,13 @@ import dev.patrickgold.florisboard.app.FlorisAppActivity
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.app.res.ProvideLocalizedResources
|
||||
import dev.patrickgold.florisboard.app.ui.components.SystemUiIme
|
||||
import dev.patrickgold.florisboard.app.ui.devtools.DevtoolsOverlay
|
||||
import dev.patrickgold.florisboard.common.ViewUtils
|
||||
import dev.patrickgold.florisboard.common.android.AndroidVersion
|
||||
import dev.patrickgold.florisboard.common.android.isOrientationLandscape
|
||||
import dev.patrickgold.florisboard.common.android.isOrientationPortrait
|
||||
import dev.patrickgold.florisboard.common.android.launchActivity
|
||||
import dev.patrickgold.florisboard.common.android.setLocale
|
||||
import dev.patrickgold.florisboard.common.android.systemServiceOrNull
|
||||
import dev.patrickgold.florisboard.common.observeAsTransformingState
|
||||
import dev.patrickgold.florisboard.debug.LogTopic
|
||||
@@ -78,13 +84,13 @@ import dev.patrickgold.florisboard.ime.keyboard.InputFeedbackController
|
||||
import dev.patrickgold.florisboard.ime.keyboard.LocalInputFeedbackController
|
||||
import dev.patrickgold.florisboard.ime.keyboard.ProvideKeyboardRowBaseHeight
|
||||
import dev.patrickgold.florisboard.ime.lifecycle.LifecycleInputMethodService
|
||||
import dev.patrickgold.florisboard.ime.media.MediaInputLayout
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel
|
||||
import dev.patrickgold.florisboard.ime.text.TextInputLayout
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SecondaryRowPlacement
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
|
||||
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.snygg.ui.SnyggSurface
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
import java.lang.ref.WeakReference
|
||||
@@ -133,12 +139,25 @@ class FlorisImeService : LifecycleInputMethodService(), EditorInstance.WordHisto
|
||||
}
|
||||
|
||||
fun showUi() {
|
||||
val ims = FlorisImeServiceReference.get() ?: return
|
||||
if (AndroidVersion.ATLEAST_API28_P) {
|
||||
FlorisImeServiceReference.get()?.requestShowSelf(0)
|
||||
ims.requestShowSelf(0)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
ims.systemServiceOrNull(InputMethodManager::class)
|
||||
?.showSoftInputFromInputMethod(ims.currentInputBinding.connectionToken, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideUi() {
|
||||
val ims = FlorisImeServiceReference.get() ?: return
|
||||
if (AndroidVersion.ATLEAST_API28_P) {
|
||||
ims.requestHideSelf(0)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
ims.systemServiceOrNull(InputMethodManager::class)
|
||||
?.hideSoftInputFromInputMethod(ims.currentInputBinding.connectionToken, 0)
|
||||
}
|
||||
FlorisImeServiceReference.get()?.requestHideSelf(0)
|
||||
}
|
||||
|
||||
@@ -182,21 +201,28 @@ class FlorisImeService : LifecycleInputMethodService(), EditorInstance.WordHisto
|
||||
}
|
||||
|
||||
private val prefs by florisPreferenceModel()
|
||||
private val clipboardManager by clipboardManager()
|
||||
private val keyboardManager by keyboardManager()
|
||||
private val nlpManager by nlpManager()
|
||||
private val subtypeManager by subtypeManager()
|
||||
private val themeManager by themeManager()
|
||||
|
||||
private val activeEditorInstance by lazy { EditorInstance(this) }
|
||||
private val activeState get() = keyboardManager.activeState
|
||||
private var inputWindowView: View? = null
|
||||
private var inputViewSize: IntSize = IntSize.Zero
|
||||
private var inputWindowView by mutableStateOf<View?>(null)
|
||||
private var inputViewSize by mutableStateOf(IntSize.Zero)
|
||||
private val inputFeedbackController by lazy { InputFeedbackController.new(this) }
|
||||
private var isWindowShown: Boolean = false
|
||||
private var resourcesContext by mutableStateOf(this as Context)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
FlorisImeServiceReference = WeakReference(this)
|
||||
activeEditorInstance.wordHistoryChangedListener = this
|
||||
subtypeManager.activeSubtype.observe(this) { subtype ->
|
||||
val config = Configuration(resources.configuration)
|
||||
config.setLocale(subtype.primaryLocale)
|
||||
resourcesContext = createConfigurationContext(config)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateInputView(): View {
|
||||
@@ -301,6 +327,7 @@ class FlorisImeService : LifecycleInputMethodService(), EditorInstance.WordHisto
|
||||
flogInfo(LogTopic.IMS_EVENTS)
|
||||
}
|
||||
isWindowShown = true
|
||||
themeManager.updateActiveTheme()
|
||||
}
|
||||
|
||||
override fun onWindowHidden() {
|
||||
@@ -330,7 +357,7 @@ class FlorisImeService : LifecycleInputMethodService(), EditorInstance.WordHisto
|
||||
flogInfo(LogTopic.IMS_EVENTS) {
|
||||
"Creating inline suggestions request because Smartbar and inline suggestions are enabled."
|
||||
}
|
||||
val stylesBundle = ThemeManager.createInlineSuggestionUiStyleBundle(this)
|
||||
val stylesBundle = themeManager.createInlineSuggestionUiStyleBundle(this)
|
||||
val spec = InlinePresentationSpec.Builder(InlineSuggestionUiSmallestSize, InlineSuggestionUiBiggestSize)
|
||||
.setStyle(stylesBundle)
|
||||
.build()
|
||||
@@ -371,9 +398,9 @@ class FlorisImeService : LifecycleInputMethodService(), EditorInstance.WordHisto
|
||||
val visibleTopY = inputWindowView.height - inputViewSize.height
|
||||
val needAdditionalOverlay =
|
||||
prefs.smartbar.enabled.get() &&
|
||||
prefs.smartbar.secondaryRowEnabled.get() &&
|
||||
prefs.smartbar.secondaryRowExpanded.get() &&
|
||||
prefs.smartbar.secondaryRowPlacement.get() == SecondaryRowPlacement.OVERLAY_APP_UI &&
|
||||
prefs.smartbar.secondaryActionsEnabled.get() &&
|
||||
prefs.smartbar.secondaryActionsExpanded.get() &&
|
||||
prefs.smartbar.secondaryActionsPlacement.get() == SecondaryRowPlacement.OVERLAY_APP_UI &&
|
||||
keyboardManager.activeState.imeUiMode == ImeUiMode.TEXT
|
||||
|
||||
outInsets.contentTopInsets = visibleTopY
|
||||
@@ -407,17 +434,14 @@ class FlorisImeService : LifecycleInputMethodService(), EditorInstance.WordHisto
|
||||
|
||||
@Composable
|
||||
private fun ImeUiWrapper() {
|
||||
ProvideLocalizedResources(this) {
|
||||
ProvideLocalizedResources(resourcesContext) {
|
||||
ProvideKeyboardRowBaseHeight {
|
||||
CompositionLocalProvider(
|
||||
LocalInputFeedbackController provides inputFeedbackController,
|
||||
LocalLayoutDirection provides LayoutDirection.Ltr,
|
||||
) {
|
||||
CompositionLocalProvider(LocalInputFeedbackController provides inputFeedbackController) {
|
||||
FlorisImeTheme {
|
||||
// Outer box is necessary as an "outer window"
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
DevtoolsUi()
|
||||
ImeUi()
|
||||
DevtoolsOverlays()
|
||||
}
|
||||
SystemUiIme()
|
||||
}
|
||||
@@ -429,78 +453,91 @@ class FlorisImeService : LifecycleInputMethodService(), EditorInstance.WordHisto
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun BoxScope.ImeUi() {
|
||||
val keyboardStyle = FlorisImeTheme.style.get(FlorisImeUi.Keyboard)
|
||||
SnyggSurface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.BottomStart)
|
||||
.onGloballyPositioned { coords -> inputViewSize = coords.size }
|
||||
// Do not remove below line or touch input may get stuck
|
||||
.pointerInteropFilter { false },
|
||||
background = keyboardStyle.background,
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val bottomOffset by if (configuration.isOrientationPortrait()) {
|
||||
prefs.keyboard.bottomOffsetPortrait
|
||||
} else {
|
||||
prefs.keyboard.bottomOffsetLandscape
|
||||
}.observeAsTransformingState { it.dp }
|
||||
Row(
|
||||
val activeState by keyboardManager.observeActiveState()
|
||||
val keyboardStyle = FlorisImeTheme.style.get(
|
||||
element = FlorisImeUi.Keyboard,
|
||||
mode = activeState.inputMode.value,
|
||||
)
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
SideEffect {
|
||||
keyboardManager.activeState.layoutDirection = layoutDirection
|
||||
}
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
SnyggSurface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
// FIXME: removing this fixes the Smartbar sizing but breaks one-handed-mode
|
||||
//.height(IntrinsicSize.Min)
|
||||
.padding(bottom = bottomOffset),
|
||||
.align(Alignment.BottomStart)
|
||||
.onGloballyPositioned { coords -> inputViewSize = coords.size }
|
||||
// Do not remove below line or touch input may get stuck
|
||||
.pointerInteropFilter { false },
|
||||
style = keyboardStyle,
|
||||
) {
|
||||
val oneHandedMode by prefs.keyboard.oneHandedMode.observeAsState()
|
||||
val oneHandedModeScaleFactor by prefs.keyboard.oneHandedModeScaleFactor.observeAsState()
|
||||
val keyboardWeight = when {
|
||||
oneHandedMode == OneHandedMode.OFF || configuration.isOrientationLandscape() -> 1f
|
||||
else -> oneHandedModeScaleFactor / 100f
|
||||
}
|
||||
if (oneHandedMode == OneHandedMode.END && configuration.isOrientationPortrait()) {
|
||||
OneHandedPanel(
|
||||
panelSide = OneHandedMode.START,
|
||||
weight = 1f - keyboardWeight,
|
||||
)
|
||||
}
|
||||
Box(
|
||||
val configuration = LocalConfiguration.current
|
||||
val bottomOffset by if (configuration.isOrientationPortrait()) {
|
||||
prefs.keyboard.bottomOffsetPortrait
|
||||
} else {
|
||||
prefs.keyboard.bottomOffsetLandscape
|
||||
}.observeAsTransformingState { it.dp }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(keyboardWeight)
|
||||
.wrapContentHeight(),
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
// FIXME: removing this fixes the Smartbar sizing but breaks one-handed-mode
|
||||
//.height(IntrinsicSize.Min)
|
||||
.padding(bottom = bottomOffset),
|
||||
) {
|
||||
val activeState by keyboardManager.observeActiveState()
|
||||
when (activeState.imeUiMode) {
|
||||
ImeUiMode.TEXT -> TextInputLayout()
|
||||
ImeUiMode.MEDIA -> {}
|
||||
ImeUiMode.CLIPBOARD -> ClipboardInputLayout()
|
||||
val oneHandedMode by prefs.keyboard.oneHandedMode.observeAsState()
|
||||
val oneHandedModeScaleFactor by prefs.keyboard.oneHandedModeScaleFactor.observeAsState()
|
||||
val keyboardWeight = when {
|
||||
oneHandedMode == OneHandedMode.OFF || configuration.isOrientationLandscape() -> 1f
|
||||
else -> oneHandedModeScaleFactor / 100f
|
||||
}
|
||||
if (oneHandedMode == OneHandedMode.END && configuration.isOrientationPortrait()) {
|
||||
OneHandedPanel(
|
||||
panelSide = OneHandedMode.START,
|
||||
weight = 1f - keyboardWeight,
|
||||
)
|
||||
}
|
||||
CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(keyboardWeight)
|
||||
.wrapContentHeight(),
|
||||
) {
|
||||
when (activeState.imeUiMode) {
|
||||
ImeUiMode.TEXT -> TextInputLayout()
|
||||
ImeUiMode.MEDIA -> MediaInputLayout()
|
||||
ImeUiMode.CLIPBOARD -> ClipboardInputLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oneHandedMode == OneHandedMode.START && configuration.isOrientationPortrait()) {
|
||||
OneHandedPanel(
|
||||
panelSide = OneHandedMode.END,
|
||||
weight = 1f - keyboardWeight,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (oneHandedMode == OneHandedMode.START && configuration.isOrientationPortrait()) {
|
||||
OneHandedPanel(
|
||||
panelSide = OneHandedMode.END,
|
||||
weight = 1f - keyboardWeight,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.DevtoolsOverlays() {
|
||||
private fun BoxScope.DevtoolsUi() = with(LocalDensity.current) {
|
||||
val devtoolsEnabled by prefs.devtools.enabled.observeAsState()
|
||||
if (devtoolsEnabled) {
|
||||
val devtoolsShowPrimaryClip by prefs.devtools.showPrimaryClip.observeAsState()
|
||||
if (devtoolsShowPrimaryClip) {
|
||||
val primaryClip by clipboardManager.primaryClip.observeAsState()
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.TopStart),
|
||||
text = primaryClip.toString(),
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
val maxHeight = inputWindowView?.measuredHeight?.let { windowHeight ->
|
||||
windowHeight - inputViewSize.height - FlorisImeSizing.Static.smartbarHeightPx
|
||||
} ?: inputViewSize.height
|
||||
DevtoolsOverlay(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.heightIn(max = maxHeight.toDp())
|
||||
.align(Alignment.TopStart),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import kotlinx.coroutines.runBlocking
|
||||
class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
private val prefs by florisPreferenceModel()
|
||||
private val dictionaryManager get() = DictionaryManager.default()
|
||||
private val spellingManager by spellingManager()
|
||||
private val spellingService by spellingService()
|
||||
private val subtypeManager by subtypeManager()
|
||||
|
||||
@@ -82,7 +83,7 @@ class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
private fun spellMultiple(
|
||||
spellingLocale: FlorisLocale,
|
||||
textInfos: Array<out TextInfo>,
|
||||
suggestionsLimit: Int
|
||||
suggestionsLimit: Int,
|
||||
): Array<SuggestionsInfo> = runBlocking {
|
||||
val retInfos = Array(textInfos.size) { n ->
|
||||
val word = textInfos[n].text ?: ""
|
||||
@@ -102,13 +103,15 @@ class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
setupSpellingIfNecessary()
|
||||
val spellingLocale = cachedSpellingLocale ?: return SpellingService.emptySuggestionsInfo()
|
||||
|
||||
return spellingService.spell(spellingLocale, textInfo.text, suggestionsLimit)
|
||||
return spellingService
|
||||
.spell(spellingLocale, textInfo.text, suggestionsLimit)
|
||||
.sendToDebugOverlayIfEnabled(textInfo)
|
||||
}
|
||||
|
||||
override fun onGetSuggestionsMultiple(
|
||||
textInfos: Array<out TextInfo>?,
|
||||
suggestionsLimit: Int,
|
||||
sequentialWords: Boolean
|
||||
sequentialWords: Boolean,
|
||||
): Array<SuggestionsInfo> {
|
||||
flogInfo(LogTopic.SPELL_EVENTS)
|
||||
|
||||
@@ -116,12 +119,12 @@ class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
setupSpellingIfNecessary()
|
||||
val spellingLocale = cachedSpellingLocale ?: return emptyArray()
|
||||
|
||||
return spellMultiple(spellingLocale, textInfos, suggestionsLimit)
|
||||
return spellMultiple(spellingLocale, textInfos, suggestionsLimit).sendToDebugOverlayIfEnabled(textInfos)
|
||||
}
|
||||
|
||||
override fun onGetSentenceSuggestionsMultiple(
|
||||
textInfos: Array<out TextInfo>?,
|
||||
suggestionsLimit: Int
|
||||
suggestionsLimit: Int,
|
||||
): Array<SentenceSuggestionsInfo> {
|
||||
flogInfo(LogTopic.SPELL_EVENTS)
|
||||
|
||||
@@ -133,12 +136,38 @@ class FlorisSpellCheckerService : SpellCheckerService() {
|
||||
flogInfo(LogTopic.SPELL_EVENTS)
|
||||
|
||||
super.onCancel()
|
||||
if (prefs.devtools.showSpellingOverlay.get()) {
|
||||
spellingManager.clearDebugOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClose() {
|
||||
flogInfo(LogTopic.SPELL_EVENTS)
|
||||
|
||||
super.onClose()
|
||||
if (prefs.devtools.showSpellingOverlay.get()) {
|
||||
spellingManager.clearDebugOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
fun SuggestionsInfo.sendToDebugOverlayIfEnabled(
|
||||
textInfo: TextInfo,
|
||||
): SuggestionsInfo {
|
||||
if (prefs.devtools.showSpellingOverlay.get()) {
|
||||
spellingManager.addToDebugOverlay(textInfo.text, this)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun Array<SuggestionsInfo>.sendToDebugOverlayIfEnabled(
|
||||
textInfos: Array<out TextInfo>,
|
||||
): Array<SuggestionsInfo> {
|
||||
if (prefs.devtools.showSpellingOverlay.get()) {
|
||||
for ((n, info) in this.withIndex()) {
|
||||
spellingManager.addToDebugOverlay(textInfos[n].text, info)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,8 @@ package dev.patrickgold.florisboard.app
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.MaterialTheme
|
||||
@@ -39,28 +36,34 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import com.google.accompanist.insets.statusBarsPadding
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.app.res.ProvideLocalizedResources
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.Routes
|
||||
import dev.patrickgold.florisboard.app.ui.components.LocalPreviewFieldController
|
||||
import dev.patrickgold.florisboard.app.ui.components.PreviewKeyboardField
|
||||
import dev.patrickgold.florisboard.app.ui.components.SystemUiApp
|
||||
import dev.patrickgold.florisboard.app.ui.components.rememberPreviewFieldController
|
||||
import dev.patrickgold.florisboard.app.ui.theme.FlorisAppTheme
|
||||
import dev.patrickgold.florisboard.common.FlorisLocale
|
||||
import dev.patrickgold.florisboard.common.android.hideAppIcon
|
||||
import dev.patrickgold.florisboard.common.android.showAppIcon
|
||||
import dev.patrickgold.florisboard.common.android.AndroidVersion
|
||||
import dev.patrickgold.florisboard.common.android.hideAppIcon
|
||||
import dev.patrickgold.florisboard.common.android.setLocale
|
||||
import dev.patrickgold.florisboard.common.android.showAppIcon
|
||||
import dev.patrickgold.florisboard.util.AppVersionUtils
|
||||
import dev.patrickgold.jetpref.datastore.ui.ProvideDefaultDialogPrefStrings
|
||||
|
||||
enum class AppTheme(val id: String) {
|
||||
AUTO("auto"),
|
||||
AUTO_AMOLED("auto_amoled"),
|
||||
LIGHT("light"),
|
||||
DARK("dark"),
|
||||
AMOLED_DARK("amoled_dark"),
|
||||
AMOLED_DARK("amoled_dark");
|
||||
}
|
||||
|
||||
val LocalNavController = staticCompositionLocalOf<NavController> {
|
||||
@@ -96,35 +99,20 @@ class FlorisAppActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
AppVersionUtils.updateVersionOnInstallAndLastUse(this, prefs)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
ProvideLocalizedResources(resourcesContext) {
|
||||
FlorisAppTheme(theme = appTheme) {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
SystemUiApp()
|
||||
if (isDatastoreReady) {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = false) {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
SystemUiApp()
|
||||
AppContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PreDraw observer for SplashScreen
|
||||
val content = findViewById<View>(android.R.id.content)
|
||||
content.viewTreeObserver.addOnPreDrawListener(
|
||||
object : ViewTreeObserver.OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
return if (isDatastoreReady) {
|
||||
content.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -143,48 +131,36 @@ class FlorisAppActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
prefs.forceSyncToDisk()
|
||||
}
|
||||
|
||||
private fun Configuration.setLocale(locale: FlorisLocale) {
|
||||
return this.setLocale(locale.base)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun AppContent() {
|
||||
val navController = rememberNavController()
|
||||
CompositionLocalProvider(LocalNavController provides navController) {
|
||||
val previewFieldController = rememberPreviewFieldController()
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalNavController provides navController,
|
||||
LocalPreviewFieldController provides previewFieldController,
|
||||
) {
|
||||
ProvideDefaultDialogPrefStrings(
|
||||
confirmLabel = stringRes(R.string.assets__action__ok),
|
||||
dismissLabel = stringRes(R.string.assets__action__cancel),
|
||||
neutralLabel = stringRes(R.string.assets__action__default),
|
||||
confirmLabel = stringRes(R.string.action__ok),
|
||||
dismissLabel = stringRes(R.string.action__cancel),
|
||||
neutralLabel = stringRes(R.string.action__default),
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.navigationBarsWithImePadding(),
|
||||
) {
|
||||
Routes.AppNavHost(
|
||||
modifier = Modifier.weight(1.0f),
|
||||
navController = navController,
|
||||
startDestination = Routes.Splash.Screen,
|
||||
)
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val previewVisible = when (navBackStackEntry?.destination?.route) {
|
||||
Routes.Setup.Screen, Routes.Settings.About, Routes.Settings.ProjectLicense,
|
||||
Routes.Settings.ThirdPartyLicenses, Routes.Settings.ImportSpellingArchive,
|
||||
Routes.Settings.ImportSpellingAffDic, Routes.Devtools.AndroidLocales,
|
||||
Routes.Devtools.AndroidSettings, Routes.Ext.View,
|
||||
Routes.Splash.Screen, Routes.Settings.SubtypeAdd,
|
||||
Routes.Settings.SubtypeEdit, Routes.Settings.SelectLocale -> false
|
||||
else -> true
|
||||
}
|
||||
AnimatedVisibility(visible = previewVisible) {
|
||||
PreviewKeyboardField()
|
||||
}
|
||||
PreviewKeyboardField(previewFieldController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SideEffect {
|
||||
navController.setOnBackPressedDispatcher(this.onBackPressedDispatcher)
|
||||
}
|
||||
|
||||
@@ -16,11 +16,15 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.prefs
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import dev.patrickgold.florisboard.app.AppTheme
|
||||
import dev.patrickgold.florisboard.app.ui.settings.theme.DisplayColorsAs
|
||||
import dev.patrickgold.florisboard.app.ui.settings.theme.DisplayKbdAfterDialogs
|
||||
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
|
||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiHairStyle
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiRecentlyUsedHelper
|
||||
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSkinTone
|
||||
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingLanguageMode
|
||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
|
||||
@@ -29,14 +33,16 @@ import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
|
||||
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.CandidatesDisplayMode
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SecondaryRowPlacement
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarRowType
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeMode
|
||||
import dev.patrickgold.florisboard.res.FlorisRef
|
||||
import dev.patrickgold.florisboard.ime.theme.extCoreTheme
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
|
||||
import dev.patrickgold.florisboard.snygg.SnyggLevel
|
||||
import dev.patrickgold.florisboard.util.VersionName
|
||||
import dev.patrickgold.jetpref.datastore.JetPref
|
||||
import dev.patrickgold.jetpref.datastore.model.PreferenceModel
|
||||
import dev.patrickgold.jetpref.datastore.preferenceModel
|
||||
import java.time.LocalTime
|
||||
|
||||
fun florisPreferenceModel() = preferenceModel(AppPrefs::class, ::AppPrefs)
|
||||
fun florisPreferenceModel() = JetPref.getOrCreatePreferenceModel(AppPrefs::class, ::AppPrefs)
|
||||
|
||||
class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
val advanced = Advanced()
|
||||
@@ -93,6 +99,10 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "clipboard__max_history_size",
|
||||
default = 20,
|
||||
)
|
||||
val clearPrimaryClipDeletesLastItem = boolean(
|
||||
key = "clipboard__clear_primary_clip_deletes_last_item",
|
||||
default = true,
|
||||
)
|
||||
}
|
||||
|
||||
val correction = Correction()
|
||||
@@ -125,6 +135,10 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "devtools__show_primary_clip",
|
||||
default = false,
|
||||
)
|
||||
val showSpellingOverlay = boolean(
|
||||
key = "devtools__show_spelling_overlay",
|
||||
default = false,
|
||||
)
|
||||
val overrideWordSuggestionsMinHeapRestriction = boolean(
|
||||
key = "devtools__override_word_suggestions_min_heap_restriction",
|
||||
default = false,
|
||||
@@ -295,7 +309,7 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
val internal = Internal()
|
||||
inner class Internal {
|
||||
val homeIsBetaToolboxCollapsed = boolean(
|
||||
key = "internal__home_is_beta_toolbox_collapsed_beta06",
|
||||
key = "internal__home_is_beta_toolbox_collapsed_0314release",
|
||||
default = false,
|
||||
)
|
||||
val isImeSetUp = boolean(
|
||||
@@ -346,6 +360,10 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "keyboard__utility_key_action",
|
||||
default = UtilityKeyAction.DYNAMIC_SWITCH_LANGUAGE_EMOJIS,
|
||||
)
|
||||
val spaceBarLanguageDisplayEnabled = boolean(
|
||||
key = "keyboard__space_bar_language_display_enabled",
|
||||
default = true,
|
||||
)
|
||||
val fontSizeMultiplierPortrait = int(
|
||||
key = "keyboard__font_size_multiplier_portrait",
|
||||
default = 100,
|
||||
@@ -424,6 +442,10 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
|
||||
val localization = Localization()
|
||||
inner class Localization {
|
||||
val displayLanguageNamesIn = enum(
|
||||
key = "localization__display_language_names_in",
|
||||
default = DisplayLanguageNamesIn.NATIVE_LOCALE,
|
||||
)
|
||||
val activeSubtypeId = long(
|
||||
key = "localization__active_subtype_id",
|
||||
default = Subtype.DEFAULT.id,
|
||||
@@ -434,42 +456,71 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
)
|
||||
}
|
||||
|
||||
val media = Media()
|
||||
inner class Media {
|
||||
val emojiRecentlyUsed = custom(
|
||||
key = "media__emoji_recently_used",
|
||||
default = emptyList(),
|
||||
serializer = EmojiRecentlyUsedHelper.Serializer,
|
||||
)
|
||||
val emojiRecentlyUsedMaxSize = int(
|
||||
key = "media__emoji_recently_used_max_size",
|
||||
default = 90,
|
||||
)
|
||||
val emojiPreferredSkinTone = enum(
|
||||
key = "media__emoji_preferred_skin_tone",
|
||||
default = EmojiSkinTone.DEFAULT,
|
||||
)
|
||||
val emojiPreferredHairStyle = enum(
|
||||
key = "media__emoji_preferred_hair_style",
|
||||
default = EmojiHairStyle.DEFAULT,
|
||||
)
|
||||
}
|
||||
|
||||
val smartbar = Smartbar()
|
||||
inner class Smartbar {
|
||||
val enabled = boolean(
|
||||
key = "smartbar__enabled",
|
||||
default = true,
|
||||
)
|
||||
val primaryRowFlipToggles = boolean(
|
||||
key = "smartbar__primary_row_flip_toggles",
|
||||
val flipToggles = boolean(
|
||||
key = "smartbar__flip_toggles",
|
||||
default = false,
|
||||
)
|
||||
val secondaryRowEnabled = boolean(
|
||||
key = "smartbar__secondary_row_enabled",
|
||||
val primaryActionsExpanded = boolean(
|
||||
key = "smartbar__primary_actions_expanded",
|
||||
default = false,
|
||||
)
|
||||
val primaryActionsRowType = enum(
|
||||
key = "smartbar__primary_actions_row_type",
|
||||
default = SmartbarRowType.QUICK_ACTIONS,
|
||||
)
|
||||
val primaryActionsAutoExpandCollapse = boolean(
|
||||
key = "smartbar__primary_actions_auto_expand_collapse",
|
||||
default = true,
|
||||
)
|
||||
val secondaryRowExpanded = boolean(
|
||||
key = "smartbar__secondary_row_expanded",
|
||||
val primaryActionsExpandWithAnimation = boolean(
|
||||
key = "smartbar__primary_actions_expand_with_animation",
|
||||
default = true,
|
||||
)
|
||||
val secondaryActionsEnabled = boolean(
|
||||
key = "smartbar__secondary_actions_enabled",
|
||||
default = true,
|
||||
)
|
||||
val secondaryActionsExpanded = boolean(
|
||||
key = "smartbar__secondary_actions_expanded",
|
||||
default = false,
|
||||
)
|
||||
val secondaryRowPlacement = enum(
|
||||
key = "smartbar__secondary_row_placement",
|
||||
val secondaryActionsPlacement = enum(
|
||||
key = "smartbar__secondary_actions_placement",
|
||||
default = SecondaryRowPlacement.ABOVE_PRIMARY,
|
||||
)
|
||||
val actionRowExpanded = boolean(
|
||||
key = "smartbar__action_row_expanded",
|
||||
default = false,
|
||||
val secondaryActionsRowType = enum(
|
||||
key = "smartbar__secondary_actions_row_type",
|
||||
default = SmartbarRowType.CLIPBOARD_CURSOR_TOOLS,
|
||||
)
|
||||
val actionRowExpandWithAnimation = boolean(
|
||||
key = "smartbar__action_row_expand_with_animation",
|
||||
default = true,
|
||||
)
|
||||
val actionRowAutoExpandCollapse = boolean(
|
||||
key = "smartbar__action_row_auto_expand_collapse",
|
||||
default = true,
|
||||
)
|
||||
val actions = string(
|
||||
key = "smartbar__actions",
|
||||
val quickActions = string(
|
||||
key = "smartbar__quick_actions",
|
||||
default = "[]",
|
||||
)
|
||||
}
|
||||
@@ -532,29 +583,39 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
|
||||
key = "theme__day_theme_adapt_to_app",
|
||||
default = false,
|
||||
)
|
||||
val dayThemeRef = custom(
|
||||
key = "theme__day_theme_ref",
|
||||
default = FlorisRef.assets("ime/theme/floris_day.json"),
|
||||
serializer = FlorisRef.Serializer,
|
||||
val dayThemeId = custom(
|
||||
key = "theme__day_theme_id",
|
||||
default = extCoreTheme("floris_day"),
|
||||
serializer = ExtensionComponentName.Serializer,
|
||||
)
|
||||
val nightThemeAdaptToApp = boolean(
|
||||
key = "theme__night_theme_adapt_to_app",
|
||||
default = false,
|
||||
)
|
||||
val nightThemeRef = custom(
|
||||
key = "theme__night_theme_ref",
|
||||
default = FlorisRef.assets("ime/theme/floris_night.json"),
|
||||
serializer = FlorisRef.Serializer,
|
||||
val nightThemeId = custom(
|
||||
key = "theme__night_theme_id",
|
||||
default = extCoreTheme("floris_night"),
|
||||
serializer = ExtensionComponentName.Serializer,
|
||||
)
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
val sunriseTime = localTime(
|
||||
key = "theme__sunrise_time",
|
||||
default = LocalTime.of(6, 0),
|
||||
//val sunriseTime = localTime(
|
||||
// key = "theme__sunrise_time",
|
||||
// default = LocalTime.of(6, 0),
|
||||
//)
|
||||
//val sunsetTime = localTime(
|
||||
// key = "theme__sunset_time",
|
||||
// default = LocalTime.of(18, 0),
|
||||
//)
|
||||
val editorDisplayColorsAs = enum(
|
||||
key = "theme__editor_display_colors_as",
|
||||
default = DisplayColorsAs.HEX8,
|
||||
)
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
val sunsetTime = localTime(
|
||||
key = "theme__sunset_time",
|
||||
default = LocalTime.of(18, 0),
|
||||
val editorDisplayKbdAfterDialogs = enum(
|
||||
key = "theme__editor_display_kbd_after_dialogs",
|
||||
default = DisplayKbdAfterDialogs.REMEMBER,
|
||||
)
|
||||
val editorLevel = enum(
|
||||
key = "theme__editor_level",
|
||||
default = SnyggLevel.ADVANCED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,15 @@
|
||||
package dev.patrickgold.florisboard.app.res
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.common.kotlin.CurlyArg
|
||||
import dev.patrickgold.florisboard.common.kotlin.curlyFormat
|
||||
@@ -40,8 +43,14 @@ fun ProvideLocalizedResources(
|
||||
resourcesContext: Context,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val layoutDirection = when (resourcesContext.resources.configuration.layoutDirection) {
|
||||
View.LAYOUT_DIRECTION_LTR -> LayoutDirection.Ltr
|
||||
View.LAYOUT_DIRECTION_RTL -> LayoutDirection.Rtl
|
||||
else -> error("Given configuration specifies invalid layout direction!")
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalResourcesContext provides resourcesContext,
|
||||
LocalLayoutDirection provides layoutDirection,
|
||||
LocalAppNameString provides stringResource(R.string.floris_app_name),
|
||||
) {
|
||||
content()
|
||||
|
||||
@@ -24,26 +24,37 @@ import androidx.navigation.compose.composable
|
||||
import dev.patrickgold.florisboard.app.ui.devtools.AndroidLocalesScreen
|
||||
import dev.patrickgold.florisboard.app.ui.devtools.AndroidSettingsScreen
|
||||
import dev.patrickgold.florisboard.app.ui.devtools.DevtoolsScreen
|
||||
import dev.patrickgold.florisboard.app.ui.ext.ExtensionEditScreen
|
||||
import dev.patrickgold.florisboard.app.ui.ext.ExtensionExportScreen
|
||||
import dev.patrickgold.florisboard.app.ui.ext.ExtensionImportScreen
|
||||
import dev.patrickgold.florisboard.app.ui.ext.ExtensionImportScreenType
|
||||
import dev.patrickgold.florisboard.app.ui.ext.ExtensionViewScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.HomeScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.about.AboutScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.about.ProjectLicenseScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.about.ThirdPartyLicensesScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.advanced.AdvancedScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.advanced.BackupScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.advanced.RestoreScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.clipboard.ClipboardScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.dictionary.DictionaryScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.dictionary.UserDictionaryScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.dictionary.UserDictionaryType
|
||||
import dev.patrickgold.florisboard.app.ui.settings.gestures.GesturesScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.keyboard.InputFeedbackScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.keyboard.KeyboardScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.localization.LocalizationScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.localization.SelectLocaleScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.localization.SubtypeEditorScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.media.MediaScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.smartbar.SmartbarScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.spelling.ImportSpellingArchiveScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.spelling.ManageSpellingDictsScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.spelling.SpellingInfoScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.spelling.SpellingScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.theme.ThemeScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.theme.ThemeManagerScreen
|
||||
import dev.patrickgold.florisboard.app.ui.settings.theme.ThemeManagerScreenAction
|
||||
import dev.patrickgold.florisboard.app.ui.settings.typing.TypingScreen
|
||||
import dev.patrickgold.florisboard.app.ui.setup.SetupScreen
|
||||
import dev.patrickgold.florisboard.app.ui.splash.SplashScreen
|
||||
@@ -69,6 +80,8 @@ object Routes {
|
||||
fun SubtypeEdit(id: Long) = SubtypeEdit.curlyFormat("id" to id)
|
||||
|
||||
const val Theme = "settings/theme"
|
||||
const val ThemeManager = "settings/theme/manage/{action}"
|
||||
fun ThemeManager(action: ThemeManagerScreenAction) = ThemeManager.curlyFormat("action" to action.id)
|
||||
|
||||
const val Keyboard = "settings/keyboard"
|
||||
const val InputFeedback = "settings/keyboard/input-feedback"
|
||||
@@ -84,12 +97,18 @@ object Routes {
|
||||
const val ImportSpellingAffDic = "settings/spelling/import-aff-dic"
|
||||
|
||||
const val Dictionary = "settings/dictionary"
|
||||
const val UserDictionary = "settings/dictionary/user-dictionary/{type}"
|
||||
fun UserDictionary(type: UserDictionaryType) = UserDictionary.curlyFormat("type" to type.id)
|
||||
|
||||
const val Gestures = "settings/gestures"
|
||||
|
||||
const val Clipboard = "settings/clipboard"
|
||||
|
||||
const val Media = "settings/media"
|
||||
|
||||
const val Advanced = "settings/advanced"
|
||||
const val Backup = "settings/advanced/backup"
|
||||
const val Restore = "settings/advanced/restore"
|
||||
|
||||
const val About = "settings/about"
|
||||
const val ProjectLicense = "settings/about/project-license"
|
||||
@@ -105,6 +124,20 @@ object Routes {
|
||||
}
|
||||
|
||||
object Ext {
|
||||
const val Edit = "ext/edit/{id}?create={serial_type}"
|
||||
fun Edit(id: String, serialType: String? = null): String {
|
||||
return Edit.curlyFormat("id" to id, "serial_type" to (serialType ?: ""))
|
||||
}
|
||||
|
||||
const val Export = "ext/export/{id}"
|
||||
fun Export(id: String) = Export.curlyFormat("id" to id)
|
||||
|
||||
const val Import = "ext/import/{type}?uuid={uuid}"
|
||||
fun Import(
|
||||
type: ExtensionImportScreenType,
|
||||
uuid: String?,
|
||||
) = Import.curlyFormat("type" to type.id, "uuid" to uuid.toString())
|
||||
|
||||
const val View = "ext/view/{id}"
|
||||
fun View(id: String) = View.curlyFormat("id" to id)
|
||||
}
|
||||
@@ -135,6 +168,12 @@ object Routes {
|
||||
}
|
||||
|
||||
composable(Settings.Theme) { ThemeScreen() }
|
||||
composable(Settings.ThemeManager) { navBackStack ->
|
||||
val action = navBackStack.arguments?.getString("action")?.let { actionId ->
|
||||
ThemeManagerScreenAction.values().firstOrNull { it.id == actionId }
|
||||
}
|
||||
ThemeManagerScreen(action)
|
||||
}
|
||||
|
||||
composable(Settings.Keyboard) { KeyboardScreen() }
|
||||
composable(Settings.InputFeedback) { InputFeedbackScreen() }
|
||||
@@ -149,12 +188,22 @@ object Routes {
|
||||
composable(Settings.ImportSpellingArchive) { ImportSpellingArchiveScreen() }
|
||||
|
||||
composable(Settings.Dictionary) { DictionaryScreen() }
|
||||
composable(Settings.UserDictionary) { navBackStack ->
|
||||
val type = navBackStack.arguments?.getString("type")?.let { typeId ->
|
||||
UserDictionaryType.values().firstOrNull { it.id == typeId }
|
||||
}
|
||||
UserDictionaryScreen(type!!)
|
||||
}
|
||||
|
||||
composable(Settings.Gestures) { GesturesScreen() }
|
||||
|
||||
composable(Settings.Clipboard) { ClipboardScreen() }
|
||||
|
||||
composable(Settings.Media) { MediaScreen() }
|
||||
|
||||
composable(Settings.Advanced) { AdvancedScreen() }
|
||||
composable(Settings.Backup) { BackupScreen() }
|
||||
composable(Settings.Restore) { RestoreScreen() }
|
||||
|
||||
composable(Settings.About) { AboutScreen() }
|
||||
composable(Settings.ProjectLicense) { ProjectLicenseScreen() }
|
||||
@@ -167,6 +216,25 @@ object Routes {
|
||||
AndroidSettingsScreen(name)
|
||||
}
|
||||
|
||||
composable(Ext.Edit) { navBackStack ->
|
||||
val extensionId = navBackStack.arguments?.getString("id")
|
||||
val serialType = navBackStack.arguments?.getString("serial_type")
|
||||
ExtensionEditScreen(
|
||||
id = extensionId.toString(),
|
||||
createSerialType = serialType.takeIf { it != null && it.isNotBlank() },
|
||||
)
|
||||
}
|
||||
composable(Ext.Export) { navBackStack ->
|
||||
val extensionId = navBackStack.arguments?.getString("id")
|
||||
ExtensionExportScreen(id = extensionId.toString())
|
||||
}
|
||||
composable(Ext.Import) { navBackStack ->
|
||||
val type = navBackStack.arguments?.getString("type")?.let { typeId ->
|
||||
ExtensionImportScreenType.values().firstOrNull { it.id == typeId }
|
||||
} ?: ExtensionImportScreenType.EXT_ANY
|
||||
val uuid = navBackStack.arguments?.getString("uuid")?.takeIf { it != "null" }
|
||||
ExtensionImportScreen(type, uuid)
|
||||
}
|
||||
composable(Ext.View) { navBackStack ->
|
||||
val extensionId = navBackStack.arguments?.getString("id")
|
||||
ExtensionViewScreen(id = extensionId.toString())
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.isUnspecified
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
infix fun TextUnit.safeTimes(other: Float): TextUnit {
|
||||
return if (this.isUnspecified) 0.sp else this.times(other)
|
||||
}
|
||||
|
||||
infix fun TextUnit.safeTimes(other: Double): TextUnit {
|
||||
return if (this.isUnspecified) this else this.times(other)
|
||||
}
|
||||
|
||||
infix fun TextUnit.safeTimes(other: Int): TextUnit {
|
||||
return if (this.isUnspecified) this else this.times(other)
|
||||
}
|
||||
|
||||
val DpSizeSaver = Saver<Dp, Float>(
|
||||
save = { it.value },
|
||||
restore = { it.dp },
|
||||
)
|
||||
@@ -25,19 +25,32 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.ui.Alignment
|
||||
|
||||
fun EnterTransition.Companion.verticalTween(duration: Int): EnterTransition {
|
||||
return fadeIn(tween(duration)) + expandVertically(tween(duration))
|
||||
fun EnterTransition.Companion.verticalTween(
|
||||
duration: Int,
|
||||
expandFrom: Alignment.Vertical = Alignment.Bottom,
|
||||
): EnterTransition {
|
||||
return fadeIn(tween(duration)) + expandVertically(tween(duration), expandFrom)
|
||||
}
|
||||
|
||||
fun ExitTransition.Companion.verticalTween(duration: Int): ExitTransition {
|
||||
return fadeOut(tween(duration)) + shrinkVertically(tween(duration))
|
||||
fun ExitTransition.Companion.verticalTween(
|
||||
duration: Int,
|
||||
shrinkTowards: Alignment.Vertical = Alignment.Bottom,
|
||||
): ExitTransition {
|
||||
return fadeOut(tween(duration)) + shrinkVertically(tween(duration), shrinkTowards)
|
||||
}
|
||||
|
||||
fun EnterTransition.Companion.horizontalTween(duration: Int): EnterTransition {
|
||||
return fadeIn(tween(duration)) + expandHorizontally(tween(duration))
|
||||
fun EnterTransition.Companion.horizontalTween(
|
||||
duration: Int,
|
||||
expandFrom: Alignment.Horizontal = Alignment.End,
|
||||
): EnterTransition {
|
||||
return fadeIn(tween(duration)) + expandHorizontally(tween(duration), expandFrom)
|
||||
}
|
||||
|
||||
fun ExitTransition.Companion.horizontalTween(duration: Int): ExitTransition {
|
||||
return fadeOut(tween(duration)) + shrinkHorizontally(tween(duration))
|
||||
fun ExitTransition.Companion.horizontalTween(
|
||||
duration: Int,
|
||||
shrinkTowards: Alignment.Horizontal = Alignment.End,
|
||||
): ExitTransition {
|
||||
return fadeOut(tween(duration)) + shrinkHorizontally(tween(duration), shrinkTowards)
|
||||
}
|
||||
|
||||
@@ -17,26 +17,21 @@
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
|
||||
@Composable
|
||||
fun FlorisAppBar(
|
||||
title: String,
|
||||
backArrowVisible: Boolean,
|
||||
navigationIcon: FlorisScreenNavigationIcon?,
|
||||
actions: @Composable RowScope.() -> Unit = { },
|
||||
) {
|
||||
TopAppBar(
|
||||
navigationIcon = backNavBtn(backArrowVisible),
|
||||
navigationIcon = navigationIcon,
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
@@ -49,19 +44,3 @@ fun FlorisAppBar(
|
||||
elevation = 0.dp,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun backNavBtn(backArrowVisible: Boolean): @Composable (() -> Unit)? {
|
||||
if (!backArrowVisible) return null
|
||||
val navController = LocalNavController.current
|
||||
return {
|
||||
IconButton(
|
||||
onClick = { navController.popBackStack() },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_back),
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
|
||||
@Composable
|
||||
fun RowScope.FlorisBulletSpacer(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colors.outline),
|
||||
)
|
||||
}
|
||||
@@ -17,32 +17,49 @@
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun FlorisButtonBar(content: @Composable FlorisButtonBarScope.() -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.primary)
|
||||
.padding(vertical = 4.dp, horizontal = 16.dp),
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colors.background,
|
||||
elevation = 8.dp,
|
||||
) {
|
||||
val scope = FlorisButtonBarScope(this)
|
||||
content(scope)
|
||||
Column {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp),
|
||||
color = MaterialTheme.colors.surface,
|
||||
) {}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp, bottom = 4.dp, start = 0.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val scope = FlorisButtonBarScope(this)
|
||||
content(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +72,11 @@ class FlorisButtonBarScope(rowScope: RowScope) : RowScope by rowScope {
|
||||
@DrawableRes iconId: Int? = null,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
TextButton(
|
||||
modifier = modifier,
|
||||
Button(
|
||||
modifier = modifier.padding(start = 16.dp),
|
||||
enabled = enabled,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary,
|
||||
contentColor = MaterialTheme.colors.onPrimary,
|
||||
),
|
||||
onClick = onClick,
|
||||
@@ -70,7 +88,34 @@ class FlorisButtonBarScope(rowScope: RowScope) : RowScope by rowScope {
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
Text(text = text.uppercase())
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ButtonBarTextButton(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
@DrawableRes iconId: Int? = null,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
TextButton(
|
||||
modifier = modifier.padding(start = 16.dp),
|
||||
enabled = enabled,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colors.primary,
|
||||
),
|
||||
onClick = onClick,
|
||||
) {
|
||||
if (iconId != null) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
painter = painterResource(iconId),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonColors
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun FlorisButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: Painter? = null,
|
||||
text: String,
|
||||
enabled: Boolean = true,
|
||||
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
||||
colors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
colors = colors,
|
||||
contentPadding = contentPadding,
|
||||
onClick = onClick,
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(end = ButtonDefaults.IconSpacing)
|
||||
.size(ButtonDefaults.IconSize),
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlorisOutlinedButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: Painter? = null,
|
||||
text: String,
|
||||
enabled: Boolean = true,
|
||||
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
||||
colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
|
||||
) {
|
||||
OutlinedButton(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
colors = colors,
|
||||
contentPadding = contentPadding,
|
||||
onClick = onClick,
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(end = ButtonDefaults.IconSpacing)
|
||||
.size(ButtonDefaults.IconSize),
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlorisTextButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: Painter? = null,
|
||||
text: String,
|
||||
enabled: Boolean = true,
|
||||
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
|
||||
colors: ButtonColors = ButtonDefaults.textButtonColors(),
|
||||
) {
|
||||
TextButton(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
colors = colors,
|
||||
contentPadding = contentPadding,
|
||||
onClick = onClick,
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(end = ButtonDefaults.IconSpacing)
|
||||
.size(ButtonDefaults.IconSize),
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlorisIconButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: Painter,
|
||||
enabled: Boolean = true,
|
||||
iconModifier: Modifier = Modifier,
|
||||
iconColor: Color = Color.Unspecified,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
) {
|
||||
val contentAlpha = if (enabled) LocalContentAlpha.current else 0.14f
|
||||
val contentColor = iconColor.takeOrElse { LocalContentColor.current }
|
||||
CompositionLocalProvider(
|
||||
LocalContentAlpha provides contentAlpha,
|
||||
LocalContentColor provides contentColor,
|
||||
) {
|
||||
Icon(
|
||||
modifier = iconModifier,
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlorisIconButtonWithInnerPadding(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: Painter,
|
||||
enabled: Boolean = true,
|
||||
iconModifier: Modifier = Modifier,
|
||||
iconColor: Color = Color.Unspecified,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
) {
|
||||
val contentAlpha = if (enabled) LocalContentAlpha.current else 0.14f
|
||||
CompositionLocalProvider(
|
||||
LocalContentAlpha provides contentAlpha,
|
||||
LocalContentColor provides iconColor,
|
||||
) {
|
||||
Box(
|
||||
modifier = iconModifier
|
||||
.padding(4.dp)
|
||||
.fillMaxHeight()
|
||||
.aspectRatio(1f),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,30 +16,55 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
|
||||
private val IconRequiredSize = 24.dp
|
||||
private val IconEndPadding = 8.dp
|
||||
|
||||
private val CardContentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||
object CardDefaults {
|
||||
val IconRequiredSize = 24.dp
|
||||
val IconSpacing = 8.dp
|
||||
|
||||
val ContentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp)
|
||||
}
|
||||
|
||||
object BoxDefaults {
|
||||
val OutlinedBoxShape = RoundedCornerShape(8.dp)
|
||||
|
||||
val ContentPadding = PaddingValues(all = 0.dp)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
@@ -49,7 +74,7 @@ fun FlorisSimpleCard(
|
||||
secondaryText: String? = null,
|
||||
backgroundColor: Color = MaterialTheme.colors.surface,
|
||||
contentColor: Color = contentColorFor(backgroundColor),
|
||||
contentPadding: PaddingValues = CardContentPadding,
|
||||
contentPadding: PaddingValues = CardDefaults.ContentPadding,
|
||||
icon: (@Composable () -> Unit)? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
@@ -85,7 +110,6 @@ fun FlorisSimpleCard(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,7 +119,7 @@ fun FlorisErrorCard(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
showIcon: Boolean = true,
|
||||
contentPadding: PaddingValues = CardContentPadding,
|
||||
contentPadding: PaddingValues = CardDefaults.ContentPadding,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
FlorisSimpleCard(
|
||||
@@ -104,7 +128,9 @@ fun FlorisErrorCard(
|
||||
contentColor = Color.White,
|
||||
onClick = onClick,
|
||||
icon = if (showIcon) ({ Icon(
|
||||
modifier = Modifier.padding(end = IconEndPadding).requiredSize(IconRequiredSize),
|
||||
modifier = Modifier
|
||||
.padding(end = CardDefaults.IconSpacing)
|
||||
.requiredSize(CardDefaults.IconRequiredSize),
|
||||
painter = painterResource(R.drawable.ic_error_outline),
|
||||
contentDescription = null,
|
||||
) }) else null,
|
||||
@@ -118,7 +144,7 @@ fun FlorisWarningCard(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
showIcon: Boolean = true,
|
||||
contentPadding: PaddingValues = CardContentPadding,
|
||||
contentPadding: PaddingValues = CardDefaults.ContentPadding,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
FlorisSimpleCard(
|
||||
@@ -127,7 +153,9 @@ fun FlorisWarningCard(
|
||||
contentColor = Color.Black,
|
||||
onClick = onClick,
|
||||
icon = if (showIcon) ({ Icon(
|
||||
modifier = Modifier.padding(end = IconEndPadding).requiredSize(IconRequiredSize),
|
||||
modifier = Modifier
|
||||
.padding(end = CardDefaults.IconSpacing)
|
||||
.requiredSize(CardDefaults.IconRequiredSize),
|
||||
painter = painterResource(R.drawable.ic_warning_outline),
|
||||
contentDescription = null,
|
||||
) }) else null,
|
||||
@@ -141,14 +169,16 @@ fun FlorisInfoCard(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
showIcon: Boolean = true,
|
||||
contentPadding: PaddingValues = CardContentPadding,
|
||||
contentPadding: PaddingValues = CardDefaults.ContentPadding,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
FlorisSimpleCard(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
icon = if (showIcon) ({ Icon(
|
||||
modifier = Modifier.padding(end = IconEndPadding).requiredSize(IconRequiredSize),
|
||||
modifier = Modifier
|
||||
.padding(end = CardDefaults.IconSpacing)
|
||||
.requiredSize(CardDefaults.IconRequiredSize),
|
||||
painter = painterResource(R.drawable.ic_info),
|
||||
contentDescription = null,
|
||||
) }) else null,
|
||||
@@ -156,3 +186,118 @@ fun FlorisInfoCard(
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlorisOutlinedBox(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
onTitleClick: (() -> Unit)? = null,
|
||||
subtitle: String? = null,
|
||||
onSubtitleClick: (() -> Unit)? = null,
|
||||
borderWidth: Dp = 1.dp,
|
||||
borderColor: Color = MaterialTheme.colors.outline,
|
||||
shape: Shape = BoxDefaults.OutlinedBoxShape,
|
||||
contentPadding: PaddingValues = BoxDefaults.ContentPadding,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
FlorisOutlinedBox(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
onTitleClick = onTitleClick,
|
||||
subtitle = if (subtitle != null) {
|
||||
{
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(start = 6.dp, end = 6.dp, bottom = 4.dp),
|
||||
text = subtitle,
|
||||
color = LocalContentColor.current.copy(alpha = 0.56f),
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 10.sp,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onSubtitleClick = onSubtitleClick,
|
||||
borderWidth = borderWidth,
|
||||
borderColor = borderColor,
|
||||
shape = shape,
|
||||
contentPadding = contentPadding,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Rework internal implementation (with same API and visual appearance) of FlorisOutlinedBox
|
||||
// to avoid too much nesting and improve performance
|
||||
@Composable
|
||||
fun FlorisOutlinedBox(
|
||||
modifier: Modifier = Modifier,
|
||||
title: (@Composable () -> Unit)? = null,
|
||||
onTitleClick: (() -> Unit)? = null,
|
||||
subtitle: (@Composable () -> Unit)? = null,
|
||||
onSubtitleClick: (() -> Unit)? = null,
|
||||
borderWidth: Dp = 1.dp,
|
||||
borderColor: Color = MaterialTheme.colors.outline,
|
||||
shape: Shape = BoxDefaults.OutlinedBoxShape,
|
||||
contentPadding: PaddingValues = BoxDefaults.ContentPadding,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(top = if (title != null) 11.dp else 0.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.border(borderWidth, borderColor, shape)
|
||||
.clip(shape)
|
||||
.padding(top = if (title != null) 11.dp else 0.dp),
|
||||
) {
|
||||
if (title != null && subtitle != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp, bottom = 4.dp)
|
||||
.rippleClickable(enabled = onSubtitleClick != null) {
|
||||
onSubtitleClick!!()
|
||||
},
|
||||
) {
|
||||
subtitle()
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
if (title != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(23.dp)
|
||||
.offset(x = 10.dp, y = (-12).dp)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.rippleClickable(enabled = onTitleClick != null) {
|
||||
onTitleClick!!()
|
||||
}
|
||||
.padding(horizontal = 6.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
title()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.defaultFlorisOutlinedBox(): Modifier {
|
||||
return this
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -43,16 +44,19 @@ fun FlorisChip(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = { },
|
||||
enabled: Boolean = true,
|
||||
color: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity),
|
||||
color: Color = Color.Unspecified,
|
||||
shape: Shape = CircleShape,
|
||||
@DrawableRes leadingIcons: List<Int> = listOf(),
|
||||
@DrawableRes trailingIcons: List<Int> = listOf(),
|
||||
) {
|
||||
val backgroundColor = color.takeOrElse {
|
||||
MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity)
|
||||
}
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
color = color,
|
||||
color = backgroundColor,
|
||||
shape = shape,
|
||||
) {
|
||||
Row(
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
||||
|
||||
@Composable
|
||||
fun FlorisConfirmDeleteDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
what: String,
|
||||
) {
|
||||
JetPrefAlertDialog(
|
||||
modifier = modifier,
|
||||
title = stringRes(R.string.action__delete_confirm_title),
|
||||
confirmLabel = stringRes(R.string.action__delete),
|
||||
onConfirm = onConfirm,
|
||||
dismissLabel = stringRes(R.string.action__cancel),
|
||||
onDismiss = onDismiss,
|
||||
) {
|
||||
Text(text = stringRes(R.string.action__delete_confirm_message, "name" to what))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlorisUnsavedChangesDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
onSave: () -> Unit,
|
||||
onDiscard: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
JetPrefAlertDialog(
|
||||
modifier = modifier,
|
||||
title = stringRes(R.string.action__discard_confirm_title),
|
||||
confirmLabel = stringRes(R.string.action__save),
|
||||
onConfirm = onSave,
|
||||
dismissLabel = stringRes(R.string.action__discard),
|
||||
onDismiss = onDiscard,
|
||||
onOutsideDismissal = onDismiss,
|
||||
neutralLabel = stringRes(R.string.action__cancel),
|
||||
onNeutral = onDismiss,
|
||||
) {
|
||||
Text(text = stringRes(R.string.action__discard_confirm_message))
|
||||
}
|
||||
}
|
||||
@@ -35,43 +35,55 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
|
||||
@Composable
|
||||
fun FlorisDropdownMenu(
|
||||
items: List<String>,
|
||||
fun <T : Any> FlorisDropdownMenu(
|
||||
items: List<T>,
|
||||
expanded: Boolean,
|
||||
selectedIndex: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
isError: Boolean = false,
|
||||
labelProvider: (@Composable (T) -> String)? = null,
|
||||
onSelectItem: (Int) -> Unit = { },
|
||||
onExpandRequest: () -> Unit = { },
|
||||
onDismissRequest: () -> Unit = { },
|
||||
) {
|
||||
@Composable
|
||||
fun asString(v: T): String {
|
||||
return labelProvider?.invoke(v) ?: v.toString()
|
||||
}
|
||||
|
||||
Box(modifier = modifier.wrapContentSize(Alignment.TopStart)) {
|
||||
val indicatorRotation by animateFloatAsState(targetValue = if (expanded) 180f else 0f)
|
||||
val index = selectedIndex.coerceIn(items.indices)
|
||||
val color = if (isError) {
|
||||
val color = if (!enabled) {
|
||||
MaterialTheme.colors.outline
|
||||
} else if (isError) {
|
||||
MaterialTheme.colors.error
|
||||
} else {
|
||||
MaterialTheme.colors.onBackground
|
||||
}
|
||||
OutlinedButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
border = if (isError) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
border = if (isError && enabled) {
|
||||
BorderStroke(ButtonDefaults.OutlinedBorderSize, MaterialTheme.colors.error)
|
||||
} else {
|
||||
ButtonDefaults.outlinedBorder
|
||||
},
|
||||
onClick = { onExpandRequest() },
|
||||
enabled = enabled,
|
||||
onClick = onExpandRequest,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1.0f),
|
||||
text = items[index],
|
||||
text = asString(items[index]),
|
||||
textAlign = TextAlign.Start,
|
||||
fontWeight = FontWeight.Normal,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
color = color,
|
||||
@@ -79,7 +91,11 @@ fun FlorisDropdownMenu(
|
||||
Icon(
|
||||
modifier = Modifier.rotate(indicatorRotation),
|
||||
painter = painterResource(R.drawable.ic_keyboard_arrow_down),
|
||||
tint = color.copy(alpha = ContentAlpha.medium),
|
||||
tint = if (enabled) {
|
||||
color.copy(alpha = ContentAlpha.medium)
|
||||
} else {
|
||||
color
|
||||
},
|
||||
contentDescription = "Dropdown indicator",
|
||||
)
|
||||
}
|
||||
@@ -94,7 +110,7 @@ fun FlorisDropdownMenu(
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = item)
|
||||
Text(text = asString(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,13 +144,14 @@ fun FlorisDropdownLikeButton(
|
||||
modifier = Modifier.weight(1.0f),
|
||||
text = item,
|
||||
textAlign = TextAlign.Start,
|
||||
fontWeight = FontWeight.Normal,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
color = color,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.rotate(-90.0f),
|
||||
painter = painterResource(R.drawable.ic_keyboard_arrow_down),
|
||||
modifier = Modifier.autoMirrorForRtl(),
|
||||
painter = painterResource(R.drawable.ic_keyboard_arrow_right),
|
||||
tint = color.copy(alpha = ContentAlpha.medium),
|
||||
contentDescription = "Dropdown indicator",
|
||||
)
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun FlorisIconButton(
|
||||
@DrawableRes iconId: Int,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
iconModifier: Modifier = Modifier,
|
||||
iconColor: Color = Color.Unspecified,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
) {
|
||||
val contentAlpha = if (enabled) LocalContentAlpha.current else 0.14f
|
||||
CompositionLocalProvider(
|
||||
LocalContentAlpha provides contentAlpha,
|
||||
LocalContentColor provides iconColor,
|
||||
) {
|
||||
Box(
|
||||
modifier = iconModifier
|
||||
.padding(4.dp)
|
||||
.fillMaxHeight()
|
||||
.aspectRatio(1f),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = iconId),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,55 +16,25 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.prefs.AppPrefs
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.jetpref.datastore.ui.PreferenceLayout
|
||||
import dev.patrickgold.jetpref.datastore.ui.PreferenceUiContent
|
||||
|
||||
@Deprecated("Deprecated in favor of FlorisScreen DSL. When writing new screens make sure to use the new DSL version of this composable. Old code can continue using this version for now.")
|
||||
@Composable
|
||||
fun FlorisScreen(
|
||||
title: String,
|
||||
backArrowVisible: Boolean = true,
|
||||
scrollable: Boolean = true,
|
||||
iconSpaceReserved: Boolean = true,
|
||||
actions: @Composable RowScope.() -> Unit = { },
|
||||
bottomBar: @Composable () -> Unit = { },
|
||||
floatingActionButton: @Composable () -> Unit = { },
|
||||
content: PreferenceUiContent<AppPrefs>,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { FlorisAppBar(title, backArrowVisible, actions) },
|
||||
bottomBar = bottomBar,
|
||||
floatingActionButton = floatingActionButton,
|
||||
) { innerPadding ->
|
||||
val modifier = if (scrollable) {
|
||||
Modifier.florisVerticalScroll()
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(modifier = modifier.padding(innerPadding)) {
|
||||
PreferenceLayout(
|
||||
florisPreferenceModel(),
|
||||
scrollable = false,
|
||||
iconSpaceReserved = iconSpaceReserved,
|
||||
) {
|
||||
content(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlorisScreen(builder: @Composable FlorisScreenScope.() -> Unit) {
|
||||
val scope = remember { FlorisScreenScopeImpl() }
|
||||
@@ -76,11 +46,14 @@ typealias FlorisScreenActions = @Composable RowScope.() -> Unit
|
||||
typealias FlorisScreenBottomBar = @Composable () -> Unit
|
||||
typealias FlorisScreenContent = PreferenceUiContent<AppPrefs>
|
||||
typealias FlorisScreenFab = @Composable () -> Unit
|
||||
typealias FlorisScreenNavigationIcon = @Composable () -> Unit
|
||||
|
||||
interface FlorisScreenScope {
|
||||
var title: String
|
||||
|
||||
var backArrowVisible: Boolean
|
||||
var navigationIconVisible: Boolean
|
||||
|
||||
var previewFieldVisible: Boolean
|
||||
|
||||
var scrollable: Boolean
|
||||
|
||||
@@ -90,21 +63,32 @@ interface FlorisScreenScope {
|
||||
|
||||
fun bottomBar(bottomBar: FlorisScreenBottomBar)
|
||||
|
||||
fun content(content: FlorisScreenContent)
|
||||
|
||||
fun floatingActionButton(fab: FlorisScreenFab)
|
||||
|
||||
fun content(content: FlorisScreenContent)
|
||||
fun navigationIcon(navigationIcon: FlorisScreenNavigationIcon)
|
||||
}
|
||||
|
||||
private class FlorisScreenScopeImpl : FlorisScreenScope {
|
||||
override var title: String by mutableStateOf("")
|
||||
override var backArrowVisible: Boolean by mutableStateOf(true)
|
||||
override var navigationIconVisible: Boolean by mutableStateOf(true)
|
||||
override var previewFieldVisible: Boolean by mutableStateOf(false)
|
||||
override var scrollable: Boolean by mutableStateOf(true)
|
||||
override var iconSpaceReserved: Boolean by mutableStateOf(true)
|
||||
|
||||
private var actions: FlorisScreenActions = { }
|
||||
private var bottomBar: FlorisScreenBottomBar = { }
|
||||
private var content: FlorisScreenContent = { }
|
||||
private var fab: FlorisScreenFab = { }
|
||||
private var actions: FlorisScreenActions = @Composable { }
|
||||
private var bottomBar: FlorisScreenBottomBar = @Composable { }
|
||||
private var content: FlorisScreenContent = @Composable { }
|
||||
private var fab: FlorisScreenFab = @Composable { }
|
||||
private var navigationIcon: FlorisScreenNavigationIcon = @Composable {
|
||||
val navController = LocalNavController.current
|
||||
FlorisIconButton(
|
||||
onClick = { navController.popBackStack() },
|
||||
modifier = Modifier.autoMirrorForRtl(),
|
||||
icon = painterResource(R.drawable.ic_arrow_back),
|
||||
)
|
||||
}
|
||||
|
||||
override fun actions(actions: FlorisScreenActions) {
|
||||
this.actions = actions
|
||||
@@ -122,10 +106,20 @@ private class FlorisScreenScopeImpl : FlorisScreenScope {
|
||||
this.fab = fab
|
||||
}
|
||||
|
||||
override fun navigationIcon(navigationIcon: FlorisScreenNavigationIcon) {
|
||||
this.navigationIcon = navigationIcon
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Render() {
|
||||
val previewFieldController = LocalPreviewFieldController.current
|
||||
|
||||
SideEffect {
|
||||
previewFieldController?.isVisible = previewFieldVisible
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { FlorisAppBar(title, backArrowVisible, actions) },
|
||||
topBar = { FlorisAppBar(title, navigationIcon.takeIf { navigationIconVisible }, actions) },
|
||||
bottomBar = bottomBar,
|
||||
floatingActionButton = fab,
|
||||
) { innerPadding ->
|
||||
@@ -133,15 +127,15 @@ private class FlorisScreenScopeImpl : FlorisScreenScope {
|
||||
Modifier.florisVerticalScroll()
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(modifier = modifier.padding(innerPadding)) {
|
||||
PreferenceLayout(
|
||||
florisPreferenceModel(),
|
||||
scrollable = false,
|
||||
iconSpaceReserved = iconSpaceReserved,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
PreferenceLayout(
|
||||
florisPreferenceModel(),
|
||||
modifier = modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth(),
|
||||
iconSpaceReserved = iconSpaceReserved,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
|
||||
private val StepHeaderPaddingVertical = 16.dp
|
||||
private val StepHeaderNumberBoxSize = 40.dp
|
||||
@@ -198,7 +199,7 @@ private fun ColumnScope.Step(
|
||||
val autoStepId by stepState.getCurrentAuto()
|
||||
val backgroundColor = when (ownStepId) {
|
||||
currentStepId -> primaryColor
|
||||
else -> MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
|
||||
else -> MaterialTheme.colors.outline
|
||||
}
|
||||
val contentVisible = ownStepId == currentStepId
|
||||
StepHeader(
|
||||
@@ -243,7 +244,8 @@ private fun ColumnScope.Step(
|
||||
) {
|
||||
Column(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.florisVerticalScroll(),
|
||||
.florisVerticalScroll()
|
||||
.padding(end = 8.dp),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProvideTextStyle
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldColors
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
import dev.patrickgold.florisboard.common.ValidationResult
|
||||
|
||||
@Composable
|
||||
fun FlorisOutlinedTextField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = TextStyle.Default,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
placeholder: String? = null,
|
||||
isError: Boolean = false,
|
||||
showValidationHint: Boolean = true,
|
||||
showValidationError: Boolean = false,
|
||||
validationResult: ValidationResult? = null,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = MaterialTheme.shapes.small,
|
||||
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(),
|
||||
) {
|
||||
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
|
||||
val textFieldValue = textFieldValueState.copy(text = value)
|
||||
|
||||
FlorisOutlinedTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = {
|
||||
textFieldValueState = it
|
||||
if (value != it.text) {
|
||||
onValueChange(it.text)
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = textStyle,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
placeholder = placeholder,
|
||||
isError = isError,
|
||||
showValidationHint = showValidationHint,
|
||||
showValidationError = showValidationError,
|
||||
validationResult = validationResult,
|
||||
visualTransformation = visualTransformation,
|
||||
interactionSource = interactionSource,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FlorisOutlinedTextField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = TextStyle.Default,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
placeholder: String? = null,
|
||||
isError: Boolean = false,
|
||||
showValidationHint: Boolean = true,
|
||||
showValidationError: Boolean = false,
|
||||
validationResult: ValidationResult? = null,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = MaterialTheme.shapes.small,
|
||||
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
unfocusedBorderColor = MaterialTheme.colors.outline,
|
||||
disabledBorderColor = MaterialTheme.colors.outline,
|
||||
),
|
||||
) {
|
||||
val textColor = textStyle.color.takeOrElse {
|
||||
colors.textColor(enabled).value
|
||||
}
|
||||
val mergedTextStyle = textStyle.copy(color = textColor, textDirection = TextDirection.Content)
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
val isErrorState = isError || (showValidationError && validationResult?.isInvalid() == true)
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
BasicTextField(
|
||||
modifier = modifier.padding(vertical = 4.dp),
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = mergedTextStyle,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
visualTransformation = visualTransformation,
|
||||
cursorBrush = SolidColor(colors.cursorColor(isErrorState).value),
|
||||
decorationBox = { innerTextField ->
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
color = colors.backgroundColor(enabled).value,
|
||||
border = if (isErrorState && enabled) {
|
||||
BorderStroke(ButtonDefaults.OutlinedBorderSize, MaterialTheme.colors.error)
|
||||
} else if (isFocused) {
|
||||
BorderStroke(ButtonDefaults.OutlinedBorderSize, MaterialTheme.colors.primary)
|
||||
} else {
|
||||
ButtonDefaults.outlinedBorder
|
||||
},
|
||||
shape = shape,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.defaultMinSize(
|
||||
minWidth = ButtonDefaults.MinWidth,
|
||||
minHeight = 40.dp,
|
||||
)
|
||||
.padding(ButtonDefaults.ContentPadding),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
ProvideTextStyle(value = mergedTextStyle) {
|
||||
innerTextField()
|
||||
}
|
||||
if (!placeholder.isNullOrBlank()) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.56f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showValidationHint && validationResult?.isValid() == true && validationResult.hasHintMessage()) {
|
||||
Text(
|
||||
text = validationResult.hintMessage(),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.56f),
|
||||
)
|
||||
}
|
||||
|
||||
if (showValidationError && validationResult?.isInvalid() == true && validationResult.hasErrorMessage()) {
|
||||
Text(
|
||||
text = validationResult.errorMessage(),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,18 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
@@ -32,78 +36,109 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.common.InputMethodUtils
|
||||
import dev.patrickgold.florisboard.common.android.showShortToast
|
||||
|
||||
private const val AnimationDuration = 200
|
||||
|
||||
private val PreviewEnterTransition = EnterTransition.verticalTween(AnimationDuration)
|
||||
private val PreviewExitTransition = ExitTransition.verticalTween(AnimationDuration)
|
||||
|
||||
val LocalPreviewFieldController = staticCompositionLocalOf<PreviewFieldController?> { null }
|
||||
|
||||
@Composable
|
||||
fun rememberPreviewFieldController(): PreviewFieldController {
|
||||
return remember { PreviewFieldController() }
|
||||
}
|
||||
|
||||
class PreviewFieldController {
|
||||
val focusRequester = FocusRequester()
|
||||
var isVisible by mutableStateOf(false)
|
||||
var text by mutableStateOf(TextFieldValue(""))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun PreviewKeyboardField(
|
||||
controller: PreviewFieldController,
|
||||
modifier: Modifier = Modifier,
|
||||
hint: String = stringRes(R.string.settings__preview_keyboard),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var hasFocus by remember { mutableStateOf(false) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
var text by remember { mutableStateOf(TextFieldValue("")) }
|
||||
SelectionContainer {
|
||||
TextField(
|
||||
modifier = modifier
|
||||
.height(56.dp)
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusEvent { hasFocus = it.isFocused },
|
||||
value = text,
|
||||
onValueChange = { text = it },
|
||||
placeholder = { Text(hint) },
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = {
|
||||
if (hasFocus) focusManager.clearFocus() else focusRequester.requestFocus()
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = when {
|
||||
hasFocus -> R.drawable.ic_keyboard_arrow_down
|
||||
else -> R.drawable.ic_keyboard_arrow_up
|
||||
}),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
if (!InputMethodUtils.showImePicker(context)) {
|
||||
Toast.makeText(
|
||||
context, "Error: InputMethodManager service not available!", Toast.LENGTH_SHORT
|
||||
).show()
|
||||
AnimatedVisibility(
|
||||
visible = controller.isVisible,
|
||||
enter = PreviewEnterTransition,
|
||||
exit = PreviewExitTransition,
|
||||
) {
|
||||
SelectionContainer {
|
||||
TextField(
|
||||
modifier = modifier
|
||||
.height(56.dp)
|
||||
.fillMaxWidth()
|
||||
.onPreviewKeyEvent { event ->
|
||||
if (event.key == Key.Back) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_keyboard),
|
||||
contentDescription = null,
|
||||
)
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(autoCorrect = true),
|
||||
singleLine = true,
|
||||
shape = RectangleShape,
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
.focusRequester(controller.focusRequester),
|
||||
value = controller.text,
|
||||
onValueChange = { controller.text = it },
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.ContentOrLtr),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = hint,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = {
|
||||
if (!InputMethodUtils.showImePicker(context)) {
|
||||
context.showShortToast("Error: InputMethodManager service not available!")
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_keyboard),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { focusManager.clearFocus() },
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(autoCorrect = true),
|
||||
singleLine = true,
|
||||
shape = RectangleShape,
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.components
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
|
||||
fun Modifier.autoMirrorForRtl() = composed {
|
||||
if (LocalLayoutDirection.current == LayoutDirection.Rtl) {
|
||||
this.scale(-1f, 1f)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,8 @@ import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -126,7 +128,8 @@ fun Modifier.florisScrollbar(
|
||||
|
||||
fun Modifier.florisScrollbar(
|
||||
state: LazyListState,
|
||||
scrollbarSize: Dp = DefaultScrollbarSize,
|
||||
size: Dp = DefaultScrollbarSize,
|
||||
color: Color = Color.Unspecified,
|
||||
isVertical: Boolean,
|
||||
): Modifier = composed {
|
||||
var isInitial by remember { mutableStateOf(true) }
|
||||
@@ -136,7 +139,7 @@ fun Modifier.florisScrollbar(
|
||||
targetValue = targetAlpha,
|
||||
animationSpec = tween(durationMillis = duration, easing = ScrollbarAnimationEasing),
|
||||
)
|
||||
val scrollbarColor = MaterialTheme.colors.onSurface.copy(alpha = 0.28f)
|
||||
val scrollbarColor = color.takeOrElse { MaterialTheme.colors.onSurface.copy(alpha = 0.28f) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(1850)
|
||||
@@ -155,16 +158,16 @@ fun Modifier.florisScrollbar(
|
||||
|
||||
if (isVertical) {
|
||||
val elementHeight = this.size.height / state.layoutInfo.totalItemsCount
|
||||
scrollbarWidth = scrollbarSize.toPx()
|
||||
scrollbarWidth = size.toPx()
|
||||
scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight
|
||||
scrollbarOffsetX = size.width - scrollbarWidth
|
||||
scrollbarOffsetX = this.size.width - scrollbarWidth
|
||||
scrollbarOffsetY = firstVisibleElementIndex * elementHeight
|
||||
} else {
|
||||
val elementWidth = this.size.width / state.layoutInfo.totalItemsCount
|
||||
scrollbarWidth = state.layoutInfo.visibleItemsInfo.size * elementWidth
|
||||
scrollbarHeight = scrollbarSize.toPx()
|
||||
scrollbarHeight = size.toPx()
|
||||
scrollbarOffsetX = firstVisibleElementIndex * elementWidth
|
||||
scrollbarOffsetY = size.height - scrollbarHeight
|
||||
scrollbarOffsetY = this.size.height - scrollbarHeight
|
||||
}
|
||||
|
||||
drawRect(
|
||||
|
||||
@@ -42,18 +42,17 @@ import dev.patrickgold.florisboard.common.android.AndroidVersion
|
||||
fun SystemUiApp() {
|
||||
val systemUiController = rememberFlorisSystemUiController()
|
||||
val useDarkIcons = MaterialTheme.colors.isLight
|
||||
val backgroundColor = MaterialTheme.colors.background
|
||||
|
||||
SideEffect {
|
||||
systemUiController.setStatusBarColor(
|
||||
color = backgroundColor,
|
||||
color = Color.Transparent,
|
||||
darkIcons = useDarkIcons,
|
||||
)
|
||||
if (AndroidVersion.ATLEAST_API26_O) {
|
||||
systemUiController.setNavigationBarColor(
|
||||
color = backgroundColor,
|
||||
color = Color.Transparent,
|
||||
darkIcons = useDarkIcons,
|
||||
navigationBarContrastEnforced = true,
|
||||
navigationBarContrastEnforced = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -194,4 +193,3 @@ private class FlorisSystemUiController(
|
||||
return if (context is ContextWrapper) context.findWindow() else null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,25 +32,27 @@ import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun AndroidLocalesScreen() = FlorisScreen(
|
||||
title = stringRes(R.string.devtools__android_locales__title),
|
||||
scrollable = false,
|
||||
) {
|
||||
fun AndroidLocalesScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.devtools__android_locales__title)
|
||||
scrollable = false
|
||||
|
||||
val availableLocales = remember { Locale.getAvailableLocales().sortedBy { it.toLanguageTag() } }
|
||||
|
||||
SelectionContainer(modifier = Modifier.fillMaxWidth()) {
|
||||
LazyColumn {
|
||||
items(availableLocales) {
|
||||
Row {
|
||||
Text(
|
||||
text = it.toLanguageTag().padEnd(12),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.weight(1.0f),
|
||||
text = it.getDisplayName(it),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
content {
|
||||
SelectionContainer(modifier = Modifier.fillMaxWidth()) {
|
||||
LazyColumn {
|
||||
items(availableLocales) {
|
||||
Row {
|
||||
Text(
|
||||
text = it.toLanguageTag().padEnd(12),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.weight(1.0f),
|
||||
text = it.getDisplayName(it),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,48 +34,50 @@ import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
||||
|
||||
@Composable
|
||||
fun AndroidSettingsScreen(name: String?) = FlorisScreen(
|
||||
fun AndroidSettingsScreen(name: String?) = FlorisScreen {
|
||||
title = when (name) {
|
||||
AndroidSettings.Global.groupId -> stringRes(R.string.devtools__android_settings_global__title)
|
||||
AndroidSettings.Secure.groupId -> stringRes(R.string.devtools__android_settings_secure__title)
|
||||
AndroidSettings.System.groupId -> stringRes(R.string.devtools__android_settings_system__title)
|
||||
else -> "invalid"
|
||||
},
|
||||
scrollable = false,
|
||||
) {
|
||||
}
|
||||
scrollable = false
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val settingsGroup = when (name) {
|
||||
AndroidSettings.Global.groupId -> AndroidSettings.Global
|
||||
AndroidSettings.Secure.groupId -> AndroidSettings.Secure
|
||||
AndroidSettings.System.groupId -> AndroidSettings.System
|
||||
else -> return@FlorisScreen
|
||||
else -> AndroidSettings.Global
|
||||
}
|
||||
val nameValueTable = remember(name) { settingsGroup.getAllKeys().toList() }
|
||||
var dialogKey by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LazyColumn {
|
||||
items(nameValueTable) { (fieldName, key) ->
|
||||
Preference(
|
||||
title = fieldName,
|
||||
summary = key,
|
||||
onClick = { dialogKey = key },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (dialogKey != null) {
|
||||
JetPrefAlertDialog(
|
||||
title = dialogKey!!,
|
||||
onDismiss = { dialogKey = null },
|
||||
) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = remember {
|
||||
(settingsGroup.getString(context, dialogKey!!) ?: "(null)").ifBlank { "(blank)" }
|
||||
},
|
||||
content {
|
||||
LazyColumn {
|
||||
items(nameValueTable) { (fieldName, key) ->
|
||||
Preference(
|
||||
title = fieldName,
|
||||
summary = key,
|
||||
onClick = { dialogKey = key },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (dialogKey != null) {
|
||||
JetPrefAlertDialog(
|
||||
title = dialogKey!!,
|
||||
onDismiss = { dialogKey = null },
|
||||
) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = remember {
|
||||
(settingsGroup.getString(context, dialogKey!!) ?: "(null)").ifBlank { "(blank)" }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.devtools
|
||||
|
||||
import android.view.textservice.SuggestionsInfo
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.patrickgold.florisboard.app.prefs.florisPreferenceModel
|
||||
import dev.patrickgold.florisboard.clipboardManager
|
||||
import dev.patrickgold.florisboard.common.FlorisLocale
|
||||
import dev.patrickgold.florisboard.common.observeAsNonNullState
|
||||
import dev.patrickgold.florisboard.spellingManager
|
||||
import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
private val CardBackground = Color.Black.copy(0.6f)
|
||||
private val DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", FlorisLocale.default().base)
|
||||
|
||||
@Composable
|
||||
fun DevtoolsOverlay(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val prefs by florisPreferenceModel()
|
||||
val context = LocalContext.current
|
||||
val clipboardManager by context.clipboardManager()
|
||||
|
||||
val showPrimaryClip by prefs.devtools.showPrimaryClip.observeAsState()
|
||||
val showSpellingOverlay by prefs.devtools.showSpellingOverlay.observeAsState()
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides Color.White,
|
||||
LocalLayoutDirection provides LayoutDirection.Ltr,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
if (showPrimaryClip) {
|
||||
val primaryClip by clipboardManager.primaryClip.observeAsState()
|
||||
Text(
|
||||
text = primaryClip.toString(),
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
if (showSpellingOverlay) {
|
||||
DevtoolsSpellingOverlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DevtoolsSpellingOverlay() {
|
||||
val context = LocalContext.current
|
||||
val spellingManager by context.spellingManager()
|
||||
|
||||
val debugOverlayVersion by spellingManager.debugOverlayVersion.observeAsNonNullState()
|
||||
val suggestionsInfos = remember(debugOverlayVersion) { spellingManager.debugOverlaySuggestionsInfos.snapshot() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(all = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.background(CardBackground),
|
||||
) {
|
||||
val sortedEntries = suggestionsInfos.entries.sortedByDescending { it.key }
|
||||
Text(
|
||||
modifier = Modifier.padding(all = 8.dp),
|
||||
text = "Spelling overlay (${sortedEntries.size})",
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
for ((timestamp, wordInfoPair) in sortedEntries) {
|
||||
val (word, info) = wordInfoPair
|
||||
val isTypo = (info.suggestionsAttributes and SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0
|
||||
val suggestions = Array(info.suggestionsCount) { n -> info.getSuggestionAt(n) }
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||
val date = DateFormat.format(Date(timestamp))
|
||||
Text(
|
||||
text = "$date - \"$word\"",
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
val details = buildString {
|
||||
appendLine("isTypo: $isTypo")
|
||||
if (isTypo) {
|
||||
appendLine("providing corrections list of size n=${suggestions.size}")
|
||||
for ((n, suggestion) in suggestions.withIndex()) {
|
||||
append(" [$n] = string[${suggestion.length}] { \"")
|
||||
append(suggestion)
|
||||
appendLine("\" }")
|
||||
}
|
||||
}
|
||||
}.prependIndent(" ")
|
||||
Text(
|
||||
text = details,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.devtools
|
||||
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -25,6 +24,7 @@ import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.Routes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisConfirmDeleteDialog
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.common.android.AndroidSettings
|
||||
import dev.patrickgold.florisboard.ime.dictionary.DictionaryManager
|
||||
@@ -33,7 +33,6 @@ import dev.patrickgold.jetpref.datastore.model.observeAsState
|
||||
import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
|
||||
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
||||
|
||||
class DebugOnPurposeCrashException : Exception(
|
||||
"Success! The app crashed purposely to display this beautiful screen we all love :)"
|
||||
@@ -42,6 +41,7 @@ class DebugOnPurposeCrashException : Exception(
|
||||
@Composable
|
||||
fun DevtoolsScreen() = FlorisScreen {
|
||||
title = stringRes(R.string.devtools__title)
|
||||
previewFieldVisible = true
|
||||
|
||||
val navController = LocalNavController.current
|
||||
val (showDialog, setShowDialog) = remember { mutableStateOf(false) }
|
||||
@@ -66,6 +66,12 @@ fun DevtoolsScreen() = FlorisScreen {
|
||||
summary = stringRes(R.string.devtools__show_primary_clip__summary),
|
||||
enabledIf = { prefs.devtools.enabled isEqualTo true },
|
||||
)
|
||||
SwitchPreference(
|
||||
prefs.devtools.showSpellingOverlay,
|
||||
title = stringRes(R.string.devtools__show_spelling_overlay__label),
|
||||
summary = stringRes(R.string.devtools__show_spelling_overlay__summary),
|
||||
enabledIf = { prefs.devtools.enabled isEqualTo true },
|
||||
)
|
||||
// TODO: remove this preference once word suggestions are re-implemented in 0.3.15/16
|
||||
SwitchPreference(
|
||||
prefs.devtools.overrideWordSuggestionsMinHeapRestriction,
|
||||
@@ -154,9 +160,7 @@ fun DevtoolsScreen() = FlorisScreen {
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
JetPrefAlertDialog(
|
||||
title = stringRes(R.string.assets__action__delete_confirm_title),
|
||||
confirmLabel = stringRes(R.string.assets__action__delete),
|
||||
FlorisConfirmDeleteDialog(
|
||||
onConfirm = {
|
||||
DictionaryManager.default().let {
|
||||
it.loadUserDictionariesIfNecessary()
|
||||
@@ -164,16 +168,9 @@ fun DevtoolsScreen() = FlorisScreen {
|
||||
}
|
||||
setShowDialog(false)
|
||||
},
|
||||
dismissLabel = stringRes(R.string.assets__action__cancel),
|
||||
onDismiss = { setShowDialog(false) },
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(
|
||||
R.string.assets__action__delete_confirm_message,
|
||||
"database_name" to FlorisUserDictionaryDatabase.DB_FILE_NAME,
|
||||
)
|
||||
)
|
||||
}
|
||||
what = FlorisUserDictionaryDatabase.DB_FILE_NAME,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.ext
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ListItem
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisIconButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisTextButton
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponent
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMeta
|
||||
|
||||
@Composable
|
||||
fun ExtensionComponentNoneFoundView() {
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
text = stringRes(R.string.ext__meta__components_none_found),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionComponentView(
|
||||
meta: ExtensionMeta,
|
||||
component: ExtensionComponent,
|
||||
modifier: Modifier = Modifier,
|
||||
onDeleteBtnClick: (() -> Unit)? = null,
|
||||
onEditBtnClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val componentName = remember(meta.id, component.id) { ExtensionComponentName(meta.id, component.id).toString() }
|
||||
FlorisOutlinedBox(
|
||||
modifier = modifier,
|
||||
title = component.label,
|
||||
subtitle = componentName,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = if (onDeleteBtnClick == null && onEditBtnClick == null) 8.dp else 0.dp,
|
||||
),
|
||||
) {
|
||||
when (component) {
|
||||
is ThemeExtensionComponent -> {
|
||||
val text = remember(
|
||||
component.authors, component.isNightTheme, component.isBorderless,
|
||||
component.isMaterialYouAware, component.stylesheetPath(),
|
||||
) {
|
||||
buildString {
|
||||
appendLine("authors = ${component.authors}")
|
||||
appendLine("isNightTheme = ${component.isNightTheme}")
|
||||
appendLine("isBorderless = ${component.isBorderless}")
|
||||
appendLine("isMaterialYouAware = ${component.isMaterialYouAware}")
|
||||
append("stylesheetPath = ${component.stylesheetPath()}")
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
|
||||
)
|
||||
}
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
if (onDeleteBtnClick != null || onEditBtnClick != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 6.dp),
|
||||
) {
|
||||
if (onDeleteBtnClick != null) {
|
||||
FlorisTextButton(
|
||||
onClick = onDeleteBtnClick,
|
||||
icon = painterResource(R.drawable.ic_delete),
|
||||
text = stringRes(R.string.action__delete),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colors.error,
|
||||
),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (onEditBtnClick != null) {
|
||||
FlorisTextButton(
|
||||
onClick = onEditBtnClick,
|
||||
icon = painterResource(R.drawable.ic_edit),
|
||||
text = stringRes(R.string.action__edit),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun <T : ExtensionComponent> ExtensionComponentListView(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
components: List<T>,
|
||||
onCreateBtnClick: (() -> Unit)? = null,
|
||||
componentGenerator: @Composable (T) -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ListItem(
|
||||
text = { Text(
|
||||
text = title,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
) },
|
||||
trailing = if (onCreateBtnClick != null) {
|
||||
@Composable {
|
||||
FlorisIconButton(
|
||||
onClick = onCreateBtnClick,
|
||||
icon = painterResource(R.drawable.ic_add),
|
||||
iconColor = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
} else { null },
|
||||
)
|
||||
if (components.isEmpty()) {
|
||||
ExtensionComponentNoneFoundView()
|
||||
} else {
|
||||
for (component in components) {
|
||||
componentGenerator(component)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,900 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.ext
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisButtonBar
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisIconButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisInfoCard
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedTextField
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisUnsavedChangesDialog
|
||||
import dev.patrickgold.florisboard.app.ui.components.autoMirrorForRtl
|
||||
import dev.patrickgold.florisboard.app.ui.components.defaultFlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.settings.advanced.RadioListItem
|
||||
import dev.patrickgold.florisboard.app.ui.settings.theme.DialogProperty
|
||||
import dev.patrickgold.florisboard.app.ui.settings.theme.ThemeEditorScreen
|
||||
import dev.patrickgold.florisboard.app.ui.theme.outline
|
||||
import dev.patrickgold.florisboard.cacheManager
|
||||
import dev.patrickgold.florisboard.common.ValidationResult
|
||||
import dev.patrickgold.florisboard.common.android.showLongToast
|
||||
import dev.patrickgold.florisboard.common.rememberValidationResult
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingExtension
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponentEditor
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponentImpl
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionEditor
|
||||
import dev.patrickgold.florisboard.res.FlorisRef
|
||||
import dev.patrickgold.florisboard.res.ZipUtils
|
||||
import dev.patrickgold.florisboard.res.cache.CacheManager
|
||||
import dev.patrickgold.florisboard.res.ext.Extension
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponent
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionComponentName
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionDefaults
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionEditor
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionJsonConfig
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMaintainer
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionManager
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionMeta
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionValidation
|
||||
import dev.patrickgold.florisboard.res.ext.validate
|
||||
import dev.patrickgold.florisboard.res.io.subFile
|
||||
import dev.patrickgold.florisboard.res.io.writeJson
|
||||
import dev.patrickgold.florisboard.snygg.SnyggStylesheetJsonConfig
|
||||
import dev.patrickgold.florisboard.themeManager
|
||||
import dev.patrickgold.jetpref.datastore.ui.Preference
|
||||
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
private val TextFieldVerticalPadding = 8.dp
|
||||
private val MetaDataContentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp)
|
||||
|
||||
private const val AnimationDuration = 300
|
||||
|
||||
private val ActionScreenEnterTransition = fadeIn(tween(AnimationDuration))
|
||||
private val ActionScreenExitTransition = fadeOut(tween(AnimationDuration))
|
||||
|
||||
sealed class EditorAction {
|
||||
object ManageMetaData : EditorAction()
|
||||
|
||||
object ManageDependencies : EditorAction()
|
||||
|
||||
object ManageFiles : EditorAction()
|
||||
|
||||
data class CreateComponent<T : ExtensionComponent>(val type: KClass<T>) : EditorAction()
|
||||
|
||||
data class ManageComponent(val editor: ExtensionComponent) : EditorAction()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionEditScreen(id: String, createSerialType: String?) {
|
||||
val context = LocalContext.current
|
||||
val cacheManager by context.cacheManager()
|
||||
val extensionManager by context.extensionManager()
|
||||
|
||||
@Suppress("unchecked_cast")
|
||||
fun <W : CacheManager.ExtEditorWorkspace<T>, T : ExtensionEditor> getOrCreateWorkspace(
|
||||
uuid: String,
|
||||
container: CacheManager.WorkspacesContainer<W>,
|
||||
ext: Extension,
|
||||
): W {
|
||||
val workspace = container.getWorkspaceByUuid(uuid)
|
||||
return workspace ?: container.new(uuid).also { newWorkspace ->
|
||||
val sourceRef = ext.sourceRef
|
||||
if (createSerialType == null) {
|
||||
checkNotNull(sourceRef) { "Extension source ref must not be null" }
|
||||
ZipUtils.unzip(context, sourceRef, newWorkspace.extDir)
|
||||
}
|
||||
newWorkspace.ext = ext
|
||||
newWorkspace.editor = ext.edit() as? T
|
||||
}
|
||||
}
|
||||
|
||||
val ext = extensionManager.getExtensionById(id) ?: remember {
|
||||
val meta = ExtensionMeta(
|
||||
id = ExtensionDefaults.createLocalId("themes", System.currentTimeMillis().toString()),
|
||||
version = "0.0.0",
|
||||
title = "My themes",
|
||||
maintainers = listOf(ExtensionMaintainer(name = "Local")),
|
||||
license = "(none specified)",
|
||||
)
|
||||
when (createSerialType) {
|
||||
ThemeExtension.SERIAL_TYPE -> ThemeExtension(meta, null, emptyList())
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
if (ext != null) {
|
||||
val uuid = rememberSaveable { UUID.randomUUID().toString() }
|
||||
val cacheWorkspace = remember {
|
||||
runCatching {
|
||||
when (ext) {
|
||||
is ThemeExtension -> {
|
||||
getOrCreateWorkspace(uuid, cacheManager.themeExtEditor, ext)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
cacheWorkspace.onSuccess { workspace ->
|
||||
if (workspace?.editor != null) {
|
||||
ExtensionEditScreenSheetSwitcher(workspace, isCreateExt = createSerialType != null)
|
||||
} else {
|
||||
ExtensionNotFoundScreen(id = id)
|
||||
}
|
||||
}.onFailure { error ->
|
||||
Text(text = remember(error) { error.stackTraceToString() })
|
||||
}
|
||||
} else {
|
||||
ExtensionNotFoundScreen(id)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionEditScreenSheetSwitcher(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
isCreateExt: Boolean,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
EditScreen(workspace, isCreateExt)
|
||||
AnimatedVisibility(
|
||||
visible = workspace.currentAction != null,
|
||||
enter = ActionScreenEnterTransition,
|
||||
exit = ActionScreenExitTransition,
|
||||
) {
|
||||
when (val action = workspace.currentAction) {
|
||||
is EditorAction.ManageMetaData -> {
|
||||
ManageMetaDataScreen(workspace, isCreateExt)
|
||||
}
|
||||
is EditorAction.ManageDependencies -> {
|
||||
ManageDependenciesScreen(workspace)
|
||||
}
|
||||
is EditorAction.ManageFiles -> {
|
||||
ManageFilesScreen(workspace)
|
||||
}
|
||||
is EditorAction.CreateComponent<*> -> {
|
||||
CreateComponentScreen(workspace, action.type)
|
||||
}
|
||||
is EditorAction.ManageComponent -> when (action.editor) {
|
||||
is ThemeExtensionComponentEditor -> {
|
||||
ThemeEditorScreen(workspace, action.editor)
|
||||
}
|
||||
else -> {
|
||||
// Render nothing
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Render nothing
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun EditScreen(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
isCreateExt: Boolean,
|
||||
) = FlorisScreen {
|
||||
title = stringRes(if (isCreateExt) {
|
||||
when (workspace.ext) {
|
||||
is KeyboardExtension -> R.string.ext__editor__title_create_keyboard
|
||||
is SpellingExtension -> R.string.ext__editor__title_create_spelling
|
||||
is ThemeExtension -> R.string.ext__editor__title_create_theme
|
||||
else -> R.string.ext__editor__title_create_any
|
||||
}
|
||||
} else {
|
||||
when (workspace.ext) {
|
||||
is KeyboardExtension -> R.string.ext__editor__title_edit_keyboard
|
||||
is SpellingExtension -> R.string.ext__editor__title_edit_spelling
|
||||
is ThemeExtension -> R.string.ext__editor__title_edit_theme
|
||||
else -> R.string.ext__editor__title_edit_any
|
||||
}
|
||||
})
|
||||
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
|
||||
val extEditor = workspace.editor ?: return@FlorisScreen
|
||||
var showUnsavedChangesDialog by remember { mutableStateOf(false) }
|
||||
var showInvalidMetadataDialog by remember { mutableStateOf(false) }
|
||||
|
||||
fun handleBackPress() {
|
||||
if (workspace.isModified) {
|
||||
showUnsavedChangesDialog = true
|
||||
} else {
|
||||
workspace.close()
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSave() {
|
||||
if (!extEditor.meta.validate()) {
|
||||
showUnsavedChangesDialog = false
|
||||
showInvalidMetadataDialog = true
|
||||
return
|
||||
}
|
||||
val manifest = extEditor.build()
|
||||
val manifestFile = workspace.saverDir.subFile(ExtensionDefaults.MANIFEST_FILE_NAME)
|
||||
manifestFile.writeJson(manifest, ExtensionJsonConfig)
|
||||
when (extEditor) {
|
||||
is ThemeExtensionEditor -> {
|
||||
for (theme in extEditor.themes) {
|
||||
val stylesheetFile = workspace.saverDir.subFile(theme.stylesheetPath())
|
||||
stylesheetFile.parentFile?.mkdirs()
|
||||
val stylesheetEditor = theme.stylesheetEditor
|
||||
if (stylesheetEditor != null) {
|
||||
val stylesheet = stylesheetEditor.build()
|
||||
stylesheetFile.writeJson(stylesheet, SnyggStylesheetJsonConfig)
|
||||
} else {
|
||||
val unmodifiedStylesheetFile = workspace.extDir.subFile(theme.stylesheetPath())
|
||||
if (unmodifiedStylesheetFile.exists()) {
|
||||
unmodifiedStylesheetFile.copyTo(stylesheetFile, overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> { }
|
||||
}
|
||||
val flexArchiveName = ExtensionDefaults.createFlexName(extEditor.meta.id)
|
||||
val flexArchiveFile = workspace.dir.subFile(flexArchiveName)
|
||||
ZipUtils.zip(workspace.saverDir, flexArchiveFile)
|
||||
val sourceRef = if (isCreateExt) {
|
||||
FlorisRef.internal(ExtensionManager.IME_THEME_PATH).subRef(flexArchiveName)
|
||||
} else {
|
||||
workspace.ext!!.sourceRef!!
|
||||
}
|
||||
flexArchiveFile.copyTo(sourceRef.absoluteFile(context), overwrite = true)
|
||||
workspace.close()
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
navigationIcon {
|
||||
FlorisIconButton(
|
||||
onClick = { handleBackPress() },
|
||||
modifier = Modifier.autoMirrorForRtl(),
|
||||
icon = painterResource(R.drawable.ic_arrow_back),
|
||||
)
|
||||
}
|
||||
|
||||
bottomBar {
|
||||
FlorisButtonBar {
|
||||
ButtonBarSpacer()
|
||||
ButtonBarTextButton(text = stringRes(R.string.action__cancel)) {
|
||||
handleBackPress()
|
||||
}
|
||||
ButtonBarButton(text = stringRes(R.string.action__save)) {
|
||||
handleSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content {
|
||||
BackHandler {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
) {
|
||||
this@content.Preference(
|
||||
onClick = { workspace.currentAction = EditorAction.ManageMetaData },
|
||||
iconId = R.drawable.ic_code,
|
||||
title = stringRes(R.string.ext__editor__metadata__title),
|
||||
)
|
||||
this@content.Preference(
|
||||
onClick = { workspace.currentAction = EditorAction.ManageDependencies },
|
||||
iconId = R.drawable.ic_library_books,
|
||||
title = stringRes(R.string.ext__editor__dependencies__title),
|
||||
)
|
||||
this@content.Preference(
|
||||
onClick = { workspace.currentAction = EditorAction.ManageFiles },
|
||||
iconId = R.drawable.ic_file_blank,
|
||||
title = stringRes(R.string.ext__editor__files__title),
|
||||
)
|
||||
}
|
||||
|
||||
when (extEditor) {
|
||||
is ThemeExtensionEditor -> {
|
||||
ExtensionComponentListView(
|
||||
title = stringRes(R.string.ext__meta__components_theme),
|
||||
components = extEditor.themes,
|
||||
onCreateBtnClick = {
|
||||
workspace.currentAction = EditorAction.CreateComponent(ThemeExtensionComponent::class)
|
||||
},
|
||||
) { component ->
|
||||
ExtensionComponentView(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
meta = extEditor.meta,
|
||||
component = component,
|
||||
onDeleteBtnClick = { workspace.update { extEditor.themes.remove(component) } },
|
||||
onEditBtnClick = { workspace.currentAction = EditorAction.ManageComponent(component) },
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Render nothing
|
||||
}
|
||||
}
|
||||
|
||||
if (showUnsavedChangesDialog) {
|
||||
FlorisUnsavedChangesDialog(
|
||||
onSave = {
|
||||
handleSave()
|
||||
},
|
||||
onDiscard = {
|
||||
navController.popBackStack()
|
||||
showUnsavedChangesDialog = false
|
||||
},
|
||||
onDismiss = {
|
||||
showUnsavedChangesDialog = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showInvalidMetadataDialog) {
|
||||
JetPrefAlertDialog(
|
||||
title = stringRes(R.string.ext__editor__metadata__title_invalid),
|
||||
confirmLabel = stringRes(R.string.action__ok),
|
||||
onConfirm = {
|
||||
showInvalidMetadataDialog = false
|
||||
},
|
||||
onDismiss = {
|
||||
showInvalidMetadataDialog = false
|
||||
},
|
||||
content = {
|
||||
Text(text = stringRes(R.string.ext__editor__metadata__message_invalid))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageMetaDataScreen(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
isCreateExt: Boolean,
|
||||
) = FlorisScreen {
|
||||
title = stringRes(R.string.ext__editor__metadata__title)
|
||||
|
||||
val meta = workspace.editor?.meta ?: return@FlorisScreen
|
||||
var showValidationErrors by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var id by rememberSaveable { mutableStateOf(meta.id) }
|
||||
val idValidation = rememberValidationResult(ExtensionValidation.MetaId, id)
|
||||
var version by rememberSaveable { mutableStateOf(meta.version) }
|
||||
val versionValidation = rememberValidationResult(ExtensionValidation.MetaVersion, version)
|
||||
var title by rememberSaveable { mutableStateOf(meta.title) }
|
||||
val titleValidation = rememberValidationResult(ExtensionValidation.MetaTitle, title)
|
||||
var description by rememberSaveable { mutableStateOf(meta.description ?: "") }
|
||||
var keywords by rememberSaveable { mutableStateOf(meta.keywords?.joinToString("\n") ?: "") }
|
||||
var homepage by rememberSaveable { mutableStateOf(meta.homepage ?: "") }
|
||||
var issueTracker by rememberSaveable { mutableStateOf(meta.issueTracker ?: "") }
|
||||
var maintainers by rememberSaveable { mutableStateOf(meta.maintainers.joinToString("\n")) }
|
||||
val maintainersValidation = rememberValidationResult(ExtensionValidation.MetaMaintainers, maintainers)
|
||||
var license by rememberSaveable { mutableStateOf(meta.license) }
|
||||
val licenseValidation = rememberValidationResult(ExtensionValidation.MetaLicense, license)
|
||||
|
||||
fun handleBackPress() {
|
||||
workspace.currentAction = null
|
||||
}
|
||||
|
||||
fun handleApply() {
|
||||
val invalid = idValidation.isInvalid() ||
|
||||
versionValidation.isInvalid() ||
|
||||
titleValidation.isInvalid() ||
|
||||
maintainersValidation.isInvalid() ||
|
||||
licenseValidation.isInvalid()
|
||||
if (invalid) {
|
||||
showValidationErrors = true
|
||||
} else {
|
||||
workspace.update {
|
||||
workspace.editor?.meta = ExtensionMeta(
|
||||
id = id.trim(),
|
||||
version = version.trim(),
|
||||
title = title.trim(),
|
||||
description = description.trim().takeIf { it.isNotBlank() },
|
||||
keywords = keywords.lines().map { it.trim() }.filter { it.isNotBlank() }.takeIf { it.isNotEmpty() },
|
||||
homepage = homepage.trim().takeIf { it.isNotBlank() },
|
||||
issueTracker = issueTracker.trim().takeIf { it.isNotBlank() },
|
||||
maintainers = maintainers.lines().map { it.trim() }.filter { it.isNotBlank() }
|
||||
.map { ExtensionMaintainer.fromOrTakeRaw(it) },
|
||||
license = license.trim(),
|
||||
)
|
||||
}
|
||||
workspace.currentAction = null
|
||||
}
|
||||
}
|
||||
|
||||
navigationIcon {
|
||||
FlorisIconButton(
|
||||
onClick = { handleBackPress() },
|
||||
icon = painterResource(R.drawable.ic_close),
|
||||
)
|
||||
}
|
||||
|
||||
bottomBar {
|
||||
FlorisButtonBar {
|
||||
ButtonBarSpacer()
|
||||
ButtonBarTextButton(text = stringRes(R.string.action__cancel)) {
|
||||
handleBackPress()
|
||||
}
|
||||
ButtonBarButton(text = stringRes(R.string.action__apply)) {
|
||||
handleApply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content {
|
||||
BackHandler {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(MetaDataContentPadding)) {
|
||||
EditorSheetTextField(
|
||||
enabled = isCreateExt,
|
||||
isRequired = true,
|
||||
value = id,
|
||||
onValueChange = { id = it },
|
||||
label = stringRes(R.string.ext__meta__id),
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = idValidation,
|
||||
)
|
||||
EditorSheetTextField(
|
||||
isRequired = true,
|
||||
value = version,
|
||||
onValueChange = { version = it },
|
||||
label = stringRes(R.string.ext__meta__version),
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = versionValidation,
|
||||
)
|
||||
EditorSheetTextField(
|
||||
isRequired = true,
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
label = stringRes(R.string.ext__meta__title),
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = titleValidation,
|
||||
)
|
||||
EditorSheetTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = stringRes(R.string.ext__meta__description),
|
||||
)
|
||||
EditorSheetTextField(
|
||||
value = keywords,
|
||||
onValueChange = { keywords = it },
|
||||
label = stringRes(R.string.ext__meta__keywords),
|
||||
singleLine = false,
|
||||
)
|
||||
EditorSheetTextField(
|
||||
value = homepage,
|
||||
onValueChange = { homepage = it },
|
||||
label = stringRes(R.string.ext__meta__homepage),
|
||||
)
|
||||
EditorSheetTextField(
|
||||
value = issueTracker,
|
||||
onValueChange = { issueTracker = it },
|
||||
label = stringRes(R.string.ext__meta__issue_tracker),
|
||||
)
|
||||
EditorSheetTextField(
|
||||
isRequired = true,
|
||||
value = maintainers,
|
||||
onValueChange = { maintainers = it },
|
||||
label = stringRes(R.string.ext__meta__maintainers),
|
||||
singleLine = false,
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = maintainersValidation,
|
||||
)
|
||||
EditorSheetTextField(
|
||||
isRequired = true,
|
||||
value = license,
|
||||
onValueChange = { license = it },
|
||||
label = stringRes(R.string.ext__meta__license),
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = licenseValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageDependenciesScreen(workspace: CacheManager.ExtEditorWorkspace<*>) = FlorisScreen {
|
||||
title = stringRes(R.string.ext__editor__dependencies__title)
|
||||
|
||||
val dependencyList = workspace.editor?.dependencies ?: return@FlorisScreen
|
||||
|
||||
fun handleBackPress() {
|
||||
workspace.currentAction = null
|
||||
}
|
||||
|
||||
navigationIcon {
|
||||
FlorisIconButton(
|
||||
onClick = { handleBackPress() },
|
||||
icon = painterResource(R.drawable.ic_close),
|
||||
)
|
||||
}
|
||||
|
||||
content {
|
||||
BackHandler {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
FlorisInfoCard(
|
||||
modifier = Modifier.padding(all = 8.dp),
|
||||
text = """
|
||||
Dependencies are currently not implemented, but are already somewhat
|
||||
integrated as a placeholder for the future.
|
||||
""".trimIndent().replace('\n', ' '),
|
||||
)
|
||||
if (dependencyList.isEmpty()) {
|
||||
Text(text = "no deps found")
|
||||
} else {
|
||||
for (dependency in dependencyList) {
|
||||
Text(text = dependency)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageFilesScreen(workspace: CacheManager.ExtEditorWorkspace<*>) = FlorisScreen {
|
||||
title = stringRes(R.string.ext__editor__files__title)
|
||||
|
||||
fun handleBackPress() {
|
||||
workspace.currentAction = null
|
||||
}
|
||||
|
||||
navigationIcon {
|
||||
FlorisIconButton(
|
||||
onClick = { handleBackPress() },
|
||||
icon = painterResource(R.drawable.ic_close),
|
||||
)
|
||||
}
|
||||
|
||||
content {
|
||||
BackHandler {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
FlorisInfoCard(
|
||||
modifier = Modifier.padding(all = 8.dp),
|
||||
text = """
|
||||
Managing archive files is currently not supported.
|
||||
""".trimIndent().replace('\n', ' '),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class CreateFrom {
|
||||
EMPTY,
|
||||
EXISTING;
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : ExtensionComponent> CreateComponentScreen(
|
||||
workspace: CacheManager.ExtEditorWorkspace<*>,
|
||||
type: KClass<T>,
|
||||
) = FlorisScreen {
|
||||
title = stringRes(when (type) {
|
||||
ThemeExtensionComponent::class -> R.string.ext__editor__create_component__title_theme
|
||||
else -> R.string.ext__editor__create_component__title
|
||||
})
|
||||
|
||||
val context = LocalContext.current
|
||||
val extensionManager by context.extensionManager()
|
||||
val themeManager by context.themeManager()
|
||||
|
||||
var createFrom by rememberSaveable { mutableStateOf(CreateFrom.EXISTING) }
|
||||
val extId = workspace.editor?.meta?.id ?: "null"
|
||||
val components = remember<Map<ExtensionComponentName, ExtensionComponent>> {
|
||||
when (val editor = workspace.editor) {
|
||||
is ThemeExtensionEditor -> buildMap {
|
||||
for (theme in editor.themes) {
|
||||
put(ExtensionComponentName(extId, theme.id), theme)
|
||||
}
|
||||
for ((componentName, theme) in themeManager.indexedThemeConfigs.value ?: emptyMap()) {
|
||||
if (componentName.extensionId != extId) {
|
||||
put(componentName, theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
var selectedComponentName by rememberSaveable(stateSaver = ExtensionComponentName.Saver) {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
var showValidationErrors by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var newId by rememberSaveable { mutableStateOf("") }
|
||||
val newIdValidation = rememberValidationResult(ExtensionValidation.ComponentId, newId)
|
||||
var newLabel by rememberSaveable { mutableStateOf("") }
|
||||
val newLabelValidation = rememberValidationResult(ExtensionValidation.ComponentLabel, newLabel)
|
||||
var newAuthors by rememberSaveable { mutableStateOf("") }
|
||||
val newAuthorsValidation = rememberValidationResult(ExtensionValidation.ComponentAuthors, newAuthors)
|
||||
|
||||
fun handleBackPress() {
|
||||
workspace.currentAction = null
|
||||
}
|
||||
|
||||
fun handleCreate() {
|
||||
val invalid = createFrom == CreateFrom.EMPTY && (newIdValidation.isInvalid() ||
|
||||
newLabelValidation.isInvalid() || newAuthorsValidation.isInvalid())
|
||||
if (invalid) {
|
||||
showValidationErrors = true
|
||||
} else {
|
||||
when (val editor = workspace.editor) {
|
||||
is ThemeExtensionEditor -> {
|
||||
when (createFrom) {
|
||||
CreateFrom.EMPTY -> {
|
||||
if (editor.themes.any { it.id == newId.trim() }) {
|
||||
context.showLongToast("A theme with this ID already exists!")
|
||||
} else {
|
||||
val componentEditor = ThemeExtensionComponentEditor(
|
||||
id = newId.trim(),
|
||||
label = newLabel.trim(),
|
||||
authors = newAuthors.lines().map { it.trim() }.filter { it.isNotBlank() },
|
||||
)
|
||||
editor.themes.add(componentEditor)
|
||||
workspace.currentAction = null
|
||||
}
|
||||
}
|
||||
CreateFrom.EXISTING -> {
|
||||
val componentName = selectedComponentName ?: return
|
||||
val componentId = if (editor.themes.any { it.id == componentName.componentId }) {
|
||||
var suffix = 1
|
||||
var tempId: String
|
||||
do {
|
||||
tempId = "${componentName.componentId}_${suffix++}"
|
||||
} while (editor.themes.any { it.id == tempId })
|
||||
tempId
|
||||
} else {
|
||||
componentName.componentId
|
||||
}
|
||||
if (componentName.extensionId == extId) {
|
||||
val component = editor.themes.find { it.id == componentName.componentId } ?: return
|
||||
val componentEditor = component.let { c ->
|
||||
ThemeExtensionComponentEditor(
|
||||
componentId, c.label, c.authors, c.isNightTheme, c.isBorderless,
|
||||
c.isMaterialYouAware, stylesheetPath = "",
|
||||
).also { it.stylesheetEditor = c.stylesheetEditor }
|
||||
}
|
||||
if (componentEditor.stylesheetEditor != null) {
|
||||
val stylesheet = componentEditor.stylesheetEditor!!.build()
|
||||
val stylesheetFile = workspace.extDir.subFile(componentEditor.stylesheetPath())
|
||||
stylesheetFile.parentFile?.mkdirs()
|
||||
stylesheetFile.writeJson(stylesheet, SnyggStylesheetJsonConfig)
|
||||
componentEditor.stylesheetEditor = null
|
||||
} else {
|
||||
val srcStylesheetFile = workspace.extDir.subFile(component.stylesheetPath())
|
||||
val dstStylesheetFile = workspace.extDir.subFile(componentEditor.stylesheetPath())
|
||||
dstStylesheetFile.parentFile?.mkdirs()
|
||||
srcStylesheetFile.copyTo(dstStylesheetFile, overwrite = true)
|
||||
}
|
||||
editor.themes.add(componentEditor)
|
||||
} else {
|
||||
val component = themeManager.indexedThemeConfigs.value?.get(componentName) ?: return
|
||||
val componentEditor = (component as? ThemeExtensionComponentImpl)?.edit() ?: return
|
||||
componentEditor.id = componentId
|
||||
componentEditor.stylesheetPath = ""
|
||||
val externalExt = extensionManager.getExtensionById(componentName.extensionId) ?: return
|
||||
val stylesheetJson = ZipUtils.readFileFromArchive(
|
||||
context, externalExt.sourceRef!!, component.stylesheetPath()
|
||||
).getOrNull() ?: return
|
||||
val dstStylesheetFile = workspace.extDir.subFile(componentEditor.stylesheetPath())
|
||||
dstStylesheetFile.parentFile?.mkdirs()
|
||||
dstStylesheetFile.writeText(stylesheetJson)
|
||||
editor.themes.add(componentEditor)
|
||||
}
|
||||
workspace.currentAction = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hasSufficientInfoForCreating(): Boolean {
|
||||
return when (createFrom) {
|
||||
CreateFrom.EMPTY -> newId.isNotBlank() && newLabel.isNotBlank() && newAuthors.isNotBlank()
|
||||
CreateFrom.EXISTING -> components.containsKey(selectedComponentName)
|
||||
}
|
||||
}
|
||||
|
||||
navigationIcon {
|
||||
FlorisIconButton(
|
||||
onClick = { handleBackPress() },
|
||||
icon = painterResource(R.drawable.ic_close),
|
||||
)
|
||||
}
|
||||
|
||||
bottomBar {
|
||||
FlorisButtonBar {
|
||||
ButtonBarSpacer()
|
||||
ButtonBarTextButton(text = stringRes(R.string.action__cancel)) {
|
||||
handleBackPress()
|
||||
}
|
||||
ButtonBarButton(
|
||||
text = stringRes(R.string.action__create),
|
||||
enabled = hasSufficientInfoForCreating(),
|
||||
) {
|
||||
handleCreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content {
|
||||
BackHandler {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
) {
|
||||
RadioListItem(
|
||||
onClick = { createFrom = CreateFrom.EXISTING },
|
||||
selected = createFrom == CreateFrom.EXISTING,
|
||||
text = stringRes(R.string.ext__editor__create_component__from_existing),
|
||||
)
|
||||
RadioListItem(
|
||||
onClick = { createFrom = CreateFrom.EMPTY },
|
||||
selected = createFrom == CreateFrom.EMPTY,
|
||||
text = stringRes(R.string.ext__editor__create_component__from_empty),
|
||||
)
|
||||
}
|
||||
|
||||
if (createFrom == CreateFrom.EXISTING) {
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
) {
|
||||
for ((componentName, component) in components) {
|
||||
RadioListItem(
|
||||
onClick = { selectedComponentName = componentName },
|
||||
selected = selectedComponentName == componentName,
|
||||
text = component.label,
|
||||
secondaryText = componentName.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (createFrom == CreateFrom.EMPTY) {
|
||||
FlorisInfoCard(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
text = stringRes(R.string.ext__editor__create_component__from_empty_warning),
|
||||
)
|
||||
DialogProperty(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.ext__meta__id),
|
||||
) {
|
||||
FlorisOutlinedTextField(
|
||||
value = newId,
|
||||
onValueChange = { newId = it },
|
||||
singleLine = true,
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = newIdValidation,
|
||||
)
|
||||
}
|
||||
DialogProperty(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.ext__meta__label),
|
||||
) {
|
||||
FlorisOutlinedTextField(
|
||||
value = newLabel,
|
||||
onValueChange = { newLabel = it },
|
||||
singleLine = true,
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = newLabelValidation,
|
||||
)
|
||||
}
|
||||
DialogProperty(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.ext__meta__authors),
|
||||
) {
|
||||
FlorisOutlinedTextField(
|
||||
value = newAuthors,
|
||||
onValueChange = { newAuthors = it },
|
||||
showValidationError = showValidationErrors,
|
||||
validationResult = newAuthorsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditorSheetTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
isRequired: Boolean = false,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
label: String,
|
||||
singleLine: Boolean = true,
|
||||
showValidationError: Boolean = false,
|
||||
validationResult: ValidationResult? = null,
|
||||
) {
|
||||
val borderColor = MaterialTheme.colors.outline
|
||||
Column(modifier = Modifier.padding(vertical = TextFieldVerticalPadding)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = TextFieldVerticalPadding),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.subtitle2,
|
||||
)
|
||||
if (isRequired) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 2.dp),
|
||||
text = "*",
|
||||
style = MaterialTheme.typography.subtitle2,
|
||||
color = MaterialTheme.colors.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
FlorisOutlinedTextField(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
enabled = enabled,
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = singleLine,
|
||||
showValidationError = showValidationError,
|
||||
validationResult = validationResult,
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
unfocusedBorderColor = borderColor,
|
||||
disabledBorderColor = borderColor,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.ext
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.common.android.showLongToast
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.res.ext.Extension
|
||||
import dev.patrickgold.florisboard.res.ext.ExtensionDefaults
|
||||
|
||||
@Composable
|
||||
fun ExtensionExportScreen(id: String) {
|
||||
val context = LocalContext.current
|
||||
val extensionManager by context.extensionManager()
|
||||
|
||||
val ext = extensionManager.getExtensionById(id)
|
||||
if (ext != null) {
|
||||
ExportScreen(ext)
|
||||
} else {
|
||||
ExtensionNotFoundScreen(id)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExportScreen(ext: Extension) = FlorisScreen {
|
||||
title = ext.meta.title
|
||||
scrollable = false
|
||||
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val extensionManager by context.extensionManager()
|
||||
|
||||
val exportLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument(),
|
||||
onResult = { uri ->
|
||||
// If uri is null it indicates that the selection activity
|
||||
// was cancelled (mostly by pressing the back button), so
|
||||
// we don't display an error message here.
|
||||
if (uri == null) {
|
||||
navController.popBackStack()
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
runCatching { extensionManager.export(ext, uri) }.onSuccess {
|
||||
context.showLongToast(R.string.ext__export__success)
|
||||
}.onFailure { error ->
|
||||
context.showLongToast(R.string.ext__export__failure, "error_message" to error.localizedMessage)
|
||||
}
|
||||
navController.popBackStack()
|
||||
},
|
||||
)
|
||||
|
||||
content {
|
||||
exportLauncher.launch(ExtensionDefaults.createFlexName(ext.meta.id))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.app.ui.ext
|
||||
|
||||
import android.text.format.Formatter
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.padding
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.patrickgold.florisboard.R
|
||||
import dev.patrickgold.florisboard.app.LocalNavController
|
||||
import dev.patrickgold.florisboard.app.res.stringRes
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisBulletSpacer
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisButtonBar
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisOutlinedButton
|
||||
import dev.patrickgold.florisboard.app.ui.components.FlorisScreen
|
||||
import dev.patrickgold.florisboard.app.ui.components.defaultFlorisOutlinedBox
|
||||
import dev.patrickgold.florisboard.app.ui.components.florisHorizontalScroll
|
||||
import dev.patrickgold.florisboard.cacheManager
|
||||
import dev.patrickgold.florisboard.common.android.showLongToast
|
||||
import dev.patrickgold.florisboard.common.kotlin.resultOk
|
||||
import dev.patrickgold.florisboard.extensionManager
|
||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
|
||||
import dev.patrickgold.florisboard.ime.nlp.NATIVE_NULLPTR
|
||||
import dev.patrickgold.florisboard.ime.spelling.SpellingExtension
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
|
||||
import dev.patrickgold.florisboard.res.FileRegistry
|
||||
import dev.patrickgold.florisboard.res.cache.CacheManager
|
||||
|
||||
enum class ExtensionImportScreenType(
|
||||
val id: String,
|
||||
@StringRes val titleResId: Int,
|
||||
val supportedFiles: List<FileRegistry.Entry>,
|
||||
) {
|
||||
EXT_ANY(
|
||||
id = "ext-any",
|
||||
titleResId = R.string.ext__import__ext_any,
|
||||
supportedFiles = listOf(FileRegistry.FlexExtension),
|
||||
),
|
||||
EXT_KEYBOARD(
|
||||
id = "ext-keyboard",
|
||||
titleResId = R.string.ext__import__ext_keyboard,
|
||||
supportedFiles = listOf(FileRegistry.FlexExtension),
|
||||
),
|
||||
EXT_SPELLING(
|
||||
id = "ext-spelling",
|
||||
titleResId = R.string.ext__import__ext_spelling,
|
||||
supportedFiles = listOf(FileRegistry.FlexExtension),
|
||||
),
|
||||
EXT_THEME(
|
||||
id = "ext-theme",
|
||||
titleResId = R.string.ext__import__ext_theme,
|
||||
supportedFiles = listOf(FileRegistry.FlexExtension),
|
||||
);
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) = FlorisScreen {
|
||||
title = stringRes(type.titleResId)
|
||||
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val cacheManager by context.cacheManager()
|
||||
val extensionManager by context.extensionManager()
|
||||
|
||||
val initWsUuid by rememberSaveable { mutableStateOf(initUuid) }
|
||||
var importResult by remember {
|
||||
val workspace = initWsUuid?.let { cacheManager.importer.getWorkspaceByUuid(it) }?.let { resultOk(it) }
|
||||
mutableStateOf(workspace)
|
||||
}
|
||||
|
||||
fun getSkipReason(fileInfo: CacheManager.FileInfo): Int {
|
||||
return when {
|
||||
!FileRegistry.matchesFileFilter(fileInfo, type.supportedFiles) -> {
|
||||
R.string.ext__import__file_skip_unsupported
|
||||
}
|
||||
fileInfo.ext != null -> {
|
||||
val ext = fileInfo.ext
|
||||
if (extensionManager.getExtensionById(ext.meta.id)?.sourceRef?.isAssets == true) {
|
||||
R.string.ext__import__file_skip_ext_core
|
||||
} else {
|
||||
NATIVE_NULLPTR
|
||||
}
|
||||
}
|
||||
fileInfo.mediaType == FileRegistry.FlexExtension.mediaType -> {
|
||||
R.string.ext__import__file_skip_ext_corrupted
|
||||
}
|
||||
else -> {
|
||||
NATIVE_NULLPTR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val importLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetMultipleContents(),
|
||||
onResult = { uriList ->
|
||||
// If uri is null it indicates that the selection activity
|
||||
// was cancelled (mostly by pressing the back button), so
|
||||
// we don't display an error message here.
|
||||
if (uriList.isNullOrEmpty()) return@rememberLauncherForActivityResult
|
||||
importResult?.getOrNull()?.close()
|
||||
importResult = runCatching { cacheManager.readFromUriIntoCache(uriList) }.map { workspace ->
|
||||
workspace.inputFileInfos.forEach { fileInfo ->
|
||||
fileInfo.skipReason = getSkipReason(fileInfo)
|
||||
}
|
||||
workspace
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
bottomBar {
|
||||
FlorisButtonBar {
|
||||
ButtonBarSpacer()
|
||||
ButtonBarTextButton(
|
||||
text = stringRes(R.string.action__cancel),
|
||||
) {
|
||||
importResult?.getOrNull()?.close()
|
||||
navController.popBackStack()
|
||||
}
|
||||
val enabled = remember(importResult) {
|
||||
importResult?.getOrNull()?.takeIf { workspace ->
|
||||
workspace.inputFileInfos.any { it.skipReason == NATIVE_NULLPTR }
|
||||
} != null
|
||||
}
|
||||
ButtonBarButton(
|
||||
text = stringRes(R.string.action__import),
|
||||
enabled = enabled,
|
||||
) {
|
||||
val workspace = importResult!!.getOrThrow()
|
||||
runCatching {
|
||||
for (fileInfo in workspace.inputFileInfos) {
|
||||
if (fileInfo.skipReason != NATIVE_NULLPTR) {
|
||||
continue
|
||||
}
|
||||
val ext = fileInfo.ext
|
||||
when (type) {
|
||||
ExtensionImportScreenType.EXT_ANY -> {
|
||||
ext?.let { extensionManager.import(it) }
|
||||
}
|
||||
ExtensionImportScreenType.EXT_KEYBOARD -> {
|
||||
ext.takeIf { it is KeyboardExtension }?.let { extensionManager.import(it) }
|
||||
}
|
||||
ExtensionImportScreenType.EXT_SPELLING -> {
|
||||
ext.takeIf { it is SpellingExtension }?.let { extensionManager.import(it) }
|
||||
}
|
||||
ExtensionImportScreenType.EXT_THEME -> {
|
||||
ext.takeIf { it is ThemeExtension }?.let { extensionManager.import(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onSuccess {
|
||||
workspace.close()
|
||||
context.showLongToast(R.string.ext__import__success)
|
||||
navController.popBackStack()
|
||||
}.onFailure { error ->
|
||||
context.showLongToast(R.string.ext__import__failure, "error_message" to error.localizedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content {
|
||||
FlorisOutlinedButton(
|
||||
onClick = {
|
||||
importLauncher.launch("*/*")
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
text = stringRes(R.string.action__select_files),
|
||||
)
|
||||
|
||||
val result = importResult
|
||||
when {
|
||||
result == null -> {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.state__no_files_selected),
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
result.isSuccess -> {
|
||||
val workspace = result.getOrThrow()
|
||||
for (fileInfo in workspace.inputFileInfos) {
|
||||
FileInfoView(fileInfo)
|
||||
}
|
||||
}
|
||||
result.isFailure -> {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = stringRes(R.string.ext__import__error_unexpected_exception),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.error,
|
||||
)
|
||||
SelectionContainer {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.florisHorizontalScroll()
|
||||
.padding(horizontal = 16.dp),
|
||||
text = result.exceptionOrNull()?.stackTraceToString() ?: "null",
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.error,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FileInfoView(
|
||||
fileInfo: CacheManager.FileInfo,
|
||||
) {
|
||||
FlorisOutlinedBox(
|
||||
modifier = Modifier.defaultFlorisOutlinedBox(),
|
||||
title = fileInfo.file.name,
|
||||
subtitle = fileInfo.mediaType ?: "application/unknown",
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
val grayColor = LocalContentColor.current.copy(alpha = 0.56f)
|
||||
val ext = fileInfo.ext
|
||||
Row {
|
||||
Text(
|
||||
text = Formatter.formatShortFileSize(LocalContext.current, fileInfo.size),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = grayColor,
|
||||
)
|
||||
if (ext != null) {
|
||||
FlorisBulletSpacer()
|
||||
Text(
|
||||
text = ext.meta.id,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = grayColor,
|
||||
)
|
||||
FlorisBulletSpacer()
|
||||
Text(
|
||||
text = ext.meta.version,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = grayColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (ext != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = ext.meta.title,
|
||||
style = MaterialTheme.typography.body2,
|
||||
)
|
||||
ext.meta.description?.let { description ->
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.body2,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val maintainers = remember(ext) {
|
||||
ext.meta.maintainers.joinToString { it.name }
|
||||
}
|
||||
Text(
|
||||
text = stringRes(R.string.ext__meta__maintainers_by, "maintainers" to maintainers),
|
||||
style = MaterialTheme.typography.body2,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
for (component in ext.components()) {
|
||||
Text(
|
||||
text = component.id,
|
||||
style = MaterialTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (fileInfo.skipReason != NATIVE_NULLPTR) {
|
||||
Box(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(19.dp)
|
||||
.padding(top = 10.dp, bottom = 8.dp)
|
||||
.background(MaterialTheme.colors.error.copy(alpha = 0.56f)))
|
||||
Text(
|
||||
text = stringRes(R.string.ext__import__file_skip),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.error,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(fileInfo.skipReason),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.error,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user