Compare commits

..

79 Commits

Author SHA1 Message Date
lm41
6aed8e1d17 Use the native android locale picker on android 13+ 2024-11-27 17:46:23 +01:00
Lars Mühlbauer
c470b792c1 Fix Keyboard height in one-handed mode (#2668) 2024-10-31 15:10:04 +01:00
Lars Mühlbauer
ff5cd1e7c2 Fix smartbar spacing (#2665) 2024-10-31 15:03:01 +01:00
Lars Mühlbauer
32fee44364 Auto clean sensitive clipboard items (#2659)
* Auto clean sensitive clipboard items

* Fix typo :D

* Apply review suggestions
2024-10-25 16:46:08 +02:00
Patrick Goldinger
97edc33d05 Fix NetworkUtils not detecting hostnames with digits correctly (#2660) 2024-10-25 15:16:51 +02:00
Lars Mühlbauer
a89af25eab Improve ExtensionEditScreen (#2656) 2024-10-22 22:47:20 +02:00
florisboard-bot
66340249d4 Update translations from Crowdin 2024-10-21 23:17:01 +02:00
Lars Mühlbauer
14147ca1b9 Fix migration in clipboard database (#2641)
* Fix migration in clipboard database

* Update database version

* Fix migration from version 3 to version 4
2024-10-21 23:15:24 +02:00
Patrick Goldinger
bdc740637b Fix inline autofill single icon style (#2628) (#2649) 2024-10-21 17:02:36 +02:00
Patrick Goldinger
eb20e80295 Fix state issues in TextKeyboardLayout (popups) (#2647)
* Fix state issues in TextKeyboardLayout (popups)

* Fix utility key not updating correctly (#2648)
2024-10-21 15:34:15 +02:00
Patrick Goldinger
453fb0253a Fix predictive back by removing generic onBackPressedHandler (#2646) 2024-10-19 15:13:35 +02:00
Lucas Sanginetto
13fc7679a2 Fix incorrect bracket labels in IPA symbols layout (#2644) 2024-10-19 11:26:25 +02:00
Patrick Goldinger
2421d13038 Update ROADMAP.md (#2639) 2024-10-18 00:34:15 +02:00
florisboard-bot
7dedfd4f7a Update translations from Crowdin 2024-10-17 19:07:32 +02:00
Patrick Goldinger
ef37194900 Disable compose strong skipping mode (#2637) 2024-10-17 16:51:02 +02:00
Lars Mühlbauer
58134b1ceb Add fix for sensitive clipboard suggestions (#2635) 2024-10-16 23:17:54 +02:00
Lars Mühlbauer
53cfbad404 Clipboard History enhancements (#2631)
* Hide sensitive clip data in clipboard history

* Add is remoteDevice flag

* Do not link password length to displayed characters

* Add backspace in clipboard history (#2615)

* Use ClipboardItem level function for the obfuscation of the text

* Move the backspace button to the header bar

* Adjust innerHeight to match the full layout

* Use KeyboardLikeButton instead of FlorisIconButtonWithInnerPadding
2024-10-14 19:31:37 +02:00
florisboard-bot
d6f724e518 Update translations from Crowdin 2024-10-10 16:00:04 +02:00
Patrick Goldinger
6c4aa36b06 Update Crowdin configuration and workflow 2024-10-10 15:54:48 +02:00
Patrick Goldinger
edc38b6c2c Fix keyboard key size not properly updating (#2625) 2024-10-10 14:48:50 +02:00
Patrick Goldinger
891a2c6bac Update README.md 2024-10-09 15:35:16 +02:00
Patrick Goldinger
229237153b Add new release configs for Obtainium 2024-10-09 15:35:16 +02:00
Patrick Goldinger
290fbb5239 Merge pull request #2613 from florisboard/rework/inline-autofill-impl
Rework inline autofill suggestions from password managers
2024-10-08 21:59:43 +02:00
Patrick Goldinger
409d4f9348 Fix theme editor state bugs (#2620)
* Fix emoji history header not using theme style

* Fix extension editor component list state not being updated
2024-10-06 10:34:26 +02:00
Patrick Goldinger
82938cda5b Fix emojis with differing meta data not being seen as identical (#2617) 2024-10-05 14:48:26 +02:00
Patrick Goldinger
f7b0a30271 Rework Smartbar layout implementation & Remove Smartbar shared auto expand 2024-10-02 16:46:30 +02:00
Patrick Goldinger
575f359a85 Fix inline autofill service potential concurrent modification exception 2024-10-01 01:15:17 +02:00
Patrick Goldinger
22591163b3 Improve state handling for inline autofill suggestions 2024-10-01 00:30:20 +02:00
Patrick Goldinger
8104ae60ca Add inline autofill debug overlay 2024-09-30 22:34:04 +02:00
Patrick Goldinger
165b682732 Rework inline autofill UI implementation 2024-09-30 21:36:54 +02:00
Patrick Goldinger
eb770fac6c Merge pull request #2611 from florisboard/rework/emoji-history
Major rework of emoji history
2024-09-30 00:58:02 +02:00
Patrick Goldinger
39c27426a4 Add ability to manually reorder emojis in history (#1975) 2024-09-29 14:29:59 +02:00
Patrick Goldinger
228d5055cc Add ability to pin emojis in emoji history (#2273) 2024-09-29 14:05:57 +02:00
Patrick Goldinger
b400e04560 Rewrite emoji history logic and data class 2024-09-28 19:35:12 +02:00
Patrick Goldinger
27c1bbf039 Fix emoji history update bug if items is max size (#2129) 2024-09-28 15:32:10 +02:00
Patrick Goldinger
f61b655f7d Add emoji history group and enable toggle (#1990) 2024-09-28 15:19:30 +02:00
Patrick Goldinger
f82af63e97 Merge pull request #2606 from florisboard/rework/emoji-suggestions
Major rework of emoji suggestions
2024-09-28 14:17:36 +02:00
Patrick Goldinger
0fbd950f6e Change default value of show emoji name to false 2024-09-28 13:41:53 +02:00
Patrick Goldinger
e97b5f54ac Update emoji data to CLDR 45 (#2607) 2024-09-27 23:28:47 +02:00
Patrick Goldinger
b611360dd5 Add emoji acceptance updates history toggle 2024-09-27 19:37:12 +02:00
Patrick Goldinger
1b9d260020 Add emoji name visibility toggle 2024-09-27 19:24:28 +02:00
Patrick Goldinger
d74fe62bc0 Fix emoji suggestions failing for non-default skin-tones (#2604) 2024-09-27 19:04:09 +02:00
Patrick Goldinger
fe6f61a282 Rework emoji suggestions prefs 2024-09-27 18:32:16 +02:00
Senthil Vignesh Kumar
8b4239d9be Added Tamil to layout extension config (#2514)
* adding tamil to characters

* Author name fixed
2024-09-24 02:32:37 +02:00
Patrick Goldinger
a0c7cf2794 Upgrade to Kotlin 2.0 (#2597) 2024-09-24 01:59:37 +02:00
Patrick Goldinger
7480d14a0f Upgrade Compose to 1.7 & Upgrade other deps (#2596)
* Upgrade dependencies

* Code base deprecation cleanup
2024-09-24 00:38:51 +02:00
Kevin
7274228a46 Update quick action reordering based on row (#2592) 2024-09-23 14:58:54 +02:00
Patrick Goldinger
a38f6a2c76 Fix utility key action not correctly applied (#2591) 2024-09-23 13:59:04 +02:00
Krzysztof Kozyra
eda6c09538 Add delete subtype on long-press (#2580)
* add delete subtype feature

* Delete dialog switch from AlertDialog.Builder (unstyled) to JetPrefAlertDialog() to match rest of the ui

* Rework code and add new changes, add translations for subtype delete dialog

* Fix: when dialog is open and screen is rotated app crashes

* Remove translations due to contribution guidelines

* Add english translation so that the project can be built again ;)

* Revert spacing in values-ru/strings.xml

---------

Co-authored-by: lm41 <lm41@lm41.xyz>
Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2024-09-22 22:50:49 +02:00
Patrick Goldinger
9e42d16cb0 Add workflow for automatically rejecting translation updates (#2588) 2024-09-22 22:30:16 +02:00
Lars Mühlbauer
11ba51c354 Remove placeholder suggestions (#2572) 2024-09-22 14:50:55 +02:00
Kevin
51f5196b8a Fix incognito icon messing up landscape input height (#2584) 2024-09-22 14:48:21 +02:00
Lars Mühlbauer
56bbe9d13c Update clipboard backup and restore behavior (#2547)
* Change Clipboard History Checkbox to TriStateCheckbox

* Fix history import error when files are already in ClipboardFileStorage

* Ahhhh yes, the nitpicks `:DD`

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>

---------

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2024-09-18 23:47:38 +02:00
Lars Mühlbauer
4d1ae52dc0 Localize debug log screen and add option for preformatted exporting (#2561)
* Add Button for copying the formatted debug log for GitHub and localize ExportDebugLogScreen

* Add review suggestions

* Ahh yes, we love to nitpick
2024-09-18 23:17:59 +02:00
Lars Mühlbauer
1e1916194b Remove trailing comma to comply with the JSON spec (#2560) 2024-09-18 21:59:40 +02:00
Patrick Goldinger
bae3c8ec9d Fix Rust Cargo.lock file getting rewritten during build (#2568) 2024-09-18 18:25:04 +02:00
Patrick Goldinger
9ff7d86a8d Add hint to find_program for Rust discovery (#2567) 2024-09-18 16:56:20 +02:00
Patrick Goldinger
89ab0731d2 Add Cargo.lock files to VCS (#2566) 2024-09-18 07:23:24 +02:00
Patrick Goldinger
887a75a482 Rework version name handling in Gradle (#2565) 2024-09-18 05:43:48 +02:00
Patrick Goldinger
e52bea2456 Rename master to main & Update workflows (#2564)
* Update workflows to main branch

* Remove OSS plugin comment (is not used anymore)
2024-09-18 04:25:11 +02:00
Patrick Goldinger
2171e16346 Release v0.4.0-rc02 2024-09-02 22:23:40 +02:00
Lars Mühlbauer
566b6fbae3 Fix incorrect drawing behavior with display cutouts (#2533)
* Fix incorrect drawing behavior with display cutouts

* Replace old conditional logic with new function
2024-09-02 22:20:03 +02:00
Book-reader
5215227793 Fix incorrect addons store url (.com -> .org) (#2535) 2024-08-28 20:43:27 +02:00
Patrick Goldinger
671f97eddb Release v0.4.0-rc01 2024-08-23 15:59:55 +02:00
Patrick Goldinger
b6c9469826 Adjust gitignore for release.sh script 2024-08-23 14:01:35 +02:00
Lars Mühlbauer
77e4414467 Move FlorisCopyToClipboardActivity to ime.clipboard (#2531)
* Move FlorisCopyToClipboardActivity to `ime.clipboard`

* Fix image alignment
2024-08-21 21:57:54 +02:00
Lars Mühlbauer
db85e05714 Disable hinted number row toggle if number row is enabled (#2532) 2024-08-21 21:22:09 +02:00
Lars Mühlbauer
51890c93d4 Fix transparent navigation bar color on older api levels (#2529) 2024-08-21 20:45:28 +02:00
Lars Mühlbauer
1f16ac2c3b Update README.md to the current status (#2528)
* Update README.md

* Apply suggestions from code review

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>

---------

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
2024-08-13 21:33:37 +02:00
Lars Mühlbauer
199211fdbf Switch to sliding navigation animation (#2527) 2024-08-13 21:13:11 +02:00
Lars Mühlbauer
c5ee414ec6 Remove preference category from ExtensionHomeScreen (#2525) 2024-08-13 02:15:25 +02:00
Patrick Goldinger
f1c5b1802b Release v0.4.0-beta03 2024-08-10 02:47:31 +02:00
Patrick Goldinger
0450c8c7a1 Fix null ptr exception in intent handling 2024-08-10 02:47:09 +02:00
florisboard-bot
6da6da74fc Update translations from Crowdin 2024-08-07 02:16:08 +02:00
Lars Mühlbauer
d7fca0aad1 Open subtype preset dialog when a new subtype is added (#2521)
* Open subtype preset dialog when a new subtype is added

* Update addons store url in build config
2024-08-07 02:13:07 +02:00
Patrick Goldinger
1af519e01d Update to Gradle 8.7 and AGP 8.5.1 (#2520) 2024-08-07 00:01:04 +02:00
Lars Mühlbauer
989d2884b1 Fix backspace icon (#2518) 2024-08-06 03:19:29 +02:00
Lars Mühlbauer
d137155ab0 Add CheckUpdatesScreen (#2509) 2024-07-15 19:12:16 +02:00
Patrick Goldinger
270ab4fe5f Add deep link support for UI (#2508) 2024-07-15 18:22:05 +02:00
175 changed files with 5700 additions and 1854 deletions

View File

@@ -9,7 +9,7 @@ insert_final_newline = true
max_line_length = 120
trim_trailing_whitespace = true
[{*.har,*.json,*yml}]
[{*.har,*.json,*yml,*.sh}]
indent_size = 2
[*.kt]

View File

@@ -2,7 +2,7 @@ name: FlorisBoard CI
on:
push:
branches: [ master ]
branches: [ main ]
paths-ignore:
- ".github/ISSUE_TEMPLATE/**"
- ".github/FUNDING.yml"
@@ -14,31 +14,28 @@ on:
- "README.md"
- "ROADMAP.md"
pull_request:
branches: [ master ]
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: gradle/wrapper-validation-action@v1
- uses: gradle/actions/wrapper-validation@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
cache: gradle
- name: Set up CMake and Ninja
uses: lukka/get-cmake@latest
- name: Build with Gradle
# MUST call gradlew separately because of an OSS license plugin issue.
# See https://github.com/google/play-services-plugins/issues/199
run: ./gradlew clean && ./gradlew assembleDebug
- uses: actions/upload-artifact@v3
run: ./gradlew clean assembleDebug
- uses: actions/upload-artifact@v4
with:
name: app-debug.apk
path: app/build/outputs/apk/debug/app-debug.apk

View File

@@ -2,7 +2,7 @@ name: Crowdin Upload Sources
on:
push:
branches: [ master ]
branches: [ main ]
paths:
- "app/src/main/res/values/strings.xml"
- ".github/workflows/crowdin-upload.yml"
@@ -13,9 +13,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Upload
uses: crowdin/github-action@1.4.0
uses: crowdin/github-action@v2
with:
config: "crowdin.yml"
upload_sources: true

View File

@@ -0,0 +1,61 @@
name: Validate no translated strings.xml included
on:
pull_request_target:
branches: [ main ]
jobs:
validate:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Precheck if validation is required
id: precheck
run: |
pr_author="${{ github.event.pull_request.user.login }}"
if [[ "$pr_author" == "florisboard-bot" ]]; then
echo "PR is by florisboard-bot, skipping validation!"
echo "require_validation=false" >> "$GITHUB_OUTPUT"
else
echo "PR is not by florisboard-bot, requiring validation!"
echo "require_validation=true" >> "$GITHUB_OUTPUT"
fi
- name: Fetch PR changed files manually
id: fetch_changed_files
if: steps.precheck.outputs.require_validation == 'true'
run: |
pr_files="$(curl -sSf https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files?per_page=1000)" || exit 11
changed_files="$(jq -r '.[].filename' <<< "$pr_files")" || exit 12
illegal_changes_list="$(grep -E '^app/src/main/res/values-.+/strings.xml$' <<< "$changed_files")" || true
if [ -n "$illegal_changes_list" ]; then
echo -e "Illegal changes detected:\n$illegal_changes_list"
else
echo "No illegal changes detected"
fi
echo "illegal_changes_list<<EOF" >> "$GITHUB_OUTPUT"
echo "$illegal_changes_list" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Create comment if illegal files detected
uses: peter-evans/create-or-update-comment@v4
if: steps.precheck.outputs.require_validation == 'true' && steps.fetch_changed_files.outputs.illegal_changes_list != ''
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
⚠️ Illegal changes detected
Hey there!
We detected illegal changes that disobey the [contribution guidelines](https://github.com/florisboard/florisboard/blob/main/CONTRIBUTING.md#translation). This is a kind reminder that pull requests must not contain translated `strings.xml` files, as those are exclusively managed from Crowdin.
Please remove changes to the following files:
```
${{ steps.fetch_changed_files.outputs.illegal_changes_list }}
```
- name: Fail workflow if illegal files detected
if: steps.precheck.outputs.require_validation == 'true' && steps.fetch_changed_files.outputs.illegal_changes_list != ''
run: echo -e "Illegal changes detected:\n${{ steps.fetch_changed_files.outputs.illegal_changes_list }}" && exit 1

5
.gitignore vendored
View File

@@ -1,7 +1,11 @@
# Built application files
*.apk
*.aab
*.ap_
# dotenv
.env
# Files for the ART/Dalvik VM
*.dex
@@ -54,6 +58,5 @@ result
# Rust
debug/
target/
Cargo.lock
**/*.rs.bk
*.pdb

View File

@@ -1,21 +1,19 @@
<img align="left" width="80" height="80"
src=".github/repo_icon.png" alt="App icon">
# FlorisBoard [![Crowdin](https://badges.crowdin.net/florisboard/localized.svg)](https://crowdin.florisboard.patrickgold.dev) [![Matrix badge](https://img.shields.io/badge/chat-%23florisboard%3amatrix.org-blue)](https://matrix.to/#/#florisboard:matrix.org) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) ![FlorisBoard CI](https://github.com/florisboard/florisboard/workflows/FlorisBoard%20CI/badge.svg?event=push)
# FlorisBoard [![Crowdin](https://badges.crowdin.net/florisboard/localized.svg)](https://crowdin.florisboard.patrickgold.dev) [![Matrix badge](https://img.shields.io/badge/chat-%23florisboard%3amatrix.org-blue)](https://matrix.to/#/#florisboard:matrix.org) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) [![FlorisBoard CI](https://github.com/florisboard/florisboard/actions/workflows/android.yml/badge.svg?event=push)](https://github.com/florisboard/florisboard/actions/workflows/android.yml)
**FlorisBoard** is a free and open-source keyboard for Android 7.0+
devices. It aims at being modern, user-friendly and customizable while
fully respecting your privacy. Currently in early-beta state.
*Note: Due to various reasons development has been quite stuck in the past year, but things are slowly improving again and new releases/features will follow in the near future, please see the [roadmap](ROADMAP.md) for details!*
<table>
<tr>
<th align="center" width="50%">
<h3>Stable <a href="https://github.com/florisboard/florisboard/releases/latest"><img alt="Latest stable release" src="https://img.shields.io/github/v/release/florisboard/florisboard"></a></h3>
<h3>Stable <a href="https://github.com/florisboard/florisboard/releases/latest"><img alt="Latest stable release" src="https://img.shields.io/github/v/release/florisboard/florisboard?sort=semver&display_name=tag&color=28a745"></a></h3>
</th>
<th align="center" width="50%">
<h3>Beta <a href="https://github.com/florisboard/florisboard/releases"><img alt="Latest beta release" src="https://img.shields.io/github/v/release/florisboard/florisboard?include_prereleases"></a></h3>
<h3>Preview <a href="https://github.com/florisboard/florisboard/releases"><img alt="Latest preview release" src="https://img.shields.io/github/v/release/florisboard/florisboard?include_prereleases&sort=semver&display_name=tag&color=fd7e14"></a></h3>
</th>
</tr>
<tr>
@@ -23,7 +21,7 @@ fully respecting your privacy. Currently in early-beta state.
<p><i>Major versions only</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>Alpha/Beta versions</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>Major + Alpha/Beta/Rc versions</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>
@@ -36,6 +34,11 @@ fully respecting your privacy. Currently in early-beta state.
</p>
<p>
**Obtainium**: [Auto-import stable config][obtainium_stable]
</p>
<p>
**Manual**: Download and install the APK from the release page.
</p>
@@ -44,7 +47,12 @@ fully respecting your privacy. Currently in early-beta state.
<p><a href="https://apt.izzysoft.de/fdroid/index/apk/dev.patrickgold.florisboard.beta"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" height="64" alt="IzzySoft repo badge"></a></p>
<p>
**Google Play**: Join the [FlorisBoard Test Group](https://groups.google.com/g/florisboard-public-alpha-test), then visit the [beta testing page](https://play.google.com/apps/testing/dev.patrickgold.florisboard.beta). Once joined and installed, updates will be delivered like for any other app. ([Store entry](https://play.google.com/store/apps/details?id=dev.patrickgold.florisboard.beta))
**Google Play**: Join the [FlorisBoard Test Group](https://groups.google.com/g/florisboard-public-alpha-test), then visit the [preview testing page](https://play.google.com/apps/testing/dev.patrickgold.florisboard.beta). Once joined and installed, updates will be delivered like for any other app. ([Store entry](https://play.google.com/store/apps/details?id=dev.patrickgold.florisboard.beta))
</p>
<p>
**Obtainium**: [Auto-import preview config][obtainium_preview]
</p>
<p>
@@ -56,16 +64,17 @@ fully respecting your privacy. Currently in early-beta state.
</tr>
</table>
Beginning with v0.4.0 FlorisBoard will follow [SemVer](https://semver.org/#summary) versioning scheme.
Beginning with v0.6.0 FlorisBoard will enter the public beta on Google Play.
## Highlighted features
- Integrated clipboard manager / history
- Advanced theming support and customization
- Integrated extension support (still evolving)
- Emoji keyboard
- Emoji keyboard / history / suggestions
Word suggestions/spell checking are not included in the current releases and are a major goal for the v0.5.0 milestone.
> [!IMPORTANT]
> Word suggestions/spell checking are not included in the current releases
> and are a major goal for the v0.5 milestone.
Feature roadmap: See [ROADMAP.md](ROADMAP.md)
@@ -73,6 +82,16 @@ Feature roadmap: See [ROADMAP.md](ROADMAP.md)
Want to contribute to FlorisBoard? That's great to hear! There are lots of
different ways to help out, please see the [contribution guidelines](CONTRIBUTING.md) for more info.
## Addons Store
The official [Addons Store](https://beta.addons.florisboard.org) offers the possibility for the community to share and download FlorisBoard extensions.
Instructions on how to publish addons can be found [here](https://github.com/florisboard/florisboard/wiki/How-to-publish-on-FlorisBoard-Addons).
Many thanks to Ali ([@4H1R](https://github.com/4H1R)) for implementing the store!
> [!NOTE]
> During the initial beta release phase, the Addons Store _will_ only accept theme extensions.
> Later on we plan to add support for language packs and keyboard extensions.
## List of permissions FlorisBoard requests
Please refer to this [page](https://github.com/florisboard/florisboard/wiki/List-of-permissions-FlorisBoard-requests)
to get more information on this topic.
@@ -80,8 +99,6 @@ to get more information on this topic.
## Used libraries, components and icons
* [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)
* [AboutLibraries](https://github.com/mikepenz/AboutLibraries) by
[mikepenz](https://github.com/mikepenz)
* [Google Material icons](https://github.com/google/material-design-icons) by
@@ -92,8 +109,6 @@ to get more information on this topic.
[Kotlin](https://github.com/Kotlin)
* [KotlinX serialization library](https://github.com/Kotlin/kotlinx.serialization) by
[Kotlin](https://github.com/Kotlin)
* [ICU4C](https://github.com/unicode-org/icu) by
[The Unicode Consortium](https://github.com/unicode-org)
Many thanks to [Nikolay Anzarov](https://www.behance.net/nikolayanzarov) ([@BloodRaven0](https://github.com/BloodRaven0)) for designing and providing the main app icons to this project!
@@ -113,3 +128,10 @@ 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.
```
<!-- BEGIN SECTION: obtainium_links -->
<!-- auto-generated link templates, do NOT edit by hand -->
<!-- see fastlane/update-readme.sh -->
[obtainium_preview]: https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22dev.patrickgold.florisboard.beta%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fflorisboard%2Fflorisboard%22%2C%22author%22%3A%22florisboard%22%2C%22name%22%3A%22FlorisBoard%20Preview%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22preview%5C%22%7D%22%7D%0A
[obtainium_stable]: https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22dev.patrickgold.florisboard%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fflorisboard%2Fflorisboard%22%2C%22author%22%3A%22florisboard%22%2C%22name%22%3A%22FlorisBoard%20Stable%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22stable%5C%22%7D%22%7D%0A
<!-- END SECTION: obtainium_links -->

View File

@@ -2,24 +2,7 @@
This feature roadmap intents to provide transparency to what is planned to be added to FlorisBoard in the foreseeable future. Note that there are no ETAs for any version milestones down below, experience has shown these won't hold anyways.
Each major milestone has associated alpha/beta releases, so if you are interested in previewing features quicker, keep an eye out! Each major 0.x release has also patch releases after the initial major release, which will be published on both the stable and beta tracks.
## 0.4
**Main focus**: Getting the project back on track, see [this announcement](https://github.com/florisboard/florisboard/discussions/2314) for details. Note that this has also replaced the previous roadmap, however this step is necessary for getting the project back on track again.
This includes, but is not exclusive to:
- Fixing the most reported bugs/issues
- Merging in the Material You theme PR -> Adds Material You support (v0.4.0-alpha05)
- Merging in other external PRs as best as possible
- Reworking the Settings UI warning boxes and hiding any UI for features related to word suggestions until they are ready
- Remove existing glide/swipe typing (see 0.5 milestone)
- Improvements in clipboard / emoji functionality (v0.4.0-beta01/beta02)
- Prepare project to have native code implemented in [Rust](https://www.rust-lang.org/) (v0.4.0-beta02)
- - Upgrade Settings UI to Material 3 (v0.4.0-beta03)
- Add support for importing extensions via system file handler APIs (relevant for Addons store) (v0.4.0-beta03)
Note that the previous versioning scheme has been dropped in favor of using a major.minor.patch versioning scheme, so versions like `0.3.16` are a thing of the past :)
Each major milestone has associated alpha/beta releases, so if you are interested in previewing features quicker, keep an eye out! Each major 0.x release has also patch releases after the initial major release, which will be published on both the stable and preview tracks.
## 0.5
@@ -28,25 +11,25 @@ Note that the previous versioning scheme has been dropped in favor of using a ma
- Add new extension type: Language Pack
- Basically groups all locale-relevant data (predictive base model, emoji suggestion data, ...)
in a dynamically importable extension file
- New text processing logic (maybe moved back / split to 0.6)
- Add floating keyboard mode
- New keyboard layout engine + file syntax based on the upcoming Unicode Keyboard v3 standard
- RFC document with technical details will be released later
- New text processing logic (maybe moved back to 0.6)
- RFC document with technical details will be released later
- Add Tablet mode / Optimizations for landscape input based on new keyboard layout engine
- Reimplementation of glide typing with the new layout engine and predictive text core
- Add support for any remaining new features introduced with Android 13
## 0.6
- Complete rework of the Emoji panel
- Recently used / Emoji history (already implemented with 0.3.14)
- Emoji search
- Emoji suggestions when using :emoji_name: syntax (already implemented with v0.4.0-beta02)
- Fully scrollable emoji list (soft category borders)
- More granular themeing options
- Layout customization (e.g. placement of category buttons)
- Maybe: consider upgrading to emoji2 for better unified system-wide emoji styles
- Reimplementation of glide typing with the new layout engine and predictive text core
- Prepare FlorisBoard repository and app store presence for public beta release on Google Play (will go live with stable 0.6)
- Rework branding images and texts of FlorisBoard for the app stores
- Focus on stability and experience improvements of the app and keyboard
- Add support for new features introduced with Android 14
- Add support for new features introduced with Android 14 / 15
- Not finalized, but planned: raise minimum required Android version from Android 7 (SDK level 24) to Android 8 (SDK level 26)
## Backlog / Planned (unassigned)
@@ -58,7 +41,6 @@ Note that the previous versioning scheme has been dropped in favor of using a ma
- Adaptive themes v2
- Voice-to-text with Mozilla's open-source voice service (or any other oss voice provider)
- Text translation
- Floating keyboard
- Stickers/GIFs
- Kaomoji panel implementation
- FlorisBoard landing web page for presentation

View File

@@ -14,11 +14,13 @@
* limitations under the License.
*/
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag
import java.io.ByteArrayOutputStream
plugins {
alias(libs.plugins.agp.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.plugin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.mannodermaus.android.junit5)
@@ -32,7 +34,7 @@ val projectBuildToolsVersion: String by project
val projectNdkVersion: String by project
val projectVersionCode: String by project
val projectVersionName: String by project
val projectVersionNameSuffix: String by project
val projectVersionNameSuffix = projectVersionName.substringAfter("-", "")
android {
namespace = "dev.patrickgold.florisboard"
@@ -48,24 +50,27 @@ android {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf(
"-Xallow-result-return-type",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-Xjvm-default=all-compatibility",
)
}
androidResources {
generateLocaleConfig = true
}
defaultConfig {
applicationId = "dev.patrickgold.florisboard"
minSdk = projectMinSdk.toInt()
targetSdk = projectTargetSdk.toInt()
versionCode = projectVersionCode.toInt()
versionName = projectVersionName
versionName = projectVersionName.substringBefore("-")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BUILD_COMMIT_HASH", "\"${getGitCommitHash()}\"")
buildConfigField("String", "FLADDONS_API_VERSION", "\"v~draft2\"")
buildConfigField("String", "FLADDONS_STORE_URL", "\"fladdonstest.patrickgold.dev\"")
buildConfigField("String", "FLADDONS_STORE_URL", "\"beta.addons.florisboard.org\"")
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
@@ -99,14 +104,10 @@ android {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
}
buildTypes {
named("debug") {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug-${getGitCommitHash(short = true)}"
versionNameSuffix = "-debug+${getGitCommitHash(short = true)}"
isDebuggable = true
isJniDebuggable = false
@@ -165,14 +166,14 @@ android {
}
}
tasks.withType<Test> {
useJUnitPlatform()
composeCompiler {
// DO NOT ENABLE STRONG SKIPPING! This project currently relies on
// recomposition on parent state change to update the UI correctly.
featureFlags.add(ComposeFeatureFlag.StrongSkipping.disabled())
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
tasks.withType<Test> {
useJUnitPlatform()
}
dependencies {
@@ -192,6 +193,7 @@ dependencies {
implementation(libs.androidx.material.icons)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.profileinstaller)
implementation(libs.androidx.appcompat)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.runtime)
implementation(libs.cache4k)

View File

@@ -0,0 +1,94 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "145ca5bf4bff8e98f71ebc70ab3b495b",
"entities": [
{
"tableName": "clipboard_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `text` TEXT, `uri` TEXT, `creationTimestampMs` INTEGER NOT NULL, `isPinned` INTEGER NOT NULL, `mimeTypes` TEXT NOT NULL, `isSensitive` INTEGER NOT NULL DEFAULT 0, `isRemoteDevice` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uri",
"columnName": "uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "creationTimestampMs",
"columnName": "creationTimestampMs",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPinned",
"columnName": "isPinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mimeTypes",
"columnName": "mimeTypes",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isSensitive",
"columnName": "isSensitive",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRemoteDevice",
"columnName": "isRemoteDevice",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"_id"
]
},
"indices": [
{
"name": "index_clipboard_history__id",
"unique": false,
"columnNames": [
"_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_clipboard_history__id` ON `${TABLE_NAME}` (`_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '145ca5bf4bff8e98f71ebc70ab3b495b')"
]
}
}

View File

@@ -0,0 +1,94 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "1dd181d116dcb4530fb5b33451ea9ab5",
"entities": [
{
"tableName": "clipboard_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `text` TEXT, `uri` TEXT, `creationTimestampMs` INTEGER NOT NULL, `isPinned` INTEGER NOT NULL, `mimeTypes` TEXT NOT NULL, `is_sensitive` INTEGER NOT NULL DEFAULT 0, `is_remote_device` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uri",
"columnName": "uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "creationTimestampMs",
"columnName": "creationTimestampMs",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPinned",
"columnName": "isPinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mimeTypes",
"columnName": "mimeTypes",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isSensitive",
"columnName": "is_sensitive",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isRemoteDevice",
"columnName": "is_remote_device",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"_id"
]
},
"indices": [
{
"name": "index_clipboard_history__id",
"unique": false,
"columnNames": [
"_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_clipboard_history__id` ON `${TABLE_NAME}` (`_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1dd181d116dcb4530fb5b33451ea9ab5')"
]
}
}

View File

@@ -42,10 +42,11 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/floris_app_icon"
android:label="@string/floris_app_name"
android:enableOnBackInvokedCallback="true"
android:roundIcon="@mipmap/floris_app_icon_round"
android:supportsRtl="true"
android:theme="@style/FlorisAppTheme"
tools:targetApi="s">
tools:targetApi="tiramisu">
<!-- Allow app to be profiled for benchmarking and baseline profile generation -->
<profileable android:shell="true"/>
@@ -75,6 +76,16 @@
<meta-data android:name="android.view.textservice.scs" android:resource="@xml/spellchecker"/>
</service>
<!-- Service for Locale handling -->
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
<!-- Main App Activity -->
<activity
android:name="dev.patrickgold.florisboard.app.FlorisAppActivity"
@@ -86,7 +97,10 @@
android:theme="@style/FlorisAppTheme.Splash"
android:exported="true">
<intent-filter>
<data android:scheme="florisboard" android:host="app-ui"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="ui" android:host="florisboard" android:pathPrefix="/" />
</intent-filter>
<intent-filter android:label="Import Extension">
<action android:name="android.intent.action.VIEW"/>
@@ -128,7 +142,7 @@
<!-- Copy to Clipboard Activity -->
<activity
android:name="dev.patrickgold.florisboard.FlorisCopyToClipboardActivity"
android:name="dev.patrickgold.florisboard.ime.clipboard.FlorisCopyToClipboardActivity"
android:theme="@style/FlorisAppTheme.Transparent"
android:exported="true">
<intent-filter>

View File

@@ -392,6 +392,12 @@
"authors": [ "patrickgold" ],
"direction": "ltr"
},
{
"id": "tamil",
"label": "Tamil",
"authors": [ "Clem0908" ],
"direction": "ltr"
},
{
"id": "thai_kedmanee",
"label": "Thai Kedmanee",

View File

@@ -64,7 +64,7 @@
{ "code": 11816, "label": "⸨" },
{ "code": 10214, "label": "⟦" },
{ "code": 10216, "label": "⟨" },
{ "code": 10218, "label": "" },
{ "code": 10218, "label": "" },
{ "code": 123, "label": "{" }
]
} },
@@ -72,7 +72,7 @@
"relevant": [
{ "code": 41, "label": ")" },
{ "code": 11817, "label": "⸩" },
{ "code": 10215, "label": "" },
{ "code": 10215, "label": "" },
{ "code": 10217, "label": "⟩" },
{ "code": 10219, "label": "⟫" },
{ "code": 125, "label": "}" }

View File

@@ -2,7 +2,7 @@
"all": {
"c": {
"relevant": [
{ "$": "auto_text_key", "code": 269, "label": "č" },
{ "$": "auto_text_key", "code": 269, "label": "č" }
]
},
"s": {

View File

@@ -1,3 +1,6 @@
# Auto-generated by emojicon.py using CLDR v45
# DO NOT EDIT MANUALLY!
[smileys_emotion]
😀;grinsendes Gesicht;Gesicht|grinsendes Gesicht|lol|lustig
😃;grinsendes Gesicht mit großen Augen;Gesicht|grinsendes Gesicht mit großen Augen|lächeln|lol|lustig

View File

@@ -1,3 +1,6 @@
# Auto-generated by emojicon.py using CLDR v45
# DO NOT EDIT MANUALLY!
[smileys_emotion]
😀;grinning face;face|grin|grinning face
😃;grinning face with big eyes;face|grinning face with big eyes|mouth|open|smile
@@ -3788,3 +3791,4 @@
🏴󠁧󠁢󠁥󠁮󠁧󠁿;flag: England;flag|flag: England
🏴󠁧󠁢󠁳󠁣󠁴󠁿;flag: Scotland;flag|flag: Scotland
🏴󠁧󠁢󠁷󠁬󠁳󠁿;flag: Wales;flag|flag: Wales

View File

@@ -1,3 +1,6 @@
# Auto-generated by emojicon.py using CLDR v45
# DO NOT EDIT MANUALLY!
[smileys_emotion]
😀;cara sonriendo;cara|cara sonriendo|divertido|feliz|sonrisa
😃;cara sonriendo con ojos grandes;cara|cara sonriendo con ojos grandes|divertido|risa|sonriendo

View File

@@ -1,3 +1,6 @@
# Auto-generated by emojicon.py using CLDR v45
# DO NOT EDIT MANUALLY!
[smileys_emotion]
😀;visage rieur;sourire|visage rieur
😃;visage souriant avec de grands yeux;sourire|visage souriant avec de grands yeux

View File

@@ -1,3 +1,6 @@
# Auto-generated by emojicon.py using CLDR v45
# DO NOT EDIT MANUALLY!
[smileys_emotion]
😀;faccina con un gran sorriso;faccina|faccina che sogghigna|faccina con un gran sorriso|risata|sogghignare
😃;faccina con un gran sorriso e occhi spalancati;faccina|faccina con un gran sorriso e occhi spalancati|faccina sorridente|risata|sorridere

View File

@@ -1,3 +1,6 @@
# Auto-generated by emojicon.py using CLDR v45
# DO NOT EDIT MANUALLY!
[smileys_emotion]
😀;rosto risonho;lol|rindo|risada|rosto|rosto risonho
😃;rosto risonho com olhos bem abertos;aberto|boca|rosto|rosto risonho com olhos bem abertos|sorriso

View File

@@ -1,3 +1,6 @@
# Auto-generated by emojicon.py using CLDR v45
# DO NOT EDIT MANUALLY!
[smileys_emotion]
😀;;
😃;;
@@ -3788,3 +3791,4 @@
🏴󠁧󠁢󠁥󠁮󠁧󠁿;;
🏴󠁧󠁢󠁳󠁣󠁴󠁿;;
🏴󠁧󠁢󠁷󠁬󠁳󠁿;;

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -87,6 +87,7 @@ import dev.patrickgold.florisboard.ime.keyboard.ProvideKeyboardRowBaseHeight
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
import dev.patrickgold.florisboard.ime.lifecycle.LifecycleInputMethodService
import dev.patrickgold.florisboard.ime.media.MediaInputLayout
import dev.patrickgold.florisboard.ime.nlp.NlpInlineAutofill
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.onehanded.OneHandedPanel
import dev.patrickgold.florisboard.ime.sheet.BottomSheetHostUi
@@ -370,7 +371,7 @@ class FlorisImeService : LifecycleInputMethodService() {
flogInfo { "(no args)" }
super.onFinishInput()
editorInstance.handleFinishInput()
nlpManager.clearInlineSuggestions()
NlpInlineAutofill.clearInlineSuggestions()
}
override fun onWindowShown() {
@@ -437,23 +438,26 @@ class FlorisImeService : LifecycleInputMethodService() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest? {
return if (prefs.smartbar.enabled.get() && prefs.suggestion.api30InlineSuggestionsEnabled.get()) {
flogInfo(LogTopic.IMS_EVENTS) {
"Creating inline suggestions request because Smartbar and inline suggestions are enabled."
}
val stylesBundle = themeManager.createInlineSuggestionUiStyleBundle(this)
val spec = InlinePresentationSpec.Builder(InlineSuggestionUiSmallestSize, InlineSuggestionUiBiggestSize)
.setStyle(stylesBundle)
.build()
InlineSuggestionsRequest.Builder(listOf(spec)).let { request ->
request.setMaxSuggestionCount(InlineSuggestionsRequest.SUGGESTION_COUNT_UNLIMITED)
request.build()
}
} else {
if (!prefs.smartbar.enabled.get() || !prefs.suggestion.api30InlineSuggestionsEnabled.get()) {
flogInfo(LogTopic.IMS_EVENTS) {
"Ignoring inline suggestions request because Smartbar and/or inline suggestions are disabled."
}
null
return null
}
flogInfo(LogTopic.IMS_EVENTS) { "Creating inline suggestions request" }
val stylesBundle = themeManager.createInlineSuggestionUiStyleBundle(this)
val spec = InlinePresentationSpec.Builder(
InlineSuggestionUiSmallestSize,
InlineSuggestionUiBiggestSize,
).run {
setStyle(stylesBundle)
build()
}
return InlineSuggestionsRequest.Builder(listOf(spec)).run {
setMaxSuggestionCount(InlineSuggestionsRequest.SUGGESTION_COUNT_UNLIMITED)
build()
}
}
@@ -463,8 +467,7 @@ class FlorisImeService : LifecycleInputMethodService() {
flogInfo(LogTopic.IMS_EVENTS) {
"Received inline suggestions response with ${inlineSuggestions.size} suggestion(s) provided."
}
nlpManager.showInlineSuggestions(inlineSuggestions)
return true
return NlpInlineAutofill.showInlineSuggestions(this, inlineSuggestions)
}
override fun onComputeInsets(outInsets: Insets?) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2021-2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,8 +31,9 @@ import dev.patrickgold.florisboard.ime.keyboard.IncognitoMode
import dev.patrickgold.florisboard.ime.keyboard.SpaceBarMode
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.EmojiHistory
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSkinTone
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSuggestionType
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.smartbar.CandidatesDisplayMode
@@ -46,16 +47,18 @@ import dev.patrickgold.florisboard.ime.text.key.KeyHintMode
import dev.patrickgold.florisboard.ime.text.key.UtilityKeyAction
import dev.patrickgold.florisboard.ime.theme.ThemeMode
import dev.patrickgold.florisboard.ime.theme.extCoreTheme
import org.florisboard.lib.android.isOrientationPortrait
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.observeAsTransformingState
import org.florisboard.lib.snygg.SnyggLevel
import dev.patrickgold.florisboard.lib.util.VersionName
import dev.patrickgold.jetpref.datastore.JetPref
import dev.patrickgold.jetpref.datastore.model.PreferenceMigrationEntry
import dev.patrickgold.jetpref.datastore.model.PreferenceModel
import dev.patrickgold.jetpref.datastore.model.PreferenceType
import dev.patrickgold.jetpref.datastore.model.observeAsState
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.florisboard.lib.android.isOrientationPortrait
import org.florisboard.lib.snygg.SnyggLevel
fun florisPreferenceModel() = JetPref.getOrCreatePreferenceModel(AppPrefs::class, ::AppPrefs)
@@ -115,6 +118,14 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "clipboard__clean_up_after",
default = 20,
)
val autoCleanSensitive = boolean(
key = "clipboard__auto_clean_sensitive",
default = false,
)
val autoCleanSensitiveAfter = int(
key = "clipboard__auto_clean_sensitive_after",
default = 20,
)
val limitHistorySize = boolean(
key = "clipboard__limit_history_size",
default = true,
@@ -171,6 +182,10 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "devtools__show_spelling_overlay",
default = false,
)
val showInlineAutofillOverlay = boolean(
key = "devtools__show_inline_autofill_overlay",
default = false,
)
val showKeyTouchBoundaries = boolean(
key = "devtools__show_touch_boundaries",
default = false,
@@ -193,6 +208,67 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
)
}
val emoji = Emoji()
inner class Emoji {
val preferredSkinTone = enum(
key = "emoji__preferred_skin_tone",
default = EmojiSkinTone.DEFAULT,
)
val preferredHairStyle = enum(
key = "emoji__preferred_hair_style",
default = EmojiHairStyle.DEFAULT,
)
val historyEnabled = boolean(
key = "emoji__history_enabled",
default = true,
)
val historyData = custom(
key = "emoji__history_data",
default = EmojiHistory.Empty,
serializer = EmojiHistory.Serializer,
)
val historyPinnedUpdateStrategy = enum(
key = "emoji__history_pinned_update_strategy",
default = EmojiHistory.UpdateStrategy.MANUAL_SORT_PREPEND,
)
val historyPinnedMaxSize = int(
key = "emoji__history_pinned_max_size",
default = EmojiHistory.MaxSizeUnlimited,
)
val historyRecentUpdateStrategy = enum(
key = "emoji__history_recent_update_strategy",
default = EmojiHistory.UpdateStrategy.AUTO_SORT_PREPEND,
)
val historyRecentMaxSize = int(
key = "emoji__history_recent_max_size",
default = 90,
)
val suggestionEnabled = boolean(
key = "emoji__suggestion_enabled",
default = true,
)
val suggestionType = enum(
key = "emoji__suggestion_type",
default = EmojiSuggestionType.LEADING_COLON,
)
val suggestionUpdateHistory = boolean(
key = "emoji__suggestion_update_history",
default = true,
)
val suggestionCandidateShowName = boolean(
key = "emoji__suggestion_candidate_show_name",
default = false,
)
val suggestionQueryMinLength = int(
key = "emoji__suggestion_query_min_length",
default = 3,
)
val suggestionCandidateMaxCount = int(
key = "emoji__suggestion_candidate_max_count",
default = 5,
)
}
val gestures = Gestures()
inner class Gestures {
val swipeUp = enum(
@@ -530,27 +606,6 @@ 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(
@@ -574,6 +629,7 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
key = "smartbar__shared_actions_expanded",
default = false,
)
@Deprecated("Always enabled due to UX issues")
val sharedActionsAutoExpandCollapse = boolean(
key = "smartbar__shared_actions_auto_expand_collapse",
default = true,
@@ -683,8 +739,7 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
"gestures__space_bar_swipe_right", "gestures__space_bar_long_press", "gestures__delete_key_swipe_left",
"gestures__delete_key_long_press", "keyboard__hinted_number_row_mode", "keyboard__hinted_symbols_mode",
"keyboard__utility_key_action", "keyboard__one_handed_mode", "keyboard__landscape_input_ui_mode",
"localization__display_language_names_in", "media__emoji_preferred_skin_tone",
"media__emoji_preferred_hair_style", "smartbar__primary_actions_row_type",
"localization__display_language_names_in", "smartbar__primary_actions_row_type",
"smartbar__secondary_actions_placement", "smartbar__secondary_actions_row_type", "spelling__language_mode",
"suggestion__display_mode", "theme__mode", "theme__editor_display_colors_as",
"theme__editor_display_kbd_after_dialogs", "theme__editor_level",
@@ -706,6 +761,32 @@ class AppPrefs : PreferenceModel("florisboard-app-prefs") {
}
}
// Migrate media prefs to emoji prefs
// Keep migration rule until: 0.6 dev cycle
"media__emoji_recently_used" -> {
val emojiValues = entry.rawValue.split(";")
val recent = emojiValues.map {
dev.patrickgold.florisboard.ime.media.emoji.Emoji(it, "", emptyList())
}
val data = EmojiHistory(emptyList(), recent)
entry.transform(key = "emoji__history_data", rawValue = Json.encodeToString(data))
}
"media__emoji_recently_used_max_size" -> {
entry.transform(key = "emoji__history_recent_max_size")
}
"media__emoji_preferred_skin_tone" -> {
entry.transform(
key = "emoji__preferred_skin_tone",
rawValue = entry.rawValue.uppercase(), // keep until: 0.5 dev cycle
)
}
"media__emoji_preferred_hair_style" -> {
entry.transform(
key = "emoji__preferred_hair_style",
rawValue = entry.rawValue.uppercase(), // keep until: 0.5 dev cycle
)
}
// Default: keep entry
else -> entry.keepAsIs()
}

View File

@@ -11,7 +11,9 @@ import dev.patrickgold.florisboard.ime.input.InputFeedbackActivationMode
import dev.patrickgold.florisboard.ime.keyboard.IncognitoMode
import dev.patrickgold.florisboard.ime.keyboard.SpaceBarMode
import dev.patrickgold.florisboard.ime.landscapeinput.LandscapeInputUiMode
import dev.patrickgold.florisboard.ime.media.emoji.EmojiHistory
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSkinTone
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSuggestionType
import dev.patrickgold.florisboard.ime.nlp.SpellingLanguageMode
import dev.patrickgold.florisboard.ime.onehanded.OneHandedMode
import dev.patrickgold.florisboard.ime.smartbar.CandidatesDisplayMode
@@ -138,6 +140,30 @@ private val ENUM_DISPLAY_ENTRIES = mapOf<Pair<KClass<*>, String>, @Composable ()
)
}
},
EmojiHistory.UpdateStrategy::class to DEFAULT to {
listPrefEntries {
entry(
key = EmojiHistory.UpdateStrategy.AUTO_SORT_PREPEND,
label = stringRes(R.string.enum__emoji_history_update_strategy__auto_sort_prepend),
description = stringRes(R.string.enum__emoji_history_update_strategy__auto_sort_prepend__description),
)
entry(
key = EmojiHistory.UpdateStrategy.AUTO_SORT_APPEND,
label = stringRes(R.string.enum__emoji_history_update_strategy__auto_sort_append),
description = stringRes(R.string.enum__emoji_history_update_strategy__auto_sort_append__description),
)
entry(
key = EmojiHistory.UpdateStrategy.MANUAL_SORT_PREPEND,
label = stringRes(R.string.enum__emoji_history_update_strategy__manual_sort_prepend),
description = stringRes(R.string.enum__emoji_history_update_strategy__manual_sort_prepend__description),
)
entry(
key = EmojiHistory.UpdateStrategy.MANUAL_SORT_APPEND,
label = stringRes(R.string.enum__emoji_history_update_strategy__manual_sort_append),
description = stringRes(R.string.enum__emoji_history_update_strategy__manual_sort_append__description),
)
}
},
EmojiSkinTone::class to DEFAULT to {
listPrefEntries {
entry(
@@ -184,6 +210,20 @@ private val ENUM_DISPLAY_ENTRIES = mapOf<Pair<KClass<*>, String>, @Composable ()
)
}
},
EmojiSuggestionType::class to DEFAULT to {
listPrefEntries {
entry(
key = EmojiSuggestionType.LEADING_COLON,
label = stringRes(R.string.enum__emoji_suggestion_type__leading_colon),
description = stringRes(R.string.enum__emoji_suggestion_type__leading_colon__description),
)
entry(
key = EmojiSuggestionType.INLINE_TEXT,
label = stringRes(R.string.enum__emoji_suggestion_type__inline_text),
description = stringRes(R.string.enum__emoji_suggestion_type__inline_text__description),
)
}
},
ExtendedActionsPlacement::class to DEFAULT to {
listPrefEntries {
entry(

View File

@@ -20,9 +20,11 @@ import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.MaterialTheme
@@ -30,12 +32,13 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.navigation.NavController
@@ -46,17 +49,18 @@ import dev.patrickgold.florisboard.app.ext.ExtensionImportScreenType
import dev.patrickgold.florisboard.app.setup.NotificationPermissionState
import dev.patrickgold.florisboard.cacheManager
import dev.patrickgold.florisboard.lib.FlorisLocale
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.android.hideAppIcon
import org.florisboard.lib.android.showAppIcon
import dev.patrickgold.florisboard.lib.compose.LocalPreviewFieldController
import dev.patrickgold.florisboard.lib.compose.PreviewKeyboardField
import dev.patrickgold.florisboard.lib.compose.ProvideLocalizedResources
import dev.patrickgold.florisboard.lib.compose.conditional
import dev.patrickgold.florisboard.lib.compose.rememberPreviewFieldController
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.util.AppVersionUtils
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.ProvideDefaultDialogPrefStrings
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.android.hideAppIcon
import org.florisboard.lib.android.showAppIcon
enum class AppTheme(val id: String) {
AUTO("auto"),
@@ -70,13 +74,13 @@ val LocalNavController = staticCompositionLocalOf<NavController> {
error("LocalNavController not initialized")
}
class FlorisAppActivity : ComponentActivity() {
class FlorisAppActivity : AppCompatActivity() {
private val prefs by florisPreferenceModel()
private val cacheManager by cacheManager()
private var appTheme by mutableStateOf(AppTheme.AUTO)
private var showAppIcon = true
private var resourcesContext by mutableStateOf(this as Context)
private var fileImportIntent by mutableStateOf<Intent?>(null)
private var intentToBeHandled by mutableStateOf<Intent?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
// Splash screen should be installed before calling super.onCreate()
@@ -90,10 +94,12 @@ class FlorisAppActivity : ComponentActivity() {
appTheme = it
}
prefs.advanced.settingsLanguage.observe(this) {
val config = Configuration(resources.configuration)
val locale = if (it == "auto") FlorisLocale.default() else FlorisLocale.fromTag(it)
config.setLocale(locale.base)
resourcesContext = createConfigurationContext(config)
val appLocale: LocaleListCompat = if (it == "auto") {
LocaleListCompat.getEmptyLocaleList()
} else {
LocaleListCompat.forLanguageTags(FlorisLocale.fromTag(it).languageTag())
}
AppCompatDelegate.setApplicationLocales(appLocale)
}
if (AndroidVersion.ATMOST_API28_P) {
prefs.advanced.showAppIcon.observe(this) {
@@ -141,19 +147,23 @@ class FlorisAppActivity : ComponentActivity() {
}
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
if (intent?.action == Intent.ACTION_VIEW && intent.data != null) {
fileImportIntent = intent
if (intent.action == Intent.ACTION_VIEW && intent.categories?.contains(Intent.CATEGORY_BROWSABLE) == true) {
intentToBeHandled = intent
return
}
if (intent?.action == Intent.ACTION_SEND && intent.clipData != null) {
fileImportIntent = intent
if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
intentToBeHandled = intent
return
}
fileImportIntent = null
if (intent.action == Intent.ACTION_SEND && intent.clipData != null) {
intentToBeHandled = intent
return
}
intentToBeHandled = null
}
@Composable
@@ -176,6 +186,9 @@ class FlorisAppActivity : ComponentActivity() {
modifier = Modifier
//.statusBarsPadding()
.navigationBarsPadding()
.conditional(LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
displayCutoutPadding()
}
.imePadding(),
) {
Routes.AppNavHost(
@@ -188,22 +201,22 @@ class FlorisAppActivity : ComponentActivity() {
}
}
LaunchedEffect(fileImportIntent) {
val intent = fileImportIntent
LaunchedEffect(intentToBeHandled) {
val intent = intentToBeHandled
if (intent != null) {
val data = if (intent.action == Intent.ACTION_VIEW) {
intent.data!!
if (intent.action == Intent.ACTION_VIEW && intent.categories?.contains(Intent.CATEGORY_BROWSABLE) == true) {
navController.handleDeepLink(intent)
} else {
intent.clipData!!.getItemAt(0).uri
val data = if (intent.action == Intent.ACTION_VIEW) {
intent.data!!
} else {
intent.clipData!!.getItemAt(0).uri
}
val workspace = runCatching { cacheManager.readFromUriIntoCache(data) }.getOrNull()
navController.navigate(Routes.Ext.Import(ExtensionImportScreenType.EXT_ANY, workspace?.uuid))
}
val workspace = runCatching { cacheManager.readFromUriIntoCache(data) }.getOrNull()
navController.navigate(Routes.Ext.Import(ExtensionImportScreenType.EXT_ANY, workspace?.uuid))
}
fileImportIntent = null
}
SideEffect {
navController.setOnBackPressedDispatcher(this.onBackPressedDispatcher)
intentToBeHandled = null
}
}
}

View File

@@ -16,15 +16,25 @@
package dev.patrickgold.florisboard.app
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import dev.patrickgold.florisboard.app.devtools.AndroidLocalesScreen
import dev.patrickgold.florisboard.app.devtools.AndroidSettingsScreen
import dev.patrickgold.florisboard.app.devtools.DevtoolsScreen
import dev.patrickgold.florisboard.app.devtools.ExportDebugLogScreen
import dev.patrickgold.florisboard.app.ext.CheckUpdatesScreen
import dev.patrickgold.florisboard.app.ext.ExtensionEditScreen
import dev.patrickgold.florisboard.app.ext.ExtensionExportScreen
import dev.patrickgold.florisboard.app.ext.ExtensionHomeScreen
@@ -144,6 +154,8 @@ object Routes {
const val View = "ext/view/{id}"
fun View(id: String) = View.curlyFormat("id" to id)
const val CheckUpdates = "ext/check-updates"
}
@Composable
@@ -152,83 +164,106 @@ object Routes {
navController: NavHostController,
startDestination: String,
) {
fun NavGraphBuilder.composableWithDeepLink(
route: String,
content: @Composable (AnimatedContentScope.(NavBackStackEntry) -> Unit),
) {
composable(
route = route,
deepLinks = listOf(navDeepLink { uriPattern = "ui://florisboard/$route" }),
content = content,
)
}
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination,
enterTransition = {
slideIn { IntOffset(it.width, 0) } + fadeIn()
},
exitTransition = {
slideOut { IntOffset(-it.width, 0) } + fadeOut()
},
popEnterTransition = {
slideIn { IntOffset(-it.width, 0) } + fadeIn()
},
popExitTransition = {
slideOut { IntOffset(it.width, 0) } + fadeOut()
}
) {
composable(Setup.Screen) { SetupScreen() }
composable(Settings.Home) { HomeScreen() }
composableWithDeepLink(Settings.Home) { HomeScreen() }
composable(Settings.Localization) { LocalizationScreen() }
composable(Settings.SelectLocale) { SelectLocaleScreen() }
composable(Settings.LanguagePackManager) { navBackStack ->
composableWithDeepLink(Settings.Localization) { LocalizationScreen() }
composableWithDeepLink(Settings.SelectLocale) { SelectLocaleScreen() }
composableWithDeepLink(Settings.LanguagePackManager) { navBackStack ->
val action = navBackStack.arguments?.getString("action")?.let { actionId ->
LanguagePackManagerScreenAction.entries.firstOrNull { it.id == actionId }
}
LanguagePackManagerScreen(action)
}
composable(Settings.SubtypeAdd) { SubtypeEditorScreen(null) }
composable(Settings.SubtypeEdit) { navBackStack ->
composableWithDeepLink(Settings.SubtypeAdd) { SubtypeEditorScreen(null) }
composableWithDeepLink(Settings.SubtypeEdit) { navBackStack ->
val id = navBackStack.arguments?.getString("id")?.toLongOrNull()
SubtypeEditorScreen(id)
}
composable(Settings.Theme) { ThemeScreen() }
composable(Settings.ThemeManager) { navBackStack ->
composableWithDeepLink(Settings.Theme) { ThemeScreen() }
composableWithDeepLink(Settings.ThemeManager) { navBackStack ->
val action = navBackStack.arguments?.getString("action")?.let { actionId ->
ThemeManagerScreenAction.entries.firstOrNull { it.id == actionId }
}
ThemeManagerScreen(action)
}
composable(Settings.Keyboard) { KeyboardScreen() }
composable(Settings.InputFeedback) { InputFeedbackScreen() }
composableWithDeepLink(Settings.Keyboard) { KeyboardScreen() }
composableWithDeepLink(Settings.InputFeedback) { InputFeedbackScreen() }
composable(Settings.Smartbar) { SmartbarScreen() }
composableWithDeepLink(Settings.Smartbar) { SmartbarScreen() }
composable(Settings.Typing) { TypingScreen() }
composableWithDeepLink(Settings.Typing) { TypingScreen() }
composable(Settings.Dictionary) { DictionaryScreen() }
composable(Settings.UserDictionary) { navBackStack ->
composableWithDeepLink(Settings.Dictionary) { DictionaryScreen() }
composableWithDeepLink(Settings.UserDictionary) { navBackStack ->
val type = navBackStack.arguments?.getString("type")?.let { typeId ->
UserDictionaryType.entries.firstOrNull { it.id == typeId }
}
UserDictionaryScreen(type!!)
}
composable(Settings.Gestures) { GesturesScreen() }
composableWithDeepLink(Settings.Gestures) { GesturesScreen() }
composable(Settings.Clipboard) { ClipboardScreen() }
composableWithDeepLink(Settings.Clipboard) { ClipboardScreen() }
composable(Settings.Media) { MediaScreen() }
composableWithDeepLink(Settings.Media) { MediaScreen() }
composable(Settings.Advanced) { AdvancedScreen() }
composable(Settings.Backup) { BackupScreen() }
composable(Settings.Restore) { RestoreScreen() }
composableWithDeepLink(Settings.Advanced) { AdvancedScreen() }
composableWithDeepLink(Settings.Backup) { BackupScreen() }
composableWithDeepLink(Settings.Restore) { RestoreScreen() }
composable(Settings.About) { AboutScreen() }
composable(Settings.ProjectLicense) { ProjectLicenseScreen() }
composable(Settings.ThirdPartyLicenses) { ThirdPartyLicensesScreen() }
composableWithDeepLink(Settings.About) { AboutScreen() }
composableWithDeepLink(Settings.ProjectLicense) { ProjectLicenseScreen() }
composableWithDeepLink(Settings.ThirdPartyLicenses) { ThirdPartyLicensesScreen() }
composable(Devtools.Home) { DevtoolsScreen() }
composable(Devtools.AndroidLocales) { AndroidLocalesScreen() }
composable(Devtools.AndroidSettings) { navBackStack ->
composableWithDeepLink(Devtools.Home) { DevtoolsScreen() }
composableWithDeepLink(Devtools.AndroidLocales) { AndroidLocalesScreen() }
composableWithDeepLink(Devtools.AndroidSettings) { navBackStack ->
val name = navBackStack.arguments?.getString("name")
AndroidSettingsScreen(name)
}
composable(Devtools.ExportDebugLog) { ExportDebugLogScreen() }
composableWithDeepLink(Devtools.ExportDebugLog) { ExportDebugLogScreen() }
composable(Ext.Home) { ExtensionHomeScreen() }
composable(Ext.List) { navBackStack ->
composableWithDeepLink(Ext.Home) { ExtensionHomeScreen() }
composableWithDeepLink(Ext.List) { navBackStack ->
val type = navBackStack.arguments?.getString("type")?.let { typeId ->
ExtensionListScreenType.entries.firstOrNull { it.id == typeId }
} ?: error("unknown type")
val showUpdate = navBackStack.arguments?.getString("showUpdate")
ExtensionListScreen(type, showUpdate == "true")
}
composable(Ext.Edit) { navBackStack ->
composableWithDeepLink(Ext.Edit) { navBackStack ->
val extensionId = navBackStack.arguments?.getString("id")
val serialType = navBackStack.arguments?.getString("serial_type")
ExtensionEditScreen(
@@ -236,21 +271,24 @@ object Routes {
createSerialType = serialType.takeIf { !it.isNullOrBlank() },
)
}
composable(Ext.Export) { navBackStack ->
composableWithDeepLink(Ext.Export) { navBackStack ->
val extensionId = navBackStack.arguments?.getString("id")
ExtensionExportScreen(id = extensionId.toString())
}
composable(Ext.Import) { navBackStack ->
composableWithDeepLink(Ext.Import) { navBackStack ->
val type = navBackStack.arguments?.getString("type")?.let { typeId ->
ExtensionImportScreenType.entries.firstOrNull { it.id == typeId }
} ?: ExtensionImportScreenType.EXT_ANY
val uuid = navBackStack.arguments?.getString("uuid")?.takeIf { it != "null" }
ExtensionImportScreen(type, uuid)
}
composable(Ext.View) { navBackStack ->
composableWithDeepLink(Ext.View) { navBackStack ->
val extensionId = navBackStack.arguments?.getString("id")
ExtensionViewScreen(id = extensionId.toString())
}
composableWithDeepLink(Ext.CheckUpdates) {
CheckUpdatesScreen()
}
}
}
}

View File

@@ -18,17 +18,13 @@ package dev.patrickgold.florisboard.app.apptheme
import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat

View File

@@ -16,6 +16,8 @@
package dev.patrickgold.florisboard.app.devtools
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
@@ -40,10 +42,12 @@ import androidx.compose.ui.unit.sp
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.clipboardManager
import dev.patrickgold.florisboard.editorInstance
import dev.patrickgold.florisboard.ime.nlp.NlpInlineAutofill
import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.observeAsNonNullState
import dev.patrickgold.florisboard.nlpManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.android.AndroidVersion
import java.text.SimpleDateFormat
import java.util.*
@@ -57,6 +61,7 @@ fun DevtoolsOverlay(modifier: Modifier = Modifier) {
val showPrimaryClip by prefs.devtools.showPrimaryClip.observeAsState()
val showInputStateOverlay by prefs.devtools.showInputStateOverlay.observeAsState()
val showSpellingOverlay by prefs.devtools.showSpellingOverlay.observeAsState()
val showInlineAutofillOverlay by prefs.devtools.showInlineAutofillOverlay.observeAsState()
CompositionLocalProvider(
LocalContentColor provides Color.White,
@@ -72,6 +77,9 @@ fun DevtoolsOverlay(modifier: Modifier = Modifier) {
if (showSpellingOverlay) {
DevtoolsSpellingOverlay()
}
if (showInlineAutofillOverlay && AndroidVersion.ATLEAST_API30_R) {
DevtoolsInlineAutofillOverlay()
}
}
}
}
@@ -117,7 +125,6 @@ private fun DevtoolsInputStateOverlay() {
}
}
@Composable
private fun DevtoolsSpellingOverlay() {
val context = LocalContext.current
@@ -160,6 +167,25 @@ private fun DevtoolsSpellingOverlay() {
}
}
@RequiresApi(Build.VERSION_CODES.R)
@Composable
private fun DevtoolsInlineAutofillOverlay() {
val inlineSuggestions by NlpInlineAutofill.suggestions.collectAsState()
DevtoolsOverlayBox(title = "Inline autofill overlay (${inlineSuggestions.size})") {
for (inlineSuggestion in inlineSuggestions) {
DevtoolsSubGroup(title = "NlpInlineSuggestion") {
val info = inlineSuggestion.info
DevtoolsText(text = "info.type: ${info.type}")
DevtoolsText(text = "info.source: ${info.source}")
DevtoolsText(text = "info.isPinned: ${info.isPinned}")
val view = inlineSuggestion.view
DevtoolsText(text = "view: ${view?.javaClass?.name}")
}
}
}
}
@Composable
private fun DevtoolsOverlayBox(
title: String,

View File

@@ -36,6 +36,7 @@ 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 org.florisboard.lib.android.AndroidVersion
class DebugOnPurposeCrashException : Exception(
"Success! The app crashed purposely to display this beautiful screen we all love :)"
@@ -84,6 +85,13 @@ fun DevtoolsScreen() = FlorisScreen {
summary = stringRes(R.string.devtools__show_spelling_overlay__summary),
enabledIf = { prefs.devtools.enabled isEqualTo true },
)
SwitchPreference(
prefs.devtools.showInlineAutofillOverlay,
title = stringRes(R.string.devtools__show_inline_autofill_overlay__label),
summary = stringRes(R.string.devtools__show_inline_autofill_overlay__summary),
enabledIf = { prefs.devtools.enabled isEqualTo true },
visibleIf = { AndroidVersion.ATLEAST_API30_R },
)
SwitchPreference(
prefs.devtools.showKeyTouchBoundaries,
title = stringRes(R.string.devtools__show_key_touch_boundaries__label),

View File

@@ -16,6 +16,8 @@
package dev.patrickgold.florisboard.app.devtools
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
@@ -35,19 +37,21 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.sp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.clipboardManager
import org.florisboard.lib.android.showShortToast
import dev.patrickgold.florisboard.lib.compose.FlorisButton
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.compose.florisScrollbar
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.devtools.Devtools
import org.florisboard.lib.android.showShortToast
// TODO: This screen is just a quick thrown-together thing and needs further enhancing in the UI and in localization
// TODO: This screen is just a quick thrown-together thing and needs further enhancing in the UI
@Composable
fun ExportDebugLogScreen() = FlorisScreen {
title = "Debug log"
title = stringRes(R.string.devtools__debuglog__title)
scrollable = false
val prefs by florisPreferenceModel()
@@ -55,21 +59,36 @@ fun ExportDebugLogScreen() = FlorisScreen {
val clipboardManager by context.clipboardManager()
var debugLog by remember { mutableStateOf<List<String>?>(null) }
var formattedDebugLog by remember { mutableStateOf<List<String>?>(null) }
LaunchedEffect(Unit) {
debugLog = Devtools.generateDebugLog(context, prefs, includeLogcat = true).lines()
formattedDebugLog = Devtools.generateDebugLogForGithub(context, prefs, includeLogcat = true).lines()
}
bottomBar {
FlorisButton(
onClick = {
clipboardManager.addNewPlaintext(debugLog!!.joinToString("\n"))
context.showShortToast("Copied debug log to clipboard")
},
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxWidth(),
text = "Export (copy to clipboard)",
enabled = debugLog != null,
)
) {
FlorisButton(
onClick = {
clipboardManager.addNewPlaintext(debugLog!!.joinToString("\n"))
context.showShortToast(context.getString(R.string.devtools__debuglog__copied_to_clipboard))
},
modifier = Modifier,
text = stringRes(R.string.devtools__debuglog__copy_log),
enabled = debugLog != null,
)
FlorisButton(
onClick = {
clipboardManager.addNewPlaintext(formattedDebugLog!!.joinToString("\n"))
context.showShortToast(context.getString(R.string.devtools__debuglog__copied_to_clipboard))
},
text = stringRes(R.string.devtools__debuglog__copy_for_github),
enabled = debugLog != null,
)
}
}
content {
@@ -86,7 +105,7 @@ fun ExportDebugLogScreen() = FlorisScreen {
val log = debugLog
if (log == null) {
item {
Text("Loading...")
Text(stringRes(R.string.devtools__debuglog__loading))
}
} else {
items(log) { logLine ->

View File

@@ -16,13 +16,13 @@ import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.lib.util.launchUrl
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.Extension
import dev.patrickgold.florisboard.lib.ext.generateUpdateUrl
import dev.patrickgold.florisboard.lib.util.launchUrl
import org.florisboard.lib.kotlin.curlyFormat
@Composable
@@ -43,7 +43,7 @@ fun UpdateBox(extensionIndex: List<Extension>) {
) {
FlorisTextButton(
onClick = {
context.launchUrl(extensionIndex.generateUpdateUrl(version = "v~draft2", host = "fladdonstest.patrickgold.dev"))
context.launchUrl(extensionIndex.generateUpdateUrl())
},
icon = Icons.Outlined.FileDownload,
text = stringRes(id = R.string.ext__update_box__search_for_updates)

View File

@@ -0,0 +1,21 @@
package dev.patrickgold.florisboard.app.ext
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.stringRes
@Composable
fun CheckUpdatesScreen() = FlorisScreen {
title = stringRes(R.string.ext__check_updates__title)
val context = LocalContext.current
val extensionManager by context.extensionManager()
val extensionIndex = extensionManager.combinedExtensionList()
content {
UpdateBox(extensionIndex)
}
}

View File

@@ -30,9 +30,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.outlined.LibraryBooks
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
@@ -322,17 +322,17 @@ private fun EditScreen(
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
) {
this@content.Preference(
Preference(
onClick = { workspace.currentAction = EditorAction.ManageMetaData },
icon = Icons.Default.Code,
title = stringRes(R.string.ext__editor__metadata__title),
)
this@content.Preference(
Preference(
onClick = { workspace.currentAction = EditorAction.ManageDependencies },
icon = Icons.Outlined.LibraryBooks,
icon = Icons.AutoMirrored.Outlined.LibraryBooks,
title = stringRes(R.string.ext__editor__dependencies__title),
)
this@content.Preference(
Preference(
onClick = { workspace.currentAction = EditorAction.ManageFiles },
icon = vectorResource(R.drawable.ic_file_blank),
title = stringRes(R.string.ext__editor__files__title),

View File

@@ -21,14 +21,13 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.lib.util.launchUrl
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.util.launchUrl
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
@Composable
fun ExtensionHomeScreen() = FlorisScreen {
@@ -74,28 +73,26 @@ fun ExtensionHomeScreen() = FlorisScreen {
UpdateBox(extensionIndex = extensionIndex)
PreferenceGroup(title = stringRes(id = R.string.ext__home__visit_store)) {
Preference(
icon = Icons.Default.Palette,
title = stringRes(R.string.ext__list__ext_theme),
onClick = {
navController.navigate(Routes.Ext.List(ExtensionListScreenType.EXT_THEME,false))
},
)
Preference(
icon = Icons.Default.Keyboard,
title = stringRes(R.string.ext__list__ext_keyboard),
onClick = {
navController.navigate(Routes.Ext.List(ExtensionListScreenType.EXT_KEYBOARD,false))
},
)
Preference(
icon = Icons.Default.Language,
title = stringRes(R.string.ext__list__ext_languagepack),
onClick = {
navController.navigate(Routes.Ext.List(ExtensionListScreenType.EXT_LANGUAGEPACK,false))
},
)
}
Preference(
icon = Icons.Default.Palette,
title = stringRes(R.string.ext__list__ext_theme),
onClick = {
navController.navigate(Routes.Ext.List(ExtensionListScreenType.EXT_THEME, false))
},
)
Preference(
icon = Icons.Default.Keyboard,
title = stringRes(R.string.ext__list__ext_keyboard),
onClick = {
navController.navigate(Routes.Ext.List(ExtensionListScreenType.EXT_KEYBOARD, false))
},
)
Preference(
icon = Icons.Default.Language,
title = stringRes(R.string.ext__list__ext_languagepack),
onClick = {
navController.navigate(Routes.Ext.List(ExtensionListScreenType.EXT_LANGUAGEPACK, false))
},
)
}
}

View File

@@ -17,10 +17,15 @@
package dev.patrickgold.florisboard.app.ext
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
@@ -33,8 +38,13 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.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.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import dev.patrickgold.florisboard.R
@@ -46,6 +56,7 @@ import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.florisScrollbar
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.ExtensionManager
import dev.patrickgold.florisboard.lib.observeAsNonNullState
@@ -80,49 +91,66 @@ enum class ExtensionListScreenType(
fun ExtensionListScreen(type: ExtensionListScreenType, showUpdate: Boolean) = FlorisScreen {
title = stringRes(type.titleResId)
previewFieldVisible = false
scrollable = false
val context = LocalContext.current
val navController = LocalNavController.current
val extensionManager by context.extensionManager()
val extensionIndex by type.getExtensionIndex(extensionManager).observeAsNonNullState()
var fabHeight by remember {
mutableStateOf(0)
}
val fabHeightDp = with(LocalDensity.current) { fabHeight.toDp()+16.dp }
val listState = rememberLazyListState()
content {
if (showUpdate) {
UpdateBox(extensionIndex = extensionIndex)
}
for (ext in extensionIndex) {
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
title = ext.meta.title,
subtitle = ext.meta.id,
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
text = ext.meta.description ?: "",
style = MaterialTheme.typography.bodySmall,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 6.dp),
LazyColumn(
modifier = Modifier
.fillMaxSize()
.florisScrollbar(state = listState, isVertical = true),
state = listState,
contentPadding = PaddingValues(bottom = fabHeightDp),
) {
if (showUpdate) {
item {
UpdateBox(extensionIndex = extensionIndex)
}
}
items(extensionIndex) { ext ->
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
title = ext.meta.title,
subtitle = ext.meta.id,
) {
FlorisTextButton(
onClick = {
navController.navigate(Routes.Ext.View(ext.meta.id))
},
icon = Icons.Outlined.Info,
text = stringRes(id = R.string.ext__list__view_details),//stringRes(R.string.action__add),
colors = ButtonDefaults.textButtonColors(),
)
Spacer(modifier = Modifier.weight(1f))
FlorisTextButton(
onClick = {
navController.navigate(Routes.Ext.Edit(ext.meta.id))
},
icon = Icons.Default.Edit,
text = stringRes(R.string.action__edit),
enabled = extensionManager.canDelete(ext),
Text(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
text = ext.meta.description ?: "",
style = MaterialTheme.typography.bodySmall,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 6.dp),
) {
FlorisTextButton(
onClick = {
navController.navigate(Routes.Ext.View(ext.meta.id))
},
icon = Icons.Outlined.Info,
text = stringRes(id = R.string.ext__list__view_details),//stringRes(R.string.action__add),
colors = ButtonDefaults.textButtonColors(),
)
Spacer(modifier = Modifier.weight(1f))
FlorisTextButton(
onClick = {
navController.navigate(Routes.Ext.Edit(ext.meta.id))
},
icon = Icons.Default.Edit,
text = stringRes(R.string.action__edit),
enabled = extensionManager.canDelete(ext),
)
}
}
}
}
@@ -142,6 +170,9 @@ fun ExtensionListScreen(type: ExtensionListScreenType, showUpdate: Boolean) = Fl
text = stringRes(id = R.string.ext__editor__title_create_any),
)
},
modifier = Modifier.onGloballyPositioned {
fabHeight = it.size.height
},
shape = FloatingActionButtonDefaults.extendedFabShape,
onClick = { type.launchExtensionCreate.invoke(navController) },
)

View File

@@ -31,7 +31,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -224,7 +224,7 @@ private fun ExtensionMetaRowSimpleText(
content: @Composable RowScope.() -> Unit,
) {
if (showDividerAbove) {
Divider()
HorizontalDivider()
}
Row(
modifier = modifier
@@ -246,7 +246,7 @@ private fun ExtensionMetaRowScrollableChips(
content: @Composable RowScope.() -> Unit,
) {
if (showDividerAbove) {
Divider()
HorizontalDivider()
}
Row(
modifier = modifier.fillMaxWidth(),

View File

@@ -16,6 +16,11 @@
package dev.patrickgold.florisboard.app.settings.advanced
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Adb
import androidx.compose.material.icons.filled.Archive
@@ -26,6 +31,7 @@ import androidx.compose.material.icons.filled.Preview
import androidx.compose.material.icons.filled.SettingsBackupRestore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.AppTheme
import dev.patrickgold.florisboard.app.LocalNavController
@@ -34,7 +40,6 @@ import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.keyboard.IncognitoMode
import dev.patrickgold.florisboard.lib.FlorisLocale
import org.florisboard.lib.android.AndroidVersion
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.jetpref.datastore.model.observeAsState
@@ -44,6 +49,7 @@ import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
import dev.patrickgold.jetpref.datastore.ui.listPrefEntries
import dev.patrickgold.jetpref.datastore.ui.vectorResource
import org.florisboard.lib.android.AndroidVersion
@Composable
fun AdvancedScreen() = FlorisScreen {
@@ -51,6 +57,10 @@ fun AdvancedScreen() = FlorisScreen {
previewFieldVisible = false
val navController = LocalNavController.current
val context = LocalContext.current
val languageSettingsLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {}
content {
ListPreference(
@@ -67,70 +77,86 @@ fun AdvancedScreen() = FlorisScreen {
AndroidVersion.ATLEAST_API31_S
},
)
ListPreference(
prefs.advanced.settingsLanguage,
icon = Icons.Default.Language,
title = stringRes(R.string.pref__advanced__settings_language__label),
entries = listPrefEntries {
listOf(
"auto",
"ar",
"bg",
"bs",
"ca",
"ckb",
"cs",
"da",
"de",
"el",
"en",
"eo",
"es",
"fa",
"fi",
"fr",
"hr",
"hu",
"in",
"it",
"iw",
"ja",
"ko-KR",
"ku",
"lv-LV",
"mk",
"nds-DE",
"nl",
"no",
"pl",
"pt",
"pt-BR",
"ru",
"sk",
"sl",
"sr",
"sv",
"tr",
"uk",
"zgh",
"zh-CN",
).map { languageTag ->
if (languageTag == "auto") {
entry(
key = "auto",
label = stringRes(R.string.settings__system_default),
if (AndroidVersion.ATLEAST_API33_T) {
Preference(
title = stringRes(R.string.pref__advanced__settings_language__label),
icon = Icons.Default.Language,
onClick = {
languageSettingsLauncher.launch(
Intent(
Settings.ACTION_APP_LOCALE_SETTINGS,
Uri.parse("package:${context.packageName}")
)
} else {
val displayLanguageNamesIn by prefs.localization.displayLanguageNamesIn.observeAsState()
val locale = FlorisLocale.fromTag(languageTag)
entry(locale.languageTag(), when (displayLanguageNamesIn) {
DisplayLanguageNamesIn.SYSTEM_LOCALE -> locale.displayName()
DisplayLanguageNamesIn.NATIVE_LOCALE -> locale.displayName(locale)
})
)
}
)
} else {
ListPreference(
prefs.advanced.settingsLanguage,
icon = Icons.Default.Language,
title = stringRes(R.string.pref__advanced__settings_language__label),
entries = listPrefEntries {
listOf(
"auto",
"ar",
"bg",
"bs",
"ca",
"ckb",
"cs",
"da",
"de",
"el",
"en",
"eo",
"es",
"fa",
"fi",
"fr",
"hr",
"hu",
"in",
"it",
"iw",
"ja",
"ko-KR",
"ku",
"lv-LV",
"mk",
"nds-DE",
"nl",
"no",
"pl",
"pt",
"pt-BR",
"ru",
"sk",
"sl",
"sr",
"sv",
"tr",
"uk",
"zgh",
"zh-CN",
).map { languageTag ->
if (languageTag == "auto") {
entry(
key = "auto",
label = stringRes(R.string.settings__system_default),
)
} else {
val displayLanguageNamesIn by prefs.localization.displayLanguageNamesIn.observeAsState()
val locale = FlorisLocale.fromTag(languageTag)
entry(locale.languageTag(), when (displayLanguageNamesIn) {
DisplayLanguageNamesIn.SYSTEM_LOCALE -> locale.displayName()
DisplayLanguageNamesIn.NATIVE_LOCALE -> locale.displayName(locale)
})
}
}
}
}
)
)
}
SwitchPreference(
prefs.advanced.showAppIcon,
icon = Icons.Default.Preview,

View File

@@ -25,13 +25,17 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Checkbox
import androidx.compose.material3.RadioButton
import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
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.platform.LocalContext
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
@@ -86,10 +90,23 @@ object Backup {
var clipboardTextItems by mutableStateOf(false)
var clipboardImageItems by mutableStateOf(false)
var clipboardVideoItems by mutableStateOf(false)
var clipboardData by mutableStateOf(false)
fun validateClipboardCheckbox(): Boolean {
return clipboardTextItems && clipboardImageItems && clipboardVideoItems
private var _clipboardData: MutableState<ToggleableState> = mutableStateOf(ToggleableState.Off)
val clipboardData: State<ToggleableState> = _clipboardData
fun updateCheckboxState() {
val newValue = if (
!clipboardVideoItems && !clipboardImageItems && !clipboardTextItems
) {
ToggleableState.Off
} else if (
clipboardVideoItems && clipboardImageItems && clipboardTextItems
) {
ToggleableState.On
} else {
ToggleableState.Indeterminate
}
_clipboardData.value = newValue
}
fun provideClipboardItems(): Boolean {
@@ -309,28 +326,31 @@ internal fun BackupFilesSelector(
text = stringRes(R.string.backup_and_restore__back_up__files_ime_theme),
)
CheckboxListItem(
TriStateCheckboxListItem(
onClick = {
if (!filesSelector.clipboardData) {
filesSelector.clipboardTextItems = true
if (
filesSelector.clipboardData.value == ToggleableState.Off ||
filesSelector.clipboardData.value == ToggleableState.Indeterminate
) {
filesSelector.clipboardImageItems = true
filesSelector.clipboardVideoItems = true
filesSelector.clipboardTextItems = true
} else {
filesSelector.clipboardTextItems = false
filesSelector.clipboardImageItems = false
filesSelector.clipboardVideoItems = false
filesSelector.clipboardTextItems = false
}
filesSelector.clipboardData = filesSelector.validateClipboardCheckbox()
filesSelector.updateCheckboxState()
},
checked = filesSelector.clipboardTextItems && filesSelector.clipboardImageItems && filesSelector.clipboardVideoItems,
text = stringRes(R.string.backup_and_restore__back_up__files_clipboard_history)
state = filesSelector.clipboardData.value,
text = stringRes(R.string.backup_and_restore__back_up__files_clipboard_history),
)
CheckboxListItem(
onClick = {
filesSelector.clipboardTextItems = !filesSelector.clipboardTextItems
filesSelector.clipboardData = filesSelector.validateClipboardCheckbox()
filesSelector.updateCheckboxState()
},
checked = filesSelector.clipboardTextItems,
text = stringRes(R.string.backup_and_restore__back_up__files_clipboard_history__clipboard_text_items),
@@ -339,7 +359,7 @@ internal fun BackupFilesSelector(
CheckboxListItem(
onClick = {
filesSelector.clipboardImageItems = !filesSelector.clipboardImageItems
filesSelector.clipboardData = filesSelector.validateClipboardCheckbox()
filesSelector.updateCheckboxState()
},
checked = filesSelector.clipboardImageItems,
text = stringRes(R.string.backup_and_restore__back_up__files_clipboard_history__clipboard_image_items),
@@ -348,7 +368,7 @@ internal fun BackupFilesSelector(
CheckboxListItem(
onClick = {
filesSelector.clipboardVideoItems = !filesSelector.clipboardVideoItems
filesSelector.clipboardData = filesSelector.validateClipboardCheckbox()
filesSelector.updateCheckboxState()
},
checked = filesSelector.clipboardVideoItems,
text = stringRes(R.string.backup_and_restore__back_up__files_clipboard_history__clipboard_video_items),
@@ -382,6 +402,30 @@ internal fun CheckboxListItem(
)
}
@Composable
internal fun TriStateCheckboxListItem(
onClick: () -> Unit,
state: ToggleableState,
text: String,
isSecondaryListItem: Boolean = false,
) {
JetPrefListItem(
modifier = Modifier.rippleClickable(onClick = onClick),
icon = {
Row {
if (isSecondaryListItem) {
Spacer(modifier = Modifier.width(40.dp))
}
TriStateCheckbox(
state = state,
onClick = null,
)
}
},
text = text,
)
}
@Composable
internal fun RadioListItem(
onClick: () -> Unit,

View File

@@ -138,7 +138,10 @@ fun RestoreScreen() = FlorisScreen {
}
restoreWorkspace = workspace
}.onFailure { error ->
context.showLongToast(R.string.backup_and_restore__restore__failure, "error_message" to error.localizedMessage)
context.showLongToast(
R.string.backup_and_restore__restore__failure,
"error_message" to error.localizedMessage,
)
}
},
)
@@ -176,15 +179,20 @@ fun RestoreScreen() = FlorisScreen {
srcDir.copyRecursively(dstDir, overwrite = true)
}
}
val clipboardManager = context.clipboardManager().value
if (shouldReset) {
clipboardManager.clearFullHistory()
ClipboardFileStorage.resetClipboardFileStorage(context)
}
if (restoreFilesSelector.provideClipboardItems()) {
val clipboardFilesDir = workspace.outputDir.subDir("clipboard")
val clipboardManager = context.clipboardManager().value
if (restoreFilesSelector.clipboardTextItems) {
val clipboardItems = clipboardFilesDir.subFile(Backup.CLIPBOARD_TEXT_ITEMS_JSON_NAME)
if (clipboardItems.exists()) {
val clipboardItemsList = clipboardItems.readJson<List<ClipboardItem>>()
clipboardManager.restoreHistory(shouldReset = shouldReset, items = clipboardItemsList.filter { it.type == ItemType.TEXT }, itemType = ItemType.TEXT)
clipboardManager.restoreHistory(items = clipboardItemsList.filter { it.type == ItemType.TEXT })
}
}
if (restoreFilesSelector.clipboardImageItems) {
@@ -192,14 +200,18 @@ fun RestoreScreen() = FlorisScreen {
if (clipboardItems.exists()) {
val clipboardItemsList = clipboardItems.readJson<List<ClipboardItem>>()
for (item in clipboardItemsList.filter { it.type == ItemType.IMAGE }) {
ClipboardFileStorage.instertFileFromBackup(
ClipboardFileStorage.insertFileFromBackupIfNotExisting(
context,
clipboardFilesDir.subFile(
relPath = "${ClipboardFileStorage.CLIPBOARD_FILES_PATH}/${item.uri!!.path!!.split('/').last()}"
relPath = "${ClipboardFileStorage.CLIPBOARD_FILES_PATH}/${
item.uri!!.path!!.split(
'/'
).last()
}"
)
)
}
clipboardManager.restoreHistory(shouldReset = shouldReset, items = clipboardItemsList.filter { it.type == ItemType.IMAGE }, itemType = ItemType.IMAGE)
clipboardManager.restoreHistory(items = clipboardItemsList.filter { it.type == ItemType.IMAGE })
}
}
if (restoreFilesSelector.clipboardVideoItems) {
@@ -207,14 +219,18 @@ fun RestoreScreen() = FlorisScreen {
if (clipboardItems.exists()) {
val clipboardItemsList = clipboardItems.readJson<List<ClipboardItem>>()
for (item in clipboardItemsList.filter { it.type == ItemType.VIDEO }) {
ClipboardFileStorage.instertFileFromBackup(
ClipboardFileStorage.insertFileFromBackupIfNotExisting(
context,
clipboardFilesDir.subFile(
relPath = "${ClipboardFileStorage.CLIPBOARD_FILES_PATH}/${item.uri!!.path!!.split('/').last()}"
relPath = "${ClipboardFileStorage.CLIPBOARD_FILES_PATH}/${
item.uri!!.path!!.split(
'/'
).last()
}"
)
)
}
clipboardManager.restoreHistory(shouldReset = shouldReset, items = clipboardItemsList.filter { it.type == ItemType.VIDEO }, itemType = ItemType.VIDEO)
clipboardManager.restoreHistory(items = clipboardItemsList.filter { it.type == ItemType.VIDEO })
}
}
}
@@ -238,7 +254,11 @@ fun RestoreScreen() = FlorisScreen {
context.showLongToast(R.string.backup_and_restore__restore__success)
navController.navigateUp()
} catch (e: Throwable) {
context.showLongToast(R.string.backup_and_restore__restore__failure, "error_message" to e.localizedMessage)
e.printStackTrace()
context.showLongToast(
R.string.backup_and_restore__restore__failure,
"error_message" to e.localizedMessage,
)
}
}
},
@@ -273,7 +293,10 @@ fun RestoreScreen() = FlorisScreen {
runCatching {
restoreDataFromFileSystemLauncher.launch("*/*")
}.onFailure { error ->
context.showLongToast(R.string.backup_and_restore__restore__failure, "error_message" to error.localizedMessage)
context.showLongToast(
R.string.backup_and_restore__restore__failure,
"error_message" to error.localizedMessage,
)
}
},
modifier = Modifier
@@ -295,15 +318,15 @@ fun RestoreScreen() = FlorisScreen {
modifier = Modifier.defaultFlorisOutlinedBox(),
title = stringRes(R.string.backup_and_restore__restore__metadata),
) {
this@content.Preference(
Preference(
icon = Icons.Default.Code,
title = workspace.metadata.packageName,
)
this@content.Preference(
Preference(
icon = Icons.Outlined.Info,
title = "${workspace.metadata.versionName} (${workspace.metadata.versionCode})",
)
this@content.Preference(
Preference(
icon = Icons.Default.Schedule,
title = remember(workspace.metadata.timestamp) {
val formatter = DateFormat.getDateTimeInstance()

View File

@@ -25,6 +25,7 @@ import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
import org.florisboard.lib.android.AndroidVersion
@OptIn(ExperimentalJetPrefDatastoreUi::class)
@Composable
@@ -71,6 +72,22 @@ fun ClipboardScreen() = FlorisScreen {
stepIncrement = 5,
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true && prefs.clipboard.cleanUpOld isEqualTo true },
)
SwitchPreference(
prefs.clipboard.autoCleanSensitive,
title = stringRes(R.string.pref__clipboard__auto_clean_sensitive__label),
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true },
visibleIf = { AndroidVersion.ATLEAST_API33_T },
)
DialogSliderPreference(
prefs.clipboard.autoCleanSensitiveAfter,
title = stringRes(R.string.pref__clipboard__auto_clean_sensitive_after__label),
valueLabel = { pluralsRes(R.plurals.unit__seconds__written, it, "v" to it) },
min = 0,
max = 300,
stepIncrement = 10,
enabledIf = { prefs.clipboard.historyEnabled isEqualTo true && prefs.clipboard.autoCleanSensitive isEqualTo true },
visibleIf = { AndroidVersion.ATLEAST_API33_T },
)
SwitchPreference(
prefs.clipboard.limitHistorySize,
title = stringRes(R.string.pref__clipboard__limit_history_size__label),

View File

@@ -24,8 +24,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
@@ -193,7 +193,7 @@ fun UserDictionaryScreen(type: UserDictionaryType) = FlorisScreen {
icon = if (currentLocale != null) {
Icons.Default.Close
} else {
Icons.Default.ArrowBack
Icons.AutoMirrored.Filled.ArrowBack
},
)
}

View File

@@ -57,6 +57,7 @@ fun KeyboardScreen() = FlorisScreen {
title = stringRes(R.string.pref__keyboard__hinted_number_row_mode__label),
summarySwitchDisabled = stringRes(R.string.state__disabled),
entries = enumDisplayEntriesOf(KeyHintMode::class),
enabledIf = { prefs.keyboard.numberRow.isFalse() }
)
ListPreference(
listPref = prefs.keyboard.hintedSymbolsMode,

View File

@@ -112,7 +112,7 @@ fun LanguagePackManagerScreen(action: LanguagePackManagerScreenAction?) = Floris
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
) {
this@content.Preference(
Preference(
onClick = { navController.navigate(
Routes.Ext.Import(ExtensionImportScreenType.EXT_LANGUAGEPACK, null)
) },

View File

@@ -16,6 +16,8 @@
package dev.patrickgold.florisboard.app.settings.localization
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
@@ -24,8 +26,13 @@ import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
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.unit.dp
@@ -33,8 +40,8 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
import dev.patrickgold.florisboard.cacheManager
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.keyboard.LayoutType
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
@@ -46,7 +53,20 @@ import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
internal val SubtypeSaver = Saver<MutableState<Subtype?>, String>(
save = {
Json.encodeToString<Subtype?>(it.value)
},
restore = {
mutableStateOf(Json.decodeFromString(it))
},
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LocalizationScreen() = FlorisScreen {
title = stringRes(R.string.settings__localization__title)
@@ -57,7 +77,7 @@ fun LocalizationScreen() = FlorisScreen {
val context = LocalContext.current
val keyboardManager by context.keyboardManager()
val subtypeManager by context.subtypeManager()
val cacheManager by context.cacheManager()
var chosenSubtypeToDelete: Subtype? by rememberSaveable(saver = SubtypeSaver) { mutableStateOf(null) }
floatingActionButton {
ExtendedFloatingActionButton(
@@ -84,7 +104,6 @@ fun LocalizationScreen() = FlorisScreen {
entries = enumDisplayEntriesOf(DisplayLanguageNamesIn::class),
)
Preference(
// icon = R.drawable.ic_edit,
title = stringRes(R.string.settings__localization__language_pack_title),
summary = stringRes(R.string.settings__localization__language_pack_summary),
onClick = {
@@ -118,17 +137,50 @@ fun LocalizationScreen() = FlorisScreen {
DisplayLanguageNamesIn.NATIVE_LOCALE -> subtype.primaryLocale.displayName(subtype.primaryLocale)
},
summary = summary,
onClick = {
navController.navigate(
Routes.Settings.SubtypeEdit(subtype.id)
)
},
modifier = Modifier.combinedClickable(
onClick = {
navController.navigate(
Routes.Settings.SubtypeEdit(subtype.id)
)
},
onLongClick = {
chosenSubtypeToDelete = subtype
},
)
)
}
}
}
}
//PreferenceGroup(title = stringRes(R.string.settings__localization__group_layouts__label)) {
//}
DeleteSubtypeConfirmationDialog(
subtypeToDelete = chosenSubtypeToDelete,
onDismiss = {
chosenSubtypeToDelete = null
},
onConfirm = {
chosenSubtypeToDelete?.let { subtypeManager.removeSubtype(subtypeToRemove = it) }
chosenSubtypeToDelete = null
}
)
}
@Composable
fun DeleteSubtypeConfirmationDialog(
subtypeToDelete: Subtype?,
onDismiss: () -> Unit,
onConfirm: () -> Unit,
) {
subtypeToDelete?.let {
JetPrefAlertDialog(
title = stringRes(R.string.settings__localization__subtype_delete_confirmation_title),
confirmLabel = stringRes(R.string.action__yes),
dismissLabel = stringRes(R.string.action__no),
onDismiss = onDismiss,
onConfirm = onConfirm,
) {
Text(stringRes(R.string.settings__localization__subtype_delete_confirmation_warning))
}
}
}

View File

@@ -47,11 +47,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Observer
import androidx.lifecycle.compose.LocalLifecycleOwner
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
@@ -207,7 +207,7 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
var layoutMap by subtypeEditor.layoutMap
var nlpProviders by subtypeEditor.nlpProviders
var showSubtypePresetsDialog by rememberSaveable { mutableStateOf(false) }
var showSubtypePresetsDialog by rememberSaveable { mutableStateOf(id == null) }
var showSelectAsError by rememberSaveable { mutableStateOf(false) }
var errorDialogStrId by rememberSaveable { mutableStateOf<Int?>(null) }
@@ -225,6 +225,24 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
onDispose { selectLocaleScreenResult?.removeObserver(observer) }
}
@Composable
fun SubtypePropertyDropdown(
title: String,
layoutType: LayoutType
) {
SubtypeProperty(title) {
SubtypeLayoutDropdown(
layoutType = layoutType,
layouts = layoutExtensions[layoutType] ?: mapOf(),
showSelectAsError = showSelectAsError,
layoutMap = layoutMap,
onLayoutMapChanged = { layoutMap = it },
selectListValues = selectListValues,
)
}
}
actions {
if (id != null) {
IconButton(onClick = {
@@ -362,17 +380,7 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
onDismissRequest = { expanded = false },
)
}
SubtypeProperty(stringRes(R.string.settings__localization__subtype_characters_layout)) {
val layoutType = LayoutType.CHARACTERS
SubtypeLayoutDropdown(
layoutType = layoutType,
layouts = layoutExtensions[layoutType] ?: mapOf(),
showSelectAsError = showSelectAsError,
layoutMap = layoutMap,
onLayoutMapChanged = { layoutMap = it },
selectListValues = selectListValues,
)
}
SubtypePropertyDropdown(stringRes(R.string.settings__localization__subtype_characters_layout), LayoutType.CHARACTERS)
SubtypeGroupSpacer()
@@ -408,28 +416,9 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
SubtypeGroupSpacer()
SubtypeProperty(stringRes(R.string.settings__localization__subtype_symbols_layout)) {
val layoutType = LayoutType.SYMBOLS
SubtypeLayoutDropdown(
layoutType = layoutType,
layouts = layoutExtensions[layoutType] ?: mapOf(),
showSelectAsError = showSelectAsError,
layoutMap = layoutMap,
onLayoutMapChanged = { layoutMap = it },
selectListValues = selectListValues,
)
}
SubtypeProperty(stringRes(R.string.settings__localization__subtype_symbols2_layout)) {
val layoutType = LayoutType.SYMBOLS2
SubtypeLayoutDropdown(
layoutType = layoutType,
layouts = layoutExtensions[layoutType] ?: mapOf(),
showSelectAsError = showSelectAsError,
layoutMap = layoutMap,
onLayoutMapChanged = { layoutMap = it },
selectListValues = selectListValues,
)
}
SubtypePropertyDropdown(stringRes(R.string.settings__localization__subtype_symbols_layout), LayoutType.SYMBOLS)
SubtypePropertyDropdown(stringRes(R.string.settings__localization__subtype_symbols2_layout), LayoutType.SYMBOLS2)
SubtypeProperty(stringRes(R.string.settings__localization__subtype_composer)) {
val composerIds = remember(composers) {
SelectListKeys + composers.keys
@@ -469,64 +458,17 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
SubtypeGroupSpacer()
SubtypeProperty(stringRes(R.string.settings__localization__subtype_numeric_layout)) {
val layoutType = LayoutType.NUMERIC
SubtypeLayoutDropdown(
layoutType = layoutType,
layouts = layoutExtensions[layoutType] ?: mapOf(),
showSelectAsError = showSelectAsError,
layoutMap = layoutMap,
onLayoutMapChanged = { layoutMap = it },
selectListValues = selectListValues,
)
}
SubtypeProperty(stringRes(R.string.settings__localization__subtype_numeric_advanced_layout)) {
val layoutType = LayoutType.NUMERIC_ADVANCED
SubtypeLayoutDropdown(
layoutType = layoutType,
layouts = layoutExtensions[layoutType] ?: mapOf(),
showSelectAsError = showSelectAsError,
layoutMap = layoutMap,
onLayoutMapChanged = { layoutMap = it },
selectListValues = selectListValues,
)
}
SubtypeProperty(stringRes(R.string.settings__localization__subtype_numeric_row_layout)) {
val layoutType = LayoutType.NUMERIC_ROW
SubtypeLayoutDropdown(
layoutType = layoutType,
layouts = layoutExtensions[layoutType] ?: mapOf(),
showSelectAsError = showSelectAsError,
layoutMap = layoutMap,
onLayoutMapChanged = { layoutMap = it },
selectListValues = selectListValues,
)
}
SubtypePropertyDropdown(stringRes(R.string.settings__localization__subtype_numeric_layout), LayoutType.NUMERIC)
SubtypePropertyDropdown(stringRes(R.string.settings__localization__subtype_numeric_advanced_layout), LayoutType.NUMERIC_ADVANCED)
SubtypePropertyDropdown(stringRes(R.string.settings__localization__subtype_numeric_row_layout), LayoutType.NUMERIC_ROW)
SubtypeGroupSpacer()
SubtypeProperty(stringRes(R.string.settings__localization__subtype_phone_layout)) {
val layoutType = LayoutType.PHONE
SubtypeLayoutDropdown(
layoutType = layoutType,
layouts = layoutExtensions[layoutType] ?: mapOf(),
showSelectAsError = showSelectAsError,
layoutMap = layoutMap,
onLayoutMapChanged = { layoutMap = it },
selectListValues = selectListValues,
)
}
SubtypeProperty(stringRes(R.string.settings__localization__subtype_phone2_layout)) {
val layoutType = LayoutType.PHONE2
SubtypeLayoutDropdown(
layoutType = layoutType,
layouts = layoutExtensions[layoutType] ?: mapOf(),
showSelectAsError = showSelectAsError,
layoutMap = layoutMap,
onLayoutMapChanged = { layoutMap = it },
selectListValues = selectListValues,
)
}
SubtypePropertyDropdown(stringRes(R.string.settings__localization__subtype_phone_layout), LayoutType.PHONE)
SubtypePropertyDropdown(stringRes(R.string.settings__localization__subtype_phone2_layout), LayoutType.PHONE2)
}
if (showSubtypePresetsDialog) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,43 +16,126 @@
package dev.patrickgold.florisboard.app.settings.media
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.EmojiSymbols
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.runtime.Composable
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
import dev.patrickgold.florisboard.ime.media.emoji.EmojiHistory
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSkinTone
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSuggestionType
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.pluralsRes
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
import dev.patrickgold.jetpref.datastore.ui.ListPreference
import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup
import dev.patrickgold.jetpref.datastore.ui.SwitchPreference
@OptIn(ExperimentalJetPrefDatastoreUi::class)
@Composable
fun MediaScreen() = FlorisScreen {
title = stringRes(R.string.settings__media__title)
previewFieldVisible = true
iconSpaceReserved = false
iconSpaceReserved = true
content {
ListPreference(
prefs.media.emojiPreferredSkinTone,
prefs.emoji.preferredSkinTone,
title = stringRes(R.string.prefs__media__emoji_preferred_skin_tone),
entries = enumDisplayEntriesOf(EmojiSkinTone::class),
)
DialogSliderPreference(
prefs.media.emojiRecentlyUsedMaxSize,
title = stringRes(R.string.prefs__media__emoji_recently_used_max_size),
valueLabel = { maxSize ->
if (maxSize == 0) {
stringRes(R.string.general__unlimited)
} else {
pluralsRes(R.plurals.unit__items__written, maxSize, "v" to maxSize)
}
},
min = 0,
max = 120,
stepIncrement = 1,
)
PreferenceGroup(title = stringRes(R.string.prefs__media__emoji_history__title)) {
SwitchPreference(
prefs.emoji.historyEnabled,
icon = Icons.Outlined.Schedule,
title = stringRes(R.string.prefs__media__emoji_history_enabled),
summary = stringRes(R.string.prefs__media__emoji_history_enabled__summary),
)
ListPreference(
prefs.emoji.historyPinnedUpdateStrategy,
title = stringRes(R.string.prefs__media__emoji_history_pinned_update_strategy),
entries = enumDisplayEntriesOf(EmojiHistory.UpdateStrategy::class),
enabledIf = { prefs.emoji.historyEnabled.isTrue() },
)
ListPreference(
prefs.emoji.historyRecentUpdateStrategy,
title = stringRes(R.string.prefs__media__emoji_history_recent_update_strategy),
entries = enumDisplayEntriesOf(EmojiHistory.UpdateStrategy::class),
enabledIf = { prefs.emoji.historyEnabled.isTrue() },
)
DialogSliderPreference(
primaryPref = prefs.emoji.historyPinnedMaxSize,
secondaryPref = prefs.emoji.historyRecentMaxSize,
title = stringRes(R.string.prefs__media__emoji_history_max_size),
primaryLabel = stringRes(R.string.emoji__history__pinned),
secondaryLabel = stringRes(R.string.emoji__history__recent),
valueLabel = { maxSize ->
if (maxSize == EmojiHistory.MaxSizeUnlimited) {
stringRes(R.string.general__unlimited)
} else {
pluralsRes(R.plurals.unit__items__written, maxSize, "v" to maxSize)
}
},
min = 0,
max = 120,
stepIncrement = 1,
enabledIf = { prefs.emoji.historyEnabled.isTrue() },
)
}
PreferenceGroup(title = stringRes(R.string.prefs__media__emoji_suggestion__title)) {
SwitchPreference(
prefs.emoji.suggestionEnabled,
icon = Icons.Outlined.EmojiSymbols,
title = stringRes(R.string.prefs__media__emoji_suggestion_enabled),
summary = stringRes(R.string.prefs__media__emoji_suggestion_enabled__summary),
)
ListPreference(
prefs.emoji.suggestionType,
title = stringRes(R.string.prefs__media__emoji_suggestion_type),
entries = enumDisplayEntriesOf(EmojiSuggestionType::class),
enabledIf = { prefs.emoji.suggestionEnabled.isTrue() },
)
SwitchPreference(
prefs.emoji.suggestionUpdateHistory,
title = stringRes(R.string.prefs__media__emoji_suggestion_update_history),
summary = stringRes(R.string.prefs__media__emoji_suggestion_update_history__summary),
enabledIf = {
prefs.emoji.suggestionEnabled.isTrue() && prefs.emoji.historyEnabled.isTrue()
},
)
SwitchPreference(
prefs.emoji.suggestionCandidateShowName,
title = stringRes(R.string.prefs__media__emoji_suggestion_candidate_show_name),
summary = stringRes(R.string.prefs__media__emoji_suggestion_candidate_show_name__summary),
enabledIf = { prefs.emoji.suggestionEnabled.isTrue() },
)
DialogSliderPreference(
prefs.emoji.suggestionQueryMinLength,
title = stringRes(R.string.prefs__media__emoji_suggestion_query_min_length),
valueLabel = { length ->
pluralsRes(R.plurals.unit__characters__written, length, "v" to length)
},
min = 1,
max = 5,
stepIncrement = 1,
enabledIf = { prefs.emoji.suggestionEnabled.isTrue() },
)
DialogSliderPreference(
prefs.emoji.suggestionCandidateMaxCount,
title = stringRes(R.string.prefs__media__emoji_suggestion_candidate_max_count),
valueLabel = { count ->
pluralsRes(R.plurals.unit__candidates__written, count, "v" to count)
},
min = 1,
max = 10,
stepIncrement = 1,
enabledIf = { prefs.emoji.suggestionEnabled.isTrue() },
)
}
}
}

View File

@@ -17,6 +17,7 @@
package dev.patrickgold.florisboard.app.settings.smartbar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.enumDisplayEntriesOf
import dev.patrickgold.florisboard.ime.smartbar.CandidatesDisplayMode
@@ -64,11 +65,16 @@ fun SmartbarScreen() = FlorisScreen {
prefs.smartbar.layout isEqualTo SmartbarLayout.SUGGESTIONS_ACTIONS_EXTENDED
},
)
// TODO: schedule to remove this preference in the future, but keep it for now so users
// know why the setting is not available anymore. Also force enable it for UI display.
SideEffect {
prefs.smartbar.sharedActionsAutoExpandCollapse.set(true)
}
SwitchPreference(
prefs.smartbar.sharedActionsAutoExpandCollapse,
title = stringRes(R.string.pref__smartbar__shared_actions_auto_expand_collapse__label),
summary = stringRes(R.string.pref__smartbar__shared_actions_auto_expand_collapse__summary),
enabledIf = { prefs.smartbar.enabled isEqualTo true },
summary = "[Since v0.4.1] Always enabled due to UX issues",
enabledIf = { false },
visibleIf = { prefs.smartbar.layout isEqualTo SmartbarLayout.SUGGESTIONS_ACTIONS_SHARED },
)
ListPreference(

View File

@@ -31,7 +31,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.automirrored.filled.HelpOutline
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -455,7 +455,7 @@ private fun PropertyValueEditor(
FlorisIconButton(
onClick = { showSyntaxHelp = !showSyntaxHelp },
modifier = Modifier.offset(x = 12.dp),
icon = Icons.Default.HelpOutline,
icon = Icons.AutoMirrored.Filled.HelpOutline,
)
},
) {

View File

@@ -40,8 +40,8 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.HelpOutline
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.filled.Pageview
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -225,7 +225,6 @@ internal fun EditRuleDialog(
else -> stringRes(R.string.snygg__rule_selector__pressed)
},
selected = pressedSelector,
color = if (pressedSelector) MaterialTheme.colorScheme.secondary else Color.Unspecified,
)
FlorisChip(
onClick = { focusSelector = !focusSelector },
@@ -235,7 +234,6 @@ internal fun EditRuleDialog(
else -> stringRes(R.string.snygg__rule_selector__focus)
},
selected = focusSelector,
color = if (focusSelector) MaterialTheme.colorScheme.secondary else Color.Unspecified,
)
FlorisChip(
onClick = { disabledSelector = !disabledSelector },
@@ -244,7 +242,6 @@ internal fun EditRuleDialog(
else -> stringRes(R.string.snygg__rule_selector__disabled)
},
selected = disabledSelector,
color = if (disabledSelector) MaterialTheme.colorScheme.secondary else Color.Unspecified,
)
}
}
@@ -291,7 +288,6 @@ internal fun EditRuleDialog(
else -> stringRes(R.string.enum__input_shift_state__unshifted)
},
selected = shiftStateUnshifted,
color = if (shiftStateUnshifted) MaterialTheme.colorScheme.secondary else Color.Unspecified,
)
FlorisChip(
onClick = { shiftStateShiftedManual = !shiftStateShiftedManual },
@@ -302,7 +298,6 @@ internal fun EditRuleDialog(
else -> stringRes(R.string.enum__input_shift_state__shifted_manual)
},
selected = shiftStateShiftedManual,
color = if (shiftStateShiftedManual) MaterialTheme.colorScheme.secondary else Color.Unspecified,
)
FlorisChip(
onClick = { shiftStateShiftedAutomatic = !shiftStateShiftedAutomatic },
@@ -313,7 +308,6 @@ internal fun EditRuleDialog(
else -> stringRes(R.string.enum__input_shift_state__shifted_automatic)
},
selected = shiftStateShiftedAutomatic,
color = if (shiftStateShiftedAutomatic) MaterialTheme.colorScheme.secondary else Color.Unspecified,
)
FlorisChip(
onClick = { shiftStateCapsLock = !shiftStateCapsLock },
@@ -324,7 +318,6 @@ internal fun EditRuleDialog(
else -> stringRes(R.string.enum__input_shift_state__caps_lock)
},
selected = shiftStateCapsLock,
color = if (shiftStateCapsLock) MaterialTheme.colorScheme.secondary else Color.Unspecified,
)
}
}
@@ -479,7 +472,7 @@ private fun EditCodeValueDialog(
FlorisIconButton(
onClick = { showKeyCodesHelp = !showKeyCodesHelp },
modifier = Modifier.offset(x = 12.dp),
icon = Icons.Default.HelpOutline,
icon = Icons.AutoMirrored.Filled.HelpOutline,
)
},
) {

View File

@@ -48,15 +48,16 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.Backspace
import androidx.compose.material.icons.filled.ClearAll
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.ToggleOff
import androidx.compose.material.icons.filled.ToggleOn
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -87,6 +88,8 @@ import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardFileStorage
import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.clipboard.provider.ItemType
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.ime.media.KeyboardLikeButton
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyData
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
@@ -193,6 +196,13 @@ fun ClipboardInputLayout(
iconColor = headerStyle.foreground.solidColor(context),
enabled = !deviceLocked && historyEnabled && !isPopupSurfaceActive(),
)
KeyboardLikeButton(
inputEventDispatcher = keyboardManager.inputEventDispatcher,
keyData = TextKeyData.DELETE,
element = FlorisImeUi.ClipboardHeader,
) {
Icon(Icons.AutoMirrored.Outlined.Backspace, null)
}
}
}
@@ -212,7 +222,7 @@ fun ClipboardInputLayout(
clip = true,
clickAndSemanticsModifier = Modifier.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
indication = ripple(),
enabled = popupItem == null,
onLongClick = {
popupItem = item
@@ -307,7 +317,7 @@ fun ClipboardInputLayout(
.fillMaxWidth()
.run { if (contentScrollInsteadOfClip) this.florisVerticalScroll() else this }
.padding(ItemPadding),
text = text,
text = item.displayText(),
style = TextStyle(textDirection = TextDirection.ContentOrLtr),
color = style.foreground.solidColor(context),
fontSize = style.fontSize.spSize(),
@@ -577,7 +587,7 @@ fun ClipboardInputLayout(
Column(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight(),
.height(FlorisImeSizing.imeUiHeight()),
) {
HeaderRow()
if (deviceLocked) {

View File

@@ -28,11 +28,6 @@ import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardHistoryDao
import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardHistoryDatabase
import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.clipboard.provider.ItemType
import org.florisboard.lib.android.AndroidClipboardManager
import org.florisboard.lib.android.AndroidClipboardManager_OnPrimaryClipChangedListener
import org.florisboard.lib.android.setOrClearPrimaryClip
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -45,6 +40,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.florisboard.lib.android.AndroidClipboardManager
import org.florisboard.lib.android.AndroidClipboardManager_OnPrimaryClipChangedListener
import org.florisboard.lib.android.setOrClearPrimaryClip
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemService
import org.florisboard.lib.kotlin.tryOrNull
import java.io.Closeable
@@ -110,7 +110,9 @@ class ClipboardManager(
val primaryClipFlow = _primaryClipFlow.asStateFlow()
inline var primaryClip
get() = primaryClipFlow.value
private set(v) { _primaryClipFlow.value = v }
private set(v) {
_primaryClipFlow.value = v
}
init {
systemClipboardManager.addPrimaryClipChangedListener(this)
@@ -252,14 +254,20 @@ class ClipboardManager(
}
private fun enforceExpiryDate(clipHistory: ClipboardHistory) {
val itemsToRemove = mutableSetOf<ClipboardItem>()
if (prefs.clipboard.cleanUpOld.get()) {
val nonPinnedItems = clipHistory.recent + clipHistory.other
val expiryTime = System.currentTimeMillis() - (prefs.clipboard.cleanUpAfter.get() * 60 * 1000)
val itemsToRemove = nonPinnedItems.filter { it.creationTimestampMs < expiryTime }
if (itemsToRemove.isNotEmpty()) {
ioScope.launch {
clipHistoryDao?.delete(itemsToRemove)
}
itemsToRemove.addAll(nonPinnedItems.filter { it.creationTimestampMs < expiryTime })
}
if (prefs.clipboard.autoCleanSensitive.get()) {
val sensitiveData = clipHistory.all.filter { it.isSensitive }
val expiryTime = System.currentTimeMillis() - (prefs.clipboard.autoCleanSensitiveAfter.get() * 1000)
itemsToRemove.addAll(sensitiveData.filter { it.creationTimestampMs < expiryTime })
}
if (itemsToRemove.isNotEmpty()) {
ioScope.launch {
clipHistoryDao?.delete(itemsToRemove.toList())
}
}
}
@@ -278,6 +286,9 @@ class ClipboardManager(
}
}
/**
* Clears all unpinned items from the clipboard history
*/
fun clearHistory() {
ioScope.launch {
for (item in history().all) {
@@ -287,6 +298,9 @@ class ClipboardManager(
}
}
/**
* Clears the full clipboard history
*/
fun clearFullHistory() {
ioScope.launch {
for (item in history().all) {
@@ -300,26 +314,15 @@ class ClipboardManager(
/**
* Restore the clipboard history from a [List]
*
* @param shouldReset if the history should be reset
* @param items the [ClipboardItem] list with the new items
*/
fun restoreHistory(items: List<ClipboardItem>, shouldReset: Boolean, itemType: ItemType) {
fun restoreHistory(items: List<ClipboardItem>) {
ioScope.launch {
if (shouldReset) {
for (item in history().all) {
item.close(appContext)
}
clipHistoryDao?.deleteAllFromType(itemType)
for (item in items) {
val currentHistory = this@ClipboardManager.history().all
for (item in items) {
if (!currentHistory.map { it.copy(id = 0) }.contains(item.copy(id = 0))) {
this@ClipboardManager.insertClip(item.copy(id = 0))
}
} else {
val currentHistory = this@ClipboardManager.history().all
for (item in items) {
if (!currentHistory.map { it.copy(id = 0) }.contains(item.copy(id = 0))) {
this@ClipboardManager.insertClip(item.copy(id = 0))
}
}
}
}
}
@@ -344,7 +347,7 @@ class ClipboardManager(
fun unpinClip(item: ClipboardItem) {
ioScope.launch {
clipHistoryDao?.update(item.copy(isPinned = false))
clipHistoryDao?.update(item.copy(isPinned = false))
}
}

View File

@@ -1,4 +1,4 @@
package dev.patrickgold.florisboard
package dev.patrickgold.florisboard.ime.clipboard
import android.content.ClipData
import android.content.Intent
@@ -10,9 +10,11 @@ import android.provider.MediaStore
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
@@ -28,6 +30,7 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.apptheme.FlorisAppTheme
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.lib.compose.ProvideLocalizedResources
@@ -84,6 +87,7 @@ class FlorisCopyToClipboardActivity : ComponentActivity() {
} else {
val uri: Uri? =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_STREAM)
} else {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
@@ -114,12 +118,14 @@ class FlorisCopyToClipboardActivity : ComponentActivity() {
)
}
bitmap?.let {
Image(
modifier = Modifier
.padding(start = 64.dp, end = 64.dp, top = 32.dp, bottom = 8.dp),
bitmap = bitmap!!.asImageBitmap(),
contentDescription = null
)
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Image(
modifier = Modifier
.padding(start = 64.dp, end = 64.dp, top = 32.dp, bottom = 8.dp),
bitmap = bitmap!!.asImageBitmap(),
contentDescription = null
)
}
}
}
}

View File

@@ -17,6 +17,8 @@
package dev.patrickgold.florisboard.ime.clipboard.provider
import android.content.ClipData
import android.content.ClipDescription.EXTRA_IS_REMOTE_DEVICE
import android.content.ClipDescription.EXTRA_IS_SENSITIVE
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
@@ -24,8 +26,11 @@ import android.net.Uri
import android.provider.BaseColumns
import android.provider.MediaStore.Images.Media
import android.provider.OpenableColumns
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.core.database.getStringOrNull
import androidx.lifecycle.LiveData
import androidx.room.AutoMigration
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Database
@@ -34,14 +39,21 @@ import androidx.room.Entity
import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.RenameColumn
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import androidx.room.Update
import androidx.room.migration.AutoMigrationSpec
import dev.patrickgold.florisboard.R
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.android.UriSerializer
import org.florisboard.lib.android.query
import kotlinx.serialization.Serializable
import org.florisboard.lib.android.stringRes
import org.florisboard.lib.kotlin.tryOrNull
private const val CLIPBOARD_HISTORY_TABLE = "clipboard_history"
@@ -67,7 +79,7 @@ enum class ItemType(val value: Int) {
*/
@Serializable
@Entity(tableName = CLIPBOARD_HISTORY_TABLE)
data class ClipboardItem(
data class ClipboardItem @OptIn(ExperimentalSerializationApi::class) constructor(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = BaseColumns._ID, index = true)
var id: Long = 0,
@@ -78,6 +90,12 @@ data class ClipboardItem(
val creationTimestampMs: Long,
val isPinned: Boolean,
val mimeTypes: Array<String>,
@EncodeDefault
@ColumnInfo(name = "is_sensitive", defaultValue = "0")
val isSensitive: Boolean = false,
@EncodeDefault
@ColumnInfo(name= "is_remote_device", defaultValue = "0")
val isRemoteDevice: Boolean = false,
) {
companion object {
/**
@@ -113,6 +131,18 @@ data class ClipboardItem(
else -> ItemType.TEXT
}
val isSensitive = if (AndroidVersion.ATLEAST_API33_T) {
data.description?.extras?.getBoolean(EXTRA_IS_SENSITIVE) ?: false
} else {
false
}
val isRemoteDevice = if (AndroidVersion.ATLEAST_API34_U) {
data.description?.extras?.getBoolean(EXTRA_IS_REMOTE_DEVICE) ?: false
} else {
false
}
val uri = if (type == ItemType.IMAGE || type == ItemType.VIDEO) {
if (dataItem.uri.authority == ClipboardMediaProvider.AUTHORITY || !cloneUri) {
dataItem.uri
@@ -151,7 +181,21 @@ data class ClipboardItem(
}
}
return ClipboardItem(0, type, text, uri, System.currentTimeMillis(), false, mimeTypes)
return ClipboardItem(0, type, text, uri, System.currentTimeMillis(), false, mimeTypes, isSensitive, isRemoteDevice)
}
}
@Composable
inline fun displayText(): String {
val context = LocalContext.current
return displayText(context)
}
fun displayText(context: Context): String {
return if (isSensitive) {
context.stringRes(R.string.clipboard__sensitive_clip_content)
} else {
stringRepresentation()
}
}
@@ -198,6 +242,7 @@ data class ClipboardItem(
if (uri != other.uri) return false
if (creationTimestampMs != other.creationTimestampMs) return false
if (!mimeTypes.contentEquals(other.mimeTypes)) return false
if (isSensitive != other.isSensitive) return false
return true
}
@@ -209,6 +254,7 @@ data class ClipboardItem(
result = 31 * result + (uri?.hashCode() ?: 0)
result = 31 * result + creationTimestampMs.hashCode()
result = 31 * result + mimeTypes.contentHashCode()
result = 31 * result + isSensitive.hashCode()
return result
}
@@ -293,11 +339,30 @@ interface ClipboardHistoryDao {
fun deleteAllUnpinned()
}
@Database(entities = [ClipboardItem::class], version = 2)
@Database(
entities = [ClipboardItem::class],
version = 4,
autoMigrations = [
AutoMigration(from = 2, to = 4),
AutoMigration(from = 3, to = 4, spec = ClipboardHistoryDatabase.MIGRATE_3_TO_4::class),
],
)
@TypeConverters(Converters::class)
abstract class ClipboardHistoryDatabase : RoomDatabase() {
abstract fun clipboardItemDao(): ClipboardHistoryDao
@RenameColumn(
tableName = CLIPBOARD_HISTORY_TABLE,
fromColumnName = "isSensitive",
toColumnName = "is_sensitive",
)
@RenameColumn(
tableName = CLIPBOARD_HISTORY_TABLE,
fromColumnName = "isRemoteDevice",
toColumnName = "is_remote_device",
)
class MIGRATE_3_TO_4 : AutoMigrationSpec
companion object {
fun new(context: Context): ClipboardHistoryDatabase {
return Room

View File

@@ -61,7 +61,28 @@ object ClipboardFileStorage {
return context.clipboardFilesDir.subFile(id.toString())
}
fun instertFileFromBackup(context: Context, file: FsFile) {
file.copyTo(context.clipboardFilesDir.subFile(file.name), overwrite = false)
/**
* Insert file from backup if not existing
*
* @param context the application context
* @param file the file to be inserted
*/
fun insertFileFromBackupIfNotExisting(context: Context, file: FsFile) {
if (!context.clipboardFilesDir.subFile(file.name).isFile) {
file.copyTo(context.clipboardFilesDir.subFile(file.name), overwrite = false)
}
}
/**
* Deletes all files from the clipboard subdirectory
*
* @param context the application context
*/
fun resetClipboardFileStorage(context: Context) {
context.clipboardFilesDir.listFiles()?.forEach {
it.delete()
}
}
}

View File

@@ -18,8 +18,15 @@ package dev.patrickgold.florisboard.ime.keyboard
import android.content.Context
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowRightAlt
import androidx.compose.material.icons.filled.Backspace
import androidx.compose.material.icons.automirrored.filled.ArrowRightAlt
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.automirrored.filled.KeyboardReturn
import androidx.compose.material.icons.automirrored.filled.Redo
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.automirrored.outlined.Assignment
import androidx.compose.material.icons.automirrored.outlined.Backspace
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.ContentCut
@@ -28,23 +35,16 @@ import androidx.compose.material.icons.filled.DeleteSweep
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.FontDownload
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.KeyboardCapslock
import androidx.compose.material.icons.filled.KeyboardReturn
import androidx.compose.material.icons.filled.KeyboardVoice
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.MoreHoriz
import androidx.compose.material.icons.filled.Redo
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.filled.SentimentSatisfiedAlt
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SpaceBar
import androidx.compose.material.icons.filled.Undo
import androidx.compose.material.icons.outlined.Assignment
import androidx.compose.ui.graphics.vector.ImageVector
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
@@ -179,10 +179,10 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
val evaluator = this
return when (data.code) {
KeyCode.ARROW_LEFT -> {
Icons.Default.KeyboardArrowLeft
Icons.AutoMirrored.Filled.KeyboardArrowLeft
}
KeyCode.ARROW_RIGHT -> {
Icons.Default.KeyboardArrowRight
Icons.AutoMirrored.Filled.KeyboardArrowRight
}
KeyCode.ARROW_UP -> {
Icons.Default.KeyboardArrowUp
@@ -213,23 +213,23 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
Icons.Default.KeyboardVoice
}
KeyCode.DELETE -> {
Icons.Default.Backspace
Icons.AutoMirrored.Outlined.Backspace
}
KeyCode.ENTER -> {
val imeOptions = evaluator.editorInfo.imeOptions
val inputAttributes = evaluator.editorInfo.inputAttributes
if (imeOptions.flagNoEnterAction || inputAttributes.flagTextMultiLine) {
Icons.Default.KeyboardReturn
Icons.AutoMirrored.Filled.KeyboardReturn
} else {
when (imeOptions.action) {
ImeOptions.Action.DONE -> Icons.Default.Done
ImeOptions.Action.GO -> Icons.Default.ArrowRightAlt
ImeOptions.Action.NEXT -> Icons.Default.ArrowRightAlt
ImeOptions.Action.NONE -> Icons.Default.KeyboardReturn
ImeOptions.Action.PREVIOUS -> Icons.Default.ArrowRightAlt
ImeOptions.Action.GO -> Icons.AutoMirrored.Filled.ArrowRightAlt
ImeOptions.Action.NEXT -> Icons.AutoMirrored.Filled.ArrowRightAlt
ImeOptions.Action.NONE -> Icons.AutoMirrored.Filled.KeyboardReturn
ImeOptions.Action.PREVIOUS -> Icons.AutoMirrored.Filled.ArrowRightAlt
ImeOptions.Action.SEARCH -> Icons.Default.Search
ImeOptions.Action.SEND -> Icons.Default.Send
ImeOptions.Action.UNSPECIFIED -> Icons.Default.KeyboardReturn
ImeOptions.Action.SEND -> Icons.AutoMirrored.Filled.Send
ImeOptions.Action.UNSPECIFIED -> Icons.AutoMirrored.Filled.KeyboardReturn
}
}
}
@@ -237,7 +237,7 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
Icons.Default.SentimentSatisfiedAlt
}
KeyCode.IME_UI_MODE_CLIPBOARD -> {
Icons.Outlined.Assignment
Icons.AutoMirrored.Outlined.Assignment
}
KeyCode.LANGUAGE_SWITCH -> {
Icons.Default.Language
@@ -263,10 +263,10 @@ fun ComputingEvaluator.computeImageVector(data: KeyData): ImageVector? {
}
}
KeyCode.UNDO -> {
Icons.Default.Undo
Icons.AutoMirrored.Filled.Undo
}
KeyCode.REDO -> {
Icons.Default.Redo
Icons.AutoMirrored.Filled.Redo
}
KeyCode.TOGGLE_ACTIONS_OVERFLOW -> {
Icons.Default.MoreHoriz

View File

@@ -36,10 +36,10 @@ import dev.patrickgold.florisboard.ime.smartbar.ExtendedActionsPlacement
import dev.patrickgold.florisboard.ime.smartbar.SmartbarLayout
import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyboard
import dev.patrickgold.florisboard.keyboardManager
import org.florisboard.lib.android.isOrientationLandscape
import dev.patrickgold.florisboard.lib.observeAsTransformingState
import dev.patrickgold.florisboard.lib.util.ViewUtils
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.android.isOrientationLandscape
private val LocalKeyboardRowBaseHeight = staticCompositionLocalOf { 65.dp }
private val LocalSmartbarHeight = staticCompositionLocalOf { 40.dp }
@@ -133,7 +133,7 @@ fun ProvideKeyboardRowBaseHeight(content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalKeyboardRowBaseHeight provides ViewUtils.px2dp(baseRowHeight).dp,
LocalSmartbarHeight provides ViewUtils.px2dp(smartbarHeight).dp,
LocalSmartbarHeight provides ViewUtils.px2dp(smartbarHeight).toInt().dp,
) {
content()
}

View File

@@ -142,9 +142,15 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
prefs.keyboard.utilityKeyEnabled.observeForever {
updateActiveEvaluators()
}
prefs.keyboard.utilityKeyAction.observeForever {
updateActiveEvaluators()
}
activeState.collectLatestIn(scope) {
updateActiveEvaluators()
}
subtypeManager.subtypesFlow.collectLatestIn(scope) {
updateActiveEvaluators()
}
subtypeManager.activeSubtypeFlow.collectLatestIn(scope) {
reevaluateInputShiftState()
updateActiveEvaluators()

View File

@@ -29,7 +29,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Backspace
import androidx.compose.material.icons.automirrored.outlined.Backspace
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -101,7 +101,7 @@ fun MediaInputLayout(
inputEventDispatcher = keyboardManager.inputEventDispatcher,
keyData = TextKeyData.DELETE,
) {
Icon(imageVector = Icons.Outlined.Backspace, contentDescription = null)
Icon(imageVector = Icons.AutoMirrored.Outlined.Backspace, contentDescription = null)
}
}
}
@@ -113,12 +113,13 @@ internal fun KeyboardLikeButton(
modifier: Modifier = Modifier,
inputEventDispatcher: InputEventDispatcher,
keyData: KeyData,
element: String = FlorisImeUi.EmojiKey,
content: @Composable RowScope.() -> Unit,
) {
val inputFeedbackController = LocalInputFeedbackController.current
var isPressed by remember { mutableStateOf(false) }
val keyStyle = FlorisImeTheme.style.get(
element = FlorisImeUi.EmojiKey,
element = element,
code = keyData.code,
isPressed = isPressed,
)

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Patrick Goldinger
* Copyright (C) 2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,6 +22,11 @@ import dev.patrickgold.florisboard.ime.keyboard.KeyData
import dev.patrickgold.florisboard.ime.popup.PopupSet
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.key.KeyType
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.stream.IntStream
import kotlin.streams.toList
@@ -42,7 +47,7 @@ enum class EmojiHairStyle(val id: Int) {
BALD(0x1F9B3);
}
data class Emoji(val value: String, val name: String, val keywords: List<String>) : KeyData {
class Emoji(val value: String, val name: String, val keywords: List<String>) : KeyData {
override val type = KeyType.CHARACTER
override val code = KeyCode.UNSPECIFIED
override val label = value
@@ -73,4 +78,24 @@ data class Emoji(val value: String, val name: String, val keywords: List<String>
override fun toString(): String {
return "Emoji { value=$value, name=$name, keywords=$keywords }"
}
override fun hashCode(): Int {
return value.hashCode()
}
override fun equals(other: Any?): Boolean {
return other is Emoji && value == other.value
}
object ValueOnlySerializer : KSerializer<Emoji> {
override val descriptor = PrimitiveSerialDescriptor("EmojiValueOnly", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Emoji) {
encoder.encodeString(value.value)
}
override fun deserialize(decoder: Decoder): Emoji {
return Emoji(decoder.decodeString(), "", emptyList())
}
}
}

View File

@@ -95,10 +95,11 @@ data class EmojiData(
// Assume it is a data line
val data = line.split(";")
if (data.size == 3) {
val base = emojiEditorList?.first()
val emoji = Emoji(
value = data[0].trim(),
name = data[1].trim(),
keywords = data[2].split("|").map { it.trim() }
name = base?.name ?: data[1].trim(),
keywords = data[2].split("|").map { it.trim() },
)
if (emojiEditorList != null) {
emojiEditorList!!.add(emoji)
@@ -113,6 +114,14 @@ data class EmojiData(
for (category in byCategory.keys) {
for (emojiSet in byCategory[category]!!) {
if (emojiSet.emojis.size == 1) {
// No variations provided, we fallback to using the base for all skin tones
val base = emojiSet.emojis.first()
for (skinTone in EmojiSkinTone.entries) {
bySkinTone[skinTone]!!.add(base)
}
continue
}
for (emoji in emojiSet.emojis) {
bySkinTone[emoji.skinTone]!!.add(emoji)
}

View File

@@ -0,0 +1,226 @@
/*
* Copyright (C) 2024 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.ime.media.emoji
import dev.patrickgold.florisboard.app.AppPrefs
import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.jetpref.datastore.model.PreferenceSerializer
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
data class EmojiHistory(
val pinned: List<@Serializable(with = Emoji.ValueOnlySerializer::class) Emoji>,
val recent: List<@Serializable(with = Emoji.ValueOnlySerializer::class) Emoji>,
) {
fun edit(): Editor {
return Editor(pinned.toMutableList(), recent.toMutableList())
}
data class Editor(
val pinned: MutableList<Emoji>,
val recent: MutableList<Emoji>,
) {
fun build(): EmojiHistory {
return EmojiHistory(pinned.toList(), recent.toList())
}
}
enum class UpdateStrategy(val isAutomatic: Boolean, val isPrepend: Boolean) {
AUTO_SORT_PREPEND(isAutomatic = true, isPrepend = true),
AUTO_SORT_APPEND(isAutomatic = true, isPrepend = false),
MANUAL_SORT_PREPEND(isAutomatic = false, isPrepend = true),
MANUAL_SORT_APPEND(isAutomatic = false, isPrepend = false);
}
object Serializer : PreferenceSerializer<EmojiHistory> {
override fun serialize(value: EmojiHistory): String {
return Json.encodeToString(value)
}
override fun deserialize(value: String): EmojiHistory {
try {
return Json.decodeFromString(value)
} catch (e: Exception) {
flogError { "Failed to deserialize EmojiHistory: $e" }
return Empty
}
}
}
companion object {
val Empty = EmojiHistory(emptyList(), emptyList())
@Suppress("ConstPropertyName")
const val MaxSizeUnlimited: Int = 0
}
}
object EmojiHistoryHelper {
private var emojiGuard = Mutex(locked = false)
suspend fun markEmojiUsed(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
if (!prefs.emoji.historyEnabled.get()) {
return
}
val dataMut = prefs.emoji.historyData.get().edit()
val pinnedUS = prefs.emoji.historyPinnedUpdateStrategy.get()
val recentUS = prefs.emoji.historyRecentUpdateStrategy.get()
val pinnedMaxSize = prefs.emoji.historyPinnedMaxSize.get().let { maxSize ->
if (maxSize == EmojiHistory.MaxSizeUnlimited) Int.MAX_VALUE else maxSize
}
val recentMaxSize = prefs.emoji.historyRecentMaxSize.get().let { maxSize ->
if (maxSize == EmojiHistory.MaxSizeUnlimited) Int.MAX_VALUE else maxSize
}
val pinnedIndex = dataMut.pinned.indexOf(emoji)
if (pinnedIndex != -1) {
if (pinnedUS.isAutomatic) {
dataMut.pinned.removeAt(pinnedIndex)
dataMut.pinned.addWithStrategy(pinnedUS, emoji)
} else {
// manual sort, keep item in place
}
} else {
val recentIndex = dataMut.recent.indexOf(emoji)
if (recentIndex != -1) {
if (recentUS.isAutomatic) {
dataMut.recent.removeAt(recentIndex)
dataMut.recent.addWithStrategy(recentUS, emoji)
} else {
// manual sort, keep item in place
}
} else {
dataMut.recent.addWithStrategy(recentUS, emoji)
}
}
prefs.emoji.historyData.set(
EmojiHistory(
pinned = dataMut.pinned.takeWithStrategy(pinnedUS, pinnedMaxSize),
recent = dataMut.recent.takeWithStrategy(recentUS, recentMaxSize),
)
)
}
suspend fun pinEmoji(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
if (!prefs.emoji.historyEnabled.get()) {
return
}
val dataMut = prefs.emoji.historyData.get().edit()
val pinnedUS = prefs.emoji.historyPinnedUpdateStrategy.get()
val recentIndex = dataMut.recent.indexOf(emoji)
if (recentIndex != -1) {
dataMut.recent.removeAt(recentIndex)
dataMut.pinned.addWithStrategy(pinnedUS, emoji)
}
prefs.emoji.historyData.set(dataMut.build())
}
suspend fun unpinEmoji(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
if (!prefs.emoji.historyEnabled.get()) {
return
}
val dataMut = prefs.emoji.historyData.get().edit()
val recentUS = prefs.emoji.historyRecentUpdateStrategy.get()
val pinnedIndex = dataMut.pinned.indexOf(emoji)
if (pinnedIndex != -1) {
dataMut.pinned.removeAt(pinnedIndex)
dataMut.recent.addWithStrategy(recentUS, emoji)
}
prefs.emoji.historyData.set(dataMut.build())
}
suspend fun moveEmoji(prefs: AppPrefs, emoji: Emoji, offset: Int) = emojiGuard.withLock {
if (!prefs.emoji.historyEnabled.get() || offset == 0) {
return
}
val dataMut = prefs.emoji.historyData.get().edit()
val pinnedIndex = dataMut.pinned.indexOf(emoji)
if (pinnedIndex != -1) {
dataMut.pinned.move(pinnedIndex, offset)
} else {
val recentIndex = dataMut.recent.indexOf(emoji)
if (recentIndex != -1) {
dataMut.recent.move(recentIndex, offset)
}
}
prefs.emoji.historyData.set(dataMut.build())
}
suspend fun removeEmoji(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
if (!prefs.emoji.historyEnabled.get()) {
return
}
val dataMut = prefs.emoji.historyData.get().edit()
val pinnedIndex = dataMut.pinned.indexOf(emoji)
if (pinnedIndex != -1) {
dataMut.pinned.removeAt(pinnedIndex)
} else {
val recentIndex = dataMut.recent.indexOf(emoji)
if (recentIndex != -1) {
dataMut.recent.removeAt(recentIndex)
}
}
prefs.emoji.historyData.set(dataMut.build())
}
private fun MutableList<Emoji>.addWithStrategy(strategy: EmojiHistory.UpdateStrategy, emoji: Emoji) {
if (strategy.isPrepend) {
add(0, emoji)
} else {
add(emoji)
}
}
private fun MutableList<Emoji>.takeWithStrategy(
strategy: EmojiHistory.UpdateStrategy,
n: Int,
): List<Emoji> {
return if (strategy.isPrepend) {
take(n)
} else {
takeLast(n)
}
}
private fun MutableList<Emoji>.move(itemIndex: Int, offset: Int) {
val newIndex = (itemIndex + offset).coerceIn(0..<size)
val item = removeAt(itemIndex)
if (newIndex == size) {
add(item)
} else {
add(newIndex, item)
}
}
}

View File

@@ -40,8 +40,14 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
@@ -61,6 +67,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@@ -85,12 +92,11 @@ import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.florisScrollbar
import dev.patrickgold.florisboard.lib.compose.header
import dev.patrickgold.florisboard.lib.compose.safeTimes
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.jetpref.datastore.model.observeAsState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.florisboard.lib.android.AndroidKeyguardManager
import org.florisboard.lib.android.showShortToast
import org.florisboard.lib.android.systemService
@@ -117,6 +123,12 @@ private val VariantsTriangleShapeRtl = GenericShape { size, _ ->
lineTo(x = 0f, y = size.height)
}
data class EmojiMappingForView(
val pinned: List<EmojiSet>,
val recent: List<EmojiSet>,
val simple: List<EmojiSet>,
)
@Composable
fun EmojiPaletteView(
fullEmojiMappings: EmojiData,
@@ -150,16 +162,61 @@ fun EmojiPaletteView(
val deviceLocked = androidKeyguardManager.let { it.isDeviceLocked || it.isKeyguardLocked }
var activeCategory by remember { mutableStateOf(EmojiCategory.RECENTLY_USED) }
val lazyListState = rememberLazyGridState()
val scope = rememberCoroutineScope()
val preferredSkinTone by prefs.media.emojiPreferredSkinTone.observeAsState()
val preferredSkinTone by prefs.emoji.preferredSkinTone.observeAsState()
val emojiHistoryEnabled by prefs.emoji.historyEnabled.observeAsState()
val fontSizeMultiplier = prefs.keyboard.fontSizeMultiplier()
val emojiKeyStyle = FlorisImeTheme.style.get(element = FlorisImeUi.EmojiKey)
val emojiKeyFontSize = emojiKeyStyle.fontSize.spSize(default = EmojiDefaultFontSize) safeTimes fontSizeMultiplier
val contentColor = emojiKeyStyle.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor())
var activeCategory by remember(emojiHistoryEnabled) {
if (emojiHistoryEnabled) {
mutableStateOf(EmojiCategory.RECENTLY_USED)
} else {
mutableStateOf(EmojiCategory.SMILEYS_EMOTION)
}
}
var recentlyUsedVersion by remember { mutableIntStateOf(0) }
val lazyListState = rememberLazyGridState()
val scope = rememberCoroutineScope()
@Composable
fun GridHeader(text: String) {
Text(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
text = text,
style = MaterialTheme.typography.titleMedium,
color = contentColor,
)
}
@Composable
fun EmojiKeyWrapper(
emojiSet: EmojiSet,
isPinned: Boolean = false,
isRecent: Boolean = false,
) {
EmojiKey(
emojiSet = emojiSet,
emojiCompatInstance = emojiCompatInstance,
preferredSkinTone = preferredSkinTone,
isPinned = isPinned,
isRecent = isRecent,
contentColor = contentColor,
fontSize = emojiKeyFontSize,
fontSizeMultiplier = fontSizeMultiplier,
onEmojiInput = { emoji ->
keyboardManager.inputEventDispatcher.sendDownUp(emoji)
scope.launch {
EmojiHistoryHelper.markEmojiUsed(prefs, emoji)
}
},
onHistoryAction = {
recentlyUsedVersion++
},
)
}
Column(modifier = modifier) {
EmojiCategoriesTabRow(
activeCategory = activeCategory,
@@ -167,6 +224,7 @@ fun EmojiPaletteView(
scope.launch { lazyListState.scrollToItem(0) }
activeCategory = category
},
emojiHistoryEnabled = emojiHistoryEnabled,
)
Box(
@@ -174,16 +232,25 @@ fun EmojiPaletteView(
.fillMaxWidth()
.weight(1f),
) {
var recentlyUsedVersion by remember { mutableIntStateOf(0) }
val emojiMapping = if (activeCategory == EmojiCategory.RECENTLY_USED) {
// Purposely using remember here to prevent recomposition, as this would cause rapid
// emoji changes for the user when in recently used category.
remember(recentlyUsedVersion) {
prefs.media.emojiRecentlyUsed.get().map { EmojiSet(listOf(it)) }
val data = prefs.emoji.historyData.get()
EmojiMappingForView(
pinned = data.pinned.map { EmojiSet(listOf(it)) },
recent = data.recent.map { EmojiSet(listOf(it)) },
simple = emptyList(),
)
}
} else {
emojiMappings[activeCategory]!!
EmojiMappingForView(
pinned = emptyList(),
recent = emptyList(),
simple = emojiMappings[activeCategory]!!,
)
}
val isEmojiHistoryEmpty = emojiMapping.pinned.isEmpty() && emojiMapping.recent.isEmpty()
if (activeCategory == EmojiCategory.RECENTLY_USED && deviceLocked) {
Column(
modifier = Modifier
@@ -191,29 +258,28 @@ fun EmojiPaletteView(
.padding(all = 8.dp),
) {
Text(
text = stringRes(R.string.emoji__recently_used__phone_locked_message),
text = stringRes(R.string.emoji__history__phone_locked_message),
color = contentColor,
)
}
} else if (activeCategory == EmojiCategory.RECENTLY_USED && emojiMapping.isEmpty()) {
} else if (activeCategory == EmojiCategory.RECENTLY_USED && isEmojiHistoryEmpty) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 8.dp),
) {
Text(
text = stringRes(R.string.emoji__recently_used__empty_message),
text = stringRes(R.string.emoji__history__empty_message),
color = contentColor,
)
Text(
modifier = Modifier.padding(top = 8.dp),
text = stringRes(R.string.emoji__recently_used__removal_tip),
text = stringRes(R.string.emoji__history__usage_tip),
color = contentColor,
fontStyle = FontStyle.Italic,
)
}
}
else key(emojiMapping) {
} else key(emojiMapping) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
LazyVerticalGrid(
modifier = Modifier
@@ -222,35 +288,26 @@ fun EmojiPaletteView(
columns = GridCells.Adaptive(minSize = EmojiBaseWidth),
state = lazyListState,
) {
items(emojiMapping) { emojiSet ->
EmojiKey(
emojiSet = emojiSet,
emojiCompatInstance = emojiCompatInstance,
preferredSkinTone = preferredSkinTone,
contentColor = contentColor,
fontSize = emojiKeyFontSize,
fontSizeMultiplier = fontSizeMultiplier,
onEmojiInput = { emoji ->
keyboardManager.inputEventDispatcher.sendDownUp(emoji)
scope.launch {
EmojiRecentlyUsedHelper.addEmoji(prefs, emoji)
}
},
onLongPress = { emoji ->
if (activeCategory == EmojiCategory.RECENTLY_USED) {
scope.launch {
EmojiRecentlyUsedHelper.removeEmoji(prefs, emoji)
recentlyUsedVersion++
withContext(Dispatchers.Main) {
context.showShortToast(
R.string.emoji__recently_used__removal_success_message,
"emoji" to emoji.value,
)
}
}
}
},
)
if (emojiMapping.pinned.isNotEmpty()) {
header("header_pinned") {
GridHeader(text = stringRes(R.string.emoji__history__pinned))
}
items(emojiMapping.pinned) { emojiSet ->
EmojiKeyWrapper(emojiSet, isPinned = true)
}
}
if (emojiMapping.recent.isNotEmpty()) {
header("header_recent") {
GridHeader(text = stringRes(R.string.emoji__history__recent))
}
items(emojiMapping.recent) { emojiSet ->
EmojiKeyWrapper(emojiSet, isRecent = true)
}
}
if (emojiMapping.simple.isNotEmpty()) {
items(emojiMapping.simple) { emojiSet ->
EmojiKeyWrapper(emojiSet)
}
}
}
}
@@ -263,6 +320,7 @@ fun EmojiPaletteView(
private fun EmojiCategoriesTabRow(
activeCategory: EmojiCategory,
onCategoryChange: (EmojiCategory) -> Unit,
emojiHistoryEnabled: Boolean,
) {
val context = LocalContext.current
val inputFeedbackController = LocalInputFeedbackController.current
@@ -271,7 +329,11 @@ private fun EmojiCategoriesTabRow(
val unselectedContentColor = tabStyle.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor())
val selectedContentColor = tabStyleFocused.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor())
val selectedTabIndex = EmojiCategoryValues.indexOf(activeCategory)
val selectedTabIndex = if (emojiHistoryEnabled) {
EmojiCategoryValues.indexOf(activeCategory)
} else {
EmojiCategoryValues.indexOf(activeCategory) - 1
}
TabRow(
modifier = Modifier
.fillMaxWidth()
@@ -288,6 +350,9 @@ private fun EmojiCategoriesTabRow(
},
) {
for (category in EmojiCategoryValues) {
if (category == EmojiCategory.RECENTLY_USED && !emojiHistoryEnabled) {
continue
}
Tab(
onClick = {
inputFeedbackController.keyPress(TextKeyData.UNSPECIFIED)
@@ -311,11 +376,13 @@ private fun EmojiKey(
emojiSet: EmojiSet,
emojiCompatInstance: EmojiCompat?,
preferredSkinTone: EmojiSkinTone,
isPinned: Boolean,
isRecent: Boolean,
contentColor: Color,
fontSize: TextUnit,
fontSizeMultiplier: Float,
onEmojiInput: (Emoji) -> Unit,
onLongPress: (Emoji) -> Unit,
onHistoryAction: () -> Unit,
) {
val inputFeedbackController = LocalInputFeedbackController.current
val base = emojiSet.base(withSkinTone = preferredSkinTone)
@@ -335,8 +402,7 @@ private fun EmojiKey(
},
onLongPress = {
inputFeedbackController.keyLongPress(TextKeyData.UNSPECIFIED)
onLongPress(base)
if (variations.isNotEmpty()) {
if (variations.isNotEmpty() || isPinned || isRecent) {
showVariantsBox = true
}
},
@@ -350,7 +416,7 @@ private fun EmojiKey(
color = contentColor,
fontSize = fontSize,
)
if (variations.isNotEmpty()) {
if (variations.isNotEmpty() || isPinned || isRecent) {
val shape = when (LocalLayoutDirection.current) {
LayoutDirection.Ltr -> VariantsTriangleShapeLtr
LayoutDirection.Rtl -> VariantsTriangleShapeRtl
@@ -364,19 +430,34 @@ private fun EmojiKey(
)
}
EmojiVariationsPopup(
variations = variations,
visible = showVariantsBox,
emojiCompatInstance = emojiCompatInstance,
fontSizeMultiplier = fontSizeMultiplier,
onEmojiTap = { emoji ->
onEmojiInput(emoji)
showVariantsBox = false
},
onDismiss = {
showVariantsBox = false
},
)
if (isPinned || isRecent) {
EmojiHistoryPopup(
emoji = base,
visible = showVariantsBox,
isCurrentlyPinned = isPinned,
onHistoryAction = {
onHistoryAction()
showVariantsBox = false
},
onDismiss = {
showVariantsBox = false
},
)
} else {
EmojiVariationsPopup(
variations = variations,
visible = showVariantsBox,
emojiCompatInstance = emojiCompatInstance,
fontSizeMultiplier = fontSizeMultiplier,
onEmojiTap = { emoji ->
onEmojiInput(emoji)
showVariantsBox = false
},
onDismiss = {
showVariantsBox = false
},
)
}
}
}
@@ -434,6 +515,113 @@ private fun EmojiVariationsPopup(
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun EmojiHistoryPopup(
emoji: Emoji,
visible: Boolean,
isCurrentlyPinned: Boolean,
onHistoryAction: () -> Unit,
onDismiss: () -> Unit,
) {
val prefs by florisPreferenceModel()
val scope = rememberCoroutineScope()
val popupStyle = FlorisImeTheme.style.get(element = FlorisImeUi.EmojiKeyPopup)
val emojiKeyHeight = FlorisImeSizing.smartbarHeight
val context = LocalContext.current
val pinnedUS by prefs.emoji.historyPinnedUpdateStrategy.observeAsState()
val recentUS by prefs.emoji.historyRecentUpdateStrategy.observeAsState()
val showMoveLeft = isCurrentlyPinned && !pinnedUS.isAutomatic || !recentUS.isAutomatic
val showMoveRight = isCurrentlyPinned && !pinnedUS.isAutomatic || !recentUS.isAutomatic
@Composable
fun Action(icon: ImageVector, action: suspend () -> Unit) {
Box(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures {
scope.launch {
action()
onHistoryAction()
}
}
}
.width(EmojiBaseWidth)
.height(emojiKeyHeight)
.padding(all = 4.dp),
) {
Icon(
modifier = Modifier.align(Alignment.Center),
imageVector = icon,
contentDescription = null,
tint = popupStyle.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor()),
)
}
}
val numActions = 1
if (visible) {
Popup(
alignment = Alignment.TopCenter,
offset = with(LocalDensity.current) {
val y = -emojiKeyHeight * ceil(numActions / 6f)
IntOffset(x = 0, y = y.toPx().toInt())
},
onDismissRequest = onDismiss,
) {
FlowRow(
modifier = Modifier
.widthIn(max = EmojiBaseWidth * 6)
.snyggShadow(popupStyle)
.snyggBorder(context, popupStyle)
.snyggBackground(context, popupStyle, fallbackColor = FlorisImeTheme.fallbackSurfaceColor()),
) {
if (isCurrentlyPinned) {
Action(
icon = Icons.Outlined.PushPin,
action = {
EmojiHistoryHelper.unpinEmoji(prefs, emoji)
},
)
} else {
Action(
icon = Icons.Outlined.PushPin,
action = {
EmojiHistoryHelper.pinEmoji(prefs, emoji)
},
)
}
if (showMoveLeft) {
Action(
icon = Icons.AutoMirrored.Default.KeyboardArrowLeft,
action = {
EmojiHistoryHelper.moveEmoji(prefs, emoji, -1)
},
)
}
if (showMoveRight) {
Action(
icon = Icons.AutoMirrored.Default.KeyboardArrowRight,
action = {
EmojiHistoryHelper.moveEmoji(prefs, emoji, 1)
},
)
}
Action(
icon = Icons.Outlined.Delete,
action = {
EmojiHistoryHelper.removeEmoji(prefs, emoji)
context.showShortToast(
R.string.emoji__history__removal_success_message,
"emoji" to emoji.value,
)
},
)
}
}
}
}
@Composable
fun EmojiText(
text: String,

View File

@@ -1,58 +0,0 @@
/*
* 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.ime.media.emoji
import dev.patrickgold.florisboard.app.AppPrefs
import dev.patrickgold.jetpref.datastore.model.PreferenceSerializer
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
object EmojiRecentlyUsedHelper {
private const val DELIMITER = ";"
private var emojiGuard = Mutex(locked = false)
suspend fun addEmoji(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
val maxSize = prefs.media.emojiRecentlyUsedMaxSize.get()
val list = prefs.media.emojiRecentlyUsed.get().toMutableList()
list.add(0, emoji)
if (maxSize > 0) {
while (list.size > maxSize) {
list.removeLast()
}
}
prefs.media.emojiRecentlyUsed.set(list.distinctBy { it.value })
}
suspend fun removeEmoji(prefs: AppPrefs, emoji: Emoji) = emojiGuard.withLock {
val list = prefs.media.emojiRecentlyUsed.get().toMutableList()
list.remove(emoji)
prefs.media.emojiRecentlyUsed.set(list.distinctBy { it.value })
}
object Serializer : PreferenceSerializer<List<Emoji>> {
override fun serialize(value: List<Emoji>): String {
return value.joinToString(DELIMITER) { it.value }
}
override fun deserialize(value: String): List<Emoji> {
return value.split(DELIMITER).mapNotNull { rawValue ->
rawValue.trim().let { if (it.isBlank()) null else Emoji(it.trim(), "", emptyList()) }
}
}
}
}

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2024 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.ime.media.emoji
import kotlinx.coroutines.Dispatchers
@@ -13,10 +29,6 @@ import dev.patrickgold.florisboard.ime.nlp.SuggestionProvider
import dev.patrickgold.florisboard.lib.FlorisLocale
import io.github.reactivecircus.cache4k.Cache
const val EMOJI_SUGGESTION_INDICATOR = ':'
const val EMOJI_SUGGESTION_MAX_COUNT = 5
private const val EMOJI_SUGGESTION_QUERY_MIN_LENGTH = 3
/**
* Provides emoji suggestions within a text input context.
*
@@ -30,7 +42,7 @@ class EmojiSuggestionProvider(private val context: Context) : SuggestionProvider
override val providerId = "org.florisboard.nlp.providers.emoji"
private val prefs by florisPreferenceModel()
private val lettersRegex = "^:[A-Za-z]*$".toRegex()
private val lettersRegex = "^[A-Za-z]*$".toRegex()
private val cachedEmojiMappings = Cache.Builder().build<FlorisLocale, EmojiDataBySkinTone>()
@@ -52,7 +64,8 @@ class EmojiSuggestionProvider(private val context: Context) : SuggestionProvider
allowPossiblyOffensive: Boolean,
isPrivateSession: Boolean
): List<SuggestionCandidate> {
val preferredSkinTone = prefs.media.emojiPreferredSkinTone.get()
val preferredSkinTone = prefs.emoji.preferredSkinTone.get()
val showName = prefs.emoji.suggestionCandidateShowName.get()
val query = validateInputQuery(content.composingText) ?: return emptyList()
val emojis = cachedEmojiMappings.get(subtype.primaryLocale)?.get(preferredSkinTone) ?: emptyList()
val candidates = withContext(Dispatchers.Default) {
@@ -62,14 +75,24 @@ class EmojiSuggestionProvider(private val context: Context) : SuggestionProvider
emoji.keywords.any { it.contains(query, ignoreCase = true) }
}
.limit(maxCandidateCount.toLong())
.map { EmojiSuggestionCandidate(it) }
.map { emoji ->
EmojiSuggestionCandidate(
emoji = emoji,
showName = showName,
sourceProvider = this@EmojiSuggestionProvider,
)
}
.collect(Collectors.toList())
}
return candidates
}
override suspend fun notifySuggestionAccepted(subtype: Subtype, candidate: SuggestionCandidate) {
// No-op
val updateHistory = prefs.emoji.suggestionUpdateHistory.get()
if (!updateHistory || candidate !is EmojiSuggestionCandidate) {
return
}
EmojiHistoryHelper.markEmojiUsed(prefs, candidate.emoji)
}
override suspend fun notifySuggestionReverted(subtype: Subtype, candidate: SuggestionCandidate) {
@@ -90,15 +113,18 @@ class EmojiSuggestionProvider(private val context: Context) : SuggestionProvider
* Validates the user input query for emoji suggestions.
*/
private fun validateInputQuery(composingText: CharSequence): String? {
if (!composingText.startsWith(EMOJI_SUGGESTION_INDICATOR)) {
val prefix = prefs.emoji.suggestionType.get().prefix
val queryMinLength = prefs.emoji.suggestionQueryMinLength.get() + prefix.length
if (prefix.isNotEmpty() && !composingText.startsWith(prefix)) {
return null
}
if (composingText.length <= EMOJI_SUGGESTION_QUERY_MIN_LENGTH) {
if (composingText.length < queryMinLength) {
return null
}
if (!lettersRegex.matches(composingText)) {
val emojiPartialName = composingText.substring(prefix.length)
if (!lettersRegex.matches(emojiPartialName)) {
return null
}
return composingText.substring(1)
return emojiPartialName
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2024 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.ime.media.emoji
enum class EmojiSuggestionType(val prefix: String) {
LEADING_COLON(":"),
INLINE_TEXT(""),
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2024 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.ime.nlp
import android.content.Context
import android.os.Build
import android.util.Size
import android.view.ViewGroup
import android.view.inputmethod.InlineSuggestion
import android.view.inputmethod.InlineSuggestionInfo
import android.widget.inline.InlineContentView
import androidx.annotation.RequiresApi
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.lib.devtools.flogInfo
import dev.patrickgold.florisboard.lib.devtools.flogWarning
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
data class NlpInlineAutofillSuggestion(
val info: InlineSuggestionInfo,
val view: InlineContentView?,
)
object NlpInlineAutofill {
private val currentSequenceId = AtomicInteger(0)
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val setterGuard = Mutex()
private val _suggestions = MutableStateFlow<List<NlpInlineAutofillSuggestion>>(emptyList())
val suggestions = _suggestions
@RequiresApi(Build.VERSION_CODES.R)
fun showInlineSuggestions(context: Context, rawSuggestions: List<InlineSuggestion>): Boolean {
val sequenceId = generateSequenceId()
if (rawSuggestions.isEmpty()) {
clearInlineSuggestions(sequenceId)
return false
}
scope.launch {
val size = Size(ViewGroup.LayoutParams.WRAP_CONTENT, FlorisImeSizing.Static.smartbarHeightPx)
val latch = CountDownLatch(rawSuggestions.size)
val suggestionsArray = Array<NlpInlineAutofillSuggestion?>(rawSuggestions.size) { null }
flogInfo { "showInlineSuggestions: [${sequenceId}] start inflating suggestions" }
for ((index, rawSuggestion) in rawSuggestions.withIndex()) {
rawSuggestion.inflate(context, size, context.mainExecutor) { view ->
suggestionsArray[index] = NlpInlineAutofillSuggestion(rawSuggestion.info, view)
latch.countDown()
}
}
if (!latch.await(2_000, TimeUnit.MILLISECONDS)) {
flogWarning { "showInlineSuggestions: [${sequenceId}] timed out while waiting for all " +
"suggestions to inflate" }
return@launch
}
val suggestions = suggestionsArray.filterNotNull().sortedByDescending { it.info.isPinned }
setterGuard.lock()
flogInfo { "showInlineSuggestions: [${sequenceId}] successfully inflated " +
"${suggestions.count { it.view != null }} out of ${suggestions.size} suggestions" }
if (currentSequenceId.get() == sequenceId) {
flogInfo { "showInlineSuggestions: [${sequenceId}] setting suggestions" }
_suggestions.value = suggestions
} else {
flogWarning { "showInlineSuggestions: [${sequenceId}] seqId != current, skip setting suggestions" }
}
setterGuard.unlock()
}
return true
}
fun clearInlineSuggestions() {
// Increment sequence id to invalidate eventual pending suggestions
clearInlineSuggestions(generateSequenceId())
}
private fun clearInlineSuggestions(sequenceId: Int) {
scope.launch {
setterGuard.lock()
flogInfo { "clearInlineSuggestions: [${sequenceId}] clearing suggestions" }
_suggestions.value = emptyList()
setterGuard.unlock()
}
}
private fun generateSequenceId(): Int {
return currentSequenceId.incrementAndGet()
}
}

View File

@@ -17,14 +17,8 @@
package dev.patrickgold.florisboard.ime.nlp
import android.content.Context
import android.os.Build
import android.os.SystemClock
import android.util.LruCache
import android.util.Size
import android.view.inputmethod.InlineSuggestion
import android.widget.inline.InlineContentView
import androidx.annotation.RequiresApi
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.clipboardManager
@@ -34,12 +28,10 @@ import dev.patrickgold.florisboard.ime.clipboard.provider.ItemType
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.editor.EditorContent
import dev.patrickgold.florisboard.ime.editor.EditorRange
import dev.patrickgold.florisboard.ime.media.emoji.EMOJI_SUGGESTION_MAX_COUNT
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSuggestionProvider
import dev.patrickgold.florisboard.ime.nlp.han.HanShapeBasedLanguageProvider
import dev.patrickgold.florisboard.ime.nlp.latin.LatinLanguageProvider
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.florisboard.lib.util.NetworkUtils
import dev.patrickgold.florisboard.subtypeManager
import kotlinx.coroutines.CoroutineScope
@@ -53,7 +45,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.florisboard.lib.kotlin.collectLatestIn
import org.florisboard.lib.kotlin.guardedByLock
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.properties.Delegates
@@ -70,7 +61,7 @@ class NlpManager(context: Context) {
private val subtypeManager by context.subtypeManager()
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val clipboardSuggestionProvider = ClipboardSuggestionProvider()
private val clipboardSuggestionProvider = ClipboardSuggestionProvider(context)
private val emojiSuggestionProvider = EmojiSuggestionProvider(context)
private val providers = guardedByLock {
mapOf(
@@ -94,10 +85,6 @@ class NlpManager(context: Context) {
_activeCandidatesFlow.value = v
}
private val inlineContentViews = Collections.synchronizedMap<InlineSuggestion, InlineContentView>(hashMapOf())
private val _inlineSuggestions = MutableLiveData<List<InlineSuggestion>>(emptyList())
val inlineSuggestions: LiveData<List<InlineSuggestion>> get() = _inlineSuggestions
val debugOverlaySuggestionsInfos = LruCache<Long, Pair<String, SpellingResult>>(10)
var debugOverlayVersion = MutableLiveData(0)
private val debugOverlayVersionSource = AtomicInteger(0)
@@ -112,6 +99,9 @@ class NlpManager(context: Context) {
prefs.suggestion.clipboardContentEnabled.observeForever {
assembleCandidates()
}
prefs.emoji.suggestionEnabled.observeForever {
assembleCandidates()
}
subtypeManager.activeSubtypeFlow.collectLatestIn(scope) { subtype ->
preload(subtype)
}
@@ -202,21 +192,45 @@ class NlpManager(context: Context) {
}
fun isSuggestionOn(): Boolean =
prefs.suggestion.enabled.get() || providerForcesSuggestionOn(subtypeManager.activeSubtype)
prefs.suggestion.enabled.get()
|| prefs.emoji.suggestionEnabled.get()
|| providerForcesSuggestionOn(subtypeManager.activeSubtype)
fun suggest(subtype: Subtype, content: EditorContent) {
val reqTime = SystemClock.uptimeMillis()
scope.launch {
val suggestions = getSuggestionProvider(subtype).suggest(
subtype = subtype,
content = content,
maxCandidateCount = 8,
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive.get(),
isPrivateSession = keyboardManager.activeState.isIncognitoMode,
)
val emojiSuggestions = when {
prefs.emoji.suggestionEnabled.get() -> {
emojiSuggestionProvider.suggest(
subtype = subtype,
content = content,
maxCandidateCount = prefs.emoji.suggestionCandidateMaxCount.get(),
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive.get(),
isPrivateSession = keyboardManager.activeState.isIncognitoMode,
)
}
else -> emptyList()
}
val suggestions = when {
emojiSuggestions.isNotEmpty() && prefs.emoji.suggestionType.get().prefix.isNotEmpty() -> {
emptyList()
}
else -> {
getSuggestionProvider(subtype).suggest(
subtype = subtype,
content = content,
maxCandidateCount = 8,
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive.get(),
isPrivateSession = keyboardManager.activeState.isIncognitoMode,
)
}
}
internalSuggestionsGuard.withLock {
if (internalSuggestions.first < reqTime) {
internalSuggestions = reqTime to suggestions
internalSuggestions = reqTime to buildList {
addAll(emojiSuggestions)
addAll(suggestions)
}
}
}
}
@@ -267,24 +281,16 @@ class NlpManager(context: Context) {
runBlocking {
val candidates = when {
isSuggestionOn() -> {
emojiSuggestionProvider.suggest(
subtype = subtypeManager.activeSubtype,
clipboardSuggestionProvider.suggest(
subtype = Subtype.DEFAULT,
content = editorInstance.activeContent,
maxCandidateCount = EMOJI_SUGGESTION_MAX_COUNT,
maxCandidateCount = 8,
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive.get(),
isPrivateSession = keyboardManager.activeState.isIncognitoMode,
).ifEmpty {
clipboardSuggestionProvider.suggest(
subtype = Subtype.DEFAULT,
content = editorInstance.activeContent,
maxCandidateCount = 8,
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive.get(),
isPrivateSession = keyboardManager.activeState.isIncognitoMode,
).ifEmpty {
buildList {
internalSuggestionsGuard.withLock {
addAll(internalSuggestions.second)
}
buildList {
internalSuggestionsGuard.withLock {
addAll(internalSuggestions.second)
}
}
}
@@ -292,68 +298,27 @@ class NlpManager(context: Context) {
else -> emptyList()
}
activeCandidates = candidates
autoExpandCollapseSmartbarActions(candidates, inlineSuggestions.value)
autoExpandCollapseSmartbarActions(candidates, NlpInlineAutofill.suggestions.value)
}
}
/**
* Inflates the given inline suggestions. Once all provided views are ready, the suggestions
* strip is updated and the Smartbar update cycle is triggered.
*
* @param inlineSuggestions A collection of inline suggestions to be inflated and shown.
*/
fun showInlineSuggestions(inlineSuggestions: List<InlineSuggestion>) {
inlineContentViews.clear()
_inlineSuggestions.postValue(inlineSuggestions)
autoExpandCollapseSmartbarActions(activeCandidates, inlineSuggestions)
}
/**
* Clears the inline suggestions and triggers the Smartbar update cycle.
*/
fun clearInlineSuggestions() {
inlineContentViews.clear()
_inlineSuggestions.postValue(emptyList())
autoExpandCollapseSmartbarActions(activeCandidates, null)
}
@RequiresApi(Build.VERSION_CODES.R)
fun inflateOrGet(
context: Context,
size: Size,
inlineSuggestion: InlineSuggestion,
callback: (InlineContentView) -> Unit,
) {
val view = inlineContentViews[inlineSuggestion]
if (view != null) {
callback(view)
} else {
try {
inlineSuggestion.inflate(context, size, context.mainExecutor) { inflatedView ->
if (inflatedView != null) {
inlineContentViews[inlineSuggestion] = inflatedView
callback(inflatedView)
}
}
} catch (e: Exception) {
flogError { e.toString() }
}
}
}
private fun autoExpandCollapseSmartbarActions(list1: List<*>?, list2: List<*>?) {
if (prefs.smartbar.enabled.get() && prefs.smartbar.sharedActionsAutoExpandCollapse.get()) {
if (keyboardManager.inputEventDispatcher.isRepeatableCodeLastDown()
|| keyboardManager.activeState.isActionsOverflowVisible
) {
return // We do not auto switch if a repeatable action key was last pressed or if the actions overflow
// menu is visible to prevent annoying UI changes
}
val isSelection = editorInstance.activeContent.selection.isSelectionMode
val isExpanded = list1.isNullOrEmpty() && list2.isNullOrEmpty() || isSelection
prefs.smartbar.sharedActionsExpandWithAnimation.set(false)
prefs.smartbar.sharedActionsExpanded.set(isExpanded)
fun autoExpandCollapseSmartbarActions(list1: List<*>?, list2: List<*>?) {
if (!prefs.smartbar.enabled.get()) {// || !prefs.smartbar.sharedActionsAutoExpandCollapse.get()) {
return
}
// TODO: this is a mess and needs to be cleaned up in v0.5 with the NLP development
/*if (keyboardManager.inputEventDispatcher.isRepeatableCodeLastDown()
&& !keyboardManager.inputEventDispatcher.isPressed(KeyCode.DELETE)
&& !keyboardManager.inputEventDispatcher.isPressed(KeyCode.FORWARD_DELETE)
|| keyboardManager.activeState.isActionsOverflowVisible
) {
return // We do not auto switch if a repeatable action key was last pressed or if the actions overflow
// menu is visible to prevent annoying UI changes
}*/
val isSelection = editorInstance.activeContent.selection.isSelectionMode
val isExpanded = list1.isNullOrEmpty() && list2.isNullOrEmpty() || isSelection
prefs.smartbar.sharedActionsExpandWithAnimation.set(false)
prefs.smartbar.sharedActionsExpanded.set(isExpanded)
}
fun addToDebugOverlay(word: String, info: SpellingResult) {
@@ -384,7 +349,7 @@ class NlpManager(context: Context) {
}
}
inner class ClipboardSuggestionProvider internal constructor() : SuggestionProvider {
inner class ClipboardSuggestionProvider internal constructor(private val context: Context) : SuggestionProvider {
private var lastClipboardItemId: Long = -1
override val providerId = "org.florisboard.nlp.providers.clipboard"
@@ -413,7 +378,10 @@ class NlpManager(context: Context) {
return buildList {
val now = System.currentTimeMillis()
if ((now - currentItem.creationTimestampMs) < prefs.suggestion.clipboardContentTimeout.get() * 1000) {
add(ClipboardSuggestionCandidate(currentItem, sourceProvider = this@ClipboardSuggestionProvider))
add(ClipboardSuggestionCandidate(currentItem, sourceProvider = this@ClipboardSuggestionProvider, context = context))
if (currentItem.isSensitive) {
return@buildList
}
if (currentItem.type == ItemType.TEXT) {
val text = currentItem.stringRepresentation()
val matches = buildList {
@@ -437,6 +405,7 @@ class NlpManager(context: Context) {
}
),
sourceProvider = this@ClipboardSuggestionProvider,
context = context,
))
}
}

View File

@@ -20,7 +20,7 @@ import android.icu.text.BreakIterator
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.editor.EditorContent
import dev.patrickgold.florisboard.ime.editor.EditorRange
import dev.patrickgold.florisboard.ime.media.emoji.EMOJI_SUGGESTION_INDICATOR
import dev.patrickgold.florisboard.ime.media.emoji.EmojiSuggestionType
/**
* Base interface for any NLP provider implementation. NLP providers maintain their own internal state and only receive
@@ -221,7 +221,7 @@ interface SuggestionProvider : NlpProvider {
// Include Emoji indicator in local composing. This is required so that emoji suggestion indicator'
// can be detected in the composing text.
(pos - 1).takeIf { updatedPos ->
textBeforeSelection.getOrNull(updatedPos) == EMOJI_SUGGESTION_INDICATOR
textBeforeSelection.getOrNull(updatedPos) == EmojiSuggestionType.LEADING_COLON.prefix.first()
} ?: pos
}
EditorRange(start, end)

View File

@@ -16,13 +16,14 @@
package dev.patrickgold.florisboard.ime.nlp
import android.content.Context
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Assignment
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material.icons.outlined.Assignment
import androidx.compose.ui.graphics.vector.ImageVector
import dev.patrickgold.florisboard.ime.clipboard.provider.ClipboardItem
import dev.patrickgold.florisboard.ime.clipboard.provider.ItemType
@@ -123,8 +124,9 @@ data class WordSuggestionCandidate(
data class ClipboardSuggestionCandidate(
val clipboardItem: ClipboardItem,
override val sourceProvider: SuggestionProvider?,
val context: Context,
) : SuggestionCandidate {
override val text: CharSequence = clipboardItem.stringRepresentation()
override val text: CharSequence = clipboardItem.displayText(context)
override val secondaryText: CharSequence? = null
@@ -139,7 +141,7 @@ data class ClipboardSuggestionCandidate(
NetworkUtils.isEmailAddress(text) -> Icons.Default.Email
NetworkUtils.isUrl(text) -> Icons.Default.Link
NetworkUtils.isPhoneNumber(text) -> Icons.Default.Phone
else -> Icons.Outlined.Assignment
else -> Icons.AutoMirrored.Outlined.Assignment
}
ItemType.IMAGE -> Icons.Default.Image
ItemType.VIDEO -> Icons.Default.Videocam
@@ -157,6 +159,7 @@ data class ClipboardSuggestionCandidate(
*/
data class EmojiSuggestionCandidate(
val emoji: Emoji,
val showName: Boolean,
override val confidence: Double = 1.0,
override val isEligibleForAutoCommit: Boolean = false,
override val isEligibleForUserRemoval: Boolean = false,
@@ -164,5 +167,5 @@ data class EmojiSuggestionCandidate(
override val sourceProvider: SuggestionProvider? = null,
) : SuggestionCandidate {
override val text = emoji.value
override val secondaryText = emoji.name
override val secondaryText = if (showName) emoji.name else null
}

View File

@@ -24,14 +24,13 @@ import dev.patrickgold.florisboard.ime.nlp.SpellingProvider
import dev.patrickgold.florisboard.ime.nlp.SpellingResult
import dev.patrickgold.florisboard.ime.nlp.SuggestionCandidate
import dev.patrickgold.florisboard.ime.nlp.SuggestionProvider
import dev.patrickgold.florisboard.ime.nlp.WordSuggestionCandidate
import org.florisboard.lib.android.readText
import dev.patrickgold.florisboard.lib.devtools.flogDebug
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import org.florisboard.lib.android.readText
import org.florisboard.lib.kotlin.guardedByLock
class LatinLanguageProvider(context: Context) : SpellingProvider, SuggestionProvider {
@@ -106,7 +105,8 @@ class LatinLanguageProvider(context: Context) : SpellingProvider, SuggestionProv
allowPossiblyOffensive: Boolean,
isPrivateSession: Boolean,
): List<SuggestionCandidate> {
val word = content.composingText.ifBlank { "next" }
return emptyList()
/*val word = content.composingText.ifBlank { "next" }
val suggestions = buildList {
for (n in 0 until maxCandidateCount) {
add(WordSuggestionCandidate(
@@ -119,7 +119,7 @@ class LatinLanguageProvider(context: Context) : SpellingProvider, SuggestionProv
))
}
}
return suggestions
return suggestions*/
}
override suspend fun notifySuggestionAccepted(subtype: Subtype, candidate: SuggestionCandidate) {

View File

@@ -22,8 +22,8 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.ZoomOutMap
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -55,7 +55,8 @@ fun RowScope.OneHandedPanel(
Column(
modifier = modifier
.weight(weight)
.snyggBackground(context, oneHandedPanelStyle),
.snyggBackground(context, oneHandedPanelStyle)
.height(FlorisImeSizing.imeUiHeight()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly,
) {
@@ -77,13 +78,13 @@ fun RowScope.OneHandedPanel(
inputFeedbackController.keyPress()
prefs.keyboard.oneHandedMode.set(panelSide)
},
modifier = Modifier.height(FlorisImeSizing.keyboardUiHeight()).fillMaxWidth()
modifier = Modifier.weight(1f).fillMaxWidth(),
) {
Icon(
imageVector = if (panelSide == OneHandedMode.START) {
Icons.Default.KeyboardArrowLeft
Icons.AutoMirrored.Filled.KeyboardArrowLeft
} else {
Icons.Default.KeyboardArrowRight
Icons.AutoMirrored.Filled.KeyboardArrowRight
},
contentDescription = stringRes(
if (panelSide == OneHandedMode.START) {

View File

@@ -48,12 +48,13 @@ import dev.patrickgold.florisboard.lib.toIntOffset
@Composable
fun rememberPopupUiController(
key1: Any?,
key2: Any?,
boundsProvider: (key: Key) -> FlorisRect,
isSuitableForBasicPopup: (key: Key) -> Boolean,
isSuitableForExtendedPopup: (key: Key) -> Boolean,
): PopupUiController {
val context = LocalContext.current
return remember(key1) {
return remember(key1, key2) {
PopupUiController(context, boundsProvider, isSuitableForBasicPopup, isSuitableForExtendedPopup)
}
}

View File

@@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import dev.patrickgold.florisboard.ime.keyboard.KeyboardState
import dev.patrickgold.florisboard.lib.compose.conditional
private val SheetOutOfBoundsBgColorInactive = Color(0x00000000)
private val SheetOutOfBoundsBgColorActive = Color(0x52000000)
@@ -53,16 +54,13 @@ fun BottomSheetHostUi(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.then(
if (isShowing) {
Modifier.pointerInput(Unit) {
detectTapGestures {
onHide()
}
.conditional(isShowing) {
pointerInput(Unit) {
detectTapGestures {
onHide()
}
} else {
Modifier
}),
}
},
)
AnimatedVisibility(
visible = isShowing,

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Patrick Goldinger
* Copyright (C) 2024 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -55,18 +55,17 @@ import dev.patrickgold.florisboard.ime.nlp.SuggestionCandidate
import dev.patrickgold.florisboard.ime.theme.FlorisImeTheme
import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.conditional
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.compose.safeTimes
import dev.patrickgold.florisboard.lib.observeAsNonNullState
import dev.patrickgold.florisboard.nlpManager
import dev.patrickgold.florisboard.subtypeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.solidColor
import org.florisboard.lib.snygg.ui.spSize
private val CandidatesRowScrollbarHeight = 2.dp
val CandidatesRowScrollbarHeight = 2.dp
@Composable
fun CandidatesRow(modifier: Modifier = Modifier) {
@@ -78,89 +77,71 @@ fun CandidatesRow(modifier: Modifier = Modifier) {
val displayMode by prefs.suggestion.displayMode.observeAsState()
val candidates by nlpManager.activeCandidatesFlow.collectAsState()
val inlineSuggestions by nlpManager.inlineSuggestions.observeAsNonNullState()
val rowStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarCandidatesRow)
val spacerStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarCandidateSpacer)
if (AndroidVersion.ATLEAST_API30_R && inlineSuggestions.isNotEmpty()) {
Row(
modifier = modifier
.fillMaxSize()
.florisHorizontalScroll(scrollbarHeight = CandidatesRowScrollbarHeight),
) {
for (inlineSuggestion in inlineSuggestions) {
InlineSuggestionView(inlineSuggestion = inlineSuggestion)
}
}
} else {
Row(
modifier = modifier
.fillMaxSize()
.snyggBackground(context, rowStyle)
.then(
if (displayMode == CandidatesDisplayMode.DYNAMIC_SCROLLABLE && candidates.size > 1) {
Modifier.florisHorizontalScroll(scrollbarHeight = CandidatesRowScrollbarHeight)
} else {
Modifier
}
),
horizontalArrangement = if (candidates.size > 1) {
Arrangement.Start
} else {
Arrangement.Center
Row(
modifier = modifier
.fillMaxSize()
.snyggBackground(context, rowStyle)
.conditional(displayMode == CandidatesDisplayMode.DYNAMIC_SCROLLABLE && candidates.size > 1) {
florisHorizontalScroll(scrollbarHeight = CandidatesRowScrollbarHeight)
},
) {
if (candidates.isNotEmpty()) {
val candidateModifier = if (candidates.size == 1) {
Modifier
.fillMaxHeight()
.weight(1f, fill = false)
} else {
Modifier
.fillMaxHeight()
.then(
if (displayMode == CandidatesDisplayMode.CLASSIC) {
Modifier.weight(1f)
} else {
Modifier.wrapContentWidth().widthIn(max = 160.dp)
}
)
}
val list = when (displayMode) {
CandidatesDisplayMode.CLASSIC -> candidates.subList(0, 3.coerceAtMost(candidates.size))
else -> candidates
}
for ((n, candidate) in list.withIndex()) {
if (n > 0) {
Spacer(
modifier = Modifier
.width(1.dp)
.fillMaxHeight(0.6f)
.align(Alignment.CenterVertically)
.snyggBackground(context, spacerStyle),
)
horizontalArrangement = if (candidates.size > 1) {
Arrangement.Start
} else {
Arrangement.Center
},
) {
if (candidates.isNotEmpty()) {
val candidateModifier = if (candidates.size == 1) {
Modifier
.fillMaxHeight()
.weight(1f, fill = false)
} else {
Modifier
.fillMaxHeight()
.conditional(displayMode == CandidatesDisplayMode.CLASSIC) {
weight(1f)
}
CandidateItem(
modifier = candidateModifier,
candidate = candidate,
displayMode = displayMode,
onClick = {
// Can't use candidate directly
keyboardManager.commitCandidate(candidates[n])
},
onLongPress = {
// Can't use candidate directly
val candidateItem = candidates[n]
if (candidateItem.isEligibleForUserRemoval) {
nlpManager.removeSuggestion(subtypeManager.activeSubtype, candidateItem)
} else {
false
}
},
longPressDelay = prefs.keyboard.longPressDelay.get().toLong(),
.conditional(displayMode != CandidatesDisplayMode.CLASSIC) {
wrapContentWidth().widthIn(max = 160.dp)
}
}
val list = when (displayMode) {
CandidatesDisplayMode.CLASSIC -> candidates.subList(0, 3.coerceAtMost(candidates.size))
else -> candidates
}
for ((n, candidate) in list.withIndex()) {
if (n > 0) {
Spacer(
modifier = Modifier
.width(1.dp)
.fillMaxHeight(0.6f)
.align(Alignment.CenterVertically)
.snyggBackground(context, spacerStyle),
)
}
CandidateItem(
modifier = candidateModifier,
candidate = candidate,
displayMode = displayMode,
onClick = {
// Can't use candidate directly
keyboardManager.commitCandidate(candidates[n])
},
onLongPress = {
// Can't use candidate directly
val candidateItem = candidates[n]
if (candidateItem.isEligibleForUserRemoval) {
nlpManager.removeSuggestion(subtypeManager.activeSubtype, candidateItem)
} else {
false
}
},
longPressDelay = prefs.keyboard.longPressDelay.get().toLong(),
)
}
}
}

View File

@@ -1,57 +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.ime.smartbar
import android.os.Build
import android.util.Size
import android.view.ViewGroup
import android.view.inputmethod.InlineSuggestion
import android.widget.inline.InlineContentView
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.viewinterop.AndroidView
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.nlpManager
@RequiresApi(Build.VERSION_CODES.R)
@Composable
fun InlineSuggestionView(inlineSuggestion: InlineSuggestion) = with(LocalDensity.current) {
val context = LocalContext.current
val nlpManager by context.nlpManager()
val size = Size(ViewGroup.LayoutParams.WRAP_CONTENT, FlorisImeSizing.smartbarHeight.toPx().toInt())
var inlineContentView by remember { mutableStateOf<InlineContentView?>(null) }
LaunchedEffect(Unit) {
nlpManager.inflateOrGet(context, size, inlineSuggestion) { view ->
inlineContentView = view
}
}
if (inlineContentView != null) {
AndroidView(
factory = { inlineContentView!! },
)
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2024 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.ime.smartbar
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
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.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.viewinterop.AndroidView
import dev.patrickgold.florisboard.ime.nlp.NlpInlineAutofillSuggestion
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.toIntOffset
@RequiresApi(Build.VERSION_CODES.R)
@Composable
fun InlineSuggestionsUi(
inlineSuggestions: List<NlpInlineAutofillSuggestion>,
modifier: Modifier = Modifier,
) {
val scrollState = rememberScrollState()
val almostEmptyRect = remember { android.graphics.Rect(0, 0, 1, 1) }
Row(
modifier
.fillMaxSize()
.florisHorizontalScroll(
state = scrollState,
scrollbarHeight = CandidatesRowScrollbarHeight,
),
) {
val xMin = scrollState.value
val xMax = scrollState.value + scrollState.viewportSize
for (inlineSuggestion in inlineSuggestions) {
if (inlineSuggestion.view == null) {
continue
}
var chipPos by remember { mutableStateOf(IntOffset.Zero) }
AndroidView(
modifier = Modifier.onGloballyPositioned { chipPos = it.positionInParent().toIntOffset() },
factory = { inlineSuggestion.view },
update = { view ->
view.clipBounds = android.graphics.Rect(
(xMin - chipPos.x).coerceAtLeast(0),
0,
(xMax - chipPos.x).coerceAtMost(view.width),
view.height,
)
// The empty rect is a workaround for a bug (I suppose) where an empty rect causes
// no clipping, but we actually want to completely hide the view.
// Thus we just show the topmost pixel of the view, which due to the round shape
// of the theme should be transparent anyways.
if (view.clipBounds.isEmpty) {
view.clipBounds = almostEmptyRect
}
}
)
}
}
}

View File

@@ -42,6 +42,7 @@ import androidx.compose.material.icons.filled.UnfoldMore
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -56,6 +57,7 @@ import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.keyboard.FlorisImeSizing
import dev.patrickgold.florisboard.ime.nlp.NlpInlineAutofill
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionButton
import dev.patrickgold.florisboard.ime.smartbar.quickaction.QuickActionsRow
import dev.patrickgold.florisboard.ime.smartbar.quickaction.ToggleOverflowPanelAction
@@ -64,8 +66,10 @@ import dev.patrickgold.florisboard.ime.theme.FlorisImeUi
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.compose.horizontalTween
import dev.patrickgold.florisboard.lib.compose.verticalTween
import dev.patrickgold.florisboard.nlpManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.vectorResource
import org.florisboard.lib.android.AndroidVersion
import org.florisboard.lib.snygg.ui.snyggBackground
import org.florisboard.lib.snygg.ui.snyggBorder
import org.florisboard.lib.snygg.ui.snyggShadow
@@ -138,6 +142,13 @@ private fun SmartbarMainRow(modifier: Modifier = Modifier) {
val prefs by florisPreferenceModel()
val context = LocalContext.current
val keyboardManager by context.keyboardManager()
val nlpManager by context.nlpManager()
val inlineSuggestions by NlpInlineAutofill.suggestions.collectAsState()
LaunchedEffect(inlineSuggestions) {
nlpManager.autoExpandCollapseSmartbarActions(null, inlineSuggestions)
}
val shouldShowInlineSuggestionsUi = AndroidVersion.ATLEAST_API30_R && inlineSuggestions.isNotEmpty()
val smartbarLayout by prefs.smartbar.layout.observeAsState()
val flipToggles by prefs.smartbar.flipToggles.observeAsState()
@@ -223,7 +234,11 @@ private fun SmartbarMainRow(modifier: Modifier = Modifier) {
enter = enterTransition,
exit = exitTransition,
) {
CandidatesRow()
if (shouldShowInlineSuggestionsUi) {
InlineSuggestionsUi(inlineSuggestions)
} else {
CandidatesRow()
}
}
androidx.compose.animation.AnimatedVisibility(
visible = expanded,
@@ -331,11 +346,19 @@ private fun SmartbarMainRow(modifier: Modifier = Modifier) {
) {
when (smartbarLayout) {
SmartbarLayout.SUGGESTIONS_ONLY -> {
CandidatesRow()
if (shouldShowInlineSuggestionsUi) {
InlineSuggestionsUi(inlineSuggestions)
} else {
CandidatesRow()
}
}
SmartbarLayout.ACTIONS_ONLY -> {
QuickActionsRow(elementName = FlorisImeUi.SmartbarSharedActionsRow)
if (shouldShowInlineSuggestionsUi) {
InlineSuggestionsUi(inlineSuggestions)
} else {
QuickActionsRow(elementName = FlorisImeUi.SmartbarSharedActionsRow)
}
}
SmartbarLayout.SUGGESTIONS_ACTIONS_SHARED -> {

View File

@@ -16,7 +16,6 @@
package dev.patrickgold.florisboard.ime.smartbar.quickaction
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -33,7 +32,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -76,7 +75,6 @@ private const val ItemNotFound = -1
private val NoopAction = QuickAction.InsertKey(TextKeyData(code = KeyCode.NOOP))
private val DragMarkerAction = QuickAction.InsertKey(TextKeyData(code = KeyCode.DRAG_MARKER))
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
val prefs by florisPreferenceModel()
@@ -105,17 +103,20 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
val headerStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarActionsEditorHeader)
val subheaderStyle = FlorisImeTheme.style.get(FlorisImeUi.SmartbarActionsEditorSubheader)
fun findItemForOffset(offset: IntOffset): LazyGridItemInfo? {
fun findItemForOffsetOrClosestInRow(offset: IntOffset): LazyGridItemInfo? {
var closestItemInRow: LazyGridItemInfo? = null
// Using manual for loop with indices instead of firstOrNull() because this method gets
// called a lot and firstOrNull allocates an iterator for each call
for (index in gridState.layoutInfo.visibleItemsInfo.indices) {
val item = gridState.layoutInfo.visibleItemsInfo[index]
if (offset.x in item.offset.x..(item.offset.x + item.size.width) &&
offset.y in item.offset.y..(item.offset.y + item.size.height)) {
return item
if (offset.y in item.offset.y..(item.offset.y + item.size.height)) {
if (offset.x in item.offset.x..(item.offset.x + item.size.width)) {
return item
}
closestItemInRow = item
}
}
return null
return closestItemInRow
}
fun indexOfStickyAction(item: LazyGridItemInfo): Int {
@@ -158,7 +159,7 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
}
fun beginDragGesture(pos: IntOffset) {
val item = findItemForOffset(pos) ?: return
val item = findItemForOffsetOrClosestInRow(pos) ?: return
val stickyActionIndex = indexOfStickyAction(item)
val dynamicActionIndex = indexOfDynamicAction(item)
val hiddenActionIndex = indexOfHiddenAction(item)
@@ -182,7 +183,7 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
if (activeDragAction == null) return
val pos = activeDragPosition + posChange
activeDragPosition = pos
val item = findItemForOffset(pos) ?: return
val item = findItemForOffsetOrClosestInRow(pos) ?: return
val stickyActionIndex = indexOfStickyAction(item)
val dynamicActionIndex = indexOfDynamicAction(item)
val hiddenActionIndex = indexOfHiddenAction(item)
@@ -263,7 +264,7 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
onClick = {
keyboardManager.activeState.isActionsEditorVisible = false
},
icon = Icons.Default.KeyboardArrowLeft,
icon = Icons.AutoMirrored.Filled.KeyboardArrowLeft,
iconColor = headerStyle.foreground.solidColor(context, default = FlorisImeTheme.fallbackContentColor()),
)
Text(
@@ -299,7 +300,7 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
}
item(key = keyOf(stickyAction)) {
QuickActionButton(
modifier = Modifier.animateItemPlacement(),
modifier = Modifier.animateItem(),
action = stickyAction,
evaluator = evaluator,
type = QuickActionBarType.STATIC_TILE,
@@ -314,7 +315,7 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
}
itemsIndexed(dynamicActions, key = { i, a -> keyOf(a) ?: i }) { _, action ->
QuickActionButton(
modifier = Modifier.animateItemPlacement(),
modifier = Modifier.animateItem(),
action = action,
evaluator = evaluator,
type = QuickActionBarType.STATIC_TILE,
@@ -329,7 +330,7 @@ fun QuickActionsEditorPanel(modifier: Modifier = Modifier) {
}
itemsIndexed(hiddenActions, key = { i, a -> keyOf(a) ?: i }) { _, action ->
QuickActionButton(
modifier = Modifier.animateItemPlacement(),
modifier = Modifier.animateItem(),
action = action,
evaluator = evaluator,
type = QuickActionBarType.STATIC_TILE,

View File

@@ -19,7 +19,6 @@ package dev.patrickgold.florisboard.ime.text
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
@@ -32,7 +31,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.keyboard.KeyboardMode
@@ -74,7 +72,7 @@ fun TextInputLayout(
val indicatorStyle = FlorisImeTheme.style.get(FlorisImeUi.IncognitoModeIndicator)
Icon(
modifier = Modifier
.requiredSize(192.dp)
.matchParentSize()
.align(Alignment.Center),
painter = painterResource(R.drawable.ic_incognito),
contentDescription = null,

View File

@@ -108,7 +108,6 @@ fun TextKeyboardLayout(
modifier: Modifier = Modifier,
evaluator: ComputingEvaluator,
isPreview: Boolean = false,
isSmartbarKeyboard: Boolean = false,
): Unit = with(LocalDensity.current) {
val prefs by florisPreferenceModel()
val context = LocalContext.current
@@ -125,7 +124,7 @@ fun TextKeyboardLayout(
val controller = remember { TextKeyboardLayoutController(context) }.also {
it.keyboard = keyboard
if (glideEnabled && !isSmartbarKeyboard && !isPreview && keyboard.mode == KeyboardMode.CHARACTERS) {
if (glideEnabled && !isPreview && keyboard.mode == KeyboardMode.CHARACTERS) {
val keys = keyboard.keys().asSequence().toList()
glideTypingManager.setLayout(keys)
}
@@ -161,13 +160,7 @@ fun TextKeyboardLayout(
BoxWithConstraints(
modifier = modifier
.fillMaxWidth()
.height(
if (isSmartbarKeyboard) {
FlorisImeSizing.smartbarHeight
} else {
FlorisImeSizing.keyboardUiHeight()
}
)
.height(FlorisImeSizing.keyboardUiHeight())
.onGloballyPositioned { coords ->
controller.size = coords.size.toSize()
}
@@ -196,7 +189,7 @@ fun TextKeyboardLayout(
}
.drawWithContent {
drawContent()
if (glideEnabled && glideShowTrail && !isSmartbarKeyboard) {
if (glideEnabled && glideShowTrail) {
val targetDist = 3.0f
val radius = 20.0f
@@ -220,56 +213,50 @@ fun TextKeyboardLayout(
}
},
) {
val keyMarginH by prefs.keyboard.keySpacingHorizontal.observeAsTransformingState { it.dp.toPx() }
val keyMarginV by prefs.keyboard.keySpacingVertical.observeAsTransformingState { it.dp.toPx() }
val desiredKey = remember { TextKey(data = TextKeyData.UNSPECIFIED) }
val keyboardWidth = constraints.maxWidth.toFloat()
val keyboardHeight = constraints.maxHeight.toFloat()
desiredKey.touchBounds.apply {
if (isSmartbarKeyboard) {
width = keyboardWidth / 8f
height = FlorisImeSizing.smartbarHeight.toPx()
} else {
width = keyboardWidth / 10f
height = when (keyboard.mode) {
KeyboardMode.CHARACTERS,
KeyboardMode.NUMERIC_ADVANCED,
KeyboardMode.SYMBOLS,
KeyboardMode.SYMBOLS2 -> {
(FlorisImeSizing.keyboardUiHeight() / keyboard.rowCount)
.coerceAtMost(FlorisImeSizing.keyboardRowBaseHeight * 1.12f).toPx()
val keyMarginH by prefs.keyboard.keySpacingHorizontal.observeAsTransformingState { it.dp.toPx() }
val keyMarginV by prefs.keyboard.keySpacingVertical.observeAsTransformingState { it.dp.toPx() }
val keyboardRowBaseHeight = FlorisImeSizing.keyboardRowBaseHeight
val desiredKey = remember(
keyboard, keyboardWidth, keyboardHeight, keyMarginH, keyMarginV,
keyboardRowBaseHeight, evaluator
) {
TextKey(data = TextKeyData.UNSPECIFIED).also { desiredKey ->
desiredKey.touchBounds.apply {
width = keyboardWidth / 10f
height = when (keyboard.mode) {
KeyboardMode.CHARACTERS,
KeyboardMode.NUMERIC_ADVANCED,
KeyboardMode.SYMBOLS,
KeyboardMode.SYMBOLS2 -> {
(keyboardHeight / keyboard.rowCount)
.coerceAtMost(keyboardRowBaseHeight.toPx() * 1.12f)
}
else -> keyboardRowBaseHeight.toPx()
}
else -> FlorisImeSizing.keyboardRowBaseHeight.toPx()
}
desiredKey.visibleBounds.applyFrom(desiredKey.touchBounds).deflateBy(keyMarginH, keyMarginV)
keyboard.layout(keyboardWidth, keyboardHeight, desiredKey, true)
}
}
desiredKey.visibleBounds.applyFrom(desiredKey.touchBounds).deflateBy(keyMarginH, keyMarginV)
keyboard.layout(keyboardWidth, keyboardHeight, desiredKey, !isSmartbarKeyboard)
val fontSizeMultiplier = prefs.keyboard.fontSizeMultiplier()
val popupUiController = rememberPopupUiController(
key1 = keyboard,
key2 = desiredKey,
boundsProvider = { key ->
val keyPopupWidth: Float
val keyPopupHeight: Float
when {
configuration.isOrientationLandscape() -> {
if (isSmartbarKeyboard) {
keyPopupWidth = key.visibleBounds.width * 1.0f
keyPopupHeight = desiredKey.visibleBounds.height * 3.0f * 1.2f
} else {
keyPopupWidth = desiredKey.visibleBounds.width * 1.0f
keyPopupHeight = desiredKey.visibleBounds.height * 3.0f
}
keyPopupWidth = desiredKey.visibleBounds.width * 1.0f
keyPopupHeight = desiredKey.visibleBounds.height * 3.0f
}
else -> {
if (isSmartbarKeyboard) {
keyPopupWidth = key.visibleBounds.width * 1.1f
keyPopupHeight = desiredKey.visibleBounds.height * 2.5f * 1.2f
} else {
keyPopupWidth = desiredKey.visibleBounds.width * 1.1f
keyPopupHeight = desiredKey.visibleBounds.height * 2.5f
}
keyPopupWidth = desiredKey.visibleBounds.width * 1.1f
keyPopupHeight = desiredKey.visibleBounds.height * 2.5f
}
}
val keyPopupDiffX = (key.visibleBounds.width - keyPopupWidth) / 2.0f
@@ -287,7 +274,7 @@ fun TextKeyboardLayout(
val numeric = keyboard.mode == KeyboardMode.NUMERIC ||
keyboard.mode == KeyboardMode.PHONE || keyboard.mode == KeyboardMode.PHONE2 ||
keyboard.mode == KeyboardMode.NUMERIC_ADVANCED && keyType == KeyType.NUMERIC
keyCode > KeyCode.SPACE && keyCode != KeyCode.MULTIPLE_CODE_POINTS && keyCode != KeyCode.CJK_SPACE && !numeric
keyCode > KeyCode.SPACE && keyCode != KeyCode.CJK_SPACE && !numeric
} else {
true
}
@@ -295,7 +282,7 @@ fun TextKeyboardLayout(
isSuitableForExtendedPopup = { key ->
if (key is TextKey) {
val keyCode = key.computedData.code
keyCode > KeyCode.SPACE && keyCode != KeyCode.MULTIPLE_CODE_POINTS && keyCode != KeyCode.CJK_SPACE || ExceptionsForKeyCodes.contains(keyCode)
keyCode > KeyCode.SPACE && keyCode != KeyCode.CJK_SPACE || ExceptionsForKeyCodes.contains(keyCode)
} else {
true
}
@@ -308,7 +295,7 @@ fun TextKeyboardLayout(
val debugShowTouchBoundaries by prefs.devtools.showKeyTouchBoundaries.observeAsState()
for (textKey in keyboard.keys()) {
TextKeyButton(
textKey, evaluator, fontSizeMultiplier, isSmartbarKeyboard,
textKey, evaluator, fontSizeMultiplier,
debugShowTouchBoundaries,
)
}
@@ -330,13 +317,12 @@ private fun TextKeyButton(
key: TextKey,
evaluator: ComputingEvaluator,
fontSizeMultiplier: Float,
isSmartbarKey: Boolean,
debugShowTouchBoundaries: Boolean,
) = with(LocalDensity.current) {
val context = LocalContext.current
val keyStyle = FlorisImeTheme.style.get(
element = if (isSmartbarKey) FlorisImeUi.SmartbarActionKey else FlorisImeUi.Key,
element = FlorisImeUi.Key,
code = key.computedData.code,
mode = evaluator.state.inputShiftState.value,
isPressed = key.isPressed && key.isEnabled,

View File

@@ -16,6 +16,8 @@
package dev.patrickgold.florisboard.ime.theme
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import dev.patrickgold.florisboard.lib.ext.Extension
import dev.patrickgold.florisboard.lib.ext.ExtensionEditor
import dev.patrickgold.florisboard.lib.ext.ExtensionMeta
@@ -41,14 +43,14 @@ class ThemeExtension(
override fun edit() = ThemeExtensionEditor(
meta = meta,
dependencies = dependencies?.toMutableList() ?: mutableListOf(),
themes = themes.map { it.edit() }.toMutableList(),
themes = mutableStateListOf(*themes.map { it.edit() }.toTypedArray()),
)
}
class ThemeExtensionEditor(
override var meta: ExtensionMeta,
override val dependencies: MutableList<String>,
val themes: MutableList<ThemeExtensionComponentEditor>,
val themes: SnapshotStateList<ThemeExtensionComponentEditor>,
) : ExtensionEditor {
override fun build() = ThemeExtension(

View File

@@ -199,70 +199,63 @@ class ThemeManager(context: Context) {
context: Context,
style: SnyggStylesheet = activeThemeInfo.value?.stylesheet ?: FlorisImeThemeBaseStyle,
): Bundle {
val chipStyle = style.getStatic(FlorisImeUi.SmartbarSharedActionsToggle)
val bgColor = chipStyle.background.solidColor(context)
val fgColor = chipStyle.foreground.solidColor(context)
val snyggStyle = style.getStatic(FlorisImeUi.SmartbarSharedActionsToggle)
val bgColor = snyggStyle.background.solidColor(context)
val fgColor = snyggStyle.foreground.solidColor(context)
val bgDrawableId = androidx.autofill.R.drawable.autofill_inline_suggestion_chip_background
val stylesBuilder = UiVersions.newStylesBuilder()
val suggestionStyle = InlineSuggestionUi.newStyleBuilder()
.setSingleIconChipStyle(
ViewStyle.Builder()
.setBackground(
Icon.createWithResource(context, bgDrawableId).setTint(bgColor.toArgb())
)
.setPadding(0, 0, 0, 0)
.build()
val bgDrawable = Icon.createWithResource(context, bgDrawableId).apply {
setTint(bgColor.toArgb())
}
val chipStyle = ViewStyle.Builder().run {
setBackground(bgDrawable)
setPadding(
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_bottom).toInt(),
)
.setChipStyle(
ViewStyle.Builder()
.setBackground(
Icon.createWithResource(context, bgDrawableId).setTint(bgColor.toArgb())
)
.setPadding(
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_bg_padding_bottom).toInt(),
)
.build()
build()
}
val iconStyle = ImageViewStyle.Builder().run {
setLayoutMargin(0, 0, 0, 0)
build()
}
val titleStyle = TextViewStyle.Builder().run {
setLayoutMargin(
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_bottom).toInt(),
)
.setStartIconStyle(
ImageViewStyle.Builder()
.setLayoutMargin(0, 0, 0, 0)
.build()
setTextColor(fgColor.toArgb())
setTextSize(16f)
build()
}
val subtitleStyle = TextViewStyle.Builder().run {
setLayoutMargin(
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_bottom).toInt(),
)
.setTitleStyle(
TextViewStyle.Builder()
.setLayoutMargin(
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_title_margin_bottom).toInt(),
)
.setTextColor(fgColor.toArgb())
.setTextSize(16f)
.build()
)
.setSubtitleStyle(
TextViewStyle.Builder()
.setLayoutMargin(
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_start).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_top).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_end).toInt(),
context.resources.getDimension(R.dimen.suggestion_chip_fg_subtitle_margin_bottom).toInt(),
)
.setTextColor(ColorUtils.setAlphaComponent(fgColor.toArgb(), 150))
.setTextSize(14f)
.build()
)
.setEndIconStyle(
ImageViewStyle.Builder()
.setLayoutMargin(0, 0, 0, 0)
.build()
)
.build()
stylesBuilder.addStyle(suggestionStyle)
return stylesBuilder.build()
setTextColor(ColorUtils.setAlphaComponent(fgColor.toArgb(), 150))
setTextSize(14f)
build()
}
val suggestionStyle = InlineSuggestionUi.newStyleBuilder().run {
setSingleIconChipStyle(chipStyle)
setChipStyle(chipStyle)
setStartIconStyle(iconStyle)
setEndIconStyle(iconStyle)
setTitleStyle(titleStyle)
setSubtitleStyle(subtitleStyle)
build()
}
return UiVersions.newStylesBuilder().run {
addStyle(suggestionStyle)
build()
}
}
private fun getColorFromThemeAttribute(

View File

@@ -361,10 +361,13 @@ class FlorisLocale private constructor(val base: Locale) {
}
}
@Suppress("NOTHING_TO_INLINE")
inline fun String.lowercase(locale: FlorisLocale): String = this.lowercase(locale.base)
@Suppress("NOTHING_TO_INLINE")
inline fun String.uppercase(locale: FlorisLocale): String = this.uppercase(locale.base)
@Suppress("NOTHING_TO_INLINE")
inline fun String.titlecase(locale: FlorisLocale = FlorisLocale.ROOT): String {
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale.base) else it.toString() }
}

View File

@@ -24,9 +24,9 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.compose.LocalLifecycleOwner
import dev.patrickgold.jetpref.datastore.model.PreferenceData
import dev.patrickgold.jetpref.datastore.model.PreferenceObserver

View File

@@ -22,7 +22,6 @@ import android.provider.OpenableColumns
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.setValue
import dev.patrickgold.florisboard.app.ext.EditorAction
import dev.patrickgold.florisboard.app.settings.advanced.Backup
@@ -188,7 +187,7 @@ class CacheManager(context: Context) {
var currentAction by mutableStateOf<EditorAction?>(null)
var ext: Extension? = null
var editor by mutableStateOf<T?>(null, neverEqualPolicy())
var editor by mutableStateOf<T?>(null)
var version by mutableIntStateOf(0)
val isModified get() = version > 0
@@ -202,7 +201,6 @@ class CacheManager(context: Context) {
inline fun <R> update(block: T.() -> R): R {
// Method is designed to only be called when editor has been previously initialized
val ret = block(editor!!)
editor = editor
version++
return ret
}

View File

@@ -18,7 +18,7 @@ package dev.patrickgold.florisboard.lib.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ripple
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@@ -32,7 +32,7 @@ fun Modifier.rippleClickable(
) = composed {
this.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
indication = ripple(),
enabled = enabled,
onClickLabel = onClickLabel,
role = role,

View File

@@ -19,9 +19,9 @@ package dev.patrickgold.florisboard.lib.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
@Composable
@NonRestartableComposable

View File

@@ -25,9 +25,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -39,15 +37,10 @@ fun FlorisChip(
onClick: () -> Unit = { },
selected: Boolean = false,
enabled: Boolean = true,
color: Color = Color.Unspecified,
shape: Shape = MaterialTheme.shapes.small,
leadingIcons: List<ImageVector> = listOf(),
trailingIcons: List<ImageVector> = listOf(),
) {
val backgroundColor = color.takeOrElse {
MaterialTheme.colorScheme.onSurface.copy()
}
InputChip(
selected = selected,
onClick = onClick,
@@ -88,42 +81,4 @@ fun FlorisChip(
}
}
)
/*Surface(
modifier = modifier,
onClick = onClick,
enabled = enabled,
color = backgroundColor,
shape = shape,
) {
Row(
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
for (leadingIcon in leadingIcons) {
Icon(
modifier = Modifier
.padding(end = 8.dp)
.size(16.dp),
imageVector = leadingIcon,
contentDescription = null,
)
}
Text(
text = text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
for (trailingIcon in trailingIcons) {
Icon(
modifier = Modifier
.padding(start = 8.dp)
.size(16.dp),
imageVector = trailingIcon,
contentDescription = null,
)
}
}
}*/
}

View File

@@ -21,8 +21,9 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
@@ -41,6 +42,7 @@ import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.jetpref.datastore.ui.PreferenceLayout
import dev.patrickgold.jetpref.datastore.ui.PreferenceUiContent
import org.florisboard.lib.android.AndroidVersion
@Composable
fun FlorisScreen(builder: @Composable FlorisScreenScope.() -> Unit) {
@@ -93,7 +95,7 @@ private class FlorisScreenScopeImpl : FlorisScreenScope {
FlorisIconButton(
onClick = { navController.popBackStack() },
modifier = Modifier.autoMirrorForRtl(),
icon = Icons.Default.ArrowBack,
icon = Icons.AutoMirrored.Filled.ArrowBack,
)
}
@@ -122,12 +124,18 @@ private class FlorisScreenScopeImpl : FlorisScreenScope {
fun Render() {
val context = LocalContext.current
val previewFieldController = LocalPreviewFieldController.current
val colorScheme = MaterialTheme.colorScheme
SideEffect {
val window = (context as Activity).window
previewFieldController?.isVisible = previewFieldVisible
window.statusBarColor = Color.Transparent.toArgb()
window.navigationBarColor = Color.Transparent.toArgb()
if (AndroidVersion.ATLEAST_API29_Q) {
window.navigationBarColor = Color.Transparent.toArgb()
window.isNavigationBarContrastEnforced = true
} else {
window.navigationBarColor = colorScheme.scrim.toArgb()
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2024 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.lib.compose
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.runtime.Composable
fun LazyGridScope.header(
key: Any? = null,
content: @Composable LazyGridItemScope.() -> Unit,
) {
item(key, span = { GridItemSpan(this.maxLineSpan) }, content = content)
}

View File

@@ -0,0 +1,11 @@
package dev.patrickgold.florisboard.lib.compose
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun Modifier.conditional(
condition: Boolean,
modifier: @Composable Modifier.() -> Modifier
): Modifier =
if (condition) then(modifier(Modifier)) else this

View File

@@ -128,7 +128,7 @@ fun PreviewKeyboardField(
keyboardActions = KeyboardActions(
onDone = { focusManager.clearFocus() },
),
keyboardOptions = KeyboardOptions(autoCorrect = true),
keyboardOptions = KeyboardOptions(autoCorrectEnabled = true),
singleLine = true,
shape = RectangleShape,
colors = TextFieldDefaults.colors(

View File

@@ -6,9 +6,9 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import org.florisboard.lib.android.AndroidSettingsHelper
import org.florisboard.lib.android.SystemSettingsObserver

View File

@@ -23,10 +23,10 @@ import android.os.Debug
import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.AppPrefs
import org.florisboard.lib.android.systemService
import dev.patrickgold.florisboard.lib.titlecase
import dev.patrickgold.florisboard.lib.util.TimeUtils
import dev.patrickgold.florisboard.lib.util.UnitUtils
import org.florisboard.lib.android.systemService
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
@@ -49,6 +49,36 @@ object Devtools {
}
}
fun generateDebugLogForGithub(context: Context, prefs: AppPrefs? = null, includeLogcat: Boolean = false): String {
return buildString {
appendLine("<details>")
appendLine("<summary>Detailed info (Debug log header)</summary>")
appendLine()
appendLine("```")
append(generateSystemInfoLog(context))
appendLine()
append(generateAppInfoLog(context))
if (prefs != null) {
appendLine()
append(generateFeatureConfigLog(prefs))
}
appendLine()
appendLine("```")
appendLine("</details>")
if (includeLogcat) {
appendLine()
appendLine("<details>")
appendLine("<summary>Debug log content</summary>")
appendLine()
appendLine("```")
append(generateLogcatDump())
appendLine()
appendLine("```")
appendLine("</details>")
}
}
}
fun generateSystemInfoLog(context: Context, withTitle: Boolean = true): String {
return buildString {
if (withTitle) appendLine("======= SYSTEM INFO =======")

View File

@@ -129,8 +129,9 @@ internal fun List<Extension>.generateUpdateUrl(
return Uri.Builder().run {
scheme("https")
authority(host)
appendPath("updates")
appendPath(version)
appendPath("check-updates")
// TODO: Uncomment when version is supported by the addons store api
//appendPath(version)
encodedFragment(
buildString {
append("data={")

View File

@@ -21,9 +21,6 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
import dev.patrickgold.florisboard.lib.ValidationRule
import org.florisboard.lib.snygg.SnyggStylesheet
import org.florisboard.lib.snygg.value.SnyggDpShapeValue
import org.florisboard.lib.snygg.value.SnyggPercentShapeValue
import org.florisboard.lib.snygg.value.SnyggSolidColorValue
import dev.patrickgold.florisboard.lib.validate
import org.florisboard.lib.snygg.value.SnyggVarValue
@@ -157,7 +154,7 @@ object ExtensionValidation {
}
val SnyggSolidColorValue = ValidationRule<String> {
forKlass = SnyggSolidColorValue::class
forKlass = org.florisboard.lib.snygg.value.SnyggSolidColorValue::class
forProperty = "color"
validator { input ->
val str = input.trim()
@@ -172,7 +169,7 @@ object ExtensionValidation {
}
val SnyggDpShapeValue = ValidationRule<String> {
forKlass = SnyggDpShapeValue::class
forKlass = org.florisboard.lib.snygg.value.SnyggDpShapeValue::class
forProperty = "corner"
validator { str ->
val floatValue = str.toFloatOrNull()
@@ -186,7 +183,7 @@ object ExtensionValidation {
}
val SnyggPercentShapeValue = ValidationRule<String> {
forKlass = SnyggPercentShapeValue::class
forKlass = org.florisboard.lib.snygg.value.SnyggPercentShapeValue::class
forProperty = "corner"
validator { str ->
val intValue = str.toIntOrNull()

View File

@@ -25,7 +25,7 @@ import org.florisboard.lib.android.AndroidVersion
object NetworkUtils {
private val Ipv4Regex = """(?<Ipv4>(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))""".toRegex()
private val Ipv6Regex = """(?<Ipv6>(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])))""".toRegex()
private val HostRegex = """(?<Host>(?:[a-zA-Z\-]+\.)+[a-zA-Z]{2,}|$Ipv4Regex|$Ipv6Regex)""".toRegex()
private val HostRegex = """(?<Host>(?:[a-zA-Z0-9][a-zA-Z0-9\-]+[a-zA-Z0-9]\.)+[a-zA-Z]{2,}|$Ipv4Regex|$Ipv6Regex)""".toRegex()
private val TcpIpPortRegex = """(?<TcpIpPort>6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|(?<![0-9])[0-5]?[0-9]{1,4}(?![0-9]))""".toRegex()
private val UrlRegex = """(?<Url>(?:(?:(?:https?:\/\/)?$HostRegex)|(?:https?:\/\/[a-zA-Z]+))(?::$TcpIpPortRegex)?(?:\/[\p{L}0-9.,;?'\\\/+&%$#=~_\-]*)?)""".toRegex()
private val EmailRegex = """(?<Email>(?:[a-z0-9!#${'$'}%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#${'$'}%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@$HostRegex)""".toRegex()

View File

@@ -18,7 +18,6 @@
package dev.patrickgold.florisboard.lib.util
import android.content.res.Resources
import android.util.DisplayMetrics
import android.view.View
import android.view.Window
import android.widget.FrameLayout
@@ -84,7 +83,7 @@ object ViewUtils {
* @return A float value to represent px equivalent to dp depending on device density
*/
fun dp2px(dp: Float): Float {
return dp * (Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
return dp * Resources.getSystem().displayMetrics.density
}
/**
@@ -96,6 +95,6 @@ object ViewUtils {
* @return A float value to represent dp equivalent to px value
*/
fun px2dp(px: Float): Float {
return px / (Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
return (px / Resources.getSystem().displayMetrics.density)
}
}

View File

@@ -0,0 +1 @@
unqualifiedResLocale=en-US

Some files were not shown because too many files have changed in this diff Show More