Compare commits
79 Commits
feat/flyt
...
feat/use-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aed8e1d17 | ||
|
|
c470b792c1 | ||
|
|
ff5cd1e7c2 | ||
|
|
32fee44364 | ||
|
|
97edc33d05 | ||
|
|
a89af25eab | ||
|
|
66340249d4 | ||
|
|
14147ca1b9 | ||
|
|
bdc740637b | ||
|
|
eb20e80295 | ||
|
|
453fb0253a | ||
|
|
13fc7679a2 | ||
|
|
2421d13038 | ||
|
|
7dedfd4f7a | ||
|
|
ef37194900 | ||
|
|
58134b1ceb | ||
|
|
53cfbad404 | ||
|
|
d6f724e518 | ||
|
|
6c4aa36b06 | ||
|
|
edc38b6c2c | ||
|
|
891a2c6bac | ||
|
|
229237153b | ||
|
|
290fbb5239 | ||
|
|
409d4f9348 | ||
|
|
82938cda5b | ||
|
|
f7b0a30271 | ||
|
|
575f359a85 | ||
|
|
22591163b3 | ||
|
|
8104ae60ca | ||
|
|
165b682732 | ||
|
|
eb770fac6c | ||
|
|
39c27426a4 | ||
|
|
228d5055cc | ||
|
|
b400e04560 | ||
|
|
27c1bbf039 | ||
|
|
f61b655f7d | ||
|
|
f82af63e97 | ||
|
|
0fbd950f6e | ||
|
|
e97b5f54ac | ||
|
|
b611360dd5 | ||
|
|
1b9d260020 | ||
|
|
d74fe62bc0 | ||
|
|
fe6f61a282 | ||
|
|
8b4239d9be | ||
|
|
a0c7cf2794 | ||
|
|
7480d14a0f | ||
|
|
7274228a46 | ||
|
|
a38f6a2c76 | ||
|
|
eda6c09538 | ||
|
|
9e42d16cb0 | ||
|
|
11ba51c354 | ||
|
|
51f5196b8a | ||
|
|
56bbe9d13c | ||
|
|
4d1ae52dc0 | ||
|
|
1e1916194b | ||
|
|
bae3c8ec9d | ||
|
|
9ff7d86a8d | ||
|
|
89ab0731d2 | ||
|
|
887a75a482 | ||
|
|
e52bea2456 | ||
|
|
2171e16346 | ||
|
|
566b6fbae3 | ||
|
|
5215227793 | ||
|
|
671f97eddb | ||
|
|
b6c9469826 | ||
|
|
77e4414467 | ||
|
|
db85e05714 | ||
|
|
51890c93d4 | ||
|
|
1f16ac2c3b | ||
|
|
199211fdbf | ||
|
|
c5ee414ec6 | ||
|
|
f1c5b1802b | ||
|
|
0450c8c7a1 | ||
|
|
6da6da74fc | ||
|
|
d7fca0aad1 | ||
|
|
1af519e01d | ||
|
|
989d2884b1 | ||
|
|
d137155ab0 | ||
|
|
270ab4fe5f |
@@ -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]
|
||||
|
||||
17
.github/workflows/android.yml
vendored
17
.github/workflows/android.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/crowdin-upload.yml
vendored
6
.github/workflows/crowdin-upload.yml
vendored
@@ -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
|
||||
|
||||
61
.github/workflows/validate-strings-no-translations.yml
vendored
Normal file
61
.github/workflows/validate-strings-no-translations.yml
vendored
Normal 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
5
.gitignore
vendored
@@ -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
|
||||
|
||||
50
README.md
50
README.md
@@ -1,21 +1,19 @@
|
||||
<img align="left" width="80" height="80"
|
||||
src=".github/repo_icon.png" alt="App icon">
|
||||
|
||||
# FlorisBoard [](https://crowdin.florisboard.patrickgold.dev) [](https://matrix.to/#/#florisboard:matrix.org) [](CODE_OF_CONDUCT.md) 
|
||||
# FlorisBoard [](https://crowdin.florisboard.patrickgold.dev) [](https://matrix.to/#/#florisboard:matrix.org) [](CODE_OF_CONDUCT.md) [](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 -->
|
||||
|
||||
34
ROADMAP.md
34
ROADMAP.md
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -392,6 +392,12 @@
|
||||
"authors": [ "patrickgold" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
{
|
||||
"id": "tamil",
|
||||
"label": "Tamil",
|
||||
"authors": [ "Clem0908" ],
|
||||
"direction": "ltr"
|
||||
},
|
||||
{
|
||||
"id": "thai_kedmanee",
|
||||
"label": "Thai Kedmanee",
|
||||
|
||||
@@ -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": "}" }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"all": {
|
||||
"c": {
|
||||
"relevant": [
|
||||
{ "$": "auto_text_key", "code": 269, "label": "č" },
|
||||
{ "$": "auto_text_key", "code": 269, "label": "č" }
|
||||
]
|
||||
},
|
||||
"s": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Auto-generated by emojicon.py using CLDR v45
|
||||
# DO NOT EDIT MANUALLY!
|
||||
|
||||
[smileys_emotion]
|
||||
😀;;
|
||||
😃;;
|
||||
@@ -3788,3 +3791,4 @@
|
||||
🏴;;
|
||||
🏴;;
|
||||
🏴;;
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
@@ -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?) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
) },
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(""),
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!! },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 =======")
|
||||
|
||||
@@ -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={")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
@@ -0,0 +1 @@
|
||||
unqualifiedResLocale=en-US
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user